ligbox-ops-platform/specs/017-vm112-domain-orchestration/spec.md
Ligbox Spec Hub edffd8b3c0 feat(desk): Serviços IaaS perf, Escopo OPS cards e blocklist UI (Spec 017/018)
Cache VM112/VM122 para lista e detalhe domínio, modal purge com loading animado,
cards Escopo OPS clicáveis (camada + Spec + navegação), blocklist visível na UI,
e documentação nas specs e anais de referência 20260625.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 18:53:57 +00:00

417 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` ~6079s).
**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 ~3360s 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 (~1015s 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/
```