"""Settings and system health routes. Moved from app.admin_dashboard in v4.7.2. The handlers still reuse legacy helpers to keep this refactor behavior-preserving. """ from fastapi import APIRouter import app.admin_dashboard as legacy from app.admin_dashboard import * # noqa: F401,F403 router = APIRouter() @router.get("/system", response_class=HTMLResponse) async def system_health_page(): metrics = get_system_health_metrics() backup = latest_backup_info() timers_raw = shell_output(["systemctl", "list-timers", "--all", "--no-pager"]) clientflow_timers = "\n".join( line for line in timers_raw.splitlines() if "clientflow" in line.lower() ) services = [ "clientflow-api.service", "clientflow-outbox.timer", "clientflow-backup.timer", "clientflow-followups.timer", ] service_rows = "" for service in services: active = shell_output(["systemctl", "is-active", service]) enabled = shell_output(["systemctl", "is-enabled", service]) ok = active in ["active", "inactive"] if service.endswith(".service") else active == "active" if service == "clientflow-api.service": ok = active == "active" service_rows += f""" {esc(service)} {status_pill(ok, active)} {esc(enabled)} """ def n(key): return metrics.get(key) or 0 db_ok = True outbox_ok = n("outbox_failed") == 0 webhook_ok = n("webhooks_errors_24h") == 0 backup_ok = bool(backup.get("exists")) body = f"""

← Voltar à visão geral · Sistema Kanban

API {status_pill(True, "OK")}
Base de dados {status_pill(db_ok, "OK" if db_ok else "ERRO")}
Backups {status_pill(backup_ok, "OK" if backup_ok else "FALTA")}
Webhooks erros 24h {n("webhooks_errors_24h")}
Tasks pendentes{n("tasks_pending")}
Tasks 24h{n("tasks_24h")}
Webhooks 24h{n("webhooks_24h")}
Outbox falhada{n("outbox_failed")}

Estado dos serviços/timers

{service_rows}
Serviço Active Enabled

Último backup

Existe{status_pill(backup_ok, "Sim" if backup_ok else "Não")}
Ficheiro{esc(backup.get("file"))}
Tamanho{esc(backup.get("size"))}
Modificado{esc(backup.get("mtime"))}

Timers ClientFlow

{esc(clientflow_timers or "Sem timers ClientFlow encontrados.")}

Resumo técnico

