TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
Ruído a entrar, sinal a sair. Um copiloto de IA que resume, prioriza e responde — sempre com a sua confirmação.
Translation pending
ProductivityTypeScript · CSSGitHub @JoviNatalliOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Your inbox, intelligently managed. SaaS de gestão inteligente de email com IA integrada como copiloto — não um chatbot ao lado, mas parte da própria experiência de gerir a inbox. Projeto de portfólio construído por fases. O plano completo (64 secções) está em docs/master-spec.md; o estado detalhado e as decisões de execução de cada fase em docs/status.md. O percurso, os problemas reais e o que ficou por fazer estão no case study. Ver ao vivo · entrar com "Explorar demo sem conta" (ou [email protected] / demo1234).Read the full README ↗
Your inbox, intelligently managed.
SaaS de gestão inteligente de email com IA integrada como copiloto — não um chatbot ao lado, mas parte da própria experiência de gerir a inbox.
Projeto de portfólio construído por fases. O plano completo (64 secções) está em
docs/master-spec.md; o estado detalhado e as decisões de execução de cada fase emdocs/status.md. O percurso, os problemas reais e o que ficou por fazer estão no case study.
Ver ao vivo · entrar com "Explorar demo sem
conta" (ou [email protected] / demo1234).
O Demo Mode é o caminho principal, não um plano B: entra sem ligar conta nenhuma e todas as funcionalidades de IA funcionam sobre um dataset fictício de 20 conversas — insights, gerador de respostas, o agente com tool calling e a pesquisa semântica.
O login com Google não funciona para um visitante qualquer, e isso é propositado. O projeto Google Cloud está em modo Testing, onde só emails adicionados como test user conseguem autenticar. Sair disso exige submeter a app a revisão da Google — desproporcionado para um projeto de portfólio, e passaria a pedir acesso ao Gmail real de desconhecidos. A integração com o Gmail e o Google Calendar está implementada e testada com contas reais (ver case study); o que o link público demonstra é o Demo Mode.
| Landing | Inbox com AI Insights |
|---|---|
| Agente a pedir confirmação | Pesquisa por significado |
|---|---|
| Calendário | Navegação mobile |
|---|---|
Capturados por pnpm screenshots (Playwright) contra o Demo Mode — nunca
contra uma conta de email real.
Gmail e Calendar reais
users.history.list)syncToken)IA
Produto
Fase 1 — Foundation
/app/*/proxy.ts protege /app/* e /onboarding (checagem otimista); cada
Server Action/Route Handler revalida a sessão antes de tocar na BDFase 2 — Email
threads, emails, attachments, labels,
threadLabels — multi-tenant (tudo escopado a userId), com pastas
(inbox/sent/drafts/archive/trash), estrelas, prioridade e categoriapnpm db:seed (idempotente, reexecutável)/app/search) — a pesquisa
semântica chegou na Fase 6, como um segundo modopnpm test:e2e) cobrindo o fluxo crítico: login →
abrir thread → estrela → arquivar/restaurar → lixo → compose/enviar →
responder → labels → searchFase 3 — Gmail
next-auth), a par do Credentials/Demo Mode
já existentes — access_type=offline+prompt=consent para garantir
refresh_token; tokens persistidos na tabela account (o mesmo adapter
da Fase 1) e renovados automaticamente quando expiram
(src/lib/google/tokens.ts)src/lib/google/): importa as últimas
30 conversas da conta ligada (threads + mensagens + labels do utilizador),
botão "Sincronizar agora" em Definições → Contasmessages.send) e rascunhos reais (users.drafts) quando a
conta tem Gmail ligado — compose/reply passam a sair de verdade; sem
Gmail ligado mantém-se o envio simulado da Fase 2 (Demo Mode)Fase 4 — IA
src/lib/ai/provider.ts, spec §4): a
app nunca importa o SDK de um provider diretamente. Anthropic e Google
(Gemini) estão implementados; trocar é mudar AI_DEFAULT_PROVIDER no
ambiente, sem tocar em lógica de negócioai_analysis para não repetir chamadas pagasFase 5 — AI Agent
src/lib/ai/agent.ts, spec §16–18): 21
ferramentas — pesquisar, ler, resumir, arquivar, marcar como lida, aplicar
estrelas e labels, criar rascunhos, enviar, responder, criar tarefas,
lembretes e eventos, detetar tarefas/reuniões num emailai_pending_action) e a UI mostra
o que vai acontecer e a quantos itens antes de qualquer coisa
acontecer. Ao confirmar, o cliente envia apenas o id da ação — os
argumentos vêm da base de dados e são revalidados/app/tasks
agrupa por dia; /app/calendar guarda os eventos (e, desde a Fase 6,
também no Google Calendar real quando ligado)<EMAIL_CONTENT> /
<TOOL_RESULTS> delimitados, com os delimitadores internos neutralizados
— um email não consegue "sair" do bloco de dados e passar por instrução.
Coberto por testes dedicadosNota — o que é IA e o que não é: os campos
priority/categoryda tabelathreadcontinuam a ser placeholders estáticos do seed (existem desde a Fase 2 e alimentam a pasta "Important"). A classificação real de IA vive noutro sítio — na tabelaai_analysis— e só aparece depois de o utilizador clicar em "Analisar com IA". Por spec (§13), a app nunca apresenta como IA aquilo que não foi gerado por IA.
Fase 6 — Pesquisa semântica e calendário real
gemini-embedding-001, 768 dimensões) e guardar em pgvector com
índice HNSW. /app/search passou a ter dois modos: "Palavras-chave"
(o ilike de sempre) e "Significado". No modo semântico cada resultado
mostra o excerto que fez o match e a proximidadesearchEmailsByMeaning entrou na caixa de ferramentas do agente, ao
lado da pesquisa por palavras — o modelo escolhe conforme o utilizador
sabe (ou não) as palavras exatascalendar.events só é pedido quando o utilizador
liga o calendário em Definições → Contas). O evento é sempre gravado
localmente primeiro e escrito no Google a seguir: se o Google recusar, o
utilizador fica com o evento na app e é avisado de que não foi para lá.
/app/calendar também LÊ os próximos eventos que já existiam no Google
(não só os criados pelo Nuvoly), mostrados lado a lado com os locais e
marcados como "só no Google" — sem duplicar os que a app já temsyncToken: antes de mostrar
/app/calendar, lê o que mudou no Google desde a última vez e
atualiza/remove os eventos locais editados ou apagados do lado de lá —
sem isto uma edição feita direto no Google nunca se refletia na apphistoryId: "Sincronizar
agora" passou a ler só o que mudou (users.history.list) a partir da
segunda sincronização, em vez de reimportar sempre as últimas 30
conversas; cai automaticamente para um sync completo se o histórico
expirar (janela de ~7 dias do Gmail)user_preference, usado pelo Calendar em vez do fuso do servidorFase 7 — Polish (nova)
md:flex). Não foi encolhida — passou a
haver uma barra inferior com os cinco destinos de uso constante e um
drawer para o resto. Alvos de toque de 44px+ (WCAG 2.5.5), com teste E2E
a medi-losC (escrever), G depois I (Inbox), e —
só com uma conversa aberta — R, A/E, S. Nunca disparam dentro de
campos de texto. Listados em Definições → Atalhos, que até aqui prometia
atalhos que não existiamprefers-reduced-motion
verificado com emulação real, não só por leitura do código| Camada | Escolha |
|---|---|
| Frontend | Next.js 16 (App Router), React 19, TypeScript strict, Tailwind CSS v4 |
| UI | Componentes próprios sobre Radix UI (padrão shadcn/ui — ver nota abaixo) |
| Auth | Auth.js v5 — Credentials + Demo Mode (Fase 1) e Google OAuth real (Fase 3) |
| Database | PostgreSQL 16 + Drizzle ORM + pgvector (embeddings da pesquisa semântica, índice HNSW) |
| IA | Camada própria de abstração sobre @google/genai (Gemini) e @anthropic-ai/sdk (Claude) — sem SDK de agente intermédio, para controlo total de structured outputs e tool calling |
| Embeddings | gemini-embedding-001 (768 dimensões) via REST — fora da abstração de provider, porque a Anthropic não tem embeddings |
| Validação | Zod (inclui os structured outputs e os argumentos das ferramentas de IA) |
| Testes | Vitest (71 unitários) + Playwright (28 E2E) |
| Animação | Framer Motion |
Nota sobre o design system: o CLI
shadcn(ui.shadcn.com) não estava acessível no ambiente onde a Fase 1 foi construída, por isso os componentes emsrc/components/ui/foram escritos à mão seguindo exatamente as mesmas convenções (Radix + CVA +cn()), tokens e estrutura do shadcn/ui — funcionalmente equivalentes, sem a dependência do CLI.pnpm dlx shadcn@latest add <componente>deve continuar a funcionar normalmente para adicionar novos componentes.
flowchart TB
subgraph browser["Browser"]
RSC["Server Components<br/>MailShell, páginas /app/*"]
RCC["Client Components<br/>ThreadDetail, AgentChatPanel, SearchView"]
end
PROXY["proxy.ts<br/>checagem otimista de sessão"]
subgraph servidor["Servidor Next.js"]
SA["Server Actions<br/>actions/emails.ts · ai.ts · agent.ts<br/>gmail.ts · calendar.ts · preferences.ts"]
RH["Route Handlers<br/>api/ai/agent · api/ai/search<br/>api/auth/[...nextauth]<br/>api/google/calendar/connect · callback"]
AUTH["auth.ts · Auth.js v5<br/>Credentials + Demo Mode + Google"]
GSYNC["lib/google/sync.ts<br/>runGmailSync · historyId"]
CSVC["lib/calendar/service.ts<br/>reconcileCalendarForUser · syncToken"]
AILAYER["lib/ai/*<br/>ver diagrama de IA"]
end
DRIZZLE["lib/db/index.ts<br/>Drizzle ORM"]
PG[("PostgreSQL + pgvector<br/>thread · email · label · task<br/>calendar_event · ai_analysis<br/>ai_pending_action · email_embedding<br/>gmail_sync · calendar_sync")]
GMAILAPI["Gmail API<br/>GMAIL_OAUTH_SCOPES"]
CALAPI["Google Calendar API<br/>CALENDAR_OAUTH_SCOPES<br/>autorização incremental, separada"]
RCC -->|"fetch / action"| SA
RCC -->|"NDJSON / stream"| RH
RSC --> DRIZZLE
PROXY -.->|"revalidado sempre no servidor"| SA
PROXY -.-> RH
SA --> DRIZZLE
RH --> DRIZZLE
SA --> AILAYER
RH --> AILAYER
AILAYER --> DRIZZLE
DRIZZLE --> PG
SA --> GSYNC
SA --> CSVC
RH --> AUTH
AUTH -->|"OAuth · tokens em account"| GMAILAPI
GSYNC -->|"users.history.list"| GMAILAPI
CSVC -->|"events.list com syncToken"| CALAPI
RH -->|"fluxo OAuth próprio"| CALAPI
Os dois scopes do Google são deliberadamente separados: o do calendário
só é pedido quando o utilizador o liga em Definições, com um fluxo OAuth
próprio e uma linha própria em account — ligar ou desligar um não mexe no
outro.
src/
├── app/ # Rotas (App Router)
│ ├── page.tsx # Landing page
│ ├── login/ # Login/signup + Demo Mode
│ ├── onboarding/ # Onboarding de 4 passos
│ ├── app/ # Shell autenticado (sidebar/topbar) + rotas /app/*
│ │ ├── inbox/, sent/, drafts/, archive/, trash/, starred/, important/
│ │ │ └── [threadId]/ # Detail da thread dentro de cada pasta
│ │ ├── labels/ # Gestão de labels + navegação por label
│ │ ├── search/ # Pesquisa por assunto/remetente/conteúdo
│ │ ├── ai/ # Copiloto (agente com tool calling)
│ │ ├── tasks/, calendar/ # Tarefas e eventos extraídos de emails
│ │ └── briefing/ # Daily AI Briefing
│ ├── api/
│ │ ├── auth/ # Route handler do Auth.js
│ │ └── ai/agent/ # Turno do agente (stream NDJSON de eventos)
│ └── actions/ # Server Actions (auth, onboarding, emails, ai, agent)
├── components/
│ ├── ui/ # Design system (Radix + CVA)
│ ├── layout/ # Sidebar, topbar, command palette, tema
│ ├── mail/ # Thread list/detail, compose, labels, search
│ ├── ai/ # Painel do agente, AI Insights, reply generator
│ ├── tasks/, calendar/, dashboard/
│ ├── auth/, onboarding/, settings/, marketing/, shared/
├── lib/
│ ├── ai/ # Providers, model routing, prompts, schemas Zod
│ │ ├── agent.ts # Ciclo do agente + ações confirmáveis
│ │ ├── tools/ # Ferramentas (§17) com schema e política de confirmação
│ │ └── briefing.ts # Dados do briefing (contagens em SQL)
│ ├── auth/ # Config Auth.js (edge-safe) + schemas Zod
│ ├── emails/ # Queries, tipos e formatação de email/thread
│ ├── google/ # Fase 3 — cliente Gmail API, tokens, sync, MIME
│ └── db/ # Client Drizzle + schema + seed (+ dados demo)
├── proxy.ts # Checagem otimista de sessão (Next.js 16)
└── types/ # Augmentation de tipos (next-auth)
drizzle/ # Migrations SQL geradas
tests/e2e/ # Suite Playwright (fluxo crítico da Fase 2)
Princípios seguidos (ver docs/master-spec.md §58–59):
server/client logic separados, nunca confiar em input externo sem validação
Zod, autorização sempre revalidada no servidor (nunca só no proxy), secrets
apenas no servidor.
flowchart TB
PEDIDO["Pedido em linguagem natural<br/>api/ai/agent/route.ts"]
PROMPT["lib/ai/prompts.ts<br/>SYSTEM · USER · EMAIL_CONTENT · TOOL_RESULTS<br/>delimitadores neutralizados"]
PROVIDER["lib/ai/provider.ts<br/>AIProvider: AnthropicProvider | GoogleProvider<br/>retry + cadeia de modelos por quota"]
LLM{{"LLM<br/>devolve nome de ferramenta + argumentos"}}
ZOD["Validação Zod<br/>lib/ai/tools/*.ts"]
REGRAS["Regras de negócio + ownership por userId<br/>requireOwnedThreads"]
POLITICA{"Política de confirmação<br/>never · bulk · always"}
PENDENTE[("ai_pending_action<br/>argumentos guardados no servidor")]
UI["UI mostra resumo + contagem de itens afetados"]
EXEC["Execução<br/>Server Actions da Fase 2/3"]
DADOS[("Base de dados + Gmail API")]
PEDIDO --> PROMPT --> PROVIDER --> LLM --> ZOD --> REGRAS --> POLITICA
POLITICA -->|"ação sensível"| PENDENTE --> UI
UI -->|"clique do utilizador<br/>envia só o id, nunca os argumentos"| EXEC
POLITICA -->|"leitura ou ação reversível"| EXEC
EXEC --> DADOS
EXEC -->|"resultado como TOOL_RESULTS"| PROMPT
O cliente nunca envia os argumentos ao confirmar — só o id da ação, que é
revalidado com o Zod da ferramenta antes de executar. Os resultados das
ferramentas voltam ao modelo dentro de <TOOL_RESULTS> com os
delimitadores neutralizados: um email não consegue sair do bloco de dados e
passar por instrução (§31).
flowchart LR
subgraph indexacao["Indexação — incremental e idempotente"]
EMAIL["email.bodyText"]
CLEAN["lib/ai/chunking.ts<br/>cleanEmailText<br/>corta assinaturas e citações"]
CHUNK["chunkText + buildEmailChunks<br/>cabeçalho Assunto/De em cada chunk"]
EMB["lib/ai/embeddings.ts<br/>gemini-embedding-001 · 768 dims"]
TAB[("email_embedding<br/>índice HNSW · cosseno")]
EMAIL --> CLEAN --> CHUNK --> EMB --> TAB
end
subgraph consulta["Consulta"]
Q["Pergunta do utilizador"]
QEMB["embedQuery<br/>RETRIEVAL_QUERY"]
BUSCA["lib/search/semantic.ts<br/>distância de cosseno, escopada por userId"]
CORTES{"Dois cortes<br/>absoluto 0.62<br/>relativo 0.07 ao topo"}
VAZIO["Zero resultados<br/>em vez dos K menos irrelevantes"]
RAG["lib/ai/rag.ts<br/>streamRagAnswer com citações"]
Q --> QEMB --> BUSCA --> CORTES
CORTES -->|"nada acima do corte"| VAZIO
CORTES -->|"passagens relevantes"| RAG
end
TAB --> BUSCA
Os dois cortes existem porque uma pesquisa vetorial devolve sempre os K mais próximos, mesmo quando nada é relevante — sem eles, perguntar por algo que não está na caixa devolvia com confiança os oito emails menos irrelevantes.
Pontos que interessam para quem for ler o código:
pnpm db:local
sobe um Postgres real (PGlite) em 127.0.0.1:5433, ver
dev-db/README.md. Alternativa: Postgres.app /
Homebrew / Docker / uma connection string remota.pnpm install
cp .env.example .env.local # já vem pronto para o dev-db; ajuste se usar outro Postgres
pnpm db:local # (terminal 1, deixe a correr) sobe o Postgres local
pnpm db:migrate # (terminal 2) aplica o schema
pnpm db:seed # cria o utilizador demo + 20 threads/24 emails de exemplo
pnpm dev
Abra http://localhost:3000 — clique "Explorar demo sem conta" no login
para entrar imediatamente, sem criar conta ([email protected] / demo1234).
| Comando | Descrição |
|---|---|
pnpm dev | Servidor de desenvolvimento (Turbopack) |
pnpm db:local | Sobe o Postgres local (PGlite), sem instalação |
pnpm build / pnpm start | Build e serve de produção |
pnpm lint / pnpm typecheck | ESLint / tsc --noEmit |
pnpm db:generate | Gera migration a partir do schema |
pnpm db:migrate | Aplica migrations pendentes |
pnpm db:seed | Recria o utilizador e o dataset de demonstração (idempotente) |
pnpm db:studio | Drizzle Studio (explorar a BD) |
pnpm test:unit | Vitest — schemas de IA, prompts, chunking, calendário, rate limiting |
pnpm test:e2e | Suite Playwright (reseeda a BD antes de correr) |
pnpm screenshots | Recaptura as imagens do README contra o Demo Mode |
PostgreSQL com pgvector. Schema em
src/lib/db/schema.ts, migrations versionadas em
drizzle/.
| Entidade | Para quê |
|---|---|
user · account · session · verificationToken | Auth.js. account tem uma linha por ligação: google (Gmail) e google-calendar são separadas |
user_preference | Onboarding, nível de IA, fuso horário reportado pelo browser |
thread · email · attachment | Conversas e mensagens, multi-tenant (tudo escopado a userId) |
label · thread_label | Labels próprias e importadas do Gmail |
ai_analysis | Cache da análise de IA por thread (§51 — evita repetir chamadas pagas) |
ai_pending_action | Ação sensível proposta pelo agente, com os argumentos validados no servidor, à espera de confirmação (§18) |
task · reminder · calendar_event | Extraídos de emails, sempre com clique do utilizador |
email_embedding | Um vetor por chunk de email — vector(768) com índice HNSW e distância de cosseno |
gmail_sync · calendar_sync | Ponto de partida das sincronizações incrementais (historyId e syncToken) |
Nota sobre o pgvector: a migração
drizzle/0005_free_klaw.sql começa com um
CREATE EXTENSION IF NOT EXISTS vector escrito à mão — o Drizzle não o
gera, e sem ele o tipo vector não existe e a migração falha numa base de
dados nova. email_embedding guarda também o nome do modelo que gerou cada
vetor: trocar de modelo de embeddings invalida o índice todo.
pnpm typecheck && pnpm lint # tsc --noEmit + ESLint
pnpm test:unit # 71 testes (Vitest)
pnpm test:e2e # 28 testes (Playwright, reseeda a BD)
O Playwright pode precisar do browser numa máquina nova:
npx playwright install chromium.
71 unitários — lógica pura: schemas Zod dos structured outputs, separação SYSTEM/EMAIL CONTENT dos prompts (com payloads de prompt injection reais), invariantes do registo de ferramentas do agente (nada destrutivo sem confirmação), limpeza e chunking de texto de email, mapeamento de erros do Calendar e o rate limiter.
28 E2E — três suites:
mail.spec.ts (16): o fluxo crítico de email, ponta a pontapolish.spec.ts (12): navegação a 375px e 768px, alvos de toque ≥44px,
cada atalho de teclado (incluindo a garantia de que não disparam dentro
de campos de texto), prefers-reduced-motion, nomes acessíveis em todos
os botões e ordem de tabulaçãoai.spec.ts (6): correm contra o modelo real. As chamadas ao LLM
acontecem no servidor e o page.route() do Playwright não as interceta;
mocká-las exigiria uma camada de injeção que só existiria para os testes.
Verificam por isso o contrato do §35 — cada fluxo acaba num resultado
válido ou numa mensagem PT-PT, nunca num erro cru ou spinner eterno —
e a invariante do §18: nenhum email sai do Inbox sem confirmação
explícitaO que não está coberto por testes automáticos, e porquê: o isolamento entre utilizadores (§29) é verificado manualmente, porque exigiria uma base de dados de teste com dois utilizadores; e a navegação mobile foi validada em viewport emulado, não em hardware real.
Deploy em Vercel (Hobby) + Neon (Postgres serverless), ambos no
tier gratuito — a mesma disciplina de custo zero da escolha do Gemini.
Neon porque tem pgvector em todos os planos, incluindo o gratuito, e
porque o pooling serverless é exatamente o que o Vercel precisa; o Supabase
traria auth e storage que este projeto não usa.
1. Base de dados (Neon)
Criar um projeto, copiar a connection string com pooling (a que tem
-pooler no host) e correr as migrações e o seed a partir da máquina
local — as variáveis do shell têm precedência sobre o .env.local:
DATABASE_URL="postgresql://…-pooler.…neon.tech/neondb?sslmode=require" pnpm db:migrate
DATABASE_URL="postgresql://…-pooler.…neon.tech/neondb?sslmode=require" pnpm db:seed
A primeira migração cria a extensão vector sozinha. O seed é idempotente
e é o que dá conteúdo ao Demo Mode.
2. Vercel
Importar JoviNatalli/IA-EMAIL-MANAGER (branch main) e definir as
variáveis no dashboard, nunca no repositório:
| Variável | Valor |
|---|---|
DATABASE_URL | Connection string com pooling do Neon |
AUTH_SECRET | npx auth secret (novo, diferente do local) |
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET | Do cliente OAuth da Google Cloud Console |
GOOGLE_GENERATIVE_AI_API_KEY | Chave gratuita do AI Studio |
AI_DEFAULT_PROVIDER | google |
NEXT_PUBLIC_APP_URL | O domínio de produção, com https:// |
Os nomes são
AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET(convenção do Auth.js v5), nãoGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET— versrc/auth.tsesrc/lib/google/tokens.ts.
3. Google Cloud Console — os redirect URIs de produção
No mesmo cliente OAuth, em Credentials → o cliente Web application →
Authorized redirect URIs, acrescentar (sem remover os de localhost):
https://<domínio>/api/auth/callback/google
https://<domínio>/api/google/calendar/callback
Ativar a API na Library não chega e não é o mesmo ecrã — são dois sítios
diferentes, e confundi-los já custou um redirect_uri_mismatch neste
projeto (ver case study). As mudanças podem demorar
minutos a propagar.
4. Notas de produção que interessam
src/lib/rate-limit.ts é uma
janela deslizante em memória: no Vercel, cada invocação pode acordar
numa instância nova com o contador a zero, e o limitador trata-a como o
primeiro pedido. Só trava rajadas que calhem na mesma instância quente.
Decisão consciente para uma demo de baixo tráfego, onde o travão real
continua a ser a quota diária do plano gratuito do Gemini; a alternativa
(Upstash Redis) punha um serviço externo no caminho crítico. Fica escrito
em vez de deixar acreditar numa proteção que não existe.max: 1 por instância e prepare: false
em serverless — o pooler do Neon corre em modo transação, onde prepared
statements com nome não sobrevivem entre pedidos.maxDuration = 60. Com
Fluid Compute o default do Vercel já é maior, mas sem ele seriam 10s — e
um turno do agente já foi medido em ~40s quando teve de percorrer a
cadeia de modelos.Ver .env.example.
DATABASE_URL e AUTH_SECRET chegam para as Fases 1/2.AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET (Google Cloud Console → Auth
Platform → Clients, tipo "Web application", redirect URI
http://localhost:3000/api/auth/callback/google) são necessárias para o
botão "Continuar com Google" — sem elas, o resto da app (Credentials +
Demo Mode) continua a funcionar normalmente.redirect_uri_mismatch real ao
testar:
AUTH_GOOGLE_ID) →
Authorized redirect URIs → adicionar
http://localhost:3000/api/google/calendar/callback (isto fica no
ecrã do CLIENTE, não no da API — é fácil confundir os dois e ativar só
a API, como aconteceu);.../auth/calendar.events.
Uma mudança nos redirect URIs pode levar de minutos a horas a propagar,
segundo a própria consola. Sem o calendário ligado a app funciona na
mesma: os eventos ficam locais e a UI diz isso.AI_DEFAULT_PROVIDER escolhe o provider de IA (google ou anthropic) e
a chave correspondente tem de estar preenchida:
GOOGLE_GENERATIVE_AI_API_KEY (gratuita em
aistudio.google.com/apikey) ou
ANTHROPIC_API_KEY. OPENAI_API_KEY está prevista mas esse provider
ainda não está implementado — pedi-lo dá um erro claro, nunca uma
resposta simulada.Privacidade — plano gratuito do Gemini: o provider ativo por omissão é o Gemini no tier gratuito, onde a Google pode usar o conteúdo enviado para melhorar os produtos dela. Isto é aceitável neste projeto porque a demonstração corre sobre o dataset fictício do Demo Mode. Se ligar uma conta Gmail real e usar as funcionalidades de IA, o conteúdo desses emails é enviado para a API do Gemini nessas condições — use um plano pago (ou o provider Anthropic) se isso não for aceitável para si.
bcrypt (nunca em texto simples)AUTH_SECRET gerado aleatoriamente, nunca commitado (.env* no .gitignore)/app/* e /onboarding revalida auth() no servidor
além da checagem otimista do proxyuserId — nenhuma thread/email é lida ou mutada sem confirmar a posse do
recurso no servidor (nunca confiar no threadId do cliente sozinho). As
ferramentas do agente repetem essa verificação: um id inventado pelo
modelo é recusado, não ignorado em silêncioaccess_type=offline+prompt=consent (garante
refresh_token); tokens Gmail nunca vão para o cliente nem para logs —
todas as chamadas à Gmail API correm em Server Actions/módulos
server-only (src/lib/google/)Email / Gmail
EmailProvider — hoje só o Gmail está
implementadohistoryId já era guardado desde a Fase 3, só não era
usado): a partir da segunda sincronização só relê as threads que mudaram,
em vez de reimportar sempre as últimas 30. Se o histórico sair da janela
de retenção do Gmail (~7 dias sem sincronizar), recomeça sozinho com um
sync completo — sem erro visível, é o comportamento corretoattachments fica vazio para threads
Gmail) — a UI nunca finge tê-losaccess_token/refresh_token
ficam na base de dados tal como o Auth.js Drizzle Adapter os grava
(padrão da biblioteca) — cifrá-los em repouso é um endurecimento razoável
antes de produção real com utilizadores externosIA
AIConversation/AIMessage/AIToolCall
do §28 ficam por fazercreateReminder grava o lembrete, mas ainda
não há nada que dispare notificaçõessyncToken
da Calendar API (mesmo padrão do historyId do Gmail): antes de mostrar
/app/calendar, lê o que mudou desde a última vez e atualiza/remove os
eventos locais que foram editados/apagados do lado do Google. Só cobre
eventos que o Nuvoly criou (têm googleEventId) — eventos "só no Google"
não têm linha local para divergirIntl.DateTimeFormat().resolvedOptions().timeZone) e guardado em
user_preference.time_zone; o Calendar passa a escrever nesse fuso em
vez do fuso do servidor assim que a sessão o reporta uma vezemail_embedding à mãoTodas as 8 fases do master-spec estão entregues. O que fica por fazer está listado em Future Improvements e no case study — nenhum deles é omissão silenciosa.
Plano detalhado por secção: docs/master-spec.md.
Estado e decisões por fase: docs/status.md.