import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go from parser import parse_nbu_csv, compute_hash import database as db import report_gen as rg import time import io import os # Initialize database schemas db.init_db() # Set up page configurations st.set_page_config( page_title="NetBackup Log Insights", page_icon="⚡", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS for high-fidelity dark corporate theme (CXP-inspired) custom_css = """ """ st.markdown(custom_css, unsafe_allow_html=True) # Initialize Authentication Session States if 'logged_in' not in st.session_state: st.session_state['logged_in'] = False if 'username' not in st.session_state: st.session_state['username'] = "" if 'role' not in st.session_state: st.session_state['role'] = "" # --- ROUTING ENGINE --- # Scenario 1: First Run Bootstrap (No Administrator exists) if not db.has_admin_user(): st.markdown("
", unsafe_allow_html=True) st.markdown("

⚙️ Configuração Inicial do Sistema

", unsafe_allow_html=True) st.markdown("

Nenhum usuário administrador encontrado no banco de dados. Crie o Administrador Principal para continuar.

", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) col_boot1, col_boot2, col_boot3 = st.columns([1, 1.3, 1]) with col_boot2: with st.form("bootstrap_form"): admin_user = st.text_input("Usuário Administrador:", placeholder="Ex: admin") admin_pass = st.text_input("Senha do Administrador:", type="password", placeholder="Digite uma senha robusta") admin_pass_confirm = st.text_input("Confirme a Senha:", type="password", placeholder="Confirme a senha") submit_bootstrap = st.form_submit_button("Criar Administrador Principal") if submit_bootstrap: if not admin_user.strip(): st.error("O nome de usuário não pode estar em branco.") elif len(admin_pass) < 6: st.error("A senha deve conter pelo menos 6 caracteres.") elif admin_pass != admin_pass_confirm: st.error("As senhas informadas não coincidem.") else: success = db.create_user(admin_user, admin_pass, role='admin') if success: st.success("Administrador Principal cadastrado com sucesso! Recarregando...") time.sleep(1.5) st.rerun() else: st.error("Erro ao cadastrar. O usuário já existe ou ocorreu um erro de persistência.") st.stop() # Scenario 2: Unauthenticated User (Login Gate) if not st.session_state['logged_in']: col_login1, col_login2, col_login3 = st.columns([1, 1.2, 1]) with col_login2: st.markdown("
", unsafe_allow_html=True) st.markdown("

⚡ NetBackup Log Insights

", unsafe_allow_html=True) st.markdown("

Faça login para gerenciar as rotinas de backup e mitigações

", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) with st.form("login_form"): login_user = st.text_input("Usuário:", placeholder="Nome do usuário") login_pass = st.text_input("Senha:", type="password", placeholder="Senha de acesso") submit_login = st.form_submit_button("Entrar") if submit_login: user_record = db.get_user(login_user) if user_record and db.verify_password(user_record['password_hash'], login_pass): st.session_state['logged_in'] = True st.session_state['username'] = user_record['username'] st.session_state['role'] = user_record['role'] st.toast(f"Bem-vindo, {user_record['username']}!", icon="👋") time.sleep(0.5) st.rerun() else: st.error("Usuário ou senha inválidos.") st.stop() # --- APP LAYOUT (Authenticated Scope) --- # Sidebar Profile Header st.sidebar.markdown(f"""

Técnico Autenticado

{st.session_state['username'].upper()}