Total tasks{n("tasks_total")}
Tasks done{n("tasks_done")}
Tasks failed{n("tasks_failed")}
Webhooks ignorados 24h{n("webhooks_ignored_24h")}
Outbox pending{n("outbox_pending")}
Outbox sent{n("outbox_sent")}
""" return layout("Sistema", "Estado técnico do ClientFlow e integrações.", body, "system") @router.get("/settings", response_class=HTMLResponse) @router.get("/configuracoes", response_class=HTMLResponse) async def settings_page(): return RedirectResponse("/system", status_code=303) @router.get("/system/health", response_class=HTMLResponse) async def system_health_operational_page(): health = get_system_health_summary() settings_info = health.get("settings") or {} timers = health.get("timers") or {} outbox = health.get("outbox") or {} documents = health.get("documents") or {} operational_metrics = health.get("operational_metrics") or {} def m(key: str) -> int: try: return int(operational_metrics.get(key) or 0) except Exception: return 0 timer_rows = "" for name, item in timers.items(): ok = bool(item.get("ok")) chip = "cf-chip-green" if ok else "cf-chip-orange" timer_rows += f""" {esc(name)}{esc(item.get('unit') or '—')}{esc(item.get('active_state') or 'unknown')} """ outbox_rows = "" for target, by_status in outbox.items(): cells = "".join(f'{esc(status)}: {esc(total)}' for status, total in by_status.items()) outbox_rows += f"{esc(target)}{cells}" if not outbox_rows: outbox_rows = 'Sem dados de outbox.' doc_rows = "" for kind, by_status in documents.items(): cells = "".join(f'{esc(status)}: {esc(total)}' for status, total in by_status.items()) doc_rows += f"{esc(kind)}{cells}" if not doc_rows: doc_rows = 'Sem documentos.' db_ok = bool((health.get("database") or {}).get("ok")) critical_count = (0 if db_ok else 1) + m("outbox_processing_stale") + m("outbox_stale") + m("outbox_blocked_or_failed") warning_count = m("ambiguous_opportunity_tasks") + m("open_opportunities_without_fiscal_customer") + m("active_incomplete_fiscal_customers") + m("products_missing_external_code") if critical_count: production_state = "Crítico" production_class = "critical" production_hint = "corrigir bloqueios antes de automatizar" elif warning_count: production_state = "Atenção" production_class = "warn" production_hint = "existem validações operacionais pendentes" else: production_state = "OK" production_class = "ok" production_hint = "sem bloqueios críticos visíveis" body = f"""
Estado para operação
{production_state}
{production_hint}
Críticos: {esc(critical_count)}Atenção: {esc(warning_count)}
{kpi_card('Estado geral', esc(health.get('status')), None, 'OK' if db_ok else 'ver base de dados', 'bi-heart-pulse', 'cf-kpi-tone-green' if db_ok else 'cf-kpi-tone-red')} {kpi_card('Jasmin', 'ativo' if settings_info.get('jasmin_enabled') else 'inativo', None, f"{esc(settings_info.get('jasmin_company_key') or '—')} · {esc(settings_info.get('jasmin_quotation_serie') or '—')}", 'bi-receipt')} {kpi_card('Packlink', 'ativo' if settings_info.get('packlink_enabled') else 'inativo', None, f"serviço {esc(settings_info.get('packlink_default_service_id') or '—')}", 'bi-truck')} {kpi_card('Ambiente', esc(settings_info.get('env') or '—'), None, esc(settings_info.get('app_name') or 'ClientFlow'), 'bi-sliders')}
{kpi_card('Tasks pendentes', esc(m('tasks_pending')), '/operations', f"done 24h: {esc(m('tasks_done_24h'))}", 'bi-list-task')} {kpi_card('Ambíguas', esc(m('ambiguous_opportunity_tasks')), '/operations?scope=ambiguas', 'associação por confirmar', 'bi-person-exclamation', 'cf-kpi-tone-orange' if m('ambiguous_opportunity_tasks') else 'cf-kpi-tone-green')} {kpi_card('Outbox stale', esc(m('outbox_stale') or m('outbox_processing_stale')), '/outbox?status=stale', f"modo: {esc(settings_info.get('outbox_stale_recovery_mode') or 'manual_only')}", 'bi-hourglass-split', 'cf-kpi-tone-red' if (m('outbox_stale') or m('outbox_processing_stale')) else 'cf-kpi-tone-green')} {kpi_card('Ações operador 24h', esc(m('operator_actions_24h')), '/events', 'auditoria operacional', 'bi-shield-check')}
{kpi_card('Oportunidades sem cliente fiscal', esc(m('open_opportunities_without_fiscal_customer')), '/operations?scope=ambiguas', 'associação por confirmar', 'bi-person-vcard', 'cf-kpi-tone-orange' if m('open_opportunities_without_fiscal_customer') else 'cf-kpi-tone-green')} {kpi_card('Clientes fiscais incompletos', esc(m('active_incomplete_fiscal_customers')), '/customers', 'em oportunidades ativas', 'bi-exclamation-diamond', 'cf-kpi-tone-orange' if m('active_incomplete_fiscal_customers') else 'cf-kpi-tone-green')} {kpi_card('Produtos sem código externo', esc(m('products_missing_external_code')), '/products', 'podem bloquear Jasmin', 'bi-box-seam', 'cf-kpi-tone-orange' if m('products_missing_external_code') else 'cf-kpi-tone-green')} {kpi_card('Último webhook Chatwoot', esc('—' if not m('seconds_since_last_chatwoot_webhook') else str(m('seconds_since_last_chatwoot_webhook')) + 's'), '/events', f"eventos: {esc(m('chatwoot_events_total'))}", 'bi-chat-dots')}

Timers systemd

Estado best-effort dos timers da outbox.
{timer_rows}
IntegraçãoUnidadeEstado

Outbox por sistema

{outbox_rows}
SistemaEstados

Documentos comerciais

{doc_rows}
TipoEstados

Base de dados

Estado: {'OK' if db_ok else 'Erro'}

{esc((health.get('database') or {}).get('error') or 'Sem erros reportados.')}
""" return layout("Saúde operacional", "Base de dados, integrações, timers e contadores", body, "system")