694 lines
30 KiB
Python
694 lines
30 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
|
|
|
|
# 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: #E2E8F0;
|
|
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: #0F172A !important;
|
|
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: #1E2640 !important;
|
|
border: 1px solid #2E3A5F;
|
|
border-radius: 12px;
|
|
padding: 20px !important;
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
|
transition: all 0.3s ease;
|
|
}
|
|
div[data-testid="stMetric"]:hover {
|
|
border-color: #00D2FF;
|
|
transform: translateY(-2px);
|
|
}
|
|
div[data-testid="stMetric"] label {
|
|
color: #94A3B8 !important;
|
|
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;
|
|
font-size: 2.2rem !important;
|
|
font-weight: 800;
|
|
}
|
|
|
|
/* Form inputs and buttons styling */
|
|
.stSelectbox, .stTextInput, .stTextArea, .stFileUploader {
|
|
background-color: #1E2640 !important;
|
|
color: #FFFFFF !important;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
/* 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.2);
|
|
color: #00C853;
|
|
border: 1px solid #00C853;
|
|
}
|
|
.badge-warning {
|
|
background-color: rgba(255, 171, 0, 0.2);
|
|
color: #FFAB00;
|
|
border: 1px solid #FFAB00;
|
|
}
|
|
|
|
/* 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.2) !important;
|
|
}
|
|
|
|
/* Table headers customize */
|
|
div[data-testid="stDataFrame"] {
|
|
background-color: #1E2640 !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:
|
|
# 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("<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, 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("<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,
|
|
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("<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), 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.")
|