ligbox-ops-platform/specs/016-onboard-self-service-prefill/spec.md
Ligbox Spec Hub 3a2c64834b Initial import: ligbox-ops-platform + specs + LAPTOP + obsidian merge (CT130)
Source: VM122 /opt + obsidian-infra + LAPTOP
Hub: CT130 spec-hub 10.10.10.130
2026-06-19 17:26:41 +00:00

122 lines
6.3 KiB
Markdown

# Feature Specification: Pré-preenchimento Self-Service → Wizard (016)
**Criado:** 2026-06-16
**Solicitado por:** Roger
**Status:** Implementação
**Prioridade:** P0 (regressão UX onboarding)
**Sistema:** Portal VM112 (`ibytera-mail-portal`) — wizard `/onboard`
**Relacionado:** Spec 012 (ticket no `onboarding.started`), chat bruto `CHAT_BRUTO_ONBOARD_INFRA_SUPORTE_20260603`
---
## Resumo
Quando o utilizador preenche o **card Self-Service** na landing (hero) ou chega via **«Criar Meu Servidor Agora»**, os dados declarados devem **propagar automaticamente** para o wizard de onboarding, em especial no **passo «Conta admin»** (criação da conta do administrador no Carbonio).
**Regra de ouro:** dados do Self-Service **têm prioridade** sobre estado antigo do wizard guardado em `sessionStorage` (domínio/localPart de sessão anterior não pode apagar o que o utilizador acabou de declarar na landing).
---
## Origem dos dados (landing)
| Campo Self-Service | Label UI | Chave persistência |
|--------------------|----------|-------------------|
| E-mail corporativo do administrador | `admin@suaempresa.com.br` | `localStorage.ligbox_planned_email` |
| Senha | campo Senha | `sessionStorage.ibytera_onboard_admin_password` |
| Login portal | telefone/nickname | `sessionStorage` (portal login id — fora do escopo conta admin) |
**Botões equivalentes:** card **Self-Service** (hero) e CTA **«Criar Meu Servidor Agora»** (scroll para o mesmo card).
**Fluxos que disparam pré-preenchimento:**
1. **Registo** → 2FA TOTP → `finishOnboarding()` → redirect `/onboard`
2. **Login** (ou login + 2FA) → redirect `/onboard`
---
## Destino no wizard (passo Conta admin — step 2)
Ao abrir ou regressar a este passo, **três valores** devem estar preenchidos:
| # | Origem Self-Service | Campo wizard | Exemplo |
|---|---------------------|--------------|---------|
| 1 | E-mail corporativo completo | `localPart` + `domain` (parte local + domínio) | `admin` + `suaempresa.com.br` |
| 2 | Domínio extraído do e-mail | `domain` (passo 0 também) | `suaempresa.com.br` |
| 3 | Senha | `password` (mascarada, reutilização) | via `AdminPasswordField` |
**Passo 0 (Domínio):** se `ligbox_planned_email` existir, o campo domínio deve iniciar com o domínio do e-mail e mostrar banner informativo.
**Passo 3 (Rever e criar):** senha em modo `confirm` — mascarada, reutilizada; revelar com olho exige re-autenticação portal (2FA).
---
## Comportamento funcional
### FR-001 — Persistência imediata no registo
Após registo portal com sucesso (antes do TOTP), gravar:
- `setAdminPassword(password)`
- `localStorage.ligbox_planned_email` = e-mail corporativo normalizado (lowercase, trim)
### FR-002 — Prioridade Self-Service sobre wizard state
Se `ligbox_planned_email` **ou** senha em `sessionStorage` existirem ao montar `/onboard`:
- **Ignorar** `domain` / `localPart` / `notifyEmail` antigos de `ibytera_onboard_wizard_state` para pré-preenchimento
- Aplicar valores derivados do Self-Service
### FR-003 — Sincronização no mount
`useEffect` no wizard reaplica pré-preenchimento se o utilizador navegou landing → onboard na mesma aba.
### FR-004 — Senha não vai para wizard state JSON
Senha permanece **apenas** em `sessionStorage` (`onboardPassword.js`) — nunca em `saveWizardState()`.
### FR-005 — Revelação de senha
Ícone olho → modal re-autenticação portal (`PasswordRevealAuth`); visível 30s; opção «Definir senha diferente».
### FR-006 — Sem Self-Service
Utilizador entra directo em `/onboard` sem landing: campos vazios ou defaults (`admin`, domínio manual) — sem regressão.
---
## Critérios de aceitação
1. **Given** registo com `admin@empresa.com` + senha `MinhaSenh@8` + TOTP concluído, **When** abre `/onboard` passo Conta admin, **Then**`admin@empresa.com`, domínio `empresa.com`, senha reutilizada (mascarada).
2. **Given** wizard state antigo com domínio `outro.com` em sessionStorage, **When** novo registo com `admin@novo.com`, **Then** domínio no wizard é `novo.com` (não `outro.com`).
3. **Given** login com `planned_corporate_email` da API, **When** redirect `/onboard`, **Then** campos pré-preenchidos.
4. **Given** F5 na mesma aba após Self-Service, **When** wizard recarrega, **Then** e-mail/domínio/senha mantêm-se (localStorage + sessionStorage).
5. **Given** nova aba sem storage, **When** `/onboard` directo, **Then** sem pré-preenchimento (comportamento legítimo).
---
## Implementação (referência código VM112 — `/opt/ligbox-wizard`)
| Ficheiro | Função |
|----------|--------|
| `frontend/src/sessionPersist.js` | `beginOnboardingForEmail()`, `syncWizardWithPlannedEmail()`, `applyPlannedEmailPrefill()`, `loadWizardStateForOnboard()` |
| `frontend/src/portalAuth.js` | `setPortalOnboardCredentials()``sessionStorage.ligbox_onboard_password` |
| `frontend/src/onboardPassword.js` | alias leitura/escrita na mesma chave `ligbox_onboard_password` (wizard) |
| `frontend/src/ligbox/components/SelfServiceCard.jsx` | registo/login/TOTP → `beginOnboardingForEmail` + credenciais |
| `frontend/src/App.jsx` | `loadWizardStateForOnboard()` no init + `useEffect` de sync |
| `frontend/src/AdminPasswordField.jsx` | senha mascarada + reveal com `verifyStepUp` (2FA) |
---
## Fora de escopo
- Enviar senha para VM122 / webhooks / Desk (nunca)
- Pré-preencher a partir de cookies cross-domain
- Sincronizar com Carbonio antes de `POST /account/create`
---
## Regressão conhecida (corrigida nesta spec)
**Causas identificadas (2026-06-16):**
1. Wizard state antigo em `sessionStorage` (`ligbox_onboard_wizard_state`) mantinha `domain`/`localPart` de sessão anterior e bloqueava o e-mail novo do Self-Service.
2. Senha gravada em chave errada (`ibytera_onboard_admin_password` em código de dev) enquanto o portal em produção lia `ligbox_onboard_password`.
3. E-mail só ia para `localStorage` após TOTP completo — registo sem `beginOnboardingForEmail()` deixava o wizard sem âncora.
**Fix aplicado:**
- `syncWizardWithPlannedEmail()` + `ligbox_wizard_planned_email` como âncora — descarta wizard stale quando o e-mail muda.
- `loadWizardStateForOnboard()` aplica sempre domínio/localPart/notify a partir de `ligbox_planned_email`.
- `SelfServiceCard` chama `beginOnboardingForEmail()` + `setPortalOnboardCredentials()` no registo, login e fim do TOTP.