# Feature Specification: Domínios VM112 — Purge & Histórico (017) **Criado:** 2026-06-16 **Actualizado:** 2026-06-25 (performance modal + UX Escopo OPS + blocklist UI) **Solicitado por:** Roger **Status:** v1 + v2 concluídos · Fase 3 VM112 pendente **Prioridade:** P1 (testes E2E + padrão de limpeza) **Sistema:** Desk VM122 + Wizard VM112 **Módulo:** `vm112-domains` **UI purge:** página **Serviços** (Spec 018) **UI histórico:** **Eventos → Histórico de purges** --- ## Resumo Técnicos **Admin** (`super_admin`, `ops_lead`) executam **purge completo** de domínios VM112 (Carbonio, site, portal, Cloudflare, Traefik/SNI, registos Desk) a partir da página **Serviços**, com timeline ao vivo no drawer lateral. **v2 (2026-06-16):** cada purge fica **persistido** em SQLite e consultável em **Eventos → Histórico de purges** — lista clicável + modal com timeline, utilizador e serviços removidos. **Uso inicial:** limpar domínios de teste para reentrarem no wizard. **Futuro:** padrão de limpeza de dados por domínio. --- ## Módulo Desk (Spec 015) | Campo | Valor | |-------|--------| | `id` | `vm112-domains` | | `label` | Domínios VM112 | | `default_enabled` | `true` | | `nav_views` | _(vazio — purge na página Serviços, histórico em Eventos)_ | --- ## RBAC | Acção | Perfis | |-------|--------| | Executar purge (Serviços) | `super_admin`, `ops_lead` + **senha Root** | | Ver histórico de purges (Eventos) | `super_admin`, `ops_lead` | | Listar / detalhe job purge | `super_admin`, `ops_lead` | Técnicos `technician` e `noc` **não** acedem. --- ## UI — Serviços (Spec 018) ### Tile E-mail Tenant → modal purge 1. **Resumo** — domínio, mail host, admin portal, contas Carbonio, zona CF 2. **Infra** — passos `get_status()` (Carbonio, DNS, SNI, Traefik) 3. **Contas** — lista e-mails Carbonio 4. **Zona perigosa — Purge** (Admin only) - Aviso irreversível - Confirmação: digitar domínio exacto - Campo **senha Root** (Desk) - Botão «Apagar domínio e todos os dados» 5. **Drawer lateral** `vm112-purge-drawer` — timeline em tempo real durante execução --- ## API Desk (VM122) | Método | Path | Descrição | |--------|------|-----------| | GET | `/api/v1/vm112/domains?q=` | Lista domínios orquestrados (proxy VM112) | | GET | `/api/v1/vm112/domains/{domain}` | Detalhe + infra status | | POST | `/api/v1/vm112/domains/{domain}/purge/jobs` | **Recomendado** — purge async + polling | | GET | `/api/v1/vm112/purge/jobs/{job_id}` | Estado / timeline do job | | POST | `/api/v1/vm112/purge/jobs/{job_id}/recover` | Recuperar job após timeout UI | | GET | `/api/v1/vm112/purge/jobs?limit=&offset=` | **v2** — lista histórico persistido | | POST | `/api/v1/vm112/domains/{domain}/purge/stream` | Purge SSE (legado Traefik) | | POST | `/api/v1/vm112/domains/{domain}/purge` | Purge síncrono (legado) | **Body purge:** ```json { "confirm_domain": "iofficebooks.com", "root_password": "********" } ``` **Validações purge:** 1. `user.role` ∈ {super_admin, ops_lead} 2. `verify_password(root_password, hash do user root)` 3. `confirm_domain` === domínio (case-insensitive) 4. Domínio ∉ blocklist (`ligbox.com.br`, etc.) 5. Proxy VM112 `POST /api/admin/domains/{domain}/purge` com `X-Api-Key` **Pós-purge Desk:** apagar `audit_domains`, `tickets`, `assist_sessions`, `audit_checks`, `domain_console_scenarios` com referência ao domínio. **Preservar** `webhook_events` e inserir marcador `domain.purged` (`source=desk.purge`). --- ## API VM112 | Método | Path | Auth | |--------|------|------| | GET | `/api/admin/domains` | `X-Api-Key` — resposta inclui `cached`, `cache_age_sec` (2026-06-25) | | GET | `/api/admin/domains/{domain}` | `X-Api-Key` | | POST | `/api/admin/domains/{domain}/purge` | `X-Api-Key` | | GET | `/api/admin/domains/purge-jobs/{job_id}` | `X-Api-Key` _(memória, efémero)_ | ### Cache listagem domínios (2026-06-25) **Problema:** `list_orchestrated_domains()` chamava `zmprov gad` em **cada** `GET /api/admin/domains` (~5s) — gargalo da página Serviços IaaS no Desk (Spec 018). **Implementação VM112** (`deploy/vm112-wizard/perf-domains-list-20260625/`): | Cache | TTL | Chave | Invalidação | |-------|-----|-------|-------------| | Lista Carbonio (`gad`) | 90s | `gad_all_domains` | `zmprov cd`, `zmprov dd`, purge concluído | | Lista orquestrada montada | 60s | `orchestrated_domains_list` | idem | | Detalhe domínio (purge modal) | 120s | `domain_detail:{domain}` | purge / dd / cd | | Infra checks (`get_status`) | 90s | `infra_status:{domain}` | idem | | Contas (`zmprov gaa`) | 45s | `accounts_list:{domain}` | purge contas / dd | **Optimizações modal purge (2026-06-25):** `get_domain_detail()` executa gaa + infra + Cloudflare **em paralelo** (`ThreadPoolExecutor`). 1.º hit ~16s → 2.º hit **~0,04s**. **Ficheiros:** `carbonio_cache.py`, `carbonio.list_all_domains()`, `domain_orchestration.get_domain_detail()`, `infrastructure.get_status()` cache. **Desk VM122** mantém cache proxy adicional 60s (`VM112_DOMAINS_CACHE_TTL`) — ver Spec 018 § Performance. --- **Purge VM112 (ordem):** 1. Apagar contas Carbonio (`zmprov da`) 2. Apagar domínio Carbonio (`zmprov dd`) 3. Remover portal users com `planned_corporate_email` no domínio 4. Apagar `/opt/ligbox-sites/domains/{domain}/` 5. Apagar zona Cloudflare (se existir na conta Ibytera) 6. Remover `mail.{domain}` do SNI + routers Traefik (CT114) 7. Apagar logs sessão JSONL com referência ao domínio --- ## Fase 2 — Jobs async + polling (implementado) `POST /api/v1/vm112/domains/{domain}/purge/jobs` inicia thread em background. UI faz polling `GET /api/v1/vm112/purge/jobs/{id}` a cada 2s. **Motivo:** SSE longo falhava via Traefik (`504` / `Failed to fetch` ~60–79s). **Fix nginx Desk:** `proxy_read_timeout 600s` em `frontend/nginx.conf`. Persistência SQLite (`vm112_purge_jobs`) criada nesta fase — base para v2. --- ## Fase 2 — SSE (implementado, legado) `POST /api/v1/vm112/domains/{domain}/purge/stream` · `text/event-stream` | type | Conteúdo | |------|----------| | `step` | `{ label, at, status, detail }` | | `heartbeat` | `{ elapsed }` — cada 5s | | `error` | purge falhou | | `done` | `{ desk, vm112, domain }` | Ordem: validação → VM112 (heartbeat) → passos VM112 → passos Desk → concluído. --- ## v2 — Histórico de purges (implementado 2026-06-16) ### Problema resolvido | Antes | Depois | |-------|--------| | Timeline só ao vivo no drawer | Histórico persistente no Desk | | Dados em SQLite sem UI | Lista + modal de detalhe | | VM112 jobs em memória (efémero) | Fonte de verdade: VM122 `ops.db` | | Purges «desapareciam» ao fechar modal | Consulta por domínio, data, utilizador | **Nota:** purges **antes** da persistência (ex.: `betinsport.com`) não aparecem no histórico. ### UI — Eventos - Aba **Webhooks** (existente) - Aba **Histórico de purges** (Admin only) - Lista: Job ID, domínio, status, utilizador, resumo Desk, data, duração VM112 - Clique na linha → modal com: 1. Cabeçalho (domínio, status, utilizador, data, job id) 2. Removido no Desk — webhook_events, tickets, audit_domains, assist_sessions, audit_checks 3. Removido na VM112 — Carbonio, portal, site, Cloudflare, Traefik 4. Timeline completa (`timeline_json`) ### Persistência | Campo | Valor | |-------|--------| | Base | `/var/lib/ligbox-ops-platform/ops.db` (Docker: `/data/ops.db`) | | Tabela | `vm112_purge_jobs` | | Colunas | `timeline_json`, `desk_json`, `vm112_json`, `by_user`, `status`, `created_at` | ### Ficheiros v2 | Ficheiro | Alteração | |----------|-----------| | `api/app/vm112_purge_jobs.py` | `list_jobs()`, schema, persistência | | `api/app/vm112_domains_routes.py` | `GET /purge/jobs` | | `frontend/assets/app.js` | `renderPurgeHistory()`, modal, aba Eventos | | `frontend/index.html` | Toolbar Eventos + `purge-history-modal` | | `frontend/assets/styles.css` | Estilos lista/modal | ### Critérios de aceitação v2 1. Admin vê aba «Histórico de purges» em Eventos. 2. Lista mostra purges com status, utilizador, data e resumo Desk. 3. Clique abre modal com timeline completa e contagens por serviço. 4. Badges correctos: `done`, `error`, `running`, `queued`. 5. `technician` / `noc` não vêem a aba. ### Consulta manual (SSH VM122) ```bash sqlite3 /var/lib/ligbox-ops-platform/ops.db \ "SELECT id, domain, status, by_user, created_at FROM vm112_purge_jobs ORDER BY created_at DESC;" ``` ```bash curl -s -H "Authorization: Bearer $TOKEN" \ "https://desk.ligbox.com.br/api/v1/vm112/purge/jobs/57845ca1c5c64b53" ``` --- ## Extensão — Spec 026 (purge Traefik validation) Validação YAML + smoke onboard pós-remoção Traefik (CT114). Incidente 2026-06-19: `dynamic.yml` inválido após purge → 404 global no onboard. **Spec dedicada:** `specs/026-purge-traefik-validation/spec.md` --- ## Extensão — Spec 032 (purge autorização extra) Domínios em `PURGE_EXTRA_AUTH_DOMAINS` (ex.: `myvexx.com`) exigem **código de autorização** gerado pelo root em **Infra** (senha Root), além da validação purge normal. | Camada | Regra | |--------|--------| | Blocklist | `ligbox.com.br`, `itecnologys.com` — purge proibido | | Extra auth | Código único + senha Root | | Normal | Senha Root (Spec 017 v1) | **Spec dedicada:** `specs/032-purge-domain-extra-auth/spec.md` **UI geração:** Infraestrutura → «Códigos autorização purge» **UI consumo:** Serviços → modal purge (campo código) --- ## Fase 3 — VM112 passos em tempo real (pendente) VM112 (`/opt/ligbox-wizard`) emitir passos individuais durante execução (Carbonio, CF, Traefik) em vez de bloco único + heartbeat. Alterações no wizard, não só no Desk. --- ## Critérios de aceitação (v1) 1. Admin executa purge a partir de Serviços. 2. Purge com senha root errada → erro na timeline. 3. Purge com domínio confirmado errado → HTTP 400. 4. Após purge, domínio ausente em Carbonio, ligbox-sites e Desk. 5. Drawer mostra progresso ao vivo; job persiste em SQLite. --- ## Fora de escopo - Purge parcial (só contas, só DNS) - Scheduler de limpeza automática - Export CSV/PDF do histórico - Filtro por domínio/data na lista de histórico - Retenção automática / purge de jobs antigos - Link directo Serviços → histórico do domínio --- ## Conclusão (2026-06-16) A Spec 017 cobre o ciclo completo de purge de domínio VM112: | Fase | Entrega | Estado | |------|---------|--------| | v1 | Purge completo via Serviços + validação Root | ✅ | | Fase 2 | Jobs async, polling, persistência SQLite | ✅ | | Fase 2 SSE | Timeline drawer (legado) | ✅ | | **v2** | Histórico em Eventos — lista + modal audit trail | ✅ | | **032** | Códigos autorização extra (myvexx.com) — Infra + Serviços | ✅ | | **026** | Validação Traefik pós-purge | ✅ | | Fase 3 | Passos VM112 em tempo real no wizard | ⏳ | **Purges registados (exemplo):** `myvexx.com`, `diarissima.com`, `ibytera.com` — visíveis em Eventos → Histórico de purges. **Próximo passo natural:** Fase 3 no wizard VM112; depois filtros/export no histórico se necessário. --- ## Incidente 2026-06-25 — Auditor 404 + purge timeout (Roger) ### Sintomas reportados | # | UI | Mensagem | Domínio / ref | |---|-----|----------|----------------| | 1 | Eventos → Auditor de Eventos | `Erro: Not Found` | webhook · **291** (`domain.purged`, `cenario-demo.ops.ligbox.com.br`) | | 2 | Eventos → Histórico de purges | badge **ERRO**, VM112 **timed out** (33s), Desk **0** | `eplacebets.com` · job `84a78c85d6b6490c` | ### Diagnóstico | Erro | Causa raiz | Impacto real | |------|------------|--------------| | Auditor 404 | Modal chamava detalhe inexistente (`GET /webhooks/events/{id}` ausente); evento **291** estava OK em SQLite | Purge `cenario-demo` **concluído** — falha só de visualização | | Purge timed out | `httpx` poll VM112 com timeout **60s** → excepção `timed out`; job marcado `error` **antes** da fase Desk; `recover` não actuava em jobs `error` | `eplacebets.com` **pode** ter ficado parcial na VM112; Desk **não limpo** | ### Correções (2026-06-25) | Camada | Alteração | |--------|-----------| | **API** | `GET /api/v1/webhooks/events/{event_id}` — detalhe para Auditor | | **API purge** | `VM112_PURGE_HTTP_TIMEOUT=300` (env); poll tolerante a falhas transitórias; auto-recuperação se domínio já ausente na VM112 | | **Jobs** | `recover_job` actua em status `error`; `_execute_job` tenta recuperação automática pós-timeout | | **UI Eventos** | Linhas clicáveis → modal **Auditor de Eventos**; render dedicado `domain.purged`; filtro origem **Purge** | | **UI Histórico** | Labels Desk actualizados (`domain.purged` preserva histórico); botão **Recuperar purge** em jobs com erro | ### Ficheiros | Ficheiro | Alteração | |----------|-----------| | `api/app/main.py` | `GET /webhooks/events/{id}` | | `api/app/vm112_domains.py` | timeout 300s, poll resiliente, recover pós-timeout | | `api/app/vm112_purge_jobs.py` | recover em `error`, auto-recover | | `frontend/index.html` | `event-auditor-modal`, filtro Purge | | `frontend/assets/app.js` | Auditor, purge desk labels, botão recover | ### Validação pós-deploy ```bash # Evento 291 — deve retornar domain.purged (HTTP 200) curl -s -H "Authorization: Bearer $TOKEN" \ "https://desk.ligbox.com.br/api/v1/webhooks/events/291" # Recuperar job eplacebets se VM112 já limpou curl -s -X POST -H "Authorization: Bearer $TOKEN" \ "https://desk.ligbox.com.br/api/v1/vm112/purge/jobs/84a78c85d6b6490c/recover" ``` ### Resultado esperado 1. Clicar evento **291** → modal mostra utilizador, ferramenta, domínio e payload (sem 404). 2. Job `84a78c85d6b6490c` → **Recuperar purge** conclui fase Desk se `eplacebets.com` já não existir na VM112; senão repetir purge após fix de timeout. 3. Novos purges longos (Carbonio) não falham aos ~33–60s por timeout HTTP do poll. ### Resultado pós-deploy (2026-06-25, VM122) | Verificação | Resultado | |-------------|-----------| | `GET /webhooks/events/291` | ✅ `domain.purged` · `desk.purge` | | `eplacebets.com` na VM112 | ✅ ausente (purge VM112 efectivo) | | Job `84a78c85d6b6490c` recover | ✅ `status=done`, Desk limpo, marcador `domain.purged` inserido | | Job posterior `e238c1efb7c24d92` | ✅ já estava `done` (retry manual anterior) | --- ## UI Serviços — loading, blocklist, Escopo OPS (2026-06-25) Melhorias UX na página **Serviços IaaS** (Spec 018) sem alterar regras de purge. ### Blocklist — apresentação | Domínio | API `purge_blocked` | UI lista clientes | UI modal purge | |---------|---------------------|-------------------|----------------| | `ligbox.com.br` | `true` | badge 🔒 (tooltip) | formulário desactivado | | `itecnologys.com` | `true` | idem | idem | | restantes | `false` | — | senha Root + confirmação | **Constante Desk:** `PURGE_BLOCKLIST` em `vm112_domains.py` — espelhada no frontend (`servicos.js`) para badges imediatos antes do detalhe API. **Painel Escopo OPS:** banner vermelho quando cliente seleccionado ∈ blocklist, listando domínios protegidos. ### Modal «Gerir» — loading animado Enquanto `GET /api/v1/vm112/domains/{domain}` corre (~10–15s cold): | Elemento | Comportamento | |----------|----------------| | Skeleton | Dados da lista clientes (admin portal, Carbonio, site) | | Barra progresso | Indeterminada (CSS `vm112-load-bar`) | | Etapas | Rotação Carbonio → Infra CT114 → Cloudflare | | Botão purge | Spinner + «A carregar…» desactivado | | A11y | `aria-busy`, `aria-live="polite"` | ### Escopo OPS — cards clicáveis Ver Spec **018** § Coluna Escopo OPS (mapa completo camada → Spec → destino). **Navegação cross-módulo:** `window.DeskNavigate.go(view, opts)` em `app.js`. ### Ficheiros (2026-06-25 UX) | Ficheiro | Alteração | |----------|-----------| | `frontend/assets/servicos.js` | `OPS_SCOPES` enriquecido, `navigateScope`, blocklist UI, loading modal | | `frontend/assets/styles.css` | `.vm112-load-*`, `.servicos-scope-*`, alinhamento lista clientes | | `frontend/assets/app.js` | `DeskNavigate` | | `frontend/index.html` | cache bust `?v=20260625align1` | ### Versão deploy VM122 (2026-06-25) ```bash # Frontend (sem rebuild — docker cp) docker cp servicos.js ligbox-ops-platform_frontend_1:/usr/share/nginx/html/assets/ docker cp styles.css ligbox-ops-platform_frontend_1:/usr/share/nginx/html/assets/ docker cp app.js ligbox-ops-platform_frontend_1:/usr/share/nginx/html/assets/ docker cp index.html ligbox-ops-platform_frontend_1:/usr/share/nginx/html/ ```