# Implementation Plan: Webhook VM112 → Ops Platform **Branch**: `001-webhook-vm112-integration` | **Date**: 2026-06-08 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/001-webhook-vm112-integration/spec.md` ## Summary Implementar integração LAN entre o portal de onboarding VM112 (`ibytera-mail-portal`) e a API Ops VM122 (`ligbox-ops-platform`). O portal emite webhooks autenticados após marcos do onboarding; o Ops regista eventos, cria tickets automaticamente e garante idempotência. Falhas de webhook são não-bloqueantes para o cliente. **Abordagem**: módulo `ops_webhook` no portal (httpx + retry), extensão mínima do receptor existente em VM122 (idempotência + índice), secret partilhado via `.env` em ambas as VMs. ## Technical Context **Language/Version**: Python 3.11+ (portal VM112 Ubuntu 24.04; Ops VM122 Debian 12) **Primary Dependencies**: FastAPI, httpx, pydantic-settings (portal); FastAPI, httpx, redis, sqlite3 (Ops — já deployados) **Storage**: SQLite `ops.db` (VM122) — tabelas `webhook_events`, `tickets` existentes; sem alteração de schema obrigatória (índice lógico para idempotência) **Testing**: curl manual + script `scripts/verify-webhook.sh`; teste portal com Ops offline **Target Platform**: VM112 `10.10.10.112:8090` → VM122 `10.10.10.122:8080` (LAN only) **Project Type**: Integração cross-VM (dois repositórios/deploy paths) **Performance Goals**: Webhook entrega < 5s p95; não adicionar > 500ms ao tempo de resposta do portal **Constraints**: LAN-only; secret em header `X-Webhook-Secret`; fail2ban inalterado; onboarding nunca bloqueado por falha Ops **Scale/Scope**: ~10–50 onboardings/dia; 4 tipos de evento MVP + 4 fase 2 ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | Princípio | Status | Notas | |-----------|--------|-------| | I. vmbr1 / LAN | ✅ PASS | Tráfego 112→122 na LAN `10.10.10.0/24` via vmbr4000 | | II. Interfaces Proxmox | ✅ PASS | Nenhuma alteração de rede | | III. Anti-scan Hetzner | ✅ PASS | Sem novas regras NAT/iptables | | IV. Mail vs Ops separation | ✅ PASS | Portal emite; Ops recebe — sem mail stack em 122 | | V. fail2ban | ✅ PASS | Sem alteração SSH | | VI. pfSense API | N/A | Não usado nesta feature | | VII. Spec-Driven | ✅ PASS | spec → plan em curso | | VIII. Documentation | ✅ PASS | Artefactos em `specs/001-*` | | IX. YAGNI | ✅ PASS | Sem novos serviços; extensão mínima | **Post-design re-check**: ✅ Nenhuma violação. Sem Complexity Tracking necessário. ## Project Structure ### Documentation (this feature) ```text specs/001-webhook-vm112-integration/ ├── spec.md ├── plan.md # este ficheiro ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── webhook-onboard.md ├── checklists/ │ └── requirements.md └── tasks.md # gerado por /speckit-tasks ``` ### Source Code (deploy targets) ```text # VM112 — /opt/ibytera-mail-portal/ (sync desde obsidian-infra/carbonio/ibytera-mail-portal/) backend/app/ ├── config.py # + ops_webhook_url, ops_webhook_secret, ops_webhook_enabled ├── services/ │ └── ops_webhook.py # NOVO: emit_event(), retry logic └── routers/ └── onboarding.py # chamar ops_webhook após account/create # VM122 — /opt/ligbox-ops-platform/ (já deployado) api/app/ └── main.py # + idempotência, índice dedup, log melhorado ``` **Structure Decision**: alterações mínimas em dois deploy paths existentes; código fonte versionado em `obsidian-infra` (portal) e `workspace/projects/ligbox-ops-platform` (ops). ## Phase 0: Research Summary Ver [research.md](./research.md) — conclusões: 1. Endpoint receptor MVP já funcional (`POST /api/v1/webhooks/onboard`) 2. `session_id` disponível via header `X-Onboarding-Session` / `request.state` 3. Portal não tem cliente webhook — criar `ops_webhook.py` 4. Idempotência: lookup `event+session_id+domain` antes de INSERT ticket 5. Secret dev `ligbox-ops-dev-secret` — rotacionar em produção ## Phase 1: Design Artifacts | Artefacto | Ficheiro | Conteúdo | |-----------|----------|----------| | Data model | [data-model.md](./data-model.md) | Payload, entidades, dedup key | | API contract | [contracts/webhook-onboard.md](./contracts/webhook-onboard.md) | Request/response, eventos | | Quickstart | [quickstart.md](./quickstart.md) | Testes manuais e deploy | ## Implementation Phases ### Phase A — Ops receptor (VM122) — ~1h 1. Adicionar verificação idempotente em `webhook_onboard` 2. Query `webhook_events` por `(event_type, session_id, domain)` antes de criar ticket 3. Melhorar subject do ticket: `[account.created] dominio.com — admin@dominio.com` 4. Log estruturado em falha 401 ### Phase B — Portal emissor (VM112) — ~2h 1. `config.py`: `ops_webhook_url`, `ops_webhook_secret`, `ops_webhook_enabled` (default true) 2. `services/ops_webhook.py`: - `emit_event(event, domain, session_id, data, timeout=5)` - Retry 3x: 1s, 3s, 9s backoff - Header `X-Webhook-Secret` - `activity_log.warn` em falha, nunca raise para o router 3. `onboarding.py` → `create_account`: após sucesso, chamar: ```python ops_webhook.emit_event("account.created", domain, session_id, {...}) ``` ### Phase C — Config + validação — ~30min 1. `.env` VM112: `OPS_WEBHOOK_URL=http://10.10.10.122:8080/api/v1/webhooks/onboard` 2. `.env` VM122: confirmar `WEBHOOK_SECRET` igual 3. Script `scripts/verify-webhook.sh` no repo ops 4. Teste E2E: criar conta teste → ticket no desk ### Phase D — Eventos P3 (opcional, pós-MVP) - `domain.validated` em `/validate-domain` - `dns.applied` em `/cloudflare/apply` - `onboarding.completed` / `onboarding.failed` nos respectivos pontos ## Risk & Mitigation | Risco | Mitigação | |-------|-----------| | Ops offline durante onboarding | Retry + non-blocking; email admin continua | | Secret exposto em log | Nunca logar secret; só "auth failed" | | Tickets duplicados | Idempotência no receptor | | Latência no portal | Fire-and-forget async (BackgroundTasks FastAPI) | ## Complexity Tracking > Nenhuma violação da constitution — tabela vazia.