MVP da plataforma que organiza o caos entre 5.000 caminhões em rota e a central de decisão de uma operação logística: todos os incidentes em uma única fila, já ordenada pelo que exige ação primeiro, com garantia de que dois analistas nunca atuam no mesmo problema ao mesmo tempo.
| Onde | O que tem | |
|---|---|---|
| 🚀 | Aplicação online | A central funcionando, com dados de demonstração |
| 📘 | Documentação da API | Swagger gerado pelo FastAPI, com todos os endpoints e contratos |
| 🎬 | Vídeo de apresentação | A aplicação em uso e o controle de concorrência entre dois analistas ao vivo |
As decisões de arquitetura, a segurança da API e a esteira de CI/CD estão descritas neste README e nos ADRs.
Para entrar, use os botões de acesso rápido na tela de login (Ana, Bruno ou Carla; senha logos2026). Para testar o tempo real, abra uma janela anônima com outro analista.
Acesso de demonstração: a tela de login tem botões "Entrar como Ana" e "Entrar como Bruno" (usuários
anaebruno, senhalogos2026). Para ver o controle de conflito, entre como Ana em uma janela e como Bruno em uma janela anônima, e tente assumir o mesmo incidente.
- Como o Logos responde ao desafio
- Arquitetura
- Rodando localmente
- Testes e qualidade
- CI/CD e deploy
- API
- Bibliotecas e por quê
- Decisões de arquitetura (ADRs)
- Próximos passos
| Pergunta do desafio | Resposta no Logos | Onde ver |
|---|---|---|
| Como apresentar 100 incidentes e o analista priorizar em milissegundos? | A API já devolve a fila ordenada por criticidade → status (o que exige ação) → mais antigo. Cada linha tem faixa lateral colorida, badge com cor + ícone + texto (não depende só de cor), tempo aberto em números tabulares e destaque quando um incidente de criticidade alta está aberto há mais de 30 min. No topo, contadores por criticidade que também filtram com um clique. | ADR-0010 |
| Gestão por exceção (filtros) | Filtros por criticidade, categoria, status, período de ocorrência (última hora, 6 h, 24 h, 7 dias ou intervalo de datas) e busca textual, guardados na URL (compartilháveis, botão voltar funciona). Por padrão, os fechados ficam ocultos: a tela mostra só o que ainda pede ação. | frontend/src/features/incidents/filters.ts |
| Como garantir que dois analistas não resolvam o mesmo problema? | Duas camadas. (1) Concorrência otimista: toda alteração envia a versão lida, e o UPDATE ... WHERE version = :v é atômico no PostgreSQL; se alguém gravou antes, a resposta é 409 com o estado atual. (2) Atribuição: ao assumir, o incidente passa a ter um responsável, e só ele pode concluí-lo (403 para os demais). Um teste dispara duas threads simultâneas e verifica que exatamente uma vence. |
ADR-0007 |
| Como o código trata o estado enquanto o banco processa? | Atualização otimista com rollback: a tela muda na hora e a linha mostra "Salvando…". Se a API confirmar, adota a resposta; se recusar, desfaz e mostra o motivo (e quem está com o incidente); se a rede cair, desfaz e avisa. Sem conexão, a alteração fica "Aguardando conexão" e é enviada quando a rede volta. | ADR-0009 |
| Resiliência a rede instável e banco lento | Backend: timeouts curtos, pool_pre_ping, retry com backoff exponencial e jitter só em erros transitórios, 503 + Retry-After quando o banco não responde, Idempotency-Key na ingestão (caminhão que reenvia não duplica). Frontend: retry com backoff, fila antiga continua visível com aviso se a atualização falhar, banner de offline. |
ADR-0008 |
| Validação e entradas inesperadas | Schemas Pydantic estritos (extra="forbid", limites de tamanho, enums, padrão de placa, data no futuro rejeitada), JSON malformado → 422, corpo grande → 413, todo erro em Problem Details (RFC 9457), sem stack trace exposto. |
backend/app/schemas/incident.py |
| Segurança básica da API | JWT para analistas, API key para sistemas da frota (comparação em tempo constante), bcrypt, login sem enumeração de usuários, rate limit no login (429), CSP e headers de segurança no nginx, segredos só em variáveis de ambiente. | ADR-0006 |
| Visão de gestão (além da operação) | Painel analítico com filtro de período: volume, taxa de resolução, tempos médios de resposta e resolução, SLA por criticidade (meta × realizado), incidentes fora do SLA agora, série diária registrados × resolvidos, recorte por categoria e comparação entre analistas, com mapa de calor de quem assume que tipo de incidente. Cada incidente também tem histórico de auditoria (quem fez o quê e quando). | ADR-0013 |
| Coordenação entre analistas em tempo real | Chat da central com avisos automáticos (quem assumiu, devolveu, fechou e quem tentou assumir e perdeu a corrida). Quem perdeu pode pedir para assumir: o pedido aparece para todos e só o responsável aceita ou recusa, com transferência atômica e auditada. O responsável também pode delegar. Tudo chega em menos de 1 s via SSE + PostgreSQL LISTEN/NOTIFY: o aviso só sai depois do commit, e a reconexão recupera o que se perdeu (Last-Event-ID). |
ADR-0014, ADR-0015 |
| Navegar por milhares de registros | Paginação na URL (25/50/100 por página, com links compartilháveis) ou "Exibir todos" numa área com rolagem própria, mantendo filtros e contadores à vista. | frontend/src/features/incidents/pagination.ts |
| Saber o que está comigo e o que já concluí | Aba Meu ambiente: cada analista vê os incidentes que assumiu (em andamento) e os que concluiu (mais recentes primeiro), com tempo médio de resolução e taxa de SLA pessoais. Na fila, o filtro Responsável mostra os de qualquer analista. | frontend/src/features/workspace/ |
| Feedback visual, modo claro/escuro, transições | Skeleton, estados vazios distintos, tela de erro com "tentar novamente", toasts de confirmação, tema claro/escuro (preferência do sistema por padrão, sem piscar), transições curtas e respeito a prefers-reduced-motion. |
frontend/src/features/incidents/components/ |
Aberto ──assumir──► Em investigação ──concluir──► Fechado
▲ │ │
└────devolver───────┘ └──► Inconclusivo ──retomar──► Em investigação
└──fechar──► Fechado
Quem assume vira o responsável; só ele altera o incidente enquanto investiga. Detalhes em ADR-0005.
┌───────────── Navegador ─────────────┐
│ React + TanStack Query │ polling 5 s, mutação otimista,
│ (filtros na URL, tema, toasts) │ retry com backoff
└───────────────┬─────────────────────┘
│ HTTPS
┌───────────────▼─────────────────────┐
│ nginx do host (TLS, subdomínio) │ VPS Oracle Cloud
└───────────────┬─────────────────────┘
┌───────────────▼─────────────────────┐ Docker Compose
│ web: nginx (SPA + proxy /api, │
│ CSP, rate limit) │
├─────────────────────────────────────┤
│ api: FastAPI │ api/ → services/ → domain/
│ │ ↘ repositories/ → models/
├─────────────────────────────────────┤
│ db: PostgreSQL 16 │
└─────────────────────────────────────┘
backend/
app/
api/ rotas HTTP e dependências (auth, sessão)
services/ casos de uso (regras de conflito, atribuição, idempotência)
domain/ regras puras: enums, máquina de estados, criticidade padrão
repositories/ todo o SQL fica aqui
models/ mapeamento ORM
schemas/ DTOs Pydantic (contrato da API)
core/ config, banco + retry, segurança, Problem Details
scripts/ seed de demonstração, exportação do OpenAPI
alembic/ migrações versionadas
tests/ unit/ e integration/ (PostgreSQL real)
frontend/
src/
api/ cliente HTTP, erros tipados, tipos gerados do OpenAPI
app/ providers, rotas, shell, error boundary
features/auth/ sessão e login
features/incidents/ fila: queries, mutações, filtros, detalhe e histórico
features/analytics/ painel: indicadores, gráficos, SLA e comparação entre analistas
shared/ componentes e hooks reutilizáveis
styles/ design tokens (identidade AutoU) e estilos globais
infra/ compose de produção, nginx do host, bootstrap e deploy da VPS
docs/adr/ decisões de arquitetura
.github/ CI, deploy e template de PR
A separação em camadas está justificada no ADR-0004.
Pré-requisitos: Docker (com Compose) e Node.js 22.
# 1. Banco + API (aplica migrações e cria os dados de demonstração)
docker compose -f docker-compose.dev.yml up --build
# 2. Frontend (em outro terminal)
cd frontend
npm ci
npm run dev- Interface: http://localhost:5173 (o Vite faz proxy de
/apipara a API) - API e Swagger: http://localhost:8000/api/docs
curl -X POST http://localhost:8000/api/incidents \
-H "X-API-Key: dev-ingestion-key" \
-H "Idempotency-Key: cam-0421-evt-0001" \
-H "Content-Type: application/json" \
-d '{"title":"Pneu furado no eixo traseiro","category":"mechanical_failure","vehicle_id":"CAM-0421","location":"BR-116, km 212"}'Repetir o comando devolve o mesmo incidente (200 + Idempotent-Replayed: true) em vez de duplicar. A criticidade, quando omitida, é derivada da categoria.
| Variável | Padrão (dev) | Descrição |
|---|---|---|
LOGOS_DATABASE_URL |
postgresql+psycopg://logos:logos@localhost:5432/logos |
Conexão com o PostgreSQL |
LOGOS_JWT_SECRET |
valor de desenvolvimento | Segredo de assinatura dos tokens |
LOGOS_INGESTION_API_KEY |
dev-ingestion-key |
Chave dos sistemas da frota |
LOGOS_SEED_DEMO_DATA |
false |
Cria 3 analistas e 30 dias de incidentes (com histórico e marcos de SLA) se o banco estiver vazio |
LOGOS_DB_STATEMENT_TIMEOUT_MS |
5000 |
Tempo máximo de uma consulta |
# Backend: lint, tipos e testes (usa o PostgreSQL do compose de dev)
docker compose -f docker-compose.dev.yml run --rm api sh -c "ruff check . && mypy app && pytest --cov=app"
# Frontend
cd frontend
npm run typecheck && npx oxlint && npm test- Backend: 122 testes,
mypy --strict. Inclui o teste de corrida entre dois analistas (e entre dois pedidos de passagem simultâneos), payloads maliciosos, idempotência, queda do banco, trilha de auditoria, cálculos de SLA e a prova, escutando o canal real do PostgreSQL, de que eventos só são publicados após o commit. - Frontend: 82 testes com Vitest + Testing Library + MSW. Cobre estados de loading/erro/vazio, a mutação otimista (sucesso, conflito, queda de rede), o painel analítico, o parser SSE, o cache do chat, as permissões do cartão de passagem, a paginação, os dropdowns de filtro e o Meu ambiente.
- Contrato: o CI regenera os tipos TypeScript a partir do OpenAPI e falha se estiverem desatualizados.
Estratégia completa em ADR-0012.
Pull request ─► Backend CI + Frontend CI ─► revisão (template de PR) ─► merge
main ─► CIs ─► imagens api/web no GHCR (tag = SHA) ─► SSH na VPS ─► deploy.sh
─► health check (API + banco) ─► rollback automático se falhar ─► smoke test público
- As imagens são construídas no GitHub, não na VPS (1 GB de RAM).
- Usuário
deploydedicado, host key fixada e segredos gerados na própria VPS. - Primeira configuração da VPS:
infra/scripts/bootstrap-vps.sh.
Decisão e trade-offs em ADR-0011.
O login de demonstração é aberto de propósito, então a instância pública se protege de abuso:
- Cotas por IP no nginx: leituras com 20 req/s, escritas (criar, alterar, excluir, mandar mensagem) com 30 por minuto, login com 10 por minuto e até 6 conexões de tempo real. Acima disso a resposta é 429.
- Tamanho e formato: corpo de no máximo 64 KB (4 KB no login), validação estrita de cada campo (campos desconhecidos são recusados), SQL sempre parametrizado e texto sempre escapado na tela (sem HTML injetável).
- Isolamento de recursos: cada container tem teto de memória e os logs têm teto de tamanho, então uma enxurrada não derruba a VPS nem enche o disco.
- Reset diário: às 04:00 (horário de Brasília) os dados criados por visitantes são apagados e a base de demonstração é recriada (
python -m app.scripts.seed --reset).
Um ataque distribuído de grande volume (muitos IPs) está fora do alcance de uma VM gratuita; o próximo passo seria colocar um CDN com proteção contra DDoS na frente.
Documentação interativa em /api/docs. Resumo:
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/auth/login |
Login do analista → JWT |
GET |
/api/incidents |
Fila priorizada; filtros severity, category, status (repetíveis), search, reported_from/reported_to (ISO 8601 com fuso), limit, offset |
GET |
/api/incidents/summary |
Contadores por criticidade e status |
GET |
/api/incidents/{id} |
Detalhe |
GET |
/api/incidents/{id}/events |
Histórico de auditoria (criação, mudanças de status, edições) |
GET |
/api/stream |
Eventos em tempo real (SSE): mensagens do chat e mudanças de incidentes; aceita Last-Event-ID |
GET/POST |
/api/chat/messages |
Histórico (paginado por before_id) e envio de mensagem |
GET/POST |
/api/incidents/{id}/handover-requests |
Pedidos de passagem do incidente / pedir para assumir |
POST |
/api/handover-requests/{id}/accept, /decline, /cancel |
Responder ou cancelar um pedido |
POST |
/api/incidents/{id}/delegate |
Responsável passa o incidente a outro analista (exige version) |
GET |
/api/users |
Analistas da central |
GET |
/api/analytics |
Indicadores: resumo, por categoria, por criticidade (SLA), por analista e série diária; filtros reported_from/reported_to e tz |
POST |
/api/incidents |
Registra incidente (JWT ou X-API-Key; aceita Idempotency-Key) |
PATCH |
/api/incidents/{id} |
Edita dados (exige version) |
PATCH |
/api/incidents/{id}/status |
Assumir, devolver, concluir (exige version) |
DELETE |
/api/incidents/{id}?version=N |
Exclui registro aberto feito por engano |
GET |
/api/health/live, /api/health/ready |
Liveness e readiness (banco) |
Erros seguem a RFC 9457 (application/problem+json). Em conflito, o corpo traz o estado atual em current:
{
"type": "/problems/version-conflict",
"title": "O incidente foi alterado por outra pessoa",
"status": 409,
"detail": "Este incidente foi alterado por outra pessoa enquanto você o visualizava. ...",
"current": { "id": "…", "status": "investigating", "version": 2, "assignee": { "display_name": "Bruno Lima" } }
}| Biblioteca | Papel | Por que ela |
|---|---|---|
| FastAPI | Framework HTTP | Validação declarativa, OpenAPI automático e injeção de dependências, com pouco código |
| Pydantic v2 | Validação/DTOs | Rejeita dados inesperados na borda; o mesmo schema gera a documentação |
| SQLAlchemy 2 + psycopg 3 | ORM e driver | UPDATE condicional atômico para a concorrência otimista; tipagem moderna |
| Alembic | Migrações | Esquema versionado, aplicado no deploy e testado com downgrade no CI |
| tenacity | Retry | Backoff exponencial com jitter, aplicado só a erros transitórios |
| PyJWT + bcrypt | Autenticação | Padrões consolidados, sem framework de auth pesado |
| React 19 + Vite | Interface | SPA estática servida pelo nginx; build rápido |
| TanStack Query | Estado do servidor | Cache, polling, retry, mutação otimista com rollback e pausa offline |
| React Router | Rotas e URL | Filtros e incidente aberto vivem na URL |
| sonner | Toasts | Feedback acessível e discreto das ações |
| lucide-react | Ícones | Leves, consistentes, com tree-shaking; ícones reforçam o significado das cores |
| Vitest + Testing Library + MSW | Testes de frontend | Testa componentes com o cliente HTTP real, interceptando a rede |
| pytest, ruff, mypy, oxlint, Prettier | Qualidade | Lint, formatação e tipos estritos verificados no CI |
Sem biblioteca de componentes de UI nem de gráficos: os componentes usam os design tokens da AutoU (Figtree, navy, azul, laranja), o modal usa o <dialog> nativo, que já traz foco preso e ESC, e os gráficos do painel são SVG próprio. Os gráficos têm tooltip, navegação por teclado e tabela equivalente, e as cores foram validadas para daltonismo e contraste nos dois temas.
Todas em docs/adr/:
0001 Registrar decisões em ADR · 0002 Monorepo · 0003 FastAPI + SQLAlchemy + PostgreSQL · 0004 Arquitetura em camadas · 0005 Ciclo de vida como máquina de estados · 0006 Autenticação · 0007 Concorrência otimista + atribuição · 0008 Resiliência e idempotência · 0009 Estado e resiliência no frontend · 0010 Design tokens e design da informação · 0011 CI/CD e deploy · 0012 Estratégia de testes · 0013 Auditoria, SLA e indicadores · 0014 Tempo real com SSE e LISTEN/NOTIFY · 0015 Chat e passagem de incidentes
- Papel de supervisor com aceite forçado de passagem e aceite automático após tempo sem resposta.
- Metas de SLA configuráveis pela interface e alerta ativo (push/e-mail) quando um incidente entra em violação.
- Séries diárias materializadas quando o volume passar de milhões de incidentes.
- Token em cookie
HttpOnlye usuáriodeployrestrito a um comando SSH forçado.