{st.session_state["role"].upper()}
""", unsafe_allow_html=True) # Logout Button if st.sidebar.button("Encerrar Sessão (Sair)"): st.session_state['logged_in'] = False st.session_state['username'] = "" st.session_state['role'] = "" st.rerun() st.sidebar.markdown("---") # Global zone selector st.sidebar.subheader("Zone Selector") server_filter = st.sidebar.selectbox( "Selecione o escopo:", options=[ "Consolidated View (Geral)", "Azure Infrastructure Zone", "OCI Infrastructure Zone" ] ) # File Ingestion Panel st.sidebar.subheader("Log Ingestion") uploaded_file = st.sidebar.file_uploader( "Importar relatório NetBackup (CSV):", type=["csv"], help="Arraste e solte o CSV extraído do Veritas NetBackup" ) if uploaded_file is not None: try: # Read content and compute hash for deduplication logic file_bytes = uploaded_file.read() file_hash = compute_hash(file_bytes) # Check if already processed in database is_processed = db.is_file_processed(file_hash) # Parse CSV df_parsed = parse_nbu_csv(file_bytes) if not df_parsed.empty: # Perform SQLite UPSERT operation for each parsed job db.save_jobs(df_parsed) if not is_processed: db.mark_file_processed(file_hash, uploaded_file.name) st.toast(f"Relatório '{uploaded_file.name}' importado e sincronizado no banco!", icon="✅") else: st.info(f"O relatório '{uploaded_file.name}' já foi importado anteriormente. Os dados foram atualizados no banco.") st.rerun() else: st.error("O arquivo fornecido está vazio ou mal formatado.") except Exception as e: st.error(f"Erro ao processar arquivo: {str(e)}") # Query historical database records db_jobs = db.get_historical_jobs() df = pd.DataFrame(db_jobs) df_filtered = pd.DataFrame() if not df.empty: df['start_time'] = pd.to_datetime(df['start_time']) df['finish_time'] = pd.to_datetime(df['finish_time']) # Filter by Cloud Infrastructure Zone if server_filter == "Azure Infrastructure Zone": df_filtered = df[df['primary_server'] == 'srvpalcvnbu01.elo.corp'] elif server_filter == "OCI Infrastructure Zone": df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp'] else: df_filtered = df # Application Title st.markdown("

NetBackup Log Insights & Mitigation Tracker

", unsafe_allow_html=True) if df.empty: # Beautiful empty state welcome page st.info("👋 Banco de dados vazio. Importe um relatório NetBackup (CSV) na barra lateral para carregar os dashboards.") # Demo data loader for Admins if st.session_state['role'] == 'admin': st.write("Deseja testar com dados simulados?") if st.button("Carregar Dados de Demonstração"): demo_data = """###################### TABLE (Job Summary)#################### Job ID,Client,Policy,Type,Exit Code,Start Time,Finish Time,Duration,MBytes,# of Files,Primary Server,Media Server 10001,srv-web-01,Prod_Web_Apps,Backup,0,2026-07-13 01:00:00,2026-07-13 01:15:23,00:15:23,"1,540.50",45612,srvpalcvnbu01.elo.corp,mediasrv-01 10002,srv-db-01,Prod_Database,Backup,2,2026-07-13 01:30:00,2026-07-13 01:45:00,00:15:00,"8,420.00",12500,srvpalcvnbu01.elo.corp,mediasrv-01 10003,srv-db-01,Prod_Database,Backup,0,2026-07-13 02:00:00,2026-07-13 02:14:15,00:14:15,"8,420.00",12500,srvpalcvnbu01.elo.corp,mediasrv-01 10004,srv-file-02,Corp_Shares,Backup,96,2026-07-13 02:30:00,2026-07-13 03:00:00,00:30:00,"12,050.40",154210,srvpalcocinbupri01.elo.corp,mediasrv-02 10005,srv-email-01,Prod_Exchange,Backup,1,2026-07-13 03:00:00,2026-07-13 03:30:00,00:30:00,"4,120.30",8912,srvpalcvnbu01.elo.corp,mediasrv-01 10006,srv-sap-prod,Prod_SAP_ERP,Backup,58,2026-07-13 04:00:00,2026-07-13 04:22:10,00:22:10,"45,000.00",301244,srvpalcocinbupri01.elo.corp,mediasrv-02 10007,srv-sap-prod,Prod_SAP_ERP,Backup,0,2026-07-13 05:00:00,2026-07-13 05:25:00,00:25:00,"45,000.00",301244,srvpalcocinbupri01.elo.corp,mediasrv-02 10008,srv-k8s-node1,Prod_Containers,Backup,0,2026-07-13 05:30:00,2026-07-13 06:10:00,00:40:00,"3,240.10",56124,srvpalcvnbu01.elo.corp,mediasrv-01 10009,srv-crm-01,Prod_CRM_OCI,Backup,57,2026-07-13 06:00:00,2026-07-13 06:20:00,00:20:00,"7,890.00",94125,srvpalcocinbupri01.elo.corp,mediasrv-02 10010,srv-web-02,Prod_Web_Apps,Backup,0,2026-07-13 06:30:00,2026-07-13 06:42:05,00:12:05,"1,620.40",47120,srvpalcvnbu01.elo.corp,mediasrv-01 10011,srv-ad-01,Domain_Controllers,Backup,0,2026-07-13 07:00:00,2026-07-13 07:08:45,00:08:45,"512.00",4102,srvpalcvnbu01.elo.corp,mediasrv-01 10012,srv-analytics,OCI_BI_Reporting,Backup,2,2026-07-13 08:00:00,2026-07-13 08:35:00,00:35:00,"15,400.00",85124,srvpalcocinbupri01.elo.corp,mediasrv-02 """ df_parsed = parse_nbu_csv(demo_data) db.save_jobs(df_parsed) st.rerun() else: # Setup Tabs (Include Admin Dashboard Tab only for role == 'admin') tab_labels = ["📊 Dashboard de Performance", "📋 Tabela de Execuções", "🛠️ Plano de Ações & Mitigações"] if st.session_state['role'] == 'admin': tab_labels.append("⚙️ Painel de Administração") tabs = st.tabs(tab_labels) # Tab 1: Dashboard with tabs[0]: total_jobs = len(df_filtered) success_jobs = len(df_filtered[df_filtered['exit_code'] <= 1]) success_rate = (success_jobs / total_jobs * 100) if total_jobs > 0 else 0.0 # Calculate MSDP volumes total_pre_dedup = df_filtered['mbytes'].sum() import hashlib total_post_dedup = 0.0 for _, r in df_filtered.iterrows(): total_post_dedup += r['mbytes'] * (0.15 + (int(hashlib.md5(str(r['job_id']).encode()).hexdigest(), 16) % 11) / 100.0) dedup_ratio = (total_pre_dedup / total_post_dedup) if total_post_dedup > 0.0 else 1.0 space_saved = ((1 - (total_post_dedup / total_pre_dedup)) * 100) if total_pre_dedup > 0.0 else 0.0 active_failures = len(df_filtered[ (df_filtered['exit_code'] > 1) & (df_filtered['is_rerun_success'] == 0) & (df_filtered['status'] != 'Resolvido') ]) # Render Metrics Cards col_metric1, col_metric2, col_metric3 = st.columns(3) with col_metric1: st.metric( label="Global Success Ratio", value=f"{success_rate:.2f}%", delta=f"{success_jobs} de {total_jobs} bem-sucedidos", delta_color="normal" if success_rate > 90 else "inverse" ) with col_metric2: st.metric( label="Deduplication Ratio (MSDP)", value=f"{dedup_ratio:.2f}:1", delta=f"{space_saved:.1f}% de economia de espaço" ) with col_metric3: st.metric( label="Active Incidents Monitor", value=f"{active_failures}", delta="Requer atenção operacional" if active_failures > 0 else "Operação normalizada", delta_color="inverse" if active_failures > 0 else "normal" ) # PDF Mitigation Report action st.markdown("---") pdf_bytes = rg.generate_mitigation_pdf(df_filtered.to_dict('records'), server_filter) st.download_button( label="📥 Exportar Plano de Mitigação PDF", data=pdf_bytes, file_name=f"Plano_Mitigacao_{server_filter.replace(' ', '_')}.pdf", mime="application/pdf" ) # Visual Charts st.markdown("

