Onboarding — Meu PACS Cloud
Guia para quem está chegando agora. Leia isso antes doARCHITECTURE.md — aqui está o contexto e a sequência lógica; lá está a referência técnica completa.
1. O que é o sistema
Plataforma SaaS multi-tenant para clínicas de imagem. As clínicas conectam seus aparelhos de raio-x, tomógrafo e ressonância ao sistema. As imagens chegam automaticamente, os médicos visualizam e escrevem laudos, e o sistema cuida de tudo no meio: armazenamento DICOM, processamento, exibição e PDF. Quem usa:- Médico radiologista — abre o estudo, visualiza imagens no viewer, escreve o laudo, exporta PDF
- Admin da clínica — gerencia usuários, unidades, presets de laudo e templates de PDF
- Super admin (Technik) — gerencia todas as clínicas, aparelhos e usuários da plataforma
2. Glossário de domínio
Antes de ler qualquer código, entenda esses termos — eles aparecem em todo lugar:| Termo | O que é |
|---|---|
| Tenant | Uma clínica. Tudo no sistema pertence a um tenant. |
| Unit | Uma filial ou unidade da clínica. Um tenant pode ter várias. |
| Modality | Um aparelho DICOM (tomógrafo, ressonância, raio-x). Identificado pelo AET (nome de rede DICOM). |
| Study | Um exame completo. Agrupa todas as imagens de um paciente numa sessão. |
| Series | Uma sequência de imagens dentro do estudo (ex.: cortes axiais de um CT). Um study tem várias series. |
| Instance | Uma imagem individual. Uma series tem várias instances (também chamadas de “frames”). |
| Report | O laudo médico de um estudo. Tem status: pending → draft → final. |
| ReportRevision | Registro imutável de auditoria. Toda ação no laudo gera uma. |
| Template | Define o layout visual do PDF do laudo (header, footer, slots de logo e assinatura). Vinculado a uma Unit. |
| Preset | Texto pré-preenchido para um tipo de exame. O médico seleciona e o texto aparece no editor. |
| AET | Application Entity Title — o “nome de rede” de um aparelho DICOM. Como um hostname, mas no protocolo DICOM. |
3. Por onde começar
3.1 Leia nesta ordem
- Este arquivo (contexto e domínio)
RUNNING.md(como rodar local, comandos, checks de saúde)ARCHITECTURE.mdseção 6 (fluxo de ingestão DICOM) — entenda como uma imagem chega ao sistemaARCHITECTURE.mdseção 10 (multi-tenant) — entenda como o isolamento funcionaARCHITECTURE.mdseção 11 (laudos) — entenda o fluxo principal de usoARCHITECTURE.mdseção 12 (OHIF + token + proxy) — entenda como o viewer funciona
3.2 Suba o ambiente
3.3 Verifique que está tudo funcionando
4. Fluxo de autenticação
Entender como o login funciona end-to-end poupa muito tempo::3000) e backend (:8000) são origens diferentes. O cookie httpOnly do backend está scoped para :8000 — o JS em :3000 não lê e o middleware Next.js em :3000 também não consegue acessá-lo. O pacs_user_role resolve isso: é scoped para :3000 e serve como indicador de sessão para o roteamento do Next.js.
Em dev e prod: o mesmo código funciona. A diferença é SESSION_SECURE_COOKIE:
- Dev:
false— cookie semSecure, funciona em HTTP - Prod:
true— cookie exige HTTPS
5. Guia por camada
5.1 Backend — Laravel
Onde fica o quê:ContainerAdicionar uma nova rota:pacs_queue: executaphp artisan queue:worke processa os jobs do webhook do Orthanc (ex:ProcessStudyMetadata). É diferente dopacs_worker(Python) — aquele processa imagens DICOM; este processa jobs Laravel. Ambos rodam em paralelo.
Atenção: O Octane mantém o processo PHP vivo entre requisições para performance. Mudanças em arquivos PHP não são detectadas automaticamente. Sempre recarregue após editar código PHP.Adicionar um campo ao banco:
5.2 Frontend — Next.js
Estrutura de rotas (App Router):axios.get() direto nos componentes.
State global:
- Auth:
frontend/store/auth-store.ts(Zustand) — guarda o usuário logado - UI local:
useState/useReducernos componentes
.tsx já recarrega no browser. Não precisa reiniciar nada.
5.3 Orthanc
O que é: servidor DICOM open-source. Recebe imagens de aparelhos via protocolo DICOM (porta 4242) e as serve via REST API e DicomWeb (porta 8042). Arquivos de configuração:5.4 Worker Python
O que faz: consome a filapacs_python_queue do Redis. Para cada mensagem:
- Baixa o DICOM do Orthanc
- Gera thumbnail 300×300 JPEG
- Salva em
/app/storage/thumbnails/{orthanc_study_id}/preview.jpg - Indexa Patient, Series e Instances no PostgreSQL
- Atualiza o array
modalitiesno Study
worker/main.py
Ver o que está acontecendo:
5.5 Redis
O Redis tem três papéis no sistema:| Uso | Chave / Fila | Quem escreve | Quem lê |
|---|---|---|---|
| Fila de jobs Laravel (default) | queues:default | WebhookController | Laravel queue worker (interno ao backend) |
| Fila do worker Python | pacs_python_queue | ProcessStudyMetadata job | Worker Python (BLPOP) |
| Tokens temporários do OHIF | viewer_token:{uuid} | ViewerTokenController | ViewerTokenController (validate) + nginx |
5.6 PostgreSQL
Acessar o banco:| Tabela | Model | Descrição |
|---|---|---|
tenants | Tenant | Clínicas |
units | Unit | Filiais das clínicas |
users | User | Médicos e admins |
modalities | Modality | Aparelhos DICOM (com AET) |
studies | Study | Exames recebidos |
patients | Patient | Pacientes (indexados do DICOM) |
series | Series | Séries de imagens por estudo |
instances | Instance | Imagens individuais |
reports | Report | Laudos |
report_revisions | ReportRevision | Auditoria imutável dos laudos |
report_templates | ReportTemplate | Templates de PDF |
report_presets | ReportPreset | Textos pré-preenchidos |
tenant_id e usam o trait BelongsToTenant para scoping automático.
5.7 OHIF + Proxy + Token
O fluxo completo de quando um médico clica em “Abrir Viewer”:<iframe>, window é o contexto do iframe — window.location.search lê a URL do próprio iframe, não da página pai. O app-config.js lê o token corretamente em ambos os casos.
6. Como debugar
Ver logs em tempo real
Testar uma rota de API manualmente
Simular o webhook do Orthanc
Ver o que está na fila do worker
Acessar o shell do backend
Verificar erro 422 em requisições
Provavelmente é um problema de validação. Veja o corpo da resposta — o Laravel retorna{"message": "...", "errors": {"campo": ["mensagem"]}}. Se o campo for booleano enviado pelo Axios, veja a armadilha abaixo.
7. Armadilhas conhecidas
Booleanos em query params (Axios + Laravel)
O Axios serializatrue (booleano JS) como a string "true" na URL. A regra de validação 'boolean' do Laravel só aceita true, false, 0, 1, "0", "1" — rejeita "true" com 422.
Regra: sempre usar $request->boolean('campo') para ler booleanos de query params. Nunca $request->validate(['campo' => 'boolean']) para parâmetros enviados pelo Axios.
Filtros opcionais: filled() vs has()
$request->has('campo') retorna true mesmo quando o valor é uma string vazia "". Use $request->filled('campo') para ignorar valores vazios.
Reload obrigatório do Octane após mudanças PHP
O Octane mantém o processo vivo para performance. Mudanças em.php não são detectadas automaticamente.
iframe e sessionStorage
Dentro de um<iframe>, window é o contexto do iframe — não da página pai. Isso significa que window.location.search, sessionStorage e localStorage são do iframe, não da janela que o contém. O app-config.js do OHIF lê o token da URL corretamente em ambos os modos (nova aba e iframe) sem nenhuma mudança.
Worker sem variáveis DB
As variáveisDB_* do worker Python vêm do .env raiz (não do backend/.env). Se o worker processar o thumbnail mas não indexar series/instances/modalities, verifique se DB_HOST, DB_DATABASE, DB_USERNAME e DB_PASSWORD estão definidas no .env raiz.
Cookie JWT não é acessível via JS
Opacs_access_token é httpOnly — document.cookie, localStorage e qualquer JS não conseguem lê-lo. Isso é intencional (proteção contra XSS). O Axios envia o cookie automaticamente via withCredentials: true. Se precisar debugar o token em dev, use as DevTools do browser (aba Application → Cookies → localhost:8000).
Não confundir o cookie de role com o cookie de autenticação
pacs_user_role (lido pelo Next.js middleware para roteamento) e pacs_access_token (lido pelo backend para autenticar) são cookies independentes com propósitos diferentes. Alguém pode forjar o pacs_user_role e ver a UI de admin, mas todas as chamadas de API vão falhar com 401/403 pois não têm o JWT válido.
Template vs Preset — sem vínculo entre os dois
Template e Preset são conceitos independentes. Um preset não temtemplate_id. O template é resolvido automaticamente pela unidade do médico (GET /tenant/my-template). O preset é selecionado pelo médico (ou sugerido automaticamente) e fornece o texto inicial. Os dois são aplicados separadamente no PDF.
Interceptor de refresh — não modificar isRefreshing/pendingQueue
Oclient.ts implementa um mecanismo de fila para evitar race conditions quando múltiplas requisições recebem 401 simultaneamente: isRefreshing trava novos refresh enquanto um está em andamento; pendingQueue acumula as requisições e drainQueue as reprocessa após o refresh completar. Modificar esse mecanismo sem entender o fluxo completo pode causar loops infinitos de refresh ou perda de requisições.
8. Convenções do projeto
- IDs: sempre UUID (não integers)
- Datas: UTC no banco, formatadas no frontend
- Soft delete: templates usam
deleted_at; presets e estudos são hard delete - HTML no banco: o conteúdo do laudo (
reports.content) e dos presets (report_presets.default_content) é armazenado como HTML gerado pelo Tiptap - Nomes de branches:
feat/nome,fix/nome,infra/nome→ PR paradevelop→ PR paramain - Nunca commitar direto em
main
9. Referências rápidas
| O que precisa | Onde encontrar |
|---|---|
| Especificação técnica completa | ARCHITECTURE.md |
| Todas as rotas de API | ARCHITECTURE.md seção 8 ou backend/routes/api.php |
| Modelos do banco | backend/app/Models/ |
| Tipos TypeScript da API | frontend/types/ |
| Layout do PDF | frontend/lib/utils/study-report-pdf.ts + frontend/types/report-template-layout.ts |
| Config do Orthanc | docker/orthanc/orthanc.json |
| Config do nginx proxy | docker/orthanc-cors-proxy/nginx.conf |
| Config do OHIF | docker/ohif/app-config.js |
| Variáveis de ambiente | ARCHITECTURE.md seção 4 |
