fix: tratar tokens de maquinas e alinhar stack/docs
All checks were successful
All checks were successful
This commit is contained in:
parent
b7e2c4cc98
commit
c030a3ac09
21 changed files with 309 additions and 36 deletions
22
agents.md
22
agents.md
|
|
@ -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)._
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
10
docs/DEV.md
10
docs/DEV.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
54
docs/alteracoes-producao-2025-12-18.md
Normal file
54
docs/alteracoes-producao-2025-12-18.md
Normal 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.
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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" })
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
43
src/server/machines/token-errors.ts
Normal file
43
src/server/machines/token-errors.ts
Normal 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}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
12
stack.yml
12
stack.yml
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue