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>
417 lines
16 KiB
Markdown
417 lines
16 KiB
Markdown
# 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/
|
||
```
|