1008 lines
46 KiB
Python
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.")
|