# Arquitetura — Plataforma de Mídia Wi-Fi Local

## 1. Objetivo

Construir uma **plataforma de mídia digital local**. O Wi-Fi público é o canal de distribuição; o produto é a mídia (vídeo publicitário/institucional), com medição, aprovação e, no futuro, comercialização de campanhas.

O visitante conecta-se ao hotspot, vê um vídeo (~30 s) no portal cativo e, ao concluir, recebe internet gratuita por um período (inicialmente **15 minutos**).

O MikroTik **não** entra nesta etapa. Toda liberação passa por uma abstração (`HotspotGateway`), com implementação de simulação agora e MikroTik depois, sem reescrever controllers.

---

## 2. Diagnóstico do projeto atual (Fase 0)

| Item | Situação |
|------|----------|
| Caminho | `/var/www/html/wifi` |
| Domínio | `https://wifi.restor.app.br` (Apache + Let's Encrypt) |
| PHP | 8.3.6 |
| Laravel | 13.26.1 (`laravel/framework` ^13.17) |
| Banco | MySQL 8, database `wifi`, charset utf8mb4 |
| Tabelas hoje | `users`, `sessions`, `password_reset_tokens`, `cache`, `jobs` (skeleton) |
| Usuários | 0 registros |
| Autenticação | Guard `web` Eloquent pronto; **sem login, policies, roles ou starter kit** |
| Frontend | Vite 8 + Tailwind CSS 4 + Blade. Fonte padrão: Instrument Sans |
| `package.json` | Apenas Vite, Tailwind, laravel-vite-plugin, concurrently |
| `node_modules` | **Ausente** (npm ainda não foi instalado neste servidor) |
| App | Só `User`, `Controller`, `AppServiceProvider` |
| Rotas | `/` → `welcome.blade.php`; health `/up` |
| Tests | PHPUnit 12, sqlite `:memory:`, testes de exemplo |
| Storage | Disk `local` = `storage/app/private` (já adequado a vídeo privado) |
| Fila / cache / sessão | `database` no `.env` |
| Redis | Configurado, não obrigatório agora |
| Dependências extras | Nenhuma (Tinker, Pint, PHPUnit em dev) |

**Conflitos / riscos**

- A welcome page e Instrument Sans **não** serão a identidade do produto.
- Sem Breeze/Livewire/Filament: evita visual de template; o painel será Blade próprio.
- `node_modules` vazio: o build Vite precisa ser feito na Fase 1 (dependências **já listadas**, sem pacote novo).
- Integração MikroTik **não** deve vazar para HTTP controllers.

**O que reutilizar**

- Laravel 13, PHP 8.3, MySQL, Blade, Tailwind 4, Vite, PHPUnit, disk `local` privado, jobs/cache já migrados.
- Não substituir stack. Não instalar starter kit genérico.

---

## 3. Princípios

1. Duas superfícies de UI, nunca misturadas: **Painel** (admin/anunciante) e **Portal** (usuário do Wi-Fi).
2. Fases curtas; não avançar sem aprovação.
3. Regras de negócio em Services; validação em Form Requests; autorização em Policies.
4. Não confiar no cliente para “vídeo concluído”.
5. LGPD: sessão do portal é anônima; sem PII desnecessária.
6. Simples e robusto: sem overengineering, mas com ganchos claros para MikroTik, cobrança e escala.

---

## 4. Arquitetura lógica

```
┌─────────────────────────────────────────────────────────────┐
│  Painel  /painel/*     (Blade + sidebar)                    │
│  Admin | Anunciante (mesmo layout, menu e policies)         │
└───────────────────────────┬─────────────────────────────────┘
                            │ Auth session Laravel
┌───────────────────────────┴─────────────────────────────────┐
│  Application                                                │
│  Controllers → FormRequests → Policies                      │
│  Services (Campaign, Video, Metrics, Playback, Reports)     │
│  HotspotGateway (contrato)                                  │
│     ├─ SimulationHotspotGateway   (agora)                   │
│     └─ MikrotikHotspotGateway     (Fase 9, stub até lá)     │
└──────────────┬──────────────────────────────┬───────────────┘
               │ MySQL                        │ storage privado
┌──────────────┴──────────┐    ┌──────────────┴───────────────┐
│ Domain data + métricas  │    │ Vídeos / thumbs / logos      │
└─────────────────────────┘    └──────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  Portal  /portal/*     (mobile first, sem sidebar)          │
│  Identifica sessão → escolhe campanha → player → heartbeat  │
│  → conclusão validada no servidor → HotspotGateway::grant() │
└─────────────────────────────────────────────────────────────┘
```

**Namespaces previstos (criar só quando a fase exigir)**

- `App\Http\Controllers\Panel\...`
- `App\Http\Controllers\Portal\...`
- `App\Domain\Hotspot\` (contrato + implementações)
- `App\Services\...`
- `App\Policies\...`
- `App\Enums\` (roles, status de vídeo/campanha)

---

## 5. Perfis

| Perfil | Login | Escopo |
|--------|--------|--------|
| Administrador | `users.role = admin` | Todo o ecossistema |
| Anunciante | `users.role = advertiser` + `advertiser_id` | Somente a própria empresa, vídeos, campanhas e métricas |
| Visitante Wi-Fi | Sem conta Laravel | Apenas portal cativo; sessão anônima própria |

Um anunciante pode ter **um usuário** no início (responsável). Vários usuários por anunciante fica para evolução, sem bloquear o modelo (`users.advertiser_id`).

---

## 6. Módulos (painel)

Agrupamento da sidebar (admin). Anunciante vê só o que a policy permitir.

| Grupo | Módulo | Fase |
|-------|--------|------|
| Visão | Dashboard | 1 (esqueleto), 8 (métricas reais) |
| Cadastros | Administradores, Anunciantes, Empresas | 1 (admins), 2 |
| Rede | Pontos de Wi-Fi | 4 (cadastro mínimo), detalhe ao longo |
| Mídia | Vídeos, Campanhas | 3, 4 |
| Audiência | Impressões, Interações/Cliques | 7 |
| Inteligência | Relatórios | 8 |
| Sistema | Configurações | 1 (mínimo: tempo de grant, driver hotspot) |

Portal **não** entra na sidebar.

---

## 7. Identidade visual (proposta)

**Nome de trabalho:** Farol  
**Promessa:** mídia no ponto de conexão — sólido, local, confiável.  
Nome comercial final pode mudar sem custo técnico.

**Por que não “SaaS roxo”**

- Sem gradiente de marca, sem roxo neon, sem cards coloridos empilhados.
- Superfície de papel quente + tinta quase preta + um acento de sinal (âmbar queimado) + um acento de rede (teal escuro).
- Tipografia corporativa (IBM Plex), não Instrument Sans / Inter / fonte “AI default”.

| Token | Uso | Valor proposto |
|-------|-----|----------------|
| Ink | sidebar, títulos | `#14181C` |
| Paper | fundo da aplicação | `#F3EEE6` |
| Surface | painéis, tabelas | `#FFFbf7` |
| Line | bordas | `#D8D1C6` |
| Ember | ação primária, “no ar” | `#C45C26` |
| Harbor | conectividade, links secundários | `#2C5F6B` |
| Mute | texto auxiliar | `#5C5852` |
| Danger | erros | `#9B2C2C` |

**UI**

- Sidebar escura (Ink), ícones lineares, agrupadores por seção, recolhível no desktop, drawer no mobile.
- Header baixo: contexto da página, usuário, ponto/ambiente.
- Tabelas como ferramenta de trabalho (densidade média, não cards de KPI em excesso).
- Portal: tela cheia, tipografia grande, player central, zero chrome de dashboard.

**Frontend:** Blade + CSS utilitário Tailwind 4 com tokens no `@theme`. Sem Livewire/Filament/Breeze, salvo aprovação explícita.

---

## 8. Modelo de dados

Princípio: normalizar cadastros; métricas em tabelas de fato ligadas a uma **sessão cativa**. Não reutilizar a tabela Laravel `sessions` para o visitante do Wi-Fi.

### 8.1 Cadastro e acesso

**users** (estender a tabela existente)

- `role` enum: `admin`, `advertiser`
- `status` enum: `active`, `inactive`
- `advertiser_id` nullable FK
- campos atuais de auth permanecem

**companies**

- `legal_name`, `trade_name`, `document` (CNPJ, unique nullable)
- `email`, `phone`, `whatsapp`
- `address_line`, `city`, `state`, `postal_code`
- `logo_path` (storage privado ou público controlado)
- `status`

**advertisers**

- `company_id` unique (1:1 no início; abre para 1:N depois)
- `contact_name`, `email`, `phone`, `whatsapp`
- `status`
- timestamps de convite/ativação quando existirem

Um anunciante **é** a conta comercial; a empresa é o cadastro jurídico/marca. O módulo “Empresas” edita `companies`; “Anunciantes” edita a conta + vínculo com usuário.

### 8.2 Rede e mídia

**wifi_points**

- `name`, `slug`/`code` (identificador estável)
- `description`, `address`, `city`
- `status` (`active`, `inactive`)
- `controller_ref` nullable (NAS / identity MikroTik, Fase 9)
- `grant_seconds` nullable (override; default nas settings)

**videos**

- `advertiser_id`
- `title`, `description`
- `disk`, `path`, `original_filename`
- `mime`, `size_bytes`, `duration_ms` (fonte de verdade no servidor)
- `thumbnail_path` nullable
- `status`: `submitted`, `in_review`, `approved`, `rejected`, `published`, `paused`, `finished`
- `submitted_at`, `reviewed_at`, `published_at`, `rejected_reason`

**campaigns**

- `advertiser_id`, `name`, `description`
- `starts_at`, `ends_at`
- `status`: `draft`, `scheduled`, `active`, `paused`, `ended`
- `impression_limit` nullable (preparado para cobrança)
- `contracted_quantity` nullable

**campaign_video** (N:N, 1 vídeo por campanha no início)

- `campaign_id`, `video_id`, `sort_order`, `is_primary`

**campaign_wifi_point**

- `campaign_id`, `wifi_point_id`

### 8.3 Audiência (anônima)

**captive_sessions**

- `public_id` (UUID)
- `wifi_point_id`
- `campaign_id`, `video_id` (escolhidos na apresentação)
- `started_at`, `last_seen_at`
- `client_hint` opcional: hash truncado de IP+UA (não armazenar IP cru se puder evitar; se necessário para MikroTik, documentar retenção)
- `mikrotik_session_ref` nullable
- **sem nome, e-mail, telefone, MAC em texto claro**

**impressions**

- `captive_session_id`, `campaign_id`, `video_id`, `advertiser_id`, `wifi_point_id`
- `occurred_at`
- criada quando a campanha é **apresentada** (player carregado)

**video_views**

- `impression_id`
- `started_at`, `completed_at` nullable
- `abandoned_at` nullable
- `watched_ms` (atualizado por heartbeat)
- `complete_method` (`heartbeat`, nunca “botão do cliente”)

**clicks**

- `impression_id` / `captive_session_id`
- `url`, `occurred_at`
- tipo (`cta`, `logo`, …) se necessário

**hotspot_authorizations**

- `captive_session_id`
- `granted_seconds` (900)
- `driver` (`simulation`, `mikrotik`)
- `status` (`granted`, `failed`, `expired`)
- `gateway_payload` json mínimo (sem senha)
- `granted_at`, `expires_at`

**settings**

- `key`, `value` (json)
- exemplos: `grant_seconds=900`, `hotspot_driver=simulation`, `max_video_mb`, `completion_ratio=0.95`

Índices previstos: FKs + `(wifi_point_id, occurred_at)` em impressões + `(campaign_id, occurred_at)` para relatórios.

---

## 9. Fluxos

### 9.1 Visitante Wi-Fi (portal)

1. Hotspot redireciona para `/portal` (hoje: URL direta / modo simulação com `?point=`).
2. Aplicação identifica **ponto** (query, hostname ou, no futuro, NAS).
3. Cria `captive_session` anônima.
4. Seleciona campanha ativa daquele ponto (regra simples na Fase 5: uma campanha ativa; rotação na Fase 7 se necessário).
5. Cria `impression`, emite **token de playback assinado**.
6. Player inicia; servidor registra `video_views.started_at`.
7. Heartbeats autenticados pelo token atualizam `watched_ms`. Seek à frente além do assistido é **rejeitado**.
8. Conclusão: `watched_ms >= duration_ms * completion_ratio` **e** tempo de parede coerente. Sem botão “concluí” como prova.
9. `HotspotGateway::grant(session, seconds)` → grava `hotspot_authorizations`.
10. UI: “Internet liberada por 15 minutos.” (simulação). Depois: login no MikroTik.

Abandono: heartbeat para ou aba fecha → `abandoned_at` se não completou.

### 9.2 Anunciante

1. Admin cadastra empresa + anunciante + usuário.
2. Anunciante entra no painel (mesmo layout, menu reduzido).
3. Envia vídeo → `submitted` / `in_review`.
4. Não publica sozinho.
5. Após `published`, cria campanha, escolhe período e pontos (pontos: só os que o admin permitir; no início todos os pontos ativos, se não houver restrição extra).
6. Acompanha métricas **somente** das próprias campanhas.

### 9.3 Administrador

1. Login no painel completo.
2. Cadastros: admins, empresas, anunciantes, pontos.
3. Fila de vídeos: analisar, aprovar/reprovar, publicar, pausar.
4. Campanhas: visão global, pausar, encerrar.
5. Configurações: tempo de grant, driver do hotspot, limites de upload.
6. Relatórios globais por período, ponto, campanha, anunciante.

---

## 10. Integração MikroTik (preparação)

**Contrato** `App\Domain\Hotspot\HotspotGateway`

```
grant(CaptiveSession $session, int $seconds): GrantResult
revoke(CaptiveSession $session): void
isAvailable(): bool
```

- `config('hotspot.driver')` = `simulation` | `mikrotik`
- Binding no `AppServiceProvider` (ou provider dedicado).
- Controllers do portal chamam **somente** o contrato.
- `SimulationHotspotGateway`: persiste autorização e devolve sucesso (Fase 6).
- `MikrotikHotspotGateway`: classe existente desde cedo, métodos `throw new RuntimeException('Não configurado')` até a Fase 9.
- Nenhum SDK RouterOS até a Fase 9 e aprovação de dependência.

Quando o equipamento existir: implementar login de usuário hotspot / IP binding / RADIUS conforme o modelo real do cliente, **sem** mudar o fluxo do portal.

---

## 11. Armazenamento e reprodução de vídeo

| Decisão | Detalhe |
|---------|---------|
| Disk | `local` → `storage/app/private/videos` |
| Público | **não** colocar MP4 em `public/` |
| Entrega | rota autenticada por token assinado (`URL::temporarySignedRoute` ou token de playback de curta duração) |
| Player | HTML5 no portal; Range requests no controller de stream |
| Thumb | gerada na Fase 3 se houver ferramenta no servidor; senão upload opcional de thumbnail |
| Duração | gravar `duration_ms` no servidor (FFmpeg/`ffprobe` **somente se já existir no SO**; senão biblioteca PHP ou extração na aprovação). **Não instalar pacote sem consultar.** |
| Limites | MIME `video/mp4` (inicial), tamanho em `settings` |
| Acesso direto | path opaco (UUID), policy: portal só com token válido; painel só dono ou admin |

Escala futura: mesmo contrato de disk permite S3 sem mudar o portal.

---

## 12. Conclusão do vídeo (anti-fraude básica)

Não é DRM. É o suficiente para não aceitar um POST “completed=true`.

1. Token de playback com `impression_id`, `video_id`, `exp`.
2. Heartbeat periódico (ex.: 5 s) com `position_ms` + `watched_ms`.
3. Servidor recusa `position_ms` muito à frente de `watched_ms`.
4. Completo só com `watched_ms` ≥ 95% de `duration_ms` (configurável).
5. Rate limit nos heartbeats.
6. Uma autorização por `captive_session` (idempotente).

---

## 13. Segurança e LGPD

- Auth Laravel (CSRF, hashed passwords, session).
- Policies por modelo (`Advertiser`, `Video`, `Campaign`, …).
- Form Requests em todo POST/PUT do painel.
- Upload: MIME + extensão + tamanho; não executar arquivos.
- Portal: rate limit por IP nas rotas de heartbeat e grant.
- Logs de aplicação para grant e aprovação de vídeo.
- Métricas sem PII; retenção a definir nas configurações (não implementar purge na Fase 0).
- `APP_DEBUG` deve ir para `false` em produção quando o painel estiver público com dados reais (avisar antes de mudar servidor).

---

## 14. Métricas e relatórios

Eventos canônicos: impressão, início, conclusão, abandono, clique, sessão, autorização.

Relatórios (Fase 8) agregam por dia:

- impressões, inícios, conclusões, taxa de conclusão, cliques, CTR
- sessões / autorizações
- dimensão: campanha, anunciante, ponto, período (hoje, ontem, 7d, 30d, custom)

Dashboard Fase 1: layout e números zerados ou placeholder honesto (“sem dados ainda”). Números reais só após Fase 7.

---

## 15. Testes

PHPUnit já configurado. Críticos por fase:

- Policy anunciante não lê o outro
- Upload rejeita MIME inválido
- Heartbeat rejeita seek e aceita conclusão só com watched_ms
- Gateway de simulação grava `hotspot_authorizations`
- Relatório filtra por `advertiser_id`

---

## 16. Fora de escopo até aprovação

- App mobile nativo
- Pagamento / gateway (Fase 10)
- Integração física MikroTik (Fase 9)
- Múltiplos vídeos rotacionados com algoritmo complexo
- Cadastro self-service de anunciante
- Livewire, Filament, Breeze, Inertia, Redis obrigatório, S3 obrigatório
