Implementa o MVP do Adaptador de Planilhas no Elo Pro.
O módulo usa a identidade Supabase, isola cada workspace e registra storage, TTL, runner e biblioteca XLSX como decisões ainda abertas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
6e23f1533b
144 files changed
+19060
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# Auditoria de integração — Adaptador de Planilhas
|
||||
|
||||
Data da inspeção: 2026-10-02. O diretório de trabalho era um repositório Git vazio, sem aplicação Elo Pro.
|
||||
|
||||
## O que não existia para reutilizar
|
||||
|
||||
- App Router, clientes Supabase, middleware e logout
|
||||
- Resolução de `workspace_id` e `workspace_members`
|
||||
- Papéis, RLS, feature flags, design system, TanStack Query, Sentry, Redis, audit log e storage
|
||||
- Padrão de Route Handlers, Vitest, CI e migrations
|
||||
|
||||
## Diferença em relação ao master context
|
||||
|
||||
O contexto pede para encaixar o módulo no Elo Pro existente. Como não havia código de plataforma, este repositório implementa o Adaptador como aplicação Next.js pronta para compartilhar a mesma identidade, com um shim SQL das tabelas `profiles`, `workspaces`, `workspace_members`, `workspace_module_flags` e `audit_log`. Essas tabelas não têm senha, hash ou refresh token. Em um monorepo Elo Pro real, a migration `0001_platform_shim.sql` não deve recriar o que já existe (`create table if not exists`).
|
||||
|
||||
## Decisões já fechadas pelo escopo V3
|
||||
|
||||
Identidade única via Supabase Auth, tenant `workspace_id`, RLS, limites do MVP, mapeamento manual 1:1, prévia obrigatória, modelo imutável, flag de módulo e papéis owner/admin/gestor/agent.
|
||||
|
||||
## OPEN DECISIONS
|
||||
|
||||
Registradas em `docs/decisions/`:
|
||||
|
||||
1. Object storage definitivo
|
||||
2. Topologia de cookie/login entre hosts
|
||||
3. Job runner durável
|
||||
4. TTL de temporários
|
||||
5. Biblioteca XLSX de produção
|
||||
6. Exclusão física versus soft delete
|
||||
|
||||
## Arquitetura
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
browser[Browser Elo Pro] --> session[Sessão Supabase]
|
||||
session --> app[Next.js Adaptador]
|
||||
app --> domain[Domínio de planilhas]
|
||||
app --> pg[(PostgreSQL + RLS)]
|
||||
app --> redis[(Redis lock e progresso)]
|
||||
app --> storage[Storage privado]
|
||||
app --> sentry[Sentry sem células]
|
||||
```
|
||||
|
||||
## Fases
|
||||
|
||||
Fundação e flag, autorização, schema, shell visual, modelos, fontes, mapeamento, jobs, Redis, auditoria/Sentry, testes, operação e revisão de gaps. A implementação seguiu essa ordem no mesmo repositório.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Fundação, rotas e feature flag
|
||||
|
||||
## Rotas de página
|
||||
|
||||
- `/login` redireciona ao login central (`ELO_PRO_LOGIN_URL`) ou, só fora de produção, oferece identidades locais sem senha
|
||||
- `/adaptador` dashboard
|
||||
- `/adaptador/operacao` jornada
|
||||
- `/adaptador/configuracoes` somente owner/admin
|
||||
|
||||
`loading.tsx` e `error.tsx` cobrem espera e falha recuperável. Sem sessão, o proxy manda para `/login`. Sem a flag `spreadsheet_adapter`, a página informa que o módulo está desligado e as APIs respondem `FEATURE_DISABLED`.
|
||||
|
||||
## APIs
|
||||
|
||||
- `GET/POST /api/spreadsheet-adapter/models`
|
||||
- `DELETE /api/spreadsheet-adapter/models/:id`
|
||||
- `GET/PATCH /api/spreadsheet-adapter/settings`
|
||||
- `POST /api/spreadsheet-adapter/sources/validate`
|
||||
- `POST /api/spreadsheet-adapter/jobs`
|
||||
- `GET /api/spreadsheet-adapter/jobs/:id`
|
||||
- `PUT /api/spreadsheet-adapter/jobs/:id/mappings`
|
||||
- `POST /api/spreadsheet-adapter/jobs/:id/preview`
|
||||
- `POST /api/spreadsheet-adapter/jobs/:id/process`
|
||||
- `PATCH/DELETE /api/spreadsheet-adapter/jobs/:id/sources/:sourceId`
|
||||
- `GET /api/spreadsheet-adapter/jobs/:id/download?format=xlsx|csv|pdf&sourceId=`
|
||||
|
||||
O `workspace_id` do corpo é ignorado como tenant. Se vier diferente da sessão, a chamada é negada e auditada.
|
||||
|
||||
## Códigos de erro estáveis
|
||||
|
||||
`UNAUTHENTICATED`, `WORKSPACE_FORBIDDEN`, `FEATURE_DISABLED`, `FORBIDDEN`, `TOO_MANY_MODELS`, `DUPLICATE_MODEL_NAME`, `TOO_MANY_SOURCES`, `FILE_TOO_LARGE`, `TOO_MANY_ROWS`, `UNSUPPORTED_FORMAT`, `MIME_MISMATCH`, `INVALID_STRUCTURE`, `ZIP_BOMB`, `EMPTY_HEADER`, `DUPLICATE_HEADER`, `HEADER_ROW_INVALID`, `SHEET_REQUIRED`, `TYPE_INCONSISTENT`, `MAPPING_CONFLICT`, `MAPPING_INCOMPLETE`, `MAPPING_UNKNOWN`, `MODEL_IN_USE`, `JOB_LOCKED`, `REDIS_UNAVAILABLE`, `MODEL_TAMPERED`, `NOT_FOUND`.
|
||||
|
||||
## Contratos
|
||||
|
||||
Upload de modelo: `multipart` com `file`, `name`, `sheet` opcional, `headerRow`, `requiredColumns` (JSON). Sem `requiredColumns`, todas as colunas nascem obrigatórias e a tela permite desmarcar.
|
||||
|
||||
Validação de fonte: `file`, `jobId`, `sheet`, `headerRow`. Várias abas devolvem `409 SHEET_REQUIRED` com a fonte já armazenada para troca de aba sem novo upload. Tipo misto devolve `422` e preserva as outras fontes.
|
||||
|
||||
Prévia: `{ preview: true, columns, rows, rowCount, truncated }`. Colunas na ordem do modelo. Não grava saída nem altera o modelo.
|
||||
|
||||
Temporários: `expires_at = agora + SPREADSHEET_TEMP_TTL_SECONDS`. Em produção a variável é obrigatória. Fora de produção, a ausência usa 3600 segundos apenas como apoio de desenvolvimento, não como política de produto.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Autenticação, workspace, permissões e RLS
|
||||
|
||||
## Fluxo
|
||||
|
||||
1. Supabase Auth (`getUser`) quando `NEXT_PUBLIC_SUPABASE_URL` e a anon key existem.
|
||||
2. Workspace ativo no cookie `elo_pro_workspace_id`, validado contra `workspace_members`.
|
||||
3. Fora de produção, `ADAPTER_DEV_AUTH=1` aceita um cookie assinado sem senha, só para identidades já existentes no banco.
|
||||
4. A flag `workspace_module_flags.module_key = spreadsheet_adapter` precisa estar ligada.
|
||||
5. Logout chama `supabase.auth.signOut()` e apaga o cookie local de desenvolvimento.
|
||||
6. `src/proxy.ts` só redireciona quem não tem cookie de sessão. A autorização real continua no servidor.
|
||||
|
||||
Não existe formulário de senha, tabela de credencial ou refresh token do Adaptador. Superadmin (`profiles.is_superadmin`) não ganha bypass dentro do módulo.
|
||||
|
||||
## Fonte do workspace
|
||||
|
||||
O identificador vem do cookie de plataforma ou da sessão de desenvolvimento. Consultas usam o valor já validado no servidor. Payload do browser não escolhe o tenant.
|
||||
|
||||
## Matriz
|
||||
|
||||
| Ação | Owner/Admin | Gestor/Agent |
|
||||
| --- | --- | --- |
|
||||
| Usar módulo habilitado | Sim | Sim |
|
||||
| Operar fonte, mapa, prévia, processo e download | Sim | Sim |
|
||||
| Enviar modelo | Sim | Só se `allow_non_admin_model_upload` |
|
||||
| Excluir modelo e alterar a flag de upload | Sim | Não |
|
||||
|
||||
## RLS
|
||||
|
||||
`migrations/0003_rls.sql` liga e força RLS. O aplicativo faz `set local role app_user` e `app.user_id`. Funções `security definer` resolvem membership, flag, upload e job ativo sem vazamento entre workspaces. A conexão de serviço (worker, seed, flag, limpeza) não passa por `app_user` e permanece só no servidor.
|
||||
|
||||
## Testes
|
||||
|
||||
`tests/integration/rls.test.ts` cobre leitura/escrita cruzada, insert bloqueado por RLS, flag desligada mesmo com contexto mentiroso e agent impedido de gravar settings. `tests/integration/journey.test.ts` cobre agent sem upload, upload após a setting e exclusão negada.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Modelo de dados
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
workspaces ||--o| spreadsheet_adapter_settings : configura
|
||||
workspaces ||--o{ spreadsheet_models : possui
|
||||
workspaces ||--o{ spreadsheet_jobs : isola
|
||||
profiles ||--o{ spreadsheet_jobs : executa
|
||||
spreadsheet_models ||--o{ spreadsheet_jobs : orienta
|
||||
spreadsheet_jobs ||--o{ spreadsheet_job_sources : contem
|
||||
spreadsheet_job_sources ||--o{ spreadsheet_job_mappings : mapeia
|
||||
workspaces ||--o{ audit_log : audita
|
||||
```
|
||||
|
||||
Migrations: `0001_platform_shim.sql`, `0002_spreadsheet_adapter.sql`, `0003_rls.sql`. Rollback do módulo: `migrations/down/0002_spreadsheet_adapter_down.sql`. Não remove tabelas de identidade do Elo Pro.
|
||||
|
||||
## Integridade
|
||||
|
||||
- Nome lógico único entre modelos `active` por workspace
|
||||
- No máximo 10 modelos ativos e 10 fontes por job, também por trigger
|
||||
- Mapeamento único nos dois sentidos (`source_column` e `model_column`)
|
||||
- Job não referencia modelo de outro workspace
|
||||
- Conteúdo da planilha fica no storage, não em JSONB de linhas
|
||||
- `status = deleted` e `deleted_at` deixam a exclusão física reversível até a decisão de produto
|
||||
|
||||
Colunas extras em fontes (`output_csv_key`, `output_xlsx_key`, `columns_schema`, `process_idempotency_key`) guardam metadados da saída temporária. O PDF é gerado na hora do download a partir do CSV privado.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Shell de UX
|
||||
|
||||
Não havia Design System publicado no repositório. Os componentes em `src/components/ui.tsx` seguem os tokens do escopo V3: Cloud White `#FBFBFE`, Midnight Ink `#071B45`, Bright Blue `#2F8FFF`, Aqua Signal `#00C2A8`, Warning `#F39A2D`, Error `#E63946`, Manrope e raio de 10/14/18/24 px. O azul de ação `#0B3D91` está registrado como provisório em `docs/decisions/0006-action-blue.md` porque o token oficial não estava no repo.
|
||||
|
||||
## Páginas
|
||||
|
||||
Dashboard, jornada (modelo, fontes, mapeamento, revisão, processamento, resultado) e configurações. `JourneyTrail` mostra continuidade da jornada, com texto de etapa concluída, não só cor.
|
||||
|
||||
## Componentes
|
||||
|
||||
Button, IconButton, Input, Select, Switch, Dialog, Card, SemanticMarker, estados de loading, vazio e erro. Lucide para ícones. Mapeamento usa `@dnd-kit` e um `select` com o mesmo efeito, utilizável por teclado.
|
||||
|
||||
## O que a interface não faz
|
||||
|
||||
Não inventa modelos de demonstração. Lista vazia é estado vazio. Flag desligada não abre a jornada.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Segurança e observabilidade
|
||||
|
||||
## Auditoria
|
||||
|
||||
`audit_log` recebe upload e exclusão de modelo, mudança da setting, criação, início, conclusão, falha e download. Metadados passam por lista de chaves bloqueadas e só aceitam escalares. O teste de jornada procura o token `=CMD()` no log e não o encontra.
|
||||
|
||||
## Sentry
|
||||
|
||||
`beforeSend` remove body, cookies, breadcrumbs e headers de autorização. Tags previstas: módulo, `job_id`, `workspace_id`, `error_code`. Validação de negócio (`AdapterError` abaixo de 500) não é capturada. Sem DSN, a inicialização não ocorre.
|
||||
|
||||
## Controles
|
||||
|
||||
- RLS e checagem de papel no serviço
|
||||
- Extensão conferida com assinatura ZIP/OLE
|
||||
- Rejeição de XLS, ODS, XML, MIME incompatível e ZIP com expansão acima de 256 MB ou taxa anômala
|
||||
- Chave de storage sem `..` e, no disco local, resolvida dentro do diretório privado
|
||||
- CSV/XLSX de saída neutraliza células que começam com `=`, `+`, `@` ou hífen que não é número
|
||||
- Download exige sessão, workspace, fonte concluída e `expires_at` futuro
|
||||
- Lock Redis (ou falha `REDIS_UNAVAILABLE`) antes de processar; clique repetido com a mesma chave de idempotência devolve o job existente
|
||||
- Nomes exibidos são basename sanitizado
|
||||
|
||||
## Threat model resumido
|
||||
|
||||
| Ameaça | Mitigação | Residual |
|
||||
| --- | --- | --- |
|
||||
| IDOR / workspace forjado | Sessão + RLS + auditoria | Conexão de serviço ignora RLS e só pode existir no servidor |
|
||||
| Bypass de papel | Serviço e policies | — |
|
||||
| Planilha em log | Scrub e auditoria escalar | Erro inesperado ainda pode ir ao Sentry se o `throw` embutir dado; o código de domínio evita isso |
|
||||
| ZIP bomb | Teto de expansão antes do ExcelJS | Estruturas com data descriptor continuam limitadas pelos 10 MB de entrada |
|
||||
| CSV injection | Prefixo `'` | Números negativos permanecem numéricos de propósito |
|
||||
| Job duplicado | Lock + idempotência | Lock em memória vale para um processo; produção exige Redis |
|
||||
|
||||
## Riscos residuais
|
||||
|
||||
PDF limita 200 linhas e 12 colunas e declara a limitação no próprio arquivo. XLSX/CSV levam o resultado completo. A formatação visual do modelo não é copiada: a saída é um arquivo novo e o binário do modelo é conferido por SHA-256 antes e depois.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Plano de testes e resultado
|
||||
|
||||
Comandos: `npm test`, `npm run typecheck`. A suíte abaixo passou na execução local desta entrega, junto com o typecheck.
|
||||
|
||||
| Área | Evidência | Resultado |
|
||||
| --- | --- | --- |
|
||||
| Cabeçalho, tipos, 1:1, permissões, nomes, CSV, cache por workspace | `tests/unit/domain.test.ts` | Passou |
|
||||
| Lock, expiração e scrub do Sentry | mesmo arquivo | Passou |
|
||||
| 50.000 linhas CSV | `tests/performance/rows.test.ts` | Passou |
|
||||
| Path traversal, MIME, XLS, ZIP | `tests/security/files.test.ts` | Passou |
|
||||
| Switch, dialog, select, loading, vazio, erro | `tests/component/ui.test.tsx` | Passou |
|
||||
| Migrations e rollback do módulo | `tests/integration/migrations.test.ts` | Passou |
|
||||
| RLS A/B, agent versus owner | `tests/integration/rls.test.ts` | Passou |
|
||||
| Jornada modelo → mapa → prévia → processo → download, modelo intacto, idempotência, limites, aba, Redis indisponível, TTL, workspace forjado | `tests/integration/journey.test.ts` | Passou |
|
||||
|
||||
## Não executado aqui
|
||||
|
||||
- E2E de browser contra o login real do Elo Pro. Não há host do ecossistema neste repositório. A jornada de API cobre o mesmo fluxo de negócio.
|
||||
- XLSX de 10 MB e 50.000 linhas pelo ExcelJS. O teto de 10 MB é rejeitado antes do parse. As 50.000 linhas foram medidas no caminho CSV/domínio.
|
||||
- Dois processos OS com Redis real. O lock em memória cobre dois usuários no mesmo processo; usuários diferentes usam chaves diferentes.
|
||||
|
||||
Não há bloqueio de teste vermelho na suíte automatizada. Os bloqueios de release estão na revisão final e são decisões de plataforma, não falhas desta suíte.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Deploy e operação
|
||||
|
||||
## Ordem
|
||||
|
||||
1. Conferir envs sem colar segredo no repositório.
|
||||
2. `npm run migrate` na `DATABASE_URL` do ambiente.
|
||||
3. Subir a aplicação com a flag ainda desligada.
|
||||
4. Subir `npm run worker` e o Redis do ambiente.
|
||||
5. Confirmar bucket privado. Não criar bucket público.
|
||||
6. Smoke test no workspace piloto e só então `npm run flag -- <workspace_id> on`.
|
||||
|
||||
## Variáveis
|
||||
|
||||
| Nome | Uso |
|
||||
| --- | --- |
|
||||
| `NEXT_PUBLIC_SUPABASE_URL` | Auth no browser/servidor |
|
||||
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Chave pública do Auth |
|
||||
| `SUPABASE_SERVICE_ROLE_KEY` | Storage no servidor. Nunca `NEXT_PUBLIC_` |
|
||||
| `SUPABASE_STORAGE_BUCKET` | Bucket privado |
|
||||
| `DATABASE_URL` | PostgreSQL. Obrigatório em produção |
|
||||
| `REDIS_URL` | Lock. Sem ela o processamento recusa iniciar |
|
||||
| `SENTRY_DSN` / `NEXT_PUBLIC_SENTRY_DSN` | Observabilidade |
|
||||
| `SENTRY_ENVIRONMENT` / `SENTRY_RELEASE` | Ambiente e release |
|
||||
| `ELO_PRO_LOGIN_URL` | Login central |
|
||||
| `SPREADSHEET_TEMP_TTL_SECONDS` | Retenção. Obrigatório em produção; valor de produto ainda aberto |
|
||||
| `ADAPTER_JOB_MODE` | `worker` em produção, `inline` em desenvolvimento |
|
||||
| `ADAPTER_DEV_AUTH` | Só fora de produção |
|
||||
| `ADAPTER_DEV_AUTH_SECRET` | Assina o cookie local. Não é senha de usuário |
|
||||
|
||||
O cookie de workspace lido pela aplicação chama-se `elo_pro_workspace_id`.
|
||||
|
||||
## Rollback
|
||||
|
||||
- Aplicação: voltar o deploy anterior.
|
||||
- Flag: `npm run flag -- <workspace_id> off`.
|
||||
- Schema: `migrations/down/0002_spreadsheet_adapter_down.sql` remove só tabelas do módulo.
|
||||
- Jobs: parar o worker. Jobs `queued` não avançam. Jobs `processing` sem lock viram `INTERRUPTED` na próxima varredura.
|
||||
|
||||
## Smoke
|
||||
|
||||
Login Elo Pro sem segunda senha; workspace piloto com flag ligada; upload de modelo; fonte; aba ou cabeçalho se preciso; mapeamento; prévia; download XLSX, CSV e PDF; workspace sem flag recebe bloqueio; download sem sessão recebe 401.
|
||||
|
||||
## Runbook
|
||||
|
||||
- Job travado em `processing`: confirmar se o lock Redis ainda existe. Se expirou, o worker marca `INTERRUPTED`. O usuário pode tentar de novo.
|
||||
- Redis indisponível: APIs de leitura seguem; processamento responde `REDIS_UNAVAILABLE` e não grava saída.
|
||||
- Storage indisponível: upload falha com `STORAGE_FAILURE` ou 503 e não deixa modelo sem arquivo.
|
||||
- Migration parcial: `schema_migrations` só registra arquivo aplicado por completo. Reexecutar `npm run migrate` continua do próximo arquivo. Não editar migration já aplicada.
|
||||
- Limpeza: `npm run cleanup` apaga blobs em chaves `/tmp/` expiradas e não mexe em `/models/`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Revisão final contra o escopo V3
|
||||
|
||||
| Requisito | Status | Evidência |
|
||||
| --- | --- | --- |
|
||||
| Sem login, senha ou token próprio | OK | `src/server/auth`, ausência de tabela de senha |
|
||||
| Sessão Supabase e cookie de workspace | OK | `context.ts`, middleware |
|
||||
| Flag de módulo, padrão desligado | OK | `workspace_module_flags`, página de módulo desligado |
|
||||
| RLS e papéis | OK | `0003_rls.sql`, testes de RLS e jornada |
|
||||
| 10 modelos, nome único, soft delete provisório | OK | triggers, unique index, `status` |
|
||||
| XLSX/CSV, 10 MB, 50k, 10 fontes | OK | domínio + testes; 50k no caminho CSV |
|
||||
| Uma aba e linha de cabeçalho | OK | jornada e `updateSource` |
|
||||
| Mapeamento 1:1, obrigatórias, select e drag | OK | domínio, UI e testes |
|
||||
| Prévia antes de processar | OK | `previewSource` e passo da jornada |
|
||||
| Lock, idempotência, Redis obrigatório em produção | OK | coordinator e teste de indisponibilidade |
|
||||
| Modelo imutável | OK | SHA-256 e comparação de bytes na jornada |
|
||||
| Temporários e limpeza | OK | `cleanupExpired`; TTL de produto aberto |
|
||||
| Exclusão só owner/admin e bloqueio com job ativo | OK | jornada |
|
||||
| Auditoria sem células | OK | scrub e teste do token `=CMD()` |
|
||||
| Sentry sem planilha | OK | `beforeSend` e teste de scrub |
|
||||
| Saída XLSX, CSV e PDF | OK | jornada gera os três |
|
||||
| UI com tokens V3, Manrope, Lucide | PARCIAL | Componentes locais no token do escopo; biblioteca oficial do Elo Pro não estava no repo |
|
||||
| Acessibilidade básica | PARCIAL | Select, switch, dialog e foco visível cobertos; não houve auditoria WCAG formal |
|
||||
| E2E no browser do Elo Pro | PARCIAL | Jornada de API completa; browser do ecossistema inexistente aqui |
|
||||
| Performance 10 MB / 50k XLSX / 10 fontes paralelas | PARCIAL | 50k CSV e rejeição de arquivo acima de 10 MB; ExcelJS de 50k linhas não medido |
|
||||
| Job runner de produção | DECISÃO ABERTA | Worker no mesmo código; fila externa não escolhida |
|
||||
| Storage de produção | DECISÃO ABERTA | Interface pronta; local só fora de produção |
|
||||
| TTL, biblioteca XLSX, soft delete, SSO entre hosts | DECISÃO ABERTA | ADRs |
|
||||
|
||||
## Bloqueios de release para o piloto no ecossistema
|
||||
|
||||
1. Apontar `DATABASE_URL` do Postgres Elo Pro e não aplicar o shim de identidade se as tabelas já existirem com outro formato.
|
||||
2. Aprovar bucket privado e preencher as envs de storage.
|
||||
3. Definir TTL numérico em `SPREADSHEET_TEMP_TTL_SECONDS`.
|
||||
4. Rodar `ADAPTER_JOB_MODE=worker` com Redis, não o modo inline, no ambiente de piloto.
|
||||
5. Confirmar a biblioteca ExcelJS (MIT, provisória) ou trocar o codec em `src/infra/spreadsheet/parse.ts`.
|
||||
6. Ligar a flag só no workspace piloto.
|
||||
|
||||
## Riscos residuais
|
||||
|
||||
PDF truncado de propósito. Formatação do modelo não é reaproveitada. Conexão de serviço ignora RLS. Lock em memória não atravessa várias instâncias.
|
||||
|
||||
## Checklist de release
|
||||
|
||||
- [x] Suíte Vitest, typecheck, lint e build de produção locais
|
||||
- [ ] Smoke no staging com Supabase, Redis e bucket reais
|
||||
- [ ] Smoke no staging com Supabase, Redis e bucket reais
|
||||
- [ ] Flag desligada para quem não é piloto
|
||||
- [ ] Sentry com DSN, sem anexo de arquivo
|
||||
Reference in new issue
Block a user