# 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.
