# Implementation Plan: Ligbox Ops Console — Operação Activa (019) **Branch:** `019-ops-console-active-operations` **Date:** 2026-06-16 **Spec:** [spec.md](./spec.md) --- ## Summary Implementar a **Ligbox Ops Console** — navegação investigativa tipo Wazuh com **`CH-*` como hub central** (chamado único, assumir, runbooks, aprovações humano + agentic, Assist Opção A). **IA:** [design/navigation-ia.md](./design/navigation-ia.md) | Camada | Host | Deploy | |--------|------|--------| | **Console UI** | **VM123** | **Docker Compose** (obrigatório — VM já tem serviços) | | **API motor** | VM122 | Docker Compose existente (`ligbox-ops-platform`) | | **Detecção** | VM104 | Wazuh manager (inalterado) | | **Eventos onboard** | VM112 | systemd wizard | **URL pública:** `https://console.ligbox.com.br` → Traefik CT114 → VM123:8100 **API:** `https://api.ops.ligbox.com.br` → VM122:8080 --- ## Technical Context **Language/Version** | Componente | Stack | |------------|-------| | Console UI | React 18 + Vite 6 + TypeScript (recomendado) ou React JS | | API extensões | Python 3.11+ FastAPI (VM122 `api/app/`) | | VM112 Assist | Python FastAPI — novo endpoint `ops-status` | | Deploy VM123 | Docker Compose v2, `nginx:alpine` | **Primary Dependencies** - Console: React Router (`/chamados/:publicId` = hub), TanStack Query, Zustand (auth) - API: FastAPI, sqlite3, httpx, websockets (Fase 6) - Agentic: gateway LLM existente (A6/A7 — Fase 6) **Storage (VM122)** Novas tabelas SQLite (ver Phase 1). MVP mantém SQLite; migração Postgres fora de escopo. **Testing** - `scripts/verify-console-health.sh` — VM123 Docker + Traefik - `scripts/verify-chamado-unico.sh` — agregação eventos → `CH-*` - `scripts/verify-runbook-r1.sh` — fila aprovação + execução mock **Target Platform** - VM123 LAN `10.10.10.123` (confirmar IP no Proxmox) - Bind UI: `127.0.0.1:8100` ou `10.10.10.123:8100` - Comunicação VM123 → VM122: LAN HTTPS via Traefik ou direct `http://10.10.10.122:8080` (dev) **Performance Goals** - Console first paint < 2s (gzip brotli via nginx) - Discover feed < 800ms (paginação 50) - Chamado detail + timeline < 500ms - Poll Assist Opção A: 30s (MVP) **Constraints** - **VM123:** zero alteração em serviços/containers pré-existentes - **Sem fork** `wazuh-dashboard` / OpenSearch - **Sem replay** browser (Opção A apenas) - Runbooks R2/R3: audit log obrigatório - JWT Spec 003 em todas as rotas Console --- ## Constitution Check | Princípio | Status | |-----------|--------| | IV. Mail vs Ops | ✅ PASS — UI na VM123; motor VM122; VM112 só webhooks + ops-status read-only | | V. Security baseline | ✅ PASS — fail2ban host VM123; container non-root; secrets em `.env` | | VII. Spec-Driven | ✅ PASS | | IX. YAGNI | ✅ PASS — SQLite; poll antes de WebSocket | --- ## Project Structure ```text specs/019-ops-console-active-operations/ ├── spec.md ├── plan.md ├── tasks.md ├── deploy/ # Copiar para /opt/ligbox-ops-console/ na VM123 │ ├── docker-compose.yml │ ├── .env.example │ ├── nginx/ │ │ └── default.conf │ └── scripts/ │ ├── preflight-vm123.sh # Inventário portas antes do deploy │ └── deploy-console.sh ├── contracts/ │ └── chamados-api.md └── design/ └── tokens.css # Referência paleta Wazuh-like # Repositório produção (a criar) /opt/ligbox-ops-console/ # VM123 ├── docker-compose.yml # ← cópia de deploy/ ├── .env ├── frontend/ # SPA React │ ├── Dockerfile │ ├── src/ │ │ ├── views/ # Overview, Discover, ChamadosList, ChamadoHub │ │ ├── components/ # Timeline, Observables, AssistPanel, DrillDownLink │ │ ├── api/ # client → api.ops.ligbox.com.br │ │ └── theme/ # tokens Wazuh-like │ └── dist/ # build → nginx volume /opt/ligbox-ops-platform/ # VM122 (existente) ├── api/app/ │ ├── main.py # + rotas /chamados, /discover, /aprovacoes │ ├── chamados.py # agregação CH-*, estados │ ├── runbooks/ # executor + políticas R0-R3 │ └── agentic/ # propostas (Fase 6) └── worker/ └── chamado_aggregator.py # merge eventos → chamado_id ``` --- ## Phase 0: Pré-deploy VM123 (obrigatório) **Objectivo:** garantir que Docker Console não conflita com serviços existentes. ```bash # Na VM123 — antes de qualquer deploy ./scripts/preflight-vm123.sh ``` Checklist: | # | Verificação | Comando | Critério | |---|-------------|---------|----------| | 1 | Porta 8100 livre | `ss -tlnp \| grep :8100` | Vazio | | 2 | Docker activo | `docker info` | OK | | 3 | Containers existentes | `docker ps` | Listar — **não parar** | | 4 | Disco | `df -h /opt` | ≥ 5 GB livres | | 5 | IP LAN | `ip -4 addr show` | Confirmar `10.10.10.123` | | 6 | Acesso VM122 | `curl -s -o /dev/null -w '%{http_code}' http://10.10.10.122:8080/api/health` | 200 | Documentar resultado em ticket interno ou `docs/network/VM123_INVENTARIO.md` (criar após 1º preflight). --- ## Phase 1: Data Model — Chamado único (VM122) ```sql -- Migração 019_001_chamados.sql CREATE TABLE chamados ( id INTEGER PRIMARY KEY AUTOINCREMENT, public_id TEXT NOT NULL UNIQUE, -- CH-2026-00042 status TEXT NOT NULL DEFAULT 'novo', assignee TEXT, domain TEXT, session_id TEXT, wizard_step TEXT, wizard_step_at TEXT, sources TEXT NOT NULL DEFAULT '[]', -- JSON array max_severity INTEGER, title TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX idx_chamados_domain ON chamados(domain); CREATE INDEX idx_chamados_session ON chamados(session_id); CREATE INDEX idx_chamados_status ON chamados(status); CREATE TABLE chamado_eventos ( id INTEGER PRIMARY KEY AUTOINCREMENT, chamado_id INTEGER NOT NULL REFERENCES chamados(id), webhook_event_id INTEGER REFERENCES webhook_events(id), event_type TEXT NOT NULL, source TEXT NOT NULL, payload TEXT, created_at TEXT NOT NULL ); CREATE TABLE runbook_execucoes ( id INTEGER PRIMARY KEY AUTOINCREMENT, chamado_id INTEGER NOT NULL REFERENCES chamados(id), runbook_code TEXT NOT NULL, nivel TEXT NOT NULL, -- R0, R1, R2, R3 status TEXT NOT NULL, -- pendente, aprovado, executando, ok, falhou, rejeitado proposta TEXT, -- JSON agentic actor TEXT, -- user | agent | system resultado TEXT, created_at TEXT NOT NULL, executed_at TEXT ); CREATE TABLE aprovacoes ( id INTEGER PRIMARY KEY AUTOINCREMENT, runbook_exec_id INTEGER NOT NULL REFERENCES runbook_execucoes(id), aprovador TEXT, decisao TEXT, -- aprovado | rejeitado nota TEXT, created_at TEXT NOT NULL ); CREATE TABLE chamado_observables ( id INTEGER PRIMARY KEY AUTOINCREMENT, chamado_id INTEGER NOT NULL REFERENCES chamados(id), tipo TEXT NOT NULL, -- domain, session_id, agent, rule_id, ip, email valor TEXT NOT NULL, fonte TEXT NOT NULL, -- wazuh, onboard, manual, extractor created_at TEXT NOT NULL, UNIQUE(chamado_id, tipo, valor) ); CREATE TABLE chamado_notas ( id INTEGER PRIMARY KEY AUTOINCREMENT, chamado_id INTEGER NOT NULL REFERENCES chamados(id), autor TEXT NOT NULL, texto TEXT NOT NULL, created_at TEXT NOT NULL ); -- Denormalizar chamado_id em webhook_events (opcional fase 1b) ALTER TABLE webhook_events ADD COLUMN chamado_id INTEGER REFERENCES chamados(id); ``` ### Lógica de agregação (`chamado_aggregator.py`) 1. Novo `webhook_event` inserido → worker busca chamado aberto: - `session_id` + `domain` match - senão `domain` + status ∉ (`fechado`, `resolvido`) + created < 72h - senão `wazuh.agent.id` + domain/IP 2. Se não encontrar → `INSERT chamados` + `public_id` sequencial 3. `INSERT chamado_eventos` + update `chamados.sources`, `max_severity`, `updated_at` 4. Eventos `account.created` / `wazuh.alert` L≥10 podem forçar criação imediata 5. **Extractor** popula `chamado_observables` a partir de cada payload (domain, agent, rule_id, …) --- ## Phase 2b: Hub API (VM122) | Endpoint | Hub section | |----------|-------------| | `GET .../chamados/{id}` | `timeline`, `observables`, `assist`, `infra`, `links` | | `POST .../notas` | append timeline | | `POST .../eventos/anexar` | Discover → hub | | `GET .../links/wazuh` | deep link VM104 | Helper `build_wazuh_deep_link(agent, from, to, rule_id)` — URL dashboard VM104. --- ## Phase 2: API VM122 Ver [contracts/chamados-api.md](./contracts/chamados-api.md). | Prioridade | Endpoint | Fase | |------------|----------|------| | P1 | `GET /api/v1/chamados` | F2 | | P1 | `GET /api/v1/chamados/{public_id}` | F2 | | P1 | `POST /api/v1/chamados/{public_id}/assumir` | F2 | | P1 | `PATCH /api/v1/chamados/{public_id}` | F2 | | P1 | `GET /api/v1/discover` | F3 | | P1 | `POST /api/v1/chamados/{id}/runbooks/{code}/executar` | F4 | | P1 | `GET /api/v1/aprovacoes` | F4 | | P1 | `POST /api/v1/aprovacoes/{id}/aprovar` | F4 | | P2 | `WS /api/v1/chamados/{public_id}/live` | F6 | **Compatibilidade:** `GET /api/v1/desk/tickets` mantém-se 90 dias; resposta inclui `chamado_public_id` quando mapeado. --- ## Phase 3: Console UI — VM123 Docker ### Stack deploy (referência) Ficheiros em [deploy/](./deploy/): ```bash # VM123 rsync -av specs/019-ops-console-active-operations/deploy/ root@10.10.10.123:/opt/ligbox-ops-console/ ssh root@10.10.10.123 'cd /opt/ligbox-ops-console && cp .env.example .env && nano .env' ssh root@10.10.10.123 'cd /opt/ligbox-ops-console && ./scripts/deploy-console.sh' ``` **Importante:** `docker compose up -d` só afecta serviços definidos neste compose — **não** `docker compose down` global na VM. ### Traefik CT114 (label router) ```yaml # Adicionar em dynamic.yml ou labels do service discovery http: routers: ligbox-ops-console: rule: Host(`console.ligbox.com.br`) entryPoints: [websecure] service: ligbox-ops-console tls: certResolver: letsencrypt services: ligbox-ops-console: loadBalancer: servers: - url: http://10.10.10.123:8100 ``` ### Views MVP (F3) | View | Rota | Conteúdo | |------|------|----------| | **Overview** | `/` | Stats, funil, alertas → link hub | | **Discover** | `/discover` | Feed + filtros → **abrir hub** | | **Chamados** | `/chamados` | Lista fila trabalho | | **Hub** | `/chamados/:publicId` | **Investigação central** — timeline + tabs | | **Tenants** | `/tenants` | Agentes/VMs; drill-down → hub | | **Aprovações** | `/aprovacoes` | Fila; cada item → hub origem | ### Componentes hub (prioridade implementação) 1. `ChamadoHubLayout` — shell 2 colunas 2. `InvestigationTimeline` — eventos + notas + runbooks 3. `ObservablesPanel` — entidades clicáveis (`DrillDownLink`) 4. `AssistPanel` — Opção A 5. `InfraScorecardEmbed` — proxy 009 6. `WazuhDeepLinkButton` — novo tab VM104 7. `DiscoverAttachBar` — seleccionar eventos → anexar ao hub activo ### Tema Wazuh-like - Ficheiro referência: [design/tokens.css](./design/tokens.css) - Navegação: [design/navigation-ia.md](./design/navigation-ia.md) --- ## Phase 4: Runbooks + aprovações (VM122) | Código | Nível | Executor | |--------|-------|----------| | `infra_recheck` | R0 | HTTP VM112 `/api/onboarding/infrastructure/status/{domain}` | | `traefik_cert_sync` | R1 | Script/API CT114 (Spec 011) | | `admin_nginx_reload` | R1 | SSH VM112 ou API futura | | `zmproxy_admin_provision` | R2 | SSH VM112 — humano obrigatório | | `domain_purge` | R3 | API VM122 → VM112 Spec 017 — dupla aprovação | | `wazuh_acknowledge` | R1 | Nota + tag chamado (VM104 ack futuro) | | `escalate_kimi_human` | R0 | Link OB-* ticket → chamado | Fluxo R1: ```text Técnico clica Executar → runbook_execucoes status=pendente → Agentic (opcional) gera proposta → aprovacoes fila → Humano Aprovar → worker executa → timeline + status ok|falhou ``` --- ## Phase 5: Assist Opção A (VM112 + Console) **VM112 — novo endpoint:** ``` GET /api/onboarding/session/{session_id}/ops-status Header: X-Ops-Secret: LAN only: 10.10.10.0/24 ``` Resposta: ```json { "session_id": "abc…", "domain": "myvexx.com", "wizard_step": "dns_cloudflare", "wizard_step_label": "DNS Cloudflare", "time_on_step_sec": 142, "last_error": null, "planned_email": "admin@myvexx.com" } ``` **VM122:** poll 30s no detalhe chamado ou cache Redis `ops:session:{id}`. **Console:** painel lateral `AssistPanel` — sem iframe wizard, sem senhas. --- ## Phase 6: Agentic + WebSocket (pós-MVP) - Agente A6 analisa chamado → propõe runbook R0/R1 - Políticas em `agentic/policies.yaml` — confiança mínima, allowlist runbooks - WebSocket `live` push: novo evento + passo wizard (substitui poll) --- ## Implementation Phases (time estimate) | Fase | Entrega | VM | ~Semanas | |------|---------|-----|----------| | **F0** | Preflight VM123 + Traefik DNS | 123, 114 | 0.5 | | **F1** | Schema + aggregator | 122 | 1–1.5 | | **F2** | API chamados | 122 | 1.5–2 | | **F3** | Console Docker MVP (3 views) | **123** | 3–4 | | **F4** | Runbooks R0–R2 + aprovações | 122, 123 | 3–4 | | **F5** | Assist ops-status | 112, 122, 123 | 2–3 | | **F6** | Agentic + WS | 122, 123 | 3–4 | **MVP (F0–F4):** ~10–12 semanas **Completo (F0–F6):** ~16–20 semanas --- ## Sequencing with other specs | Spec | Relação | |------|---------| | **001/002** | Eventos ingress → aggregator alimenta chamados | | **004** | Funil widget no Overview; timeline enriquece chamado | | **009** | Tenants view + scorecard drill-down | | **010** | Runbooks admin + `admin.validation.failed` → chamado | | **017/018** | Purge R3 + tile Serviços linka chamado | **Ordem recomendada:** F1+F2 (API) em paralelo com F0+F3 scaffold UI Docker; F4 após API estável; F5 quando chamado único validado. --- ## Risk & Mitigation | Risco | Mitigação | |-------|-----------| | Porta 8100 ocupada na VM123 | `preflight-vm123.sh`; variável `CONSOLE_HOST_PORT` no `.env` | | Docker compose conflita rede | Rede bridge dedicada `ligbox-console`; não usar `network_mode: host` | | CORS Console ↔ API | Allowlist `console.ligbox.com.br` no FastAPI VM122 | | Agregação errada (chamados duplicados) | Regra session_id first; ferramenta merge manual ops | | Runbook destrutivo sem R3 | Middleware bloqueia `domain_purge` sem dupla aprovação | | VM123 serviços desconhecidos | Inventário obrigatório F0 antes deploy | --- ## Deploy checklist (cutover) - [ ] `preflight-vm123.sh` OK - [ ] `docker compose ps` — só `ligbox-ops-console-ui` healthy - [ ] `curl -I https://console.ligbox.com.br/health` → 200 - [ ] Login JWT Spec 003 funcional - [ ] Chamado teste `CH-*` com evento Wazuh + onboard - [ ] `desk.ligbox.com.br` → 302 `console.ligbox.com.br` (após 90d aviso) - [ ] Rollback: `docker compose down` no dir `/opt/ligbox-ops-console` apenas --- ## Referências - [spec.md](./spec.md) - [tasks.md](./tasks.md) - [deploy/docker-compose.yml](./deploy/docker-compose.yml) - [contracts/chamados-api.md](./contracts/chamados-api.md)