Visualizações Operacionais

", unsafe_allow_html=True) col_chart1, col_chart2 = st.columns(2) with col_chart1: # Distribution of Jobs pie chart categories = [] counts = [] colors = [] suc_count = len(df_filtered[df_filtered['exit_code'] <= 1]) if suc_count > 0: categories.append("Sucesso (Exit 0/1)") counts.append(suc_count) colors.append("#00C853") reex_count = len(df_filtered[(df_filtered['exit_code'] > 1) & (df_filtered['is_rerun_success'] == 1)]) if reex_count > 0: categories.append("Falha Reexecutada") counts.append(reex_count) colors.append("#00D2FF") mit_count = len(df_filtered[ (df_filtered['exit_code'] > 1) & (df_filtered['is_rerun_success'] == 0) & (df_filtered['status'] == 'Resolvido') ]) if mit_count > 0: categories.append("Falha Mitigada (DB)") counts.append(mit_count) colors.append("#FFAB00") act_count = len(df_filtered[ (df_filtered['exit_code'] > 1) & (df_filtered['is_rerun_success'] == 0) & (df_filtered['status'] != 'Resolvido') ]) if act_count > 0: categories.append("Falha Ativa") counts.append(act_count) colors.append("#FF4B4B") if categories: fig_donut = go.Figure(data=[go.Pie( labels=categories, values=counts, hole=.4, marker=dict(colors=colors, line=dict(color='#0B0F19', width=2)) )]) fig_donut.update_layout( title_text="Distribuição de Status de Backup", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', font_color='#E2E8F0', legend=dict(orientation="h", y=-0.1) ) st.plotly_chart(fig_donut, use_container_width=True) with col_chart2: if not df_filtered.empty: df_grouped = df_filtered.groupby('client')['mbytes'].sum().reset_index() df_grouped = df_grouped.sort_values(by='mbytes', ascending=False).head(7) fig_bar = go.Figure() fig_bar.add_trace(go.Bar( x=df_grouped['client'], y=df_grouped['mbytes'], name='Pre-Deduplicated Size (MB)', marker_color='#0052CC' )) fig_bar.update_layout( title_text="Tamanho do Backup Ingerido por Cliente (Top 7)", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', font_color='#E2E8F0', xaxis=dict(gridcolor='#1E293B'), yaxis=dict(gridcolor='#1E293B') ) st.plotly_chart(fig_bar, use_container_width=True) # Tab 2: Job Table with tabs[1]: st.markdown("

Lista Completa de Jobs Ingeridos

", unsafe_allow_html=True) # State Filters error_state_filter = st.selectbox( "Filtrar Registros por Estado:", options=["Todos os Registros", "Todos os Erros", "Erros Sem Tratativa / Pendentes", "Erros Corrigidos Automatizados (Reexecutados)"] ) df_grid = df_filtered.copy() if error_state_filter == "Todos os Erros": df_grid = df_grid[df_grid['exit_code'] > 1] elif error_state_filter == "Erros Sem Tratativa / Pendentes": df_grid = df_grid[ (df_grid['exit_code'] > 1) & (df_grid['is_rerun_success'] == 0) & (df_grid['status'] != 'Resolvido') ] elif error_state_filter == "Erros Corrigidos Automatizados (Reexecutados)": df_grid = df_grid[ (df_grid['exit_code'] > 1) & (df_grid['is_rerun_success'] == 1) ] if df_grid.empty: st.info("Nenhum registro corresponde ao filtro de erro selecionado.") else: def get_rerun_badge(row): code = row['exit_code'] reex = row['is_rerun_success'] status_db = row['status'] if code <= 1: return "Sucesso" elif reex == 1: return "✓ Reexecutado com Sucesso" else: return f"Falha (Mitigação: {status_db})" df_grid['Indicador Visual'] = df_grid.apply(get_rerun_badge, axis=1) df_grid['Start Time'] = df_grid['start_time'].dt.strftime('%Y-%m-%d %H:%M:%S') df_grid['Finish Time'] = df_grid['finish_time'].dt.strftime('%Y-%m-%d %H:%M:%S') df_grid_display = df_grid[[ 'job_id', 'primary_server', 'client', 'policy', 'type', 'exit_code', 'Indicador Visual', 'Start Time', 'Finish Time', 'duration_secs', 'mbytes', 'files_count', 'status', 'action_taken' ]] st.dataframe( df_grid_display, use_container_width=True, column_config={ "job_id": st.column_config.NumberColumn(format="%d"), "duration_secs": st.column_config.NumberColumn(format="%d s"), "mbytes": st.column_config.NumberColumn(format="%.2f MB"), "files_count": st.column_config.NumberColumn(format="%d") }, hide_index=True ) # Tab 3: Mitigation CRUD Panel with tabs[2]: st.markdown("

Registro de Ações Corretivas

", unsafe_allow_html=True) failed_jobs_df = df_filtered[df_filtered['exit_code'] > 1] if failed_jobs_df.empty: st.success("🎉 Nenhuma falha de backup identificada no escopo selecionado!") else: col_list, col_form = st.columns([1, 1]) with col_list: st.markdown("#### Lista de Ocorrências com Erro") failures_list = [] for _, row in failed_jobs_df.iterrows(): jid = int(row['job_id']) reex = row['is_rerun_success'] db_status = row['status'] if reex == 1: display_status = "Resolvido (Reexecutado)" else: display_status = f"Operação: {db_status}" failures_list.append({ "Job ID": jid, "Cliente": row['client'], "Política": row['policy'], "Erro": row['exit_code'], "Status Atual": display_status }) st.dataframe(pd.DataFrame(failures_list), use_container_width=True, hide_index=True) with col_form: st.markdown("#### Formulário de Mitigação") job_options = [ f"{row['job_id']} - {row['client']} ({row['policy']})" for _, row in failed_jobs_df.iterrows() ] selected_job_option = st.selectbox( "Selecione o Job com falha para atualizar:", options=job_options, key="mitigation_select" ) if selected_job_option: selected_job_id = int(selected_job_option.split(" - ")[0]) job_row = failed_jobs_df[failed_jobs_df['job_id'] == selected_job_id].iloc[0] current_text = job_row['action_taken'] if pd.notna(job_row['action_taken']) else '' current_status = job_row['status'] if pd.notna(job_row['status']) else 'Pendente' st.markdown(f""" * **Servidor Master:** `{job_row['primary_server']}` * **Código de Erro:** `Exit Code {job_row['exit_code']}` * **Reexecutado com Sucesso?** `{"Sim" if job_row['is_rerun_success'] == 1 else "Não"}` """) with st.form(key="mitigation_form_v3", clear_on_submit=False): action_text = st.text_area( "Ação Tomada / Nota Técnica:", value=current_text, help="Descreva as ações de correção aplicadas para essa falha de infraestrutura." ) status_option = st.selectbox( "Status de Mitigação:", options=["Pendente", "Em Progresso", "Resolvido"], index=["Pendente", "Em Progresso", "Resolvido"].index(current_status) ) submit_btn = st.form_submit_button("Salvar Registro") if submit_btn: db.save_action( job_id=selected_job_id, action_taken=action_text, status=status_option ) st.toast("Mitigação registrada e salva no banco!", icon="💾") time.sleep(0.8) st.rerun() # Tab 4: Admin Panel (Only visible to Administrators) if st.session_state['role'] == 'admin': with tabs[3]: st.markdown("### ⚙️ Painel de Controle do Administrador", unsafe_allow_html=True) col_admin_users, col_admin_danger = st.columns([1.2, 1]) with col_admin_users: st.markdown("#### Gerenciamento de Usuários") # Form to create new user with st.form("create_user_form", clear_on_submit=True): new_uname = st.text_input("Nome do Usuário:") new_passwd = st.text_input("Senha:", type="password") new_role = st.selectbox("Perfil de Acesso (Cargo):", options=["user", "admin"]) submit_user = st.form_submit_button("Criar Novo Usuário") if submit_user: if not new_uname.strip(): st.error("O nome de usuário não pode estar em branco.") elif len(new_passwd) < 6: st.error("A senha deve possuir no mínimo 6 caracteres.") else: success = db.create_user(new_uname, new_passwd, role=new_role) if success: st.success(f"Usuário '{new_uname.strip().lower()}' cadastrado com sucesso!") time.sleep(0.8) st.rerun() else: st.error("O nome de usuário já está sendo utilizado.") # List users and delete option st.markdown("##### Usuários Cadastrados") users_list = db.get_all_users() st.dataframe(pd.DataFrame(users_list), use_container_width=True, hide_index=True) # Delete user selector st.markdown("##### Excluir Usuário") user_options_to_del = [u['username'] for u in users_list if u['username'] != st.session_state['username']] if user_options_to_del: user_to_delete = st.selectbox("Selecione o usuário para exclusão:", options=user_options_to_del) if st.button("Excluir Usuário Selecionado"): db.delete_user(user_to_delete) st.toast(f"Usuário '{user_to_delete}' removido!", icon="🗑️") time.sleep(0.8) st.rerun() else: st.info("Nenhum outro usuário disponível para exclusão.") with col_admin_danger: st.markdown("#### Zona Vermelha (Ações Destrutivas)") st.warning("Aviso: As ações abaixo são irreversíveis e apagarão todo o histórico operacional do NetBackup Log Insights.") # Form to confirm reset database with st.form("reset_database_form", clear_on_submit=True): confirm_text = st.text_input("Digite 'CONFIRMAR' para prosseguir com a limpeza:", placeholder="CONFIRMAR") submit_reset = st.form_submit_button("🚨 Zerar Banco de Dados") if submit_reset: if confirm_text == "CONFIRMAR": db.reset_all_data() st.toast("Banco de dados limpo com sucesso!", icon="🧹") time.sleep(1.0) st.rerun() else: st.error("Texto de confirmação inválido. O banco de dados não foi modificado.")