# Implementation Plan: Funil de Onboarding Completo (004) **Branch**: `004-onboard-funnel-events` | **Date**: 2026-06-08 | **Spec**: [spec.md](./spec.md) ## Summary Completar o funil VM112 → VM122: portal emite 6 tipos de evento adicionais; Ops agrega por `session_id`, expõe API de funil/timeline, e UI Dashboard mostra pipeline activo. Continuação directa da Phase D da feature 001. ## Technical Context **Language/Version**: Python 3.11+ (portal VM112, Ops VM122) **Primary Dependencies**: FastAPI, httpx, sqlite3, vanilla JS (frontend existente) **Storage**: SQLite — novas tabelas opcionais `onboard_sessions` (cache funil) ou query agregada sobre `webhook_events` + `tickets.payload` **Testing**: `scripts/verify-funnel-webhook.sh` + E2E wizard staging **Target Platform**: VM112 `:8090` → VM122 `:8080` LAN **Performance Goals**: Widget funil < 500ms; emissão webhook não adiciona > 200ms perceived latency **Constraints**: Non-blocking portal; idempotência; LAN-only ## Constitution Check | Princípio | Status | |-----------|--------| | IV. Mail vs Ops | ✅ PASS | | VII. Spec-Driven | ✅ PASS | | IX. YAGNI | ✅ PASS — reutilizar `webhook_events`, sem Postgres | ## Project Structure ```text specs/004-onboard-funnel-events/ ├── spec.md ├── plan.md ├── research.md ├── contracts/webhook-funnel-events.md ├── checklists/requirements.md └── tasks.md # VM112 backend/app/routers/onboarding.py # hooks emit_event em 5 endpoints backend/app/services/ops_webhook.py # (sem alteração estrutural) # VM122 api/app/main.py # funnel + timeline endpoints; ticket enrichment frontend/assets/app.js # widget funil + timeline ticket frontend/assets/styles.css # estilos funil scripts/verify-funnel-webhook.sh # teste simulado multi-evento ``` ## Phase 0: Research Summary Ver [research.md](./research.md). ## Phase 1: Design ### Mapeamento evento → hook portal | Evento | Endpoint / momento | |--------|-------------------| | `onboarding.started` | `POST /validate-domain` — 1ª vez por sessão | | `domain.validated` | `POST /validate-domain` — sucesso | | `dns.applied` | `POST /dns/cloudflare/apply` — `applied.length > 0` | | `account.created` | `POST /account/create` — ✅ já existe | | `infra.synced` | após `infrastructure.provision` OK em create_account | | `onboarding.completed` | fim de create_account (antes do return) | | `onboarding.failed` | except CarbonioError / HTTP 400 críticos | ### API Ops (novos endpoints) ``` GET /api/v1/onboard/funnel → { stages: { started: N, domain_validated: N, ... }, active_sessions: [...] } GET /api/v1/onboard/sessions/{session_id}/timeline → { session_id, domain, events: [{ event_type, created_at, data }] } GET /api/v1/desk/tickets/{id} (extend) → + timeline: [...] (eventos mesma session_id do ticket) ``` ### Lógica ticket - `_process_ingress` / `webhook_onboard`: após insert evento, se ticket existente para `session_id`, append nota timeline (JSON em payload ou tabela `ticket_events`). - `onboarding.completed`: PATCH interno ticket → tag/note `ready_for_ops`. - `TICKET_EVENTS`: manter `account.created`, `onboarding.failed`; outros só timeline. ## Implementation Phases ### Phase A — Ops API funil (~2h) 1. Função `_session_timeline(session_id)` query `webhook_events` 2. Função `_funnel_summary()` — agrega última fase por sessão (48h) 3. Endpoints GET funnel + timeline 4. Enriquecer `get_ticket` com timeline 5. Actualizar `TICKET_EVENTS_BY_SOURCE` se necessário ### Phase B — Portal emissor (~2h) 1. Helper `_emit_once(session_id, event, ...)` com flag in-memory ou check activity log (preferir idempotência no Ops) 2. Hooks nos 5 pontos do router onboarding 3. `onboarding.failed` no except de create_account ### Phase C — UI Desk (~2h) 1. Dashboard: card funil com barras/contadores por fase 2. Ticket detail: secção timeline vertical 3. Link sessão → filtro eventos ### Phase D — Validação (~1h) 1. Script verify-funnel-webhook.sh 2. Deploy VM112 + VM122 3. Teste wizard domínio teste ## Risk & Mitigation | Risco | Mitigação | |-------|-----------| | Eventos duplicados validate-domain | Idempotência Ops + emit started só 1x via cache sessão portal | | Payload grande | Truncar data no ticket; payload completo em webhook_events | | Funil lento com muitos eventos | Limitar active_sessions a 50; índice SQL em session_id (JSON extract ou coluna denormalizada fase 2) |