Importação inicial do projeto Shivão (Diário de Bordo do veleiro) em estado pronto pra produção, já incluindo as 5 mudanças de hardening implementadas pela squad shivao-melhoria em 2026-04-27. Mudanças de hardening (HANDOFF.md seção "✅ Pronto"): 1. **Rate limiting nos 3 endpoints públicos de share** - express-rate-limit ^8.4.1, 60 req/min/IP - server/src/index.js linhas 38, 262, 271, 279 2. **Tamanho de upload reduzido pra 50MB** (era 200MB) - multer linha 84 3. **Validação Zod nos endpoints autenticados** (POST /api/data e /zones) - novo arquivo server/src/schemas/index.js - middleware validate() retorna 400 com top 5 issues 4. **Audit log de ações sensíveis** - nova tabela audit_log + funções db.audit() e db.recentAudit() - 6 endpoints instrumentados (state_set, media_insert/delete, share_create/revoke/zones_update) - novo endpoint GET /api/audit (autenticado) 5. **Catch silencioso de webhook em zona PROIBIDA tratado** - app/diario-bordo.html + server/public/index.html linha 3280 - agora loga erro + exibe toast ao usuário Status final do HANDOFF: - 🔴 Críticos restantes: 1 (CORS — decisão consciente single-tenant, não-acionável) - 🟡 Importantes: 4 itens (testes, vigia reconnect, refator frontend, demais catches) - 🟢 Bom-ter: 5 itens Stack: Node 20 ESM + Express + better-sqlite3 + Docker (Coolify) Single-tenant pessoal · single-file frontend HTML · offline-first Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.1 KiB
API Reference
Servidor Express em Node.js. Todas as rotas autenticadas exigem header:
Authorization: Bearer <BOAT_TOKEN>
Salvo onde indicado como público (sem auth).
Convenções
- Content-Type:
application/json(exceto/api/mediaque é multipart) - Timestamps: Unix ms (números inteiros)
- Coordenadas:
lat,lng(decimais) - Velocidade: m/s no servidor, knots no frontend (conversão x1.94384)
- Distância: metros no servidor, milhas náuticas no frontend (m / 1852)
Endpoints públicos
GET /api/health
Health check. Útil para monitoramento.
Response:
{ "ok": true, "ts": 1730000000000 }
GET /share/:token
Página HTML pública com mapa em tempo real do barco.
- Renderiza Leaflet com marcador do barco e trilha das últimas posições
- Inclui zonas de geofencing (proibidas/atenção) se foram salvas no share
- Auto-refresh a cada 15 segundos
- Retorna 404 se token inválido, 410 se expirado
GET /api/share/:token/info
Metadata do share (sem expor BOAT_TOKEN).
Response:
{
"boatName": "Shivao",
"expiresAt": 1730086400000,
"zones": [
{ "id": "...", "name": "Pedras", "type": "forbidden",
"center": { "lat": -23.0, "lng": -43.0 }, "radius": 100 }
]
}
GET /api/share/:token/positions
Posições históricas do share (até 500 últimas).
Response:
[
{ "lat": -23.0, "lng": -43.0, "speed": 3.2, "ts": 1730000000000 },
...
]
Server info
GET /api/info
Quais canais de notificação estão configurados.
Response:
{
"channels": ["telegram", "email", "ntfy"],
"heartbeatTimeoutSec": 300,
"version": "1.0"
}
Sync de dados
GET /api/data
Retorna todo o estado armazenado.
Response:
{
"data": { ... estado completo ... },
"updated_at": 1730000000000
}
POST /api/data
Substitui todo o estado.
Body:
{ "data": { ... estado completo ... } }
Nota: o servidor não merge nem versiona — substitui inteiro. Para multi-dispositivo, é responsabilidade do cliente puxar antes de modificar ou implementar uma estratégia de merge.
Mídia
POST /api/media
Upload de arquivo. Multipart form-data:
file: arquivo binárioid: ID único (cliente gera)parent_id: ID da viagem/manutenção pai (opcional)parent_type:trip,maint, oupendingkind:photo,video,audiocreated_at: timestamp ms
Limite: 200 MB por arquivo (revisar antes de produção — ver HANDOFF.md).
Response:
{ "ok": true, "id": "abc123", "url": "/api/media/abc123" }
GET /api/media/list
Lista todas as mídias (metadata, sem o conteúdo).
Response:
[
{ "id": "abc", "parent_id": "trip_1", "kind": "photo",
"mime": "image/jpeg", "size": 123456, "created_at": 1730000000000 }
]
GET /api/media/:id
Stream do arquivo. Retorna o blob com Content-Type original.
DELETE /api/media/:id
Remove a mídia (DB + filesystem).
Vigia de fundeio + dead-man switch
POST /api/anchor/start
Registra início de uma vigia no servidor.
Body:
{
"boat_name": "Shivao",
"anchor_lat": -23.0, "anchor_lng": -43.0,
"radius": 50
}
POST /api/anchor/heartbeat
App envia a cada 30s enquanto vigia ativa. Reseta o timer do dead-man.
Body:
{ "lat": -23.001, "lng": -43.001, "distance": 12.5 }
POST /api/anchor/alarm
Disparar alarme manualmente. Aciona TODOS os canais configurados em paralelo.
Body:
{
"boat_name": "Shivao",
"lat": -23.005, "lng": -43.005,
"distance": 87.3,
"radius": 50,
"reason": "drift"
}
Response:
{
"sent": ["telegram", "email", "ntfy"],
"failed": [{ "channel": "sms", "error": "Twilio rate limit" }]
}
POST /api/anchor/stop
Encerra vigia. Limpa estado no servidor.
GET /api/anchor/status
Estado atual da vigia.
Response:
{
"active": 1,
"boat_name": "Shivao",
"anchor_lat": -23.0, "anchor_lng": -43.0,
"radius": 50,
"started_at": 1730000000000,
"last_heartbeat": 1730000300000,
"last_lat": -23.001, "last_lng": -43.001,
"last_distance": 12.5,
"alarm_fired": 0
}
Dead-man switch
Roda em background a cada 30s. Se last_heartbeat está há mais de
HEARTBEAT_TIMEOUT_SEC (padrão 5 min) e active=1, dispara alarme com
reason: "heartbeat_lost".
Não envia spam — só dispara uma vez por episódio (até receber novo heartbeat ou stop).
Compartilhamento ao vivo
POST /api/share/create
Cria link público temporário.
Body:
{
"durationMinutes": 360,
"boatName": "Shivao",
"zones": [ ... opcional, snapshot das zonas atuais ... ]
}
Response:
{
"token": "Aljg29x71kqp...",
"expiresAt": 1730086400000,
"url": "https://shivao.exemplo.com/share/Aljg29x71kqp..."
}
GET /api/share/list
Lista shares ativos (não expirados, não revogados).
DELETE /api/share/:token
Revoga share (marca como inativo). Página pública passa a retornar 404.
POST /api/share/:token/zones
Atualiza zonas do share (caso o dono adicione/remova zonas após criar o share).
Body:
{ "zones": [ ... ] }
POST /api/share/position
Posta posição atual. App chama a cada 30s enquanto há share ativo.
A posição vai para todos os shares ativos do barco (filtrados por boat_name).
Body:
{ "lat": -23.0, "lng": -43.0, "speed": 3.2, "boatName": "Shivao" }
Response:
{ "ok": true, "posted": 2 }
Outros
POST /api/test
Dispara mensagem de teste em todos os canais configurados (sem urgência).
Response:
{ "sent": ["telegram"], "failed": [] }
GET /api/alarms
Histórico dos últimos 50 alarmes (incluindo testes).
Response:
[
{
"id": 1, "ts": 1730000000000, "type": "drift",
"payload": "{...JSON...}",
"sent": "[\"telegram\",\"ntfy\"]",
"failed": "[]"
}
]
Cleanup automático
Job rodando 1x/dia:
- Remove
share_positionsde shares cujoexpires_atpassou há mais de 7 dias - Remove os próprios
sharesexpirados