fix: tratar tokens de maquinas e alinhar stack/docs
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 6s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m41s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
Quality Checks / Lint, Test and Build (push) Successful in 4m0s

This commit is contained in:
rever-tecnologia 2025-12-18 18:20:35 -03:00
parent b7e2c4cc98
commit c030a3ac09
21 changed files with 309 additions and 36 deletions

View file

@ -19,10 +19,10 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas
- Seeds de usuários/tickets demo: `convex/seed.ts`. - Seeds de usuários/tickets demo: `convex/seed.ts`.
- Para DEV: rode `bun run convex:dev:bun` e acesse `/dev/seed` uma vez para popular dados realistas. - Para DEV: rode `bun run convex:dev:bun` e acesse `/dev/seed` uma vez para popular dados realistas.
## Stack atual (06/11/2025) ## Stack atual (18/12/2025)
- **Next.js**: `16.0.8` (Turbopack por padrão; webpack fica como fallback). - **Next.js**: `16.0.10` (Turbopack por padrão; webpack fica como fallback).
- Whitelist de domínios em `src/config/allowed-hosts.ts` é aplicada pelo `middleware.ts`. - Whitelist de domínios em `src/config/allowed-hosts.ts` é aplicada pelo `middleware.ts`.
- **React / React DOM**: `19.2.0`. - **React / React DOM**: `19.2.1`.
- **Trilha de testes**: Vitest (`bun test`) sem modo watch por padrão (`--run --passWithNoTests`). - **Trilha de testes**: Vitest (`bun test`) sem modo watch por padrão (`--run --passWithNoTests`).
- **CI**: workflow `Quality Checks` (`.github/workflows/quality-checks.yml`) roda `bun install`, `bun run prisma:generate`, `bun run lint`, `bun test`, `bun run build:bun`. Variáveis críticas (`BETTER_AUTH_SECRET`, `NEXT_PUBLIC_APP_URL`, etc.) são definidas apenas no runner — não afetam a VPS. - **CI**: workflow `Quality Checks` (`.github/workflows/quality-checks.yml`) roda `bun install`, `bun run prisma:generate`, `bun run lint`, `bun test`, `bun run build:bun`. Variáveis críticas (`BETTER_AUTH_SECRET`, `NEXT_PUBLIC_APP_URL`, etc.) são definidas apenas no runner — não afetam a VPS.
- **Disciplina pós-mudanças**: sempre que fizer alterações locais, rode **obrigatoriamente** `bun run lint`, `bun run build:bun` e `bun test` antes de entregar ou abrir PR. Esses comandos são mandatórios também para os agentes/automations, garantindo que o projeto continua íntegro. - **Disciplina pós-mudanças**: sempre que fizer alterações locais, rode **obrigatoriamente** `bun run lint`, `bun run build:bun` e `bun test` antes de entregar ou abrir PR. Esses comandos são mandatórios também para os agentes/automations, garantindo que o projeto continua íntegro.
@ -38,7 +38,7 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas
BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=dev-only-long-random-string BETTER_AUTH_SECRET=dev-only-long-random-string
NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210
DATABASE_URL=file:./prisma/db.dev.sqlite DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados
``` ```
3. `bun run auth:seed` 3. `bun run auth:seed`
4. (Opcional) `bun run queues:ensure` 4. (Opcional) `bun run queues:ensure`
@ -47,8 +47,8 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas
7. Acesse `http://localhost:3000` e valide login com os usuários padrão. 7. Acesse `http://localhost:3000` e valide login com os usuários padrão.
### Banco de dados ### Banco de dados
- Local (DEV): `DATABASE_URL=file:./prisma/db.dev.sqlite` (guardado em `prisma/prisma/`). - Local (DEV): PostgreSQL local (ex.: `postgres:18`) com `DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados`.
- Produção: SQLite persistido no volume Swarm `sistema_sistema_db`. Migrations em PROD devem apontar para esse volume (ver `docs/DEPLOY-RUNBOOK.md`). - Produção: PostgreSQL no Swarm (serviço `postgres` em uso hoje; `postgres18` provisionado para migração). Migrations em PROD devem apontar para o `DATABASE_URL` ativo (ver `docs/OPERATIONS.md`).
- Limpeza de legados: `node scripts/remove-legacy-demo-users.mjs` remove contas demo antigas (Cliente Demo, gestores fictícios etc.). - Limpeza de legados: `node scripts/remove-legacy-demo-users.mjs` remove contas demo antigas (Cliente Demo, gestores fictícios etc.).
### Verificações antes de PR/deploy ### Verificações antes de PR/deploy
@ -104,12 +104,12 @@ bun run build:bun
ln -sfn /home/renan/apps/sistema.build.<novo> /home/renan/apps/sistema.current ln -sfn /home/renan/apps/sistema.build.<novo> /home/renan/apps/sistema.current
docker service update --force sistema_web docker service update --force sistema_web
``` ```
- Resolver `P3009` (migration falhou) sempre no volume `sistema_sistema_db`: - Resolver `P3009` (migration falhou) no PostgreSQL ativo:
```bash ```bash
docker service scale sistema_web=0 docker service scale sistema_web=0
docker run --rm -it -e DATABASE_URL=file:/app/data/db.sqlite \ docker run --rm -it --network traefik_public \
--env-file /home/renan/apps/sistema.current/.env \
-v /home/renan/apps/sistema.current:/app \ -v /home/renan/apps/sistema.current:/app \
-v sistema_sistema_db:/app/data -w /app \
oven/bun:1 bash -lc "bun install --frozen-lockfile && bun x prisma migrate resolve --rolled-back <migration> && bun x prisma migrate deploy" oven/bun:1 bash -lc "bun install --frozen-lockfile && bun x prisma migrate resolve --rolled-back <migration> && bun x prisma migrate deploy"
docker service scale sistema_web=1 docker service scale sistema_web=1
``` ```
@ -164,7 +164,7 @@ bun run build:bun
- **Docs complementares**: - **Docs complementares**:
- `docs/DEV.md` — guia diário atualizado. - `docs/DEV.md` — guia diário atualizado.
- `docs/STATUS-2025-10-16.md` — snapshot do estado atual e backlog. - `docs/STATUS-2025-10-16.md` — snapshot do estado atual e backlog.
- `docs/DEPLOY-RUNBOOK.md` — runbook do Swarm. - `docs/OPERATIONS.md` — runbook do Swarm.
- `docs/admin-inventory-ui.md`, `docs/plano-app-desktop-maquinas.md` — detalhes do inventário/agente. - `docs/admin-inventory-ui.md`, `docs/plano-app-desktop-maquinas.md` — detalhes do inventário/agente.
## Regras de Codigo ## Regras de Codigo
@ -211,4 +211,4 @@ Para manter acessibilidade em botoes apenas com icone, prefira usar `aria-label`
``` ```
--- ---
_Última atualização: 15/12/2025 (Next.js 16, build padrão com Turbopack e fallback webpack documentado)._ _Última atualização: 18/12/2025 (Next.js 16, build padrão com Turbopack e fallback webpack documentado)._

View file

@ -1,11 +1,11 @@
# Deploy Manual via VPS # Deploy Manual via VPS
## Acesso rápido ## Acesso rápido
- Host: 31.220.78.20 - Host: 154.12.253.40
- Usuário: root - Usuário: root
- Caminho do projeto: /srv/apps/sistema - Caminho do projeto: /srv/apps/sistema
- Chave SSH (local): ./codex_ed25519 (chmod 600) - Chave SSH (local): ./codex_ed25519 (chmod 600)
- Login: `ssh -i ./codex_ed25519 root@31.220.78.20` - Login: `ssh -i ./codex_ed25519 root@154.12.253.40`
## Passo a passo resumido ## Passo a passo resumido
1. Conectar na VPS usando o comando acima. 1. Conectar na VPS usando o comando acima.

View file

@ -1,4 +1,4 @@
# Guia de Desenvolvimento — 18/10/2025 # Guia de Desenvolvimento — 18/12/2025
Este documento consolida o estado atual do ambiente de desenvolvimento, descreve como rodar lint/test/build localmente (e no CI) e registra erros recorrentes com as respectivas soluções. Este documento consolida o estado atual do ambiente de desenvolvimento, descreve como rodar lint/test/build localmente (e no CI) e registra erros recorrentes com as respectivas soluções.
@ -6,7 +6,7 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve
- **Bun (runtime padrão)**: 1.3+ já instalado no runner e VPS (`bun --version`). Após instalar localmente, exporte `PATH="$HOME/.bun/bin:$PATH"` para tornar o binário disponível. Use `bun install`, `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun test` como fluxo principal (scripts Node continuam disponíveis como fallback). - **Bun (runtime padrão)**: 1.3+ já instalado no runner e VPS (`bun --version`). Após instalar localmente, exporte `PATH="$HOME/.bun/bin:$PATH"` para tornar o binário disponível. Use `bun install`, `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun test` como fluxo principal (scripts Node continuam disponíveis como fallback).
- **Node.js**: mantenha a versão 20.9+ instalada para ferramentas auxiliares (Prisma CLI, scripts legados em Node) quando não estiver usando o runtime do Bun. - **Node.js**: mantenha a versão 20.9+ instalada para ferramentas auxiliares (Prisma CLI, scripts legados em Node) quando não estiver usando o runtime do Bun.
- **Next.js 16**: Projeto roda em `next@16.0.8` com Turbopack como bundler padrão (dev e build); webpack continua disponível como fallback. - **Next.js 16**: Projeto roda em `next@16.0.10` com Turbopack como bundler padrão (dev e build); webpack continua disponível como fallback.
- **Lint/Test/Build**: `bun run lint`, `bun test`, `bun run build:bun`. O test runner do Bun já roda em modo não interativo; utilize `bunx vitest --watch` apenas quando precisar do modo watch manualmente. - **Lint/Test/Build**: `bun run lint`, `bun test`, `bun run build:bun`. O test runner do Bun já roda em modo não interativo; utilize `bunx vitest --watch` apenas quando precisar do modo watch manualmente.
- **Banco DEV**: PostgreSQL local (Docker recomendado). Defina `DATABASE_URL` apontando para seu PostgreSQL. - **Banco DEV**: PostgreSQL local (Docker recomendado). Defina `DATABASE_URL` apontando para seu PostgreSQL.
- **Desktop (Tauri)**: fonte em `apps/desktop`. Usa Radix tabs + componentes shadcn-like, integra com os endpoints `/api/machines/*` e suporta atualização automática via GitHub Releases. - **Desktop (Tauri)**: fonte em `apps/desktop`. Usa Radix tabs + componentes shadcn-like, integra com os endpoints `/api/machines/*` e suporta atualização automática via GitHub Releases.
@ -47,7 +47,7 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve
## Next.js 16 (estável) ## Next.js 16 (estável)
- Mantemos o projeto em `next@16.0.8`, com React 19 e o App Router completo. - Mantemos o projeto em `next@16.0.10`, com React 19 e o App Router completo.
- **Bundlers**: Turbopack permanece habilitado no `next dev`/`bun run dev:bun` e agora também no `next build --turbopack`. Use `next build --webpack` somente para reproduzir bugs ou comparar saídas. - **Bundlers**: Turbopack permanece habilitado no `next dev`/`bun run dev:bun` e agora também no `next build --turbopack`. Use `next build --webpack` somente para reproduzir bugs ou comparar saídas.
- **Whitelist de hosts**: o release estável continua sem aceitar `server.allowedHosts` (vide [`invalid-next-config`](https://nextjs.org/docs/messages/invalid-next-config)), portanto bloqueamos domínios exclusivamente via `middleware.ts`. - **Whitelist de hosts**: o release estável continua sem aceitar `server.allowedHosts` (vide [`invalid-next-config`](https://nextjs.org/docs/messages/invalid-next-config)), portanto bloqueamos domínios exclusivamente via `middleware.ts`.
@ -200,8 +200,8 @@ PY
## Referências úteis ## Referências úteis
- **Deploy (Swarm)**: veja `docs/DEPLOY-RUNBOOK.md`. - **Deploy (Swarm)**: veja `docs/OPERATIONS.md`.
- **Plano do agente desktop / heartbeat**: `docs/plano-app-desktop-maquinas.md`. - **Plano do agente desktop / heartbeat**: `docs/archive/plano-app-desktop-dispositivos.md`.
- **Histórico de incidentes**: `docs/historico-agente-desktop-2025-10-10.md`. - **Histórico de incidentes**: `docs/historico-agente-desktop-2025-10-10.md`.
> Última revisão: 18/10/2025. Atualize este guia sempre que o fluxo de DEV ou automações mudarem. > Última revisão: 18/10/2025. Atualize este guia sempre que o fluxo de DEV ou automações mudarem.

View file

@ -18,7 +18,7 @@ Estrategia: nenhuma limpeza automatica ligada. Usamos apenas monitoramento e, se
- Export/backup local de tickets: endpoint `POST /api/admin/tickets/archive-local` (staff) grava tickets resolvidos mais antigos que N dias em JSONL dentro de `ARCHIVE_DIR` (padrão `./archives`). Usa `exportResolvedTicketsToDisk` com segredo interno (`INTERNAL_HEALTH_TOKEN`/`REPORTS_CRON_SECRET`). - Export/backup local de tickets: endpoint `POST /api/admin/tickets/archive-local` (staff) grava tickets resolvidos mais antigos que N dias em JSONL dentro de `ARCHIVE_DIR` (padrão `./archives`). Usa `exportResolvedTicketsToDisk` com segredo interno (`INTERNAL_HEALTH_TOKEN`/`REPORTS_CRON_SECRET`).
## Como acessar tickets antigos sem perda ## Como acessar tickets antigos sem perda
- Base quente: Prisma (SQLite) guarda todos os tickets; nenhuma rotina remove ou trunca tickets. - Base quente: Prisma (PostgreSQL) guarda todos os tickets; nenhuma rotina remove ou trunca tickets.
- Se um dia for preciso offload (ex.: >50k tickets): - Se um dia for preciso offload (ex.: >50k tickets):
- Exportar em lotes (ex.: JSONL mensais) para storage frio (S3/compat). - Exportar em lotes (ex.: JSONL mensais) para storage frio (S3/compat).
- Gravar um marcador de offload no DB quente (ex.: `ticket_archived_at`, `archive_key`). - Gravar um marcador de offload no DB quente (ex.: `ticket_archived_at`, `archive_key`).
@ -33,7 +33,7 @@ Estrategia: nenhuma limpeza automatica ligada. Usamos apenas monitoramento e, se
## Checks operacionais sugeridos (manuais) ## Checks operacionais sugeridos (manuais)
- Tamanho do banco do Convex: `ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "ls -lh /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3"` - Tamanho do banco do Convex: `ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "ls -lh /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3"`
- Memoria do Convex: `ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "docker stats --no-stream | grep convex"` - Memoria do Convex: `ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "docker stats --no-stream | grep convex"`
- Alvos: <100-200 MB para o SQLite e <5 GB de RAM. Acima disso, abrir janela curta, fazer backup e avaliar limpeza ou arquivamento pontual. - Alvos: <100-200 MB para o SQLite do Convex e <5 GB de RAM. Acima disso, abrir janela curta, fazer backup e avaliar limpeza ou arquivamento pontual.
## Estado atual e proximos passos ## Estado atual e proximos passos
- Cron de limpeza segue desativado. Prioridade: monitorar 2-4 semanas para validar estabilidade pos-correcoes. - Cron de limpeza segue desativado. Prioridade: monitorar 2-4 semanas para validar estabilidade pos-correcoes.

View file

@ -121,7 +121,7 @@ docker run -d \
-p 5432:5432 \ -p 5432:5432 \
-e POSTGRES_PASSWORD=dev \ -e POSTGRES_PASSWORD=dev \
-e POSTGRES_DB=sistema_chamados \ -e POSTGRES_DB=sistema_chamados \
postgres:16 postgres:18
# Criar arquivo .env # Criar arquivo .env
cp .env.example .env cp .env.example .env
@ -230,7 +230,7 @@ docker start postgres-dev
# Ou recriar # Ou recriar
docker rm -f postgres-dev docker rm -f postgres-dev
docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:16 docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18
``` ```
## Convex (Backend de Tempo Real) ## Convex (Backend de Tempo Real)
@ -248,5 +248,5 @@ bun run dev:bun
## Mais Informacoes ## Mais Informacoes
- **Desenvolvimento detalhado:** `docs/DEV.md` - **Desenvolvimento detalhado:** `docs/DEV.md`
- **Deploy e operacoes:** `docs/DEPLOY-RUNBOOK.md` - **Deploy e operacoes:** `docs/OPERATIONS.md`
- **CI/CD Forgejo:** `docs/FORGEJO-CI-CD.md` - **CI/CD Forgejo:** `docs/FORGEJO-CI-CD.md`

View file

@ -0,0 +1,54 @@
# Alteracoes de producao - 2025-12-18
Este documento registra as mudancas aplicadas na VPS para estabilizar o ambiente e padronizar o uso do PostgreSQL 18.
## Resumo
- Migracao do banco principal do sistema para o servico `postgres18`.
- Desativacao do servico `postgres` (pg16) no Swarm.
- Convex backend fixado na tag `ghcr.io/get-convex/convex-backend:6690a911bced1e5e516eafc0409a7239fb6541bb`.
- `CONVEX_INTERNAL_URL` ajustado para o endpoint publico, evitando falhas de DNS interno (`ENOTFOUND sistema_convex_backend`).
- Tratamento explicito para tokens revogados/expirados/invalidos nas rotas `/api/machines/*` e chat.
- Limpeza de documento legado no Convex (`liveChatSessions` id `pd71bvfbxx7th3npdj519hcf3s7xbe2j`).
## Backups gerados
- `/root/pg-backups/sistema_chamados_pg16_20251218215925.dump`
- `/root/pg-backups/sistema_chamados_pg18_20251218215925.dump`
- Convex: `/var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3.backup-20251218165717`
- Observacao: foi gerado um arquivo extra `db.sqlite3.backup-` (sem timestamp) por comando incorreto.
## Procedimento (principais comandos)
```
# 1) Backup dos bancos
docker exec -u postgres <pg16> pg_dump -Fc -d sistema_chamados -f /tmp/sistema_chamados_pg16_20251218215925.dump
docker exec -u postgres <pg18> pg_dump -Fc -d sistema_chamados -f /tmp/sistema_chamados_pg18_20251218215925.dump
# 2) Parar o web durante a migracao
docker service scale sistema_web=0
# 3) Restaurar dump do pg16 no pg18
docker exec -u postgres <pg18> psql -c "DROP DATABASE IF EXISTS sistema_chamados;"
docker exec -u postgres <pg18> psql -c "CREATE DATABASE sistema_chamados OWNER sistema;"
docker cp /root/pg-backups/sistema_chamados_pg16_20251218215925.dump <pg18>:/tmp/sistema_chamados_restore.dump
docker exec -u postgres <pg18> pg_restore -d sistema_chamados -Fc /tmp/sistema_chamados_restore.dump
# 4) Atualizar stack (com variaveis exportadas)
set -a; . /srv/apps/sistema/.env; set +a
docker stack deploy --with-registry-auth -c /srv/apps/sistema/stack.yml sistema
# 5) Desativar pg16
docker service scale postgres=0
```
## Ajustes em stack.yml
- `DATABASE_URL` apontando para `postgres18:5432`.
- `CONVEX_INTERNAL_URL` apontando para `https://convex.esdrasrenan.com.br`.
- Imagem do Convex ajustada para a tag acima.
## Resultado
- `sistema_web` voltou com 2 replicas saudaveis.
- `sistema_convex_backend` rodando na tag informada.
- `postgres` (pg16) desativado no Swarm.
- Healthcheck OK: `GET /api/health` e `GET /version`.
## Observacoes operacionais
- O deploy do stack precisa de variaveis exportadas do `.env`. Sem isso, `NEXT_PUBLIC_*` fica vazio e o `POSTGRES_PASSWORD` nao e propagado, causando `P1000` no Prisma.

View file

@ -112,7 +112,39 @@ Critérios de sucesso:
--- ---
## 6. Referências rápidas ## 6. Registro de alterações manuais
### 2025-12-18 — liveChatSessions com versão legada (shape_inference)
Motivo: logs do Convex mostravam `shape_inference` recorrente apontando para o documento
`pd71bvfbxx7th3npdj519hcf3s7xbe2j` (sessão de chat antiga com status `ACTIVE` em versão histórica).
Comandos executados:
```bash
# 1) Parar Convex
docker service scale sistema_convex_backend=0
# 2) Backup
cp /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3 \
/var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3.backup-20251218165717
# 3) Remover versões antigas do documento (mantendo a mais recente)
docker run --rm -v sistema_convex_data:/convex/data nouchka/sqlite3 /convex/data/db.sqlite3 \
"DELETE FROM documents \
WHERE json_extract(json_value, '$._id') = 'pd71bvfbxx7th3npdj519hcf3s7xbe2j' \
AND ts < (SELECT MAX(ts) FROM documents \
WHERE json_extract(json_value, '$._id') = 'pd71bvfbxx7th3npdj519hcf3s7xbe2j');"
# 4) Subir Convex
docker service scale sistema_convex_backend=1
```
Resultado: versões antigas do documento foram removidas e os erros de `shape_inference` pararam após o restart.
---
## 7. Referências rápidas
- Volume Convex: `sistema_convex_data` - Volume Convex: `sistema_convex_data`
- Banco: `/convex/data/db.sqlite3` - Banco: `/convex/data/db.sqlite3`
@ -122,4 +154,4 @@ Critérios de sucesso:
--- ---
Última revisão: **18/11/2025** — sanado por remoção dos registros incompatíveis e rerun bem-sucedido do export `gg20vw5b479d9a2jprjpe3pxg57vk9wa`. Última revisão: **18/12/2025** — limpeza da versão legada de `liveChatSessions` (`pd71bvfbxx7th3npdj519hcf3s7xbe2j`) e restart do Convex.

View file

@ -4,6 +4,7 @@ import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit"
const attachmentUrlSchema = z.object({ const attachmentUrlSchema = z.object({
@ -87,6 +88,16 @@ export async function POST(request: Request) {
return jsonWithCors({ url }, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) return jsonWithCors({ url }, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit))
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS,
rateLimitHeaders(rateLimit)
)
}
console.error("[machines.chat.attachments.url] Falha ao obter URL de anexo", error) console.error("[machines.chat.attachments.url] Falha ao obter URL de anexo", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao obter URL de anexo", details }, 500, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) return jsonWithCors({ error: "Falha ao obter URL de anexo", details }, 500, origin, CORS_METHODS, rateLimitHeaders(rateLimit))

View file

@ -4,6 +4,7 @@ import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit"
import { withRetry } from "@/server/retry" import { withRetry } from "@/server/retry"
@ -115,6 +116,15 @@ export async function POST(request: Request) {
}) })
return jsonWithCors(result, 200, origin, CORS_METHODS) return jsonWithCors(result, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.chat.messages] Falha ao listar mensagens", error) console.error("[machines.chat.messages] Falha ao listar mensagens", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao listar mensagens", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao listar mensagens", details }, 500, origin, CORS_METHODS)
@ -159,6 +169,15 @@ export async function POST(request: Request) {
) )
return jsonWithCors(result, 200, origin, CORS_METHODS) return jsonWithCors(result, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.chat.messages] Falha ao enviar mensagem", error) console.error("[machines.chat.messages] Falha ao enviar mensagem", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao enviar mensagem", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao enviar mensagem", details }, 500, origin, CORS_METHODS)

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit"
const pollSchema = z.object({ const pollSchema = z.object({
@ -68,6 +69,16 @@ export async function POST(request: Request) {
}) })
return jsonWithCors(result, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) return jsonWithCors(result, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit))
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS,
rateLimitHeaders(rateLimit)
)
}
console.error("[machines.chat.poll] Falha ao verificar atualizacoes", error) console.error("[machines.chat.poll] Falha ao verificar atualizacoes", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao verificar atualizacoes", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao verificar atualizacoes", details }, 500, origin, CORS_METHODS)

