diff --git a/.env.local.example b/.env.local.example index f0574b6..4298638 100644 --- a/.env.local.example +++ b/.env.local.example @@ -12,8 +12,8 @@ DIRECT_URL=postgresql://postgres.YOUR_REF:PASSWORD@aws-0-sa-east-1.pooler.supaba # Auth — lista de emails permitidos separados por vírgula ALLOWED_EMAILS=giordanna@example.com,john@example.com -# App -NEXT_PUBLIC_APP_URL=https://archtime.netlify.app +# App — em produção é https://archtime.app; no dev local use a origem local +NEXT_PUBLIC_APP_URL=http://localhost:3000 # Security — segredo legado/transitório usado para assinar hashes de ponto ENTRY_HASH_SECRET=replace-with-a-long-random-secret diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index e2e77be..25e7ac2 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -2,7 +2,8 @@ name: build-image # Builda a imagem de container e publica no GitHub Container Registry (ghcr.io), # de onde o Azure App Service (Brazil South) puxa. Roda em push na main (ignorando -# mudanças só de documentação) e sob demanda. NÃO afeta a produção atual da Netlify. +# mudanças só de documentação) e sob demanda. Em push na main, dispara o deploy de +# produção no Azure App Service via webhook de CD (passo final deste workflow). # # Os NEXT_PUBLIC_* são valores públicos inlinados no bundle client em build time # (secrets do repo). Os segredos de RUNTIME (DATABASE_URL, ENTRY_HASH_SECRET, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bd7ef5..30ec09f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,12 @@ jobs: # módulos client. Nenhum segredo real é necessário — os testes mockam o Prisma. NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder-anon-key - NEXT_PUBLIC_APP_URL: https://archtime.netlify.app + NEXT_PUBLIC_APP_URL: https://archtime.app steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 # espelha NODE_VERSION do netlify.toml + node-version: 22 # mesma major do runtime de produção (Dockerfile: node:22-bookworm-slim) cache: npm - run: npm ci - run: npx prisma generate diff --git a/AGENTS.md b/AGENTS.md index b022d3f..950cf48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,7 @@ Observações: - Todas as escritas devem passar por API Routes. Não escreva direto no banco a partir de Client Components. - Escritas relevantes devem preservar auditoria em `AuditLog`. -- Clock-out calcula hash SHA-256 da entrada. +- Clock-out e edição de sessão calculam um HMAC-SHA256 sobre os campos de integridade. Com o keyring configurado (produção), o formato é versionado por `keyId` (`hmac-v1::`); sem keyring, o fallback legado emite `hmac-v1:`. Ver ADR 0005. - Leituras simples podem usar Supabase client com RLS. - Agregações, joins e lógica sensível devem usar API Route com Prisma. - `src/app/layout.tsx` é Server Component. @@ -97,10 +97,10 @@ Regras duras: ## Infraestrutura e Performance -- Deploy em **Azure App Service** (container Linux B1, região Brazil South); produção em `https://archtime.app`. Imagem em `ghcr.io/johnlaff/archtime`, publicada pelo workflow `build-image` (push na `main`) e puxada pelo App Service via webhook de continuous deployment. +- Deploy em **Azure App Service** (container Linux B1, região Brazil South); produção em `https://archtime.app`. Imagem em `ghcr.io/johnlaff/archtime`, publicada pelo workflow `build-image` (push na `main`, exceto mudanças só de docs) e puxada pelo App Service via webhook de continuous deployment. - Banco em Supabase PostgreSQL (`sa-east-1`/São Paulo — mesma região do App Service, latência app↔banco baixa). -- Há latência relevante entre funções serverless e banco; evite colocar reads simples no caminho crítico de navegação quando client-direct com RLS for suficiente. -- Cold starts podem afetar navegação. Prefira cache, lazy loading, code splitting e leituras client-direct quando seguro. +- App e banco estão co-locados no Brasil (App Service Brazil South + Supabase São Paulo) e o B1 roda com **Always On** — a instância fica sempre carregada, sem cold start de ociosidade. Em regime, nem a latência ao banco nem cold start são gargalos de navegação; o custo residual de um read no servidor é o hop extra (cliente→app→banco) e a disputa pelo worker único do B1. Prefira client-direct com RLS para reads simples que dispensam o servidor. +- Cold start só aparece após deploy/restart (a imagem nova recicla a instância), não em navegação estável. Cache, lazy loading e code splitting continuam valendo como boa prática. - Performance de navegação não pode degradar. - Para bibliotecas pesadas, prefira lazy-load/code-split em vez de reimplementar tudo custom por reflexo. diff --git a/Dockerfile b/Dockerfile index 424fa2a..f73f44d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ # syntax=docker/dockerfile:1 # -# Imagem de deploy do ArchTime para Azure App Service (container, região Brazil South). -# A Netlify NÃO usa este arquivo — o build de produção lá segue inalterado. O output -# `standalone` só é ligado aqui via BUILD_STANDALONE=true (o build da Netlify não seta -# essa var), então a produção atual não sofre regressão. +# Imagem de deploy do ArchTime para Azure App Service (container, região Brazil South) — +# o hosting de produção. O output `standalone` só é ligado no build da imagem via +# BUILD_STANDALONE=true; fora dela, o output permanece o padrão do Next. FROM node:22-bookworm-slim AS base # NODE_ENV NÃO é setado aqui de propósito: `deps` herda de `base`, e `npm ci` com diff --git a/README.md b/README.md index 513e45b..7b438d1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ sem rede** — o registro entra numa fila local e sincroniza sozinho quando a co | 🎨 | **Projetos com cores** | Cada sessão é alocada a um projeto; cores dão leitura visual imediata no histórico e nos gráficos. | | 📊 | **Insights** | Heatmap anual e barras semanais **relativos à jornada** — enxerga padrões de esforço, não só totais brutos. | | 🗂️ | **Histórico auditável** | Edição de registros com trilha de auditoria; filtros por projeto e atividade sem deslocar a UI. | -| 🔐 | **Integridade dos registros** | Cada sessão fechada carrega um **HMAC-SHA256** com `keyId`; `/api/integrity` distingue formato inválido, adulteração e uma chave histórica indisponível. | +| 🔐 | **Integridade dos registros** | Cada sessão fechada carrega um **HMAC-SHA256** (versionado por `keyId` quando o keyring está configurado, como em produção); `/api/integrity` distingue formato inválido, adulteração e uma chave histórica indisponível. | | 📲 | **PWA instalável** | Service worker (Serwist), ícone/manifest e experiência de app nativo no celular. | ## Arquitetura @@ -115,7 +115,7 @@ src/ Produção em **Azure App Service** (container Linux B1, Brazil South), servida em **[archtime.app](https://archtime.app)** (TLS gerenciado; `www` → apex). A imagem é publicada no **GitHub Container Registry** (`ghcr.io/johnlaff/archtime`) pelo workflow `build-image` a cada push -na `main`, e o App Service faz o pull automático via **webhook** de continuous deployment. +na `main` (exceto mudanças só de documentação), e o App Service faz o pull automático via **webhook** de continuous deployment. > [!IMPORTANT] > O banco (Supabase) é **compartilhado entre ambientes** — cuidado com dados reais ao validar diff --git a/next.config.ts b/next.config.ts index 5e59892..86ad820 100644 --- a/next.config.ts +++ b/next.config.ts @@ -16,9 +16,8 @@ const CSP_REPORT_ONLY = [ ].join('; ') const nextConfig: NextConfig = { - // Standalone só é ligado no build do container (Azure), via BUILD_STANDALONE=true. - // O build da Netlify NÃO seta essa var, então lá o output permanece o padrão - // (o @netlify/plugin-nextjs gerencia o output) — zero regressão na produção atual. + // Standalone só é ligado no build do container (Azure, o hosting de produção) via + // BUILD_STANDALONE=true. Sem essa var, o output permanece o padrão do Next. output: process.env.BUILD_STANDALONE === 'true' ? 'standalone' : undefined, cacheComponents: true, experimental: { diff --git a/plans/README.md b/plans/README.md index ddce94d..3da5bec 100644 --- a/plans/README.md +++ b/plans/README.md @@ -35,10 +35,10 @@ foi re-despachado; os achados de RLS (#2, #3) e Host (#7) vieram da 2ª. | 011 | Higiene: README real, SVGs órfãos, Node 22 | P3 | S | 001 | DONE | | 012 | Guard de resposta obsoleta no useSupabaseQuery | P3 | S | — | DONE | | 013 | Spike de design: faturamento (horas × hourlyRate) | P3 | M | 006 | DONE (design em docs/plans/2026-07-03-faturamento-design.md; 6 questões abertas aguardando o mantenedor) | -| 014 | Sincronizar schema Prisma + completar lockdown RLS (migration 0007) | P1 | S | — | DONE (migration 0007 criada; APLICAÇÃO em produção pendente do operador — ver STOP condition) | +| 014 | Sincronizar schema Prisma + completar lockdown RLS (migration 0007) | P1 | S | — | DONE (migration 0007 aplicada e verificada em produção em 2026-07-10 — colunas/índices de drift presentes; RLS de `users`/`user_settings` sem policy INSERT/UPDATE) | | 015 | Tratar falha do IndexedDB no clock-in offline | P1 | S | — | DONE | | 016 | Guarda condicional no UPDATE de clock-out (race) | P2 | M | — | DONE | -| 017 | Remover código morto e duplicado (sweep de hygiene) | P3 | S | — | DONE (migration 0008 criada; APLICAÇÃO em produção pendente do operador — ver STOP condition) | +| 017 | Remover código morto e duplicado (sweep de hygiene) | P3 | S | — | DONE (migration 0008 aplicada e verificada em produção em 2026-07-10 — coluna `audit_log.ip_address` removida) | | 018 | Corrigir env var fantasma e docs de setup/segurança | P3 | S | 014 (recomendado) | DONE | | 019 | Gate de react-doctor no CI | P2 | S | — | DONE | | 020 | Adicionar linter (Biome) e remover `eslint-disable` mortos | P3 | M | — | DONE (linter-only: formatter e recommended desligados p/ evitar reformatar 342 linhas / 95 violações de estilo — full recommended fica p/ sweep separado) | @@ -58,9 +58,9 @@ Status values: TODO | IN PROGRESS | DONE | BLOCKED (com motivo em uma linha) | R e o 005 renomeia o mock de `@/lib/hour-bank` usado neles. - **003 antes de 008 (recomendado)**: a verificação de hash assume que escrita client-direct já não existe; sem o lockdown ela ainda funciona, mas o cenário limpo é pós-003. -- **011 requer 001**: o bump de Node atualiza `netlify.toml` e o `ci.yml` juntos. +- **011 requer 001**: à época, o bump de Node atualizava `netlify.toml` e o `ci.yml` juntos. - **013 requer 006**: o spike de faturamento consome o contrato numérico de `hourlyRate` na API. -- **003 e 008 tocam produção** (banco único, previews compartilham o DB — ADR 0003): a APLICAÇÃO +- **003 e 008 tocam produção** (banco único; à época, os previews da Netlify compartilhavam o DB — ADR 0003): a APLICAÇÃO da migration do 003 é do operador, nunca do executor (STOP condition explícita no plano). ### Rodada 2 @@ -76,8 +76,9 @@ Status values: TODO | IN PROGRESS | DONE | BLOCKED (com motivo em uma linha) | R conflitam (gates diferentes); podem ir em qualquer ordem ou em paralelo. - **023 e 024 são aditivos**: só criam test files, não tocam código de produção. Podem rodar em paralelo entre si e com qualquer outro plano. -- **014 e 017 tocam produção** (migrations 0007/0008): a APLICAÇÃO em produção é do operador, - nunca do executor (STOP conditions explícitas em ambos). +- **014 e 017 tocaram produção** (migrations 0007/0008): a aplicação foi do operador, nunca do + executor (STOP conditions explícitas em ambos). Ambas já estão aplicadas e verificadas em + produção (ledger `_prisma_migrations` com 0000–0008 completas em 2026-07-10). - **015 é independente e LOW risk**: toca só `src/hooks/use-clock.ts` (cliente); pode rodar a qualquer momento, inclusive antes de 016 (que toca o server). diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b6b986c..f9262e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,7 +17,7 @@ const fraunces = Fraunces({ axes: ['opsz'], }) -const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'https://archtime.netlify.app' +const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'https://archtime.app' export const metadata: Metadata = { title: 'ArchTime', diff --git a/src/lib/server/security.ts b/src/lib/server/security.ts index 6eba1af..836dc41 100644 --- a/src/lib/server/security.ts +++ b/src/lib/server/security.ts @@ -46,7 +46,7 @@ export function validateMutationOrigin(req: NextRequest): NextResponse | null { if (appOrigin) allowed.add(appOrigin) // req.nextUrl.origin deriva do header Host, que só é confiável quando a infra o - // saneia (o edge do Netlify o faz). Só confie nele sozinho quando casa com + // saneia (o App Service roteia por hostname e rejeita Host não-configurado). Só confie nele sozinho quando casa com // NEXT_PUBLIC_APP_URL ou é um origin local (dev) — nunca incondicionalmente, senão // um Host spoofado (Host: evil.com) passaria no check de CSRF. Ver ADR 0004. const nextUrlOrigin = normalizeOrigin(req.nextUrl.origin)