# 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: <OPS_STATUS_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)
