feat: add offline PDF status code lookup, date range filter, and daily jobs timeline line chart
This commit is contained in:
Binary file not shown.
@@ -8,69 +8,159 @@ import report_gen as rg
|
|||||||
import time
|
import time
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import urllib.request
|
import json
|
||||||
import urllib.parse
|
|
||||||
import re
|
|
||||||
|
|
||||||
def search_netbackup_code_online(code):
|
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():
|
||||||
"""
|
"""
|
||||||
Crawls DuckDuckGo HTML search page to pull standard Veritas troubleshooting steps
|
Check if nbu_status_codes.json exists. If not, parse NBU_StatusCode.pdf
|
||||||
for the specified exit code. Runs with a strict timeout and fallback mechanism.
|
using pypdf to generate it.
|
||||||
"""
|
"""
|
||||||
|
if os.path.exists(STATUS_CODES_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.exists(PDF_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = f"veritas netbackup status code {code} explanation solution"
|
import pypdf
|
||||||
url = "https://html.duckduckgo.com/html/?q=" + urllib.parse.quote(query)
|
import re
|
||||||
req = urllib.request.Request(
|
reader = pypdf.PdfReader(PDF_FILE)
|
||||||
url,
|
full_text_list = []
|
||||||
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
|
for page in reader.pages:
|
||||||
)
|
text = page.extract_text()
|
||||||
with urllib.request.urlopen(req, timeout=5) as response:
|
if text:
|
||||||
html = response.read().decode('utf-8', errors='ignore')
|
full_text_list.append(text)
|
||||||
|
full_text = "\n".join(full_text_list)
|
||||||
|
|
||||||
snippets = re.findall(r'<a class="result__snippet"[^>]*>(.*?)</a>', html, re.DOTALL)
|
# Parse status codes
|
||||||
if snippets:
|
pattern = re.compile(r'NetBackup\s*status\s*code\s*:\s*(\d+)', re.IGNORECASE)
|
||||||
cleaned = []
|
matches = list(pattern.finditer(full_text))
|
||||||
for s in snippets[:2]:
|
|
||||||
clean = re.sub(r'<[^>]*>', '', s)
|
parsed = {}
|
||||||
clean = clean.replace('"', '"').replace('&', '&').replace('<', '<').replace('>', '>')
|
bullets = ['■', '-', '*', '•']
|
||||||
cleaned.append(clean.strip())
|
|
||||||
return "\n\n".join(cleaned)
|
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:
|
except Exception as e:
|
||||||
return f"Não foi possível consultar a internet para obter informações suplementares: {str(e)}"
|
print(f"Error compiling status codes: {e}")
|
||||||
return "Nenhum detalhe extra encontrado na busca rápida."
|
|
||||||
|
# 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):
|
def get_status_code_info(code):
|
||||||
"""
|
"""
|
||||||
Aggregates local expert system knowledge with real-time web crawler lookups.
|
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 = {
|
local_dict = {
|
||||||
2: {
|
2: {
|
||||||
"desc": "Conexões de rede não sucedidas (None of the requested connections were successful)",
|
"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."
|
"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: {
|
25: {
|
||||||
"desc": "Impossível conectar ao socket do daemon (Cannot connect on socket)",
|
"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."
|
"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: {
|
26: {
|
||||||
"desc": "Erro de gravação no socket pelo cliente (Client crashed or connection dropped)",
|
"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."
|
"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: {
|
57: {
|
||||||
"desc": "Conexão com o Media Manager falhou (Media manager connection failed)",
|
"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."
|
"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: {
|
58: {
|
||||||
"desc": "Estouro de tempo limite na comunicação com o cliente (Can't connect to client / Timeout)",
|
"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."
|
"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: {
|
96: {
|
||||||
"desc": "Sem mídias ou volumes disponíveis no pool (Unable to allocate new media)",
|
"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>."
|
"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: {
|
156: {
|
||||||
"desc": "Falha na criação do Snapshot da máquina virtual (Snapshot creation failed)",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,26 +169,44 @@ def get_status_code_info(code):
|
|||||||
except Exception:
|
except Exception:
|
||||||
code_int = 0
|
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 = ""
|
desc = ""
|
||||||
action = ""
|
action = ""
|
||||||
|
|
||||||
if code_int in local_dict:
|
if code_int in local_dict:
|
||||||
desc = local_dict[code_int]["desc"]
|
if pdf_desc:
|
||||||
action = local_dict[code_int]["action"]
|
desc = f"{local_dict[code_int]['desc']} (PDF: {pdf_desc})"
|
||||||
|
|
||||||
# Query online search to enrich/fallback
|
|
||||||
online_details = search_netbackup_code_online(code_int)
|
|
||||||
|
|
||||||
if not desc:
|
|
||||||
desc = f"Código de status {code_int} do NetBackup"
|
|
||||||
|
|
||||||
if online_details and not online_details.startswith("Não foi possível"):
|
|
||||||
if action:
|
|
||||||
action = f"{action}\\n\\n🔍 Detalhes de Análise Online Suplementar:\\n{online_details}"
|
|
||||||
else:
|
else:
|
||||||
action = f"Ação Recomendada (Coletada Online):\\n{online_details}"
|
desc = local_dict[code_int]['desc']
|
||||||
elif online_details.startswith("Não foi possível") and not action:
|
|
||||||
action = f"Ação Recomendada:\\nInvestigue os logs do NetBackup (Activity Monitor) para este código de erro.\\n({online_details})"
|
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 {
|
return {
|
||||||
"desc": desc,
|
"desc": desc,
|
||||||
@@ -400,13 +508,42 @@ if not df.empty:
|
|||||||
df['start_time'] = pd.to_datetime(df['start_time'])
|
df['start_time'] = pd.to_datetime(df['start_time'])
|
||||||
df['finish_time'] = pd.to_datetime(df['finish_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
|
# Filter by Cloud Infrastructure Zone
|
||||||
if server_filter == "Azure Infrastructure Zone":
|
if server_filter == "Azure Infrastructure Zone":
|
||||||
df_filtered = df[df['primary_server'] == 'srvpalcvnbu01.elo.corp']
|
df_filtered = df[df['primary_server'] == 'srvpalcvnbu01.elo.corp'].copy()
|
||||||
elif server_filter == "OCI Infrastructure Zone":
|
elif server_filter == "OCI Infrastructure Zone":
|
||||||
df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp']
|
df_filtered = df[df['primary_server'] == 'srvpalcocinbupri01.elo.corp'].copy()
|
||||||
else:
|
else:
|
||||||
df_filtered = df
|
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
|
# Application Title
|
||||||
st.markdown("<h1 style='margin-bottom: 25px;'>NetBackup Log Insights & Mitigation Tracker</h1>", unsafe_allow_html=True)
|
st.markdown("<h1 style='margin-bottom: 25px;'>NetBackup Log Insights & Mitigation Tracker</h1>", unsafe_allow_html=True)
|
||||||
@@ -594,6 +731,39 @@ else:
|
|||||||
)
|
)
|
||||||
st.plotly_chart(fig_bar, width="stretch")
|
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
|
# Tab 2: Job Table
|
||||||
with tabs[1]:
|
with tabs[1]:
|
||||||
st.markdown("<h3 style='margin-bottom:15px;'>Lista Completa de Jobs Ingeridos</h3>", unsafe_allow_html=True)
|
st.markdown("<h3 style='margin-bottom:15px;'>Lista Completa de Jobs Ingeridos</h3>", unsafe_allow_html=True)
|
||||||
@@ -659,7 +829,21 @@ else:
|
|||||||
with tabs[2]:
|
with tabs[2]:
|
||||||
st.markdown("<h3 style='margin-bottom:15px;'>Registro de Ações Corretivas</h3>", unsafe_allow_html=True)
|
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]
|
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:
|
if failed_jobs_df.empty:
|
||||||
st.success("🎉 Nenhuma falha de backup identificada no escopo selecionado!")
|
st.success("🎉 Nenhuma falha de backup identificada no escopo selecionado!")
|
||||||
@@ -718,7 +902,7 @@ else:
|
|||||||
|
|
||||||
# If the job has not re-executed successfully, display priority troubleshooting info
|
# If the job has not re-executed successfully, display priority troubleshooting info
|
||||||
if job_row['is_rerun_success'] == 0:
|
if job_row['is_rerun_success'] == 0:
|
||||||
with st.spinner("Buscando explicação do código de erro na base de conhecimento online..."):
|
with st.spinner("Consultando base de conhecimento local do NetBackup..."):
|
||||||
err_info = get_status_code_info(job_row['exit_code'])
|
err_info = get_status_code_info(job_row['exit_code'])
|
||||||
|
|
||||||
st.markdown(f"""
|
st.markdown(f"""
|
||||||
|
|||||||
+18278
File diff suppressed because one or more lines are too long
@@ -2,3 +2,4 @@ streamlit
|
|||||||
pandas
|
pandas
|
||||||
plotly
|
plotly
|
||||||
fpdf2
|
fpdf2
|
||||||
|
pypdf
|
||||||
|
|||||||
@@ -119,6 +119,19 @@ netbackup-insights/
|
|||||||
* The PDF report table must contain a dedicated **Infra/Cloud** column.
|
* The PDF report table must contain a dedicated **Infra/Cloud** column.
|
||||||
* **Rules:** If `Primary Server` contains `srvpalcvnbu01`, classify as `Azure`. If it contains `srvpalcocinbupri01`, classify as `OCI`. Otherwise, label as `Outro`.
|
* **Rules:** If `Primary Server` contains `srvpalcvnbu01`, classify as `Azure`. If it contains `srvpalcocinbupri01`, classify as `OCI`. Otherwise, label as `Outro`.
|
||||||
|
|
||||||
### 6.4 Live Internet Troubleshooting & Diagnostic Engine
|
### 6.4 Offline PDF Troubleshooting & Diagnostic Engine (v2.2 Shift)
|
||||||
* For failed jobs that are not cleared by re-runs (`is_rerun_success == 0`), the UI must resolve troubleshooting steps dynamically.
|
* For failed jobs that are not cleared by re-runs (`is_rerun_success == 0`), the UI must resolve troubleshooting steps dynamically.
|
||||||
* **Mechanism:** Fall back to a local database dictionary (for status codes 2, 25, 26, 57, 58, 96, 156), and perform a real-time HTTP search query against `html.duckduckgo.com` to fetch supplemental resolution notes online.
|
* **Mechanism:** Query a compiled local JSON database (`nbu_status_codes.json`) parsed from the offline reference guide `NBU_StatusCode.pdf`.
|
||||||
|
* **Details:**
|
||||||
|
* For status codes 2, 25, 26, 57, 58, 96, and 156, it merges custom Portuguese guidelines with the official PDF manual guidelines.
|
||||||
|
* For all other status codes, it falls back to the official PDF's description and the first recommended troubleshooting action as a suggestion.
|
||||||
|
* The diagnostic engine runs entirely offline without any internet lookup.
|
||||||
|
|
||||||
|
### 6.5 Sidebar Date Range Filter (v2.3 Shift)
|
||||||
|
* **Objective:** Support daily ingestion and navigation through historical backups.
|
||||||
|
* **Mechanism:** Display a date range selector (`st.sidebar.date_input`) in the sidebar, derived from database bounds.
|
||||||
|
* **Traceability Integration:** Allow ignoring the date filter in the Mitigation Actions tab (Tab 3) via a toggle, ensuring technicians can address unresolved active failures across all dates.
|
||||||
|
|
||||||
|
### 6.6 Daily Executed Jobs Line Chart
|
||||||
|
* **Objective:** Render chronological executed job statistics.
|
||||||
|
* **Mechanism:** Group execution counts by date and display a line chart with markers in the Performance Dashboard, showing a maximum of 30 days of execution.
|
||||||
Reference in New Issue
Block a user