View file

@ -4,6 +4,7 @@ import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit"
const readSchema = z.object({ const readSchema = z.object({
@ -69,9 +70,18 @@ export async function POST(request: Request) {
}) })
return jsonWithCors(result, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) return jsonWithCors(result, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit))
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS,
rateLimitHeaders(rateLimit)
)
}
console.error("[machines.chat.read] Falha ao marcar mensagens como lidas", error) console.error("[machines.chat.read] Falha ao marcar mensagens como lidas", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao marcar mensagens como lidas", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao marcar mensagens como lidas", details }, 500, origin, CORS_METHODS)
} }
} }

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit"
const sessionsSchema = z.object({ const sessionsSchema = z.object({
@ -66,6 +67,16 @@ export async function POST(request: Request) {
}) })
return jsonWithCors({ sessions }, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) return jsonWithCors({ sessions }, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit))
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS,
rateLimitHeaders(rateLimit)
)
}
console.error("[machines.chat.sessions] Falha ao listar sessoes", error) console.error("[machines.chat.sessions] Falha ao listar sessoes", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao listar sessoes", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao listar sessoes", details }, 500, origin, CORS_METHODS)

View file

@ -1,6 +1,7 @@
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveCorsOrigin } from "@/server/cors" import { resolveCorsOrigin } from "@/server/cors"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
export const runtime = "nodejs" export const runtime = "nodejs"
export const dynamic = "force-dynamic" export const dynamic = "force-dynamic"
@ -45,9 +46,10 @@ export async function GET(request: Request) {
try { try {
await client.query(api.liveChat.checkMachineUpdates, { machineToken: token }) await client.query(api.liveChat.checkMachineUpdates, { machineToken: token })
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Token invalido" const tokenError = resolveMachineTokenError(error)
const message = tokenError?.message ?? (error instanceof Error ? error.message : "Token invalido")
return new Response(message, { return new Response(message, {
status: 401, status: tokenError?.status ?? 401,
headers: { headers: {
"Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Origin": resolvedOrigin,
"Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false",
@ -110,6 +112,15 @@ export async function GET(request: Request) {
previousState = currentState previousState = currentState
} }
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
sendEvent("error", { code: tokenError.code, message: tokenError.message })
isAborted = true
clearInterval(pollInterval)
clearInterval(heartbeatInterval)
controller.close()
return
}
console.error("[SSE] Poll error:", error) console.error("[SSE] Poll error:", error)
// Enviar erro e fechar conexao // Enviar erro e fechar conexao
sendEvent("error", { message: "Poll failed" }) sendEvent("error", { message: "Poll failed" })

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
const uploadUrlSchema = z.object({ const uploadUrlSchema = z.object({
machineToken: z.string().min(1), machineToken: z.string().min(1),
@ -60,6 +61,15 @@ export async function POST(request: Request) {
}) })
return jsonWithCors(result, 200, origin, CORS_METHODS) return jsonWithCors(result, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.chat.upload] Falha ao gerar URL de upload", error) console.error("[machines.chat.upload] Falha ao gerar URL de upload", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
const heartbeatSchema = z.object({ const heartbeatSchema = z.object({
machineToken: z.string().min(1), machineToken: z.string().min(1),
@ -59,6 +60,15 @@ export async function POST(request: Request) {
const response = await client.mutation(api.devices.heartbeat, payload) const response = await client.mutation(api.devices.heartbeat, payload)
return jsonWithCors(response, 200, origin, CORS_METHODS) return jsonWithCors(response, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.heartbeat] Falha ao registrar heartbeat", error) console.error("[machines.heartbeat] Falha ao registrar heartbeat", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao registrar heartbeat", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao registrar heartbeat", details }, 500, origin, CORS_METHODS)

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
const tokenModeSchema = z.object({ const tokenModeSchema = z.object({
machineToken: z.string().min(1), machineToken: z.string().min(1),
@ -77,6 +78,15 @@ export async function POST(request: Request) {
}) })
return jsonWithCors({ ok: true, machineId: result.machineId, expiresAt: result.expiresAt }, 200, origin, CORS_METHODS) return jsonWithCors({ ok: true, machineId: result.machineId, expiresAt: result.expiresAt }, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.inventory:token] Falha ao atualizar inventário", error) console.error("[machines.inventory:token] Falha ao atualizar inventário", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao atualizar inventário", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao atualizar inventário", details }, 500, origin, CORS_METHODS)
@ -94,8 +104,8 @@ export async function POST(request: Request) {
macAddresses: provParsed.data.macAddresses, macAddresses: provParsed.data.macAddresses,
serialNumbers: provParsed.data.serialNumbers, serialNumbers: provParsed.data.serialNumbers,
inventory: provParsed.data.inventory, inventory: provParsed.data.inventory,
metrics: provParsed.data.metrics, metrics: provParsed.data.metrics,
registeredBy: provParsed.data.registeredBy ?? "agent:inventory", registeredBy: provParsed.data.registeredBy ?? "agent:inventory",
}) })
return jsonWithCors({ ok: true, machineId: result.machineId, status: result.status }, 200, origin, CORS_METHODS) return jsonWithCors({ ok: true, machineId: result.machineId, status: result.status }, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
@ -107,3 +117,4 @@ export async function POST(request: Request) {
return jsonWithCors({ error: "Formato de payload não suportado" }, 400, origin, CORS_METHODS) return jsonWithCors({ error: "Formato de payload não suportado" }, 400, origin, CORS_METHODS)
} }

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
export const runtime = "nodejs" export const runtime = "nodejs"
@ -54,6 +55,15 @@ export async function POST(request: Request) {
const response = await client.mutation(api.devices.upsertRemoteAccessViaToken, payload) const response = await client.mutation(api.devices.upsertRemoteAccessViaToken, payload)
return jsonWithCors({ ok: true, remoteAccess: response?.remoteAccess ?? null }, 200, origin, METHODS) return jsonWithCors({ ok: true, remoteAccess: response?.remoteAccess ?? null }, 200, origin, METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
METHODS
)
}
console.error("[machines.remote-access:token] Falha ao registrar acesso remoto", error) console.error("[machines.remote-access:token] Falha ao registrar acesso remoto", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao registrar acesso remoto", details }, 500, origin, METHODS) return jsonWithCors({ error: "Falha ao registrar acesso remoto", details }, 500, origin, METHODS)

View file

@ -2,6 +2,7 @@ import { NextResponse } from "next/server"
import { z } from "zod" import { z } from "zod"
import { createMachineSession, MachineInactiveError } from "@/server/machines-session" import { createMachineSession, MachineInactiveError } from "@/server/machines-session"
import { applyCorsHeaders, createCorsPreflight, jsonWithCors } from "@/server/cors" import { applyCorsHeaders, createCorsPreflight, jsonWithCors } from "@/server/cors"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
import { import {
MACHINE_CTX_COOKIE, MACHINE_CTX_COOKIE,
serializeMachineCookie, serializeMachineCookie,
@ -133,7 +134,17 @@ export async function POST(request: Request) {
CORS_METHODS CORS_METHODS
) )
} }
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.sessions] Falha ao criar sessão", error) console.error("[machines.sessions] Falha ao criar sessão", error)
return jsonWithCors({ error: "Falha ao autenticar dispositivo" }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao autenticar dispositivo" }, 500, origin, CORS_METHODS)
} }
} }

View file

@ -3,6 +3,7 @@ import { z } from "zod"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createCorsPreflight, jsonWithCors } from "@/server/cors"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { resolveMachineTokenError } from "@/server/machines/token-errors"
const getPolicySchema = z.object({ const getPolicySchema = z.object({
machineToken: z.string().min(1), machineToken: z.string().min(1),
@ -54,6 +55,15 @@ export async function GET(request: Request) {
appliedAt: pendingPolicy.appliedAt, appliedAt: pendingPolicy.appliedAt,
}, 200, origin, CORS_METHODS) }, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.usb-policy] Falha ao buscar politica USB", error) console.error("[machines.usb-policy] Falha ao buscar politica USB", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao buscar politica USB", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao buscar politica USB", details }, 500, origin, CORS_METHODS)
@ -90,6 +100,15 @@ export async function POST(request: Request) {
const response = await client.mutation(api.usbPolicy.reportUsbPolicyStatus, payload) const response = await client.mutation(api.usbPolicy.reportUsbPolicyStatus, payload)
return jsonWithCors(response, 200, origin, CORS_METHODS) return jsonWithCors(response, 200, origin, CORS_METHODS)
} catch (error) { } catch (error) {
const tokenError = resolveMachineTokenError(error)
if (tokenError) {
return jsonWithCors(
{ error: tokenError.message, code: tokenError.code },
tokenError.status,
origin,
CORS_METHODS
)
}
console.error("[machines.usb-policy] Falha ao reportar status de politica USB", error) console.error("[machines.usb-policy] Falha ao reportar status de politica USB", error)
const details = error instanceof Error ? error.message : String(error) const details = error instanceof Error ? error.message : String(error)
return jsonWithCors({ error: "Falha ao reportar status", details }, 500, origin, CORS_METHODS) return jsonWithCors({ error: "Falha ao reportar status", details }, 500, origin, CORS_METHODS)

View file

@ -0,0 +1,43 @@
type MachineTokenErrorKind = "invalid" | "expired" | "revoked"
type MachineTokenErrorMatch = {
match: string
kind: MachineTokenErrorKind
}
const MACHINE_TOKEN_ERROR_MATCHES: MachineTokenErrorMatch[] = [
{ match: "Token de dispositivo inválido", kind: "invalid" },
{ match: "Token de dispositivo invalido", kind: "invalid" },
{ match: "Token de dispositivo expirado", kind: "expired" },
{ match: "Token de dispositivo revogado", kind: "revoked" },
{ match: "Token de maquina invalido ou revogado", kind: "revoked" },
{ match: "Token de máquina inválido", kind: "invalid" },
{ match: "Token de maquina invalido", kind: "invalid" },
{ match: "Token de máquina expirado", kind: "expired" },
{ match: "Token de maquina expirado", kind: "expired" },
{ match: "Token de máquina revogado", kind: "revoked" },
{ match: "Token de maquina revogado", kind: "revoked" },
]
export type MachineTokenErrorInfo = {
kind: MachineTokenErrorKind
message: string
status: number
code: string
}
export function resolveMachineTokenError(error: unknown): MachineTokenErrorInfo | null {
const message = error instanceof Error ? error.message : String(error)
const match = MACHINE_TOKEN_ERROR_MATCHES.find((entry) => message.includes(entry.match))
if (!match) {
return null
}
const status = match.kind === "revoked" ? 403 : 401
return {
kind: match.kind,
message: match.match,
status,
code: `machine_token_${match.kind}`,
}
}

View file

@ -21,18 +21,18 @@ services:
# IMPORTANTE: "NEXT_PUBLIC_*" é consumida pelo navegador (cliente). Use a URL pública do Convex. # IMPORTANTE: "NEXT_PUBLIC_*" é consumida pelo navegador (cliente). Use a URL pública do Convex.
# Não use o hostname interno do Swarm aqui, pois o browser não consegue resolvê-lo. # Não use o hostname interno do Swarm aqui, pois o browser não consegue resolvê-lo.
NEXT_PUBLIC_CONVEX_URL: "${NEXT_PUBLIC_CONVEX_URL}" NEXT_PUBLIC_CONVEX_URL: "${NEXT_PUBLIC_CONVEX_URL}"
# URLs consumidas apenas pelo backend/SSR podem usar o hostname interno # URLs consumidas apenas pelo backend/SSR usam o endpoint publico para evitar falhas de DNS interno
CONVEX_INTERNAL_URL: "http://sistema_convex_backend:3210" CONVEX_INTERNAL_URL: "https://convex.esdrasrenan.com.br"
# URLs públicas do app (evita fallback para localhost) # URLs públicas do app (evita fallback para localhost)
NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}" NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}"
BETTER_AUTH_URL: "${BETTER_AUTH_URL}" BETTER_AUTH_URL: "${BETTER_AUTH_URL}"
BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET}" BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET}"
REPORTS_CRON_SECRET: "${REPORTS_CRON_SECRET}" REPORTS_CRON_SECRET: "${REPORTS_CRON_SECRET}"
REPORTS_CRON_BASE_URL: "${REPORTS_CRON_BASE_URL}" REPORTS_CRON_BASE_URL: "${REPORTS_CRON_BASE_URL}"
# PostgreSQL connection string (usa o servico 'postgres' existente na rede traefik_public) # PostgreSQL connection string (usa o servico 'postgres18' existente na rede traefik_public)
# connection_limit: maximo de conexoes por replica (2 replicas x 10 = 20 conexoes) # connection_limit: maximo de conexoes por replica (2 replicas x 10 = 20 conexoes)
# pool_timeout: tempo maximo para aguardar conexao disponivel # pool_timeout: tempo maximo para aguardar conexao disponivel
DATABASE_URL: "postgresql://${POSTGRES_USER:-sistema}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-sistema_chamados}?connection_limit=10&pool_timeout=10" DATABASE_URL: "postgresql://${POSTGRES_USER:-sistema}:${POSTGRES_PASSWORD}@postgres18:5432/${POSTGRES_DB:-sistema_chamados}?connection_limit=10&pool_timeout=10"
# Evita apt-get na inicialização porque a imagem já vem com toolchain pronta # Evita apt-get na inicialização porque a imagem já vem com toolchain pronta
SKIP_APT_BOOTSTRAP: "true" SKIP_APT_BOOTSTRAP: "true"
# Usado para forçar novo rollout a cada deploy (setado pelo CI) # Usado para forçar novo rollout a cada deploy (setado pelo CI)
@ -87,12 +87,12 @@ services:
# O novo container só entra em serviço APÓS passar no healthcheck # O novo container só entra em serviço APÓS passar no healthcheck
start_period: 180s start_period: 180s
# PostgreSQL: usando o servico 'postgres' existente na rede traefik_public # PostgreSQL: usando o servico 'postgres18' existente na rede traefik_public
# Nao e necessario definir aqui pois ja existe um servico global # Nao e necessario definir aqui pois ja existe um servico global
convex_backend: convex_backend:
# Versao estavel - crons movidos para /api/cron/* chamados via crontab do Linux # Versao estavel - crons movidos para /api/cron/* chamados via crontab do Linux
image: ghcr.io/get-convex/convex-backend:precompiled-2025-12-04-cc6af4c image: ghcr.io/get-convex/convex-backend:6690a911bced1e5e516eafc0409a7239fb6541bb
stop_grace_period: 10s stop_grace_period: 10s
stop_signal: SIGINT stop_signal: SIGINT
volumes: volumes: