Files
netbackup-insights/app.py
T

1008 lines
46 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 json
STATUS_CODES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "nbu_status_codes.json")
PDF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "NBU_StatusCode.pdf")
def compile_status_codes_if_needed():
"""
Check if nbu_status_codes.json exists. If not, parse NBU_StatusCode.pdf
using pypdf to generate it.
"""
if os.path.exists(STATUS_CODES_FILE):
return
if not os.path.exists(PDF_FILE):
return
try:
import pypdf
import re
reader = pypdf.PdfReader(PDF_FILE)
full_text_list = []
for page in reader.pages:
text = page.extract_text()
if text:
full_text_list.append(text)
full_text = "\n".join(full_text_list)
# Parse status codes
pattern = re.compile(r'NetBackup\s*status\s*code\s*:\s*(\d+)', re.IGNORECASE)
matches = list(pattern.finditer(full_text))
parsed = {}
bullets = ['', '-', '*', '']
for idx, match in enumerate(matches):
code_str = match.group(1)
code_num = int(code_str)
start_pos = match.start()
end_pos = matches[idx + 1].start() if idx + 1 < len(matches) else len(full_text)
chunk = full_text[start_pos:end_pos]
msg_match = re.search(r'Message\s*:\s*(.*)', chunk, re.IGNORECASE)
if msg_match:
expl_match = re.search(r'Explanation\s*:\s*', chunk, re.IGNORECASE)
desc = ""
msg_header_match = re.search(r'Message\s*:\s*', chunk, re.IGNORECASE)
msg_start = msg_header_match.end()
if expl_match:
desc = chunk[msg_start:expl_match.start()].strip()
else:
desc = chunk[msg_start:].strip()
desc = re.sub(r'\s+', ' ', desc).strip()
rec_match = re.search(r'Recommended\s*Action\s*:\s*', chunk, re.IGNORECASE)
action_full = ""
if rec_match:
action_start = rec_match.end()
click_match = re.search(r'Click\s*here\s*to\s*view\s*technical\s*notes', chunk[action_start:], re.IGNORECASE)
if click_match:
action_end = action_start + click_match.start()
else:
action_end = len(chunk)
action_full = chunk[action_start:action_end].strip()
else:
action_full = "No specific recommended action found in the manual."
action_full = re.sub(r'\d+\s*NetBackup\s*status\s*codes', '', action_full, flags=re.IGNORECASE)
action_full = re.sub(r'NetBackup\s*status\s*codes', '', action_full, flags=re.IGNORECASE)
lines = [line.strip() for line in action_full.splitlines() if line.strip()]
action_clean = "\n".join(lines)
# Get first action
first_action = "No specific recommended action found in the manual."
if lines:
first_bullet = None
for line in lines:
if any(line.startswith(b) for b in bullets) or re.match(r'^\d+\.', line):
cleaned_line = line
for b in bullets:
if cleaned_line.startswith(b):
cleaned_line = cleaned_line[len(b):].strip()
break
first_bullet = cleaned_line
break
if first_bullet:
first_action = first_bullet
else:
for line in lines:
if line.endswith(':') and len(line) < 40:
continue
first_action = line
break
parsed[code_num] = {
"code": code_num,
"desc": desc,
"first_action": first_action,
"full_action": action_clean
}
with open(STATUS_CODES_FILE, "w", encoding="utf-8") as f:
json.dump({str(k): v for k, v in parsed.items()}, f, indent=4, ensure_ascii=False)
except Exception as e:
print(f"Error compiling status codes: {e}")
# Compile on load if needed
compile_status_codes_if_needed()
# Load compiled database
NBU_STATUS_CODES = {}
if os.path.exists(STATUS_CODES_FILE):
try:
with open(STATUS_CODES_FILE, "r", encoding="utf-8") as f:
NBU_STATUS_CODES = json.load(f)
except Exception as e:
print(f"Error loading status codes: {e}")
def get_status_code_info(code):
"""
Resolves troubleshooting steps from the offline database compiled from the PDF.
If the code is one of the common codes, it combines local Portuguese knowledge with the PDF.
"""
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
code_str = str(code_int)
pdf_desc = ""
pdf_first_action = ""
pdf_full_action = ""
if code_str in NBU_STATUS_CODES:
info = NBU_STATUS_CODES[code_str]
pdf_desc = info.get("desc", "")
pdf_first_action = info.get("first_action", "")
pdf_full_action = info.get("full_action", "")
desc = ""
action = ""
if code_int in local_dict:
if pdf_desc:
desc = f"{local_dict[code_int]['desc']} (PDF: {pdf_desc})"
else:
desc = local_dict[code_int]['desc']
local_act = local_dict[code_int]['action']
if pdf_first_action:
action = f"{local_act}\n\n💡 [Manual PDF - Primeira Ação Recomendada]:\n{pdf_first_action}"
else:
action = local_act
else:
if pdf_desc:
desc = f"PDF Description: {pdf_desc}"
else:
desc = f"Código de status {code_int} do NetBackup"
if pdf_first_action:
action = f"Ação Recomendada (Manual PDF - Primeira Ação):\n{pdf_first_action}"
if pdf_full_action:
action += f"\n\nOutras ações descritas no manual:\n{pdf_full_action}"
else:
action = f"Ação Recomendada:\nInvestigue os logs do NetBackup (Activity Monitor) para este código de erro."
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'])
# Calculate min and max dates in database
min_date = df['start_time'].min().date()
max_date = df['start_time'].max().date()
# Date Range Selector in Sidebar
st.sidebar.markdown("---")
st.sidebar.subheader("Filtro de Período")
selected_dates = st.sidebar.date_input(
"Selecione o período de análise:",
value=(min_date, max_date),
min_value=min_date,
max_value=max_date,
help="Filtre os dados do dashboard e tabelas para o período selecionado."
)
# Filter by Cloud Infrastructure Zone
if server_filter == "Azure Infrastructure Zone":
df_filtered = df[df['primary_server'] == 'srvpalcvnbu01.elo.corp'].copy()
elif server_filter == "OCI Infrastructure Zone":
df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp'].copy()
else:
df_filtered = df.copy()
# Apply date filter range
if isinstance(selected_dates, tuple) and len(selected_dates) == 2:
start_date, end_date = selected_dates
df_filtered = df_filtered[
(df_filtered['start_time'].dt.date >= start_date) &
(df_filtered['start_time'].dt.date <= end_date)
]
elif isinstance(selected_dates, tuple) and len(selected_dates) == 1:
start_date = selected_dates[0]
df_filtered = df_filtered[
df_filtered['start_time'].dt.date >= start_date
]
# 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")
# Daily Jobs Timeline Chart
if not df_filtered.empty:
st.markdown("---")
df_line = df_filtered.copy()
df_line['day'] = df_line['start_time'].dt.date
df_daily_counts = df_line.groupby('day').size().reset_index(name='job_count')
df_daily_counts = df_daily_counts.sort_values(by='day').tail(30)
fig_line = go.Figure()
fig_line.add_trace(go.Scatter(
x=df_daily_counts['day'],
y=df_daily_counts['job_count'],
mode='lines+markers',
name='Jobs Executados',
line=dict(color='#00D2FF', width=3), # Vibrant Cyan
marker=dict(size=8, color='#0052CC', symbol='circle')
))
fig_line.update_layout(
title_text="Quantidade de Jobs Executados por Dia (Limitar a 30 dias)",
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
font_color='#F1F5F9',
xaxis=dict(
gridcolor='#1E293B',
title="Data de Execução",
type='category'
),
yaxis=dict(gridcolor='#1E293B', title="Total de Jobs"),
margin=dict(l=40, r=40, t=50, b=40)
)
st.plotly_chart(fig_line, use_container_width=True)
# 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)
show_all_failures = st.checkbox(
"Mostrar falhas de todo o histórico (ignorar filtro de data)",
value=True,
help="Ative para visualizar e trabalhar em todas as falhas ativas pendentes de mitigação, ignorando o filtro de período da barra lateral."
)
if show_all_failures:
if server_filter == "Azure Infrastructure Zone":
failed_jobs_df = df[(df['exit_code'] > 1) & (df['primary_server'] == 'srvpalcvnbu01.elo.corp')].copy()
elif server_filter == "OCI Infrastructure Zone":
failed_jobs_df = df[(df['exit_code'] > 1) & (df['primary_server'] == 'srvpalcocinbupri01.elo.corp')].copy()
else:
failed_jobs_df = df[df['exit_code'] > 1].copy()
else:
failed_jobs_df = df_filtered[df_filtered['exit_code'] > 1].copy()
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("Consultando base de conhecimento local do NetBackup..."):
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.")