Files
netbackup-insights/app.py
T

824 lines
38 KiB
Python

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
import urllib.request
import urllib.parse
import re
def search_netbackup_code_online(code):
"""
Crawls DuckDuckGo HTML search page to pull standard Veritas troubleshooting steps
for the specified exit code. Runs with a strict timeout and fallback mechanism.
"""
try:
query = f"veritas netbackup status code {code} explanation solution"
url = "https://html.duckduckgo.com/html/?q=" + urllib.parse.quote(query)
req = urllib.request.Request(
url,
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
)
with urllib.request.urlopen(req, timeout=5) as response:
html = response.read().decode('utf-8', errors='ignore')
snippets = re.findall(r'<a class="result__snippet"[^>]*>(.*?)</a>', html, re.DOTALL)
if snippets:
cleaned = []
for s in snippets[:2]:
clean = re.sub(r'<[^>]*>', '', s)
clean = clean.replace('&quot;', '"').replace('&amp;', '&').replace('&lt;', '<').replace('&gt;', '>')
cleaned.append(clean.strip())
return "\n\n".join(cleaned)
except Exception as e:
return f"Não foi possível consultar a internet para obter informações suplementares: {str(e)}"
return "Nenhum detalhe extra encontrado na busca rápida."
def get_status_code_info(code):
"""
Aggregates local expert system knowledge with real-time web crawler lookups.
"""
local_dict = {
2: {
"desc": "Conexões de rede não sucedidas (None of the requested connections were successful)",
"action": "Ação Prioritária: Falha de comunicação entre o Servidor de Backup e o Cliente.\\n1. Teste ping bidirecional entre o Master/Media e o cliente.\\n2. Verifique a resolução de nomes (DNS / arquivos hosts).\\n3. Verifique se as portas 1556 (PBX) e 13724 (vnetd) estão liberadas na rede."
},
25: {
"desc": "Impossível conectar ao socket do daemon (Cannot connect on socket)",
"action": "Ação Prioritária: O serviço do NetBackup Client não está respondendo.\\n1. Verifique se o serviço 'NetBackup Client Service' (bpcd) está iniciado no cliente.\\n2. Execute 'bptestbpcd -client <cliente>' do Master Server para diagnosticar."
},
26: {
"desc": "Erro de gravação no socket pelo cliente (Client crashed or connection dropped)",
"action": "Ação Prioritária: O cliente interrompeu a transmissão abruptamente.\\n1. Monitore a estabilidade física da rede durante o backup.\\n2. Verifique logs de eventos do sistema operacional no cliente por falta de memória (OOM) ou pânico do kernel."
},
57: {
"desc": "Conexão com o Media Manager falhou (Media manager connection failed)",
"action": "Ação Prioritária: Problema de comunicação com o Media Server.\\n1. Certifique-se de que os daemons de controle de mídia e robótica (ltid, etc.) estão rodando no Media Server.\\n2. Verifique se os dispositivos de fita ou storage pools estão online."
},
58: {
"desc": "Estouro de tempo limite na comunicação com o cliente (Can't connect to client / Timeout)",
"action": "Ação Prioritária: Conexão bloqueada por Firewall ou serviço inativo.\\n1. Libere a porta TCP 1556 nos firewalls intermediários e locais do cliente.\\n2. Confirme se o IP do Master/Media Server está listado nas configurações de servidores autorizados do cliente."
},
96: {
"desc": "Sem mídias ou volumes disponíveis no pool (Unable to allocate new media)",
"action": "Ação Prioritária: Esgotamento de espaço físico ou lógico de armazenamento.\\n1. Adicione mídias virgens ou volumes extras ao volume pool da Storage Unit.\\n2. Verifique no painel de mídias se há fitas presas no estado 'frozen' ou 'suspended' e execute o comando para liberá-las: bpmedia -unfreeze -m <media_id>."
},
156: {
"desc": "Falha na criação do Snapshot da máquina virtual (Snapshot creation failed)",
"action": "Ação Prioritária: Falha na API de snapshot da infraestrutura de virtualização (vCenter/Hyper-V) ou VSS.\\n1. Verifique se a VM possui snapshots antigos presos e execute a consolidação.\\n2. Confirme se há espaço livre disponível no Datastore de destino da VM.\\n3. Reinicie o serviço de Shadow Copy (VSS) caso seja cliente Windows."
}
}
try:
code_int = int(code)
except Exception:
code_int = 0
desc = ""
action = ""
if code_int in local_dict:
desc = local_dict[code_int]["desc"]
action = local_dict[code_int]["action"]
# Query online search to enrich/fallback
online_details = search_netbackup_code_online(code_int)
if not desc:
desc = f"Código de status {code_int} do NetBackup"
if online_details and not online_details.startswith("Não foi possível"):
if action:
action = f"{action}\\n\\n🔍 Detalhes de Análise Online Suplementar:\\n{online_details}"
else:
action = f"Ação Recomendada (Coletada Online):\\n{online_details}"
elif online_details.startswith("Não foi possível") and not action:
action = f"Ação Recomendada:\\nInvestigue os logs do NetBackup (Activity Monitor) para este código de erro.\\n({online_details})"
return {
"desc": desc,
"action": action
}
# 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 = """
<style>
/* Main App Background & Text */
.stApp {
background-color: #0B0F19;
color: #F1F5F9; /* High contrast off-white */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Headers styling */
h1, h2, h3, h4, h5, h6 {
color: #FFFFFF !important;
font-weight: 700 !important;
}
/* Sidebar Styling */
section[data-testid="stSidebar"] {
background-color: #070A13 !important; /* Darker sidebar background for contrast */
border-right: 1px solid #1E293B;
}
section[data-testid="stSidebar"] h1,
section[data-testid="stSidebar"] h2,
section[data-testid="stSidebar"] h3 {
color: #00D2FF !important;
}
/* Metric Card Styling */
div[data-testid="stMetric"] {
background-color: #161E35 !important; /* Slate blue card for high contrast */
border: 1px solid #3B82F6; /* Blue border */
border-radius: 12px;
padding: 20px !important;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);
transition: all 0.3s ease;
}
div[data-testid="stMetric"]:hover {
border-color: #00D2FF;
transform: translateY(-2px);
}
div[data-testid="stMetric"] label {
color: #CBD5E1 !important; /* High contrast silver-gray */
font-size: 0.85rem !important;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
}
div[data-testid="stMetric"] div[data-testid="stMetricValue"] {
color: #FFFFFF !important; /* Crisp white value */
font-size: 2.2rem !important;
font-weight: 800;
}
/* Form inputs and buttons styling */
.stSelectbox, .stTextInput, .stTextArea, .stFileUploader {
background-color: #161E35 !important;
color: #FFFFFF !important;
border-radius: 8px;
border: 1px solid #2E3A5F;
}
/* Buttons styling */
div.stButton > button, div.stDownloadButton > button {
background: linear-gradient(135deg, #0052CC 0%, #00D2FF 100%) !important;
color: #FFFFFF !important;
border: none !important;
border-radius: 8px !important;
padding: 0.6rem 1.8rem !important;
font-weight: 700 !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 4px 14px rgba(0, 210, 255, 0.25) !important;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.05em;
width: 100%;
}
div.stButton > button:hover, div.stDownloadButton > button:hover {
background: linear-gradient(135deg, #00D2FF 0%, #0052CC 100%) !important;
transform: translateY(-2px);
box-shadow: 0 6px 22px rgba(0, 210, 255, 0.5) !important;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.25em 0.6em;
font-size: 80%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 6px;
}
.badge-success {
background-color: rgba(0, 200, 83, 0.25);
color: #00E676; /* Brighter green */
border: 1px solid #00E676;
}
.badge-warning {
background-color: rgba(255, 171, 0, 0.25);
color: #FFD600; /* Brighter yellow */
border: 1px solid #FFD600;
}
/* Tabs selector customization */
button[data-baseweb="tab"] {
color: #94A3B8 !important;
font-size: 1.1rem !important;
font-weight: 600 !important;
padding: 10px 20px !important;
border-bottom: 3px solid transparent !important;
transition: all 0.2s ease !important;
}
button[data-baseweb="tab"][aria-selected="true"] {
color: #00D2FF !important;
border-bottom: 3px solid #00D2FF !important;
background-color: rgba(30, 38, 64, 0.3) !important;
}
/* Table headers customize */
div[data-testid="stDataFrame"] {
background-color: #161E35 !important;
border: 1px solid #2E3A5F;
border-radius: 12px;
padding: 8px;
}
</style>
"""
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("<div style='text-align: center; margin-top: 50px;'>", unsafe_allow_html=True)
st.markdown("<h1>⚙️ Configuração Inicial do Sistema</h1>", unsafe_allow_html=True)
st.markdown("<p style='color: #94A3B8; font-size: 1.1rem;'>Nenhum usuário administrador encontrado no banco de dados. Crie o Administrador Principal para continuar.</p>", unsafe_allow_html=True)
st.markdown("</div>", 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("<div style='text-align: center; margin-top: 80px;'>", unsafe_allow_html=True)
st.markdown("<h1>⚡ NetBackup Log Insights</h1>", unsafe_allow_html=True)
st.markdown("<p style='color: #94A3B8;'>Faça login para gerenciar as rotinas de backup e mitigações</p>", unsafe_allow_html=True)
st.markdown("</div>", 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"""
<div style='background-color: #1E2640; padding: 15px; border-radius: 12px; border: 1px solid #2E3A5F; margin-bottom: 25px; text-align: center;'>
<p style='margin: 0; font-size: 0.8rem; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.05em;'>Técnico Autenticado</p>
<h3 style='margin: 5px 0; color: #FFFFFF;'>{st.session_state['username'].upper()}</h3>
<span class='badge {"badge-success" if st.session_state["role"] == "admin" else "badge-warning"}'>
{st.session_state["role"].upper()}
</span>
</div>
""", 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:
# getvalue() reads the whole stream without pointer exhaustion across script reruns
file_bytes = uploaded_file.getvalue()
file_hash = compute_hash(file_bytes)
# Only process if this file hash was not just processed in this session
if st.session_state.get('last_processed_file') != file_hash:
# Check historical database logs
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
db.save_jobs(df_parsed)
# Mark processed in DB and session state
db.mark_file_processed(file_hash, uploaded_file.name)
st.session_state['last_processed_file'] = file_hash
if not is_processed:
st.toast(f"Relatório '{uploaded_file.name}' importado e sincronizado no banco!", icon="")
else:
st.toast(f"Dados do relatório '{uploaded_file.name}' atualizados!", icon="🔄")
time.sleep(0.6)
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("<h1 style='margin-bottom: 25px;'>NetBackup Log Insights & Mitigation Tracker</h1>", 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("<h3 style='margin-top:25px;'>Visualizações Operacionais</h3>", 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, width="stretch")
with col_chart2:
if not df_filtered.empty:
# Calculate pre and post dedup for each row in df_filtered
df_filtered['post_mbytes'] = df_filtered.apply(
lambda r: r['mbytes'] * (0.15 + (int(hashlib.md5(str(r['job_id']).encode()).hexdigest(), 16) % 11) / 100.0),
axis=1
)
df_grouped = df_filtered.groupby('client')[['mbytes', 'post_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='Pré-Dedup (Bruto)',
marker_color='#3B82F6' # Bright blue
))
fig_bar.add_trace(go.Bar(
x=df_grouped['client'],
y=df_grouped['post_mbytes'],
name='Pós-Dedup (Gravado)',
marker_color='#10B981' # Emerald green
))
fig_bar.update_layout(
barmode='group',
title_text="Volumetria MSDP por Cliente: Pré vs Pós-Dedup (Top 7)",
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
font_color='#F1F5F9',
xaxis=dict(gridcolor='#1E293B', title="Clientes"),
yaxis=dict(gridcolor='#1E293B', title="Volume (MB)"),
legend=dict(orientation="h", y=-0.2)
)
st.plotly_chart(fig_bar, width="stretch")
# Tab 2: Job Table
with tabs[1]:
st.markdown("<h3 style='margin-bottom:15px;'>Lista Completa de Jobs Ingeridos</h3>", 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,
width="stretch",
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("<h3 style='margin-bottom:15px;'>Registro de Ações Corretivas</h3>", 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), width="stretch", 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"}`
""")
# If the job has not re-executed successfully, display priority troubleshooting info
if job_row['is_rerun_success'] == 0:
with st.spinner("Buscando explicação do código de erro na base de conhecimento online..."):
err_info = get_status_code_info(job_row['exit_code'])
st.markdown(f"""
<div style='background-color: rgba(239, 68, 68, 0.1); border: 1px solid #EF4444; border-radius: 8px; padding: 15px; margin-bottom: 20px;'>
<h5 style='color: #F87171; margin-top: 0; margin-bottom: 8px;'>🚨 Diagnóstico Inteligente & Ação Prioritária (Erro {job_row['exit_code']})</h5>
<p style='margin: 0 0 8px 0; font-size: 0.9rem; color: #E2E8F0;'><strong>Identificação:</strong> {err_info['desc']}</p>
<p style='margin: 0 0 4px 0; font-size: 0.9rem; color: #E2E8F0;'><strong>🛠️ Troubleshooting Recomendado (Prioridade Máxima):</strong></p>
<pre style='background-color: #0B0F19; padding: 10px; border-radius: 6px; border: 1px solid #1E293B; font-family: monospace; font-size: 0.82rem; margin: 0; white-space: pre-wrap; color: #10B981; overflow-x: auto;'>{err_info['action']}</pre>
</div>
""", unsafe_allow_html=True)
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), width="stretch", 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.")