Files
scoreodonto.com/doc/RX_ARQUITETURA_scoreodonto-rx-clientWindows-wasabi.md
T
VPS 4 Builder 5049ce9b8b
build-and-push / build (push) Successful in 1m10s
build-and-push / deploy (push) Has been skipped
docs: unifica documentação em doc/ (fonte única, sem sobreposição)
- Pasta única scoreodonto.com/doc/ com 9 docs que prestam:
  VERDADE-HOJE (estado, principal), DEPLOY-PRODUCAO (runbook), BANCO-DEV-ESTRATEGIA,
  REGISTRY, AUDITORIA-FUNCIONAL, BACKLOG-FINANCEIRO-V1, CRM_Ortodontia, RX_ARQUITETURA, RX_BACKLOG.
- Removidos os docs de infra sobrepostos/desatualizados (estado consolidado em VERDADE-HOJE;
  histórico fica no git): arquitetura.md, deploy.md, CHECKLIST-DEPLOY-PRODUCAO.md,
  PENDENCIAS.md, PENDENCIAS_E_DEPLOY.md.
- Pasta documentação/ eliminada.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:04:08 +02:00

15 KiB
Raw Blame History

Arquitetura RX — scoreodonto.com → rx.scoreodonto.com → clientWindows.exe → Wasabi

Documento de revisão / engenharia reversa (gerado em 2026-06-12, sem alterar código). Fonte da verdade: rx.scoreodonto.com/dental-server (servidor) lido linha a linha. O .exe do client Windows está em repo separado (Gitea) — aqui seu comportamento é descrito pelo protocolo que o servidor expõe/aceita (Socket.IO + machine_token).


1. Visão geral (quem é quem)

┌──────────────────────────┐         ┌───────────────────────────────────────────┐
│  CLÍNICA (PC com sensor)  │         │        rx.scoreodonto.com (dental-server)  │
│                          │         │        Node + Express + Socket.IO          │
│  clientWindows.exe       │  WSS    │                                            │
│  - captura RX do sensor  │────────▶│  io.on('send-image')                       │
│  - auth: machine_token   │ socket  │   → storage.saveImage()                    │
│  - emite 'send-image'    │  .io    │   → INSERT images (Postgres dental_images) │
└──────────────────────────┘         │                                            │
                                     │  HTTP API (/api/images, /api/admin/...)    │
┌──────────────────────────┐  HTTPS  │   - lista pacientes/imagens                │
│  scoreodonto.com (plugin)│────────▶│   - cria devices (/api/admin/clinics)      │
│  - superadmin: config     │  proxy  │                                            │
│    global (rx_url+token)  │ no nosso│            │ saveImage / getImageBuffer    │
│  - backend proxeia leitura│ backend │            ▼                              │
└──────────────────────────┘         │   ┌─────────────────────────────────────┐ │
                                     │   │ WASABI (S3) bucket                   │ │
                                     │   │ key: clinic/client/patient/file.png  │ │
                                     │   └─────────────────────────────────────┘ │
                                     │   (+ cache local em ./uploads como fallback)│
                                     └───────────────────────────────────────────┘

Quatro peças:

  1. clientWindows.exe — instalado no PC da clínica; captura a radiografia e envia via Socket.IO.
  2. rx.scoreodonto.com / dental-server — recebe, persiste metadados no Postgres e o binário no Wasabi; serve listagens e imagens.
  3. Wasabi — storage S3-compatível (objeto = clinic/client/patient/arquivo).
  4. scoreodonto.com (plugin RX) — administra/visualiza via API, com token global (nosso trabalho recente).

2. Banco de dados do RX (Postgres dental_images, em database.js)

Tabela Campos-chave Papel
clinics_devices clinic_name, email (UNIQUE), password_hash, pc_name, machine_token (UUID UNIQUE), is_active, last_ip 1 linha por (clínica, PC). Credencial do client Windows. Criada por POST /api/admin/clinics.
images image_guid, patient_name, client_name (=PC), clinic_name, filename, thumb_filename, doctor, remark, enabled, created_at 1 linha por radiografia. Filtros de leitura hoje: client_name e patient_name (e agora clinic_name, ver §7).
users username, email, password_hash, is_admin Usuários admin/web (painel).
api_tokens name, token Tokens estáticos de API (viram admin, não expiram).
system_config key, value Key-value. Guarda wasabi_config.
gtos / gto_images gto_number, patient_name, … Documentos GTO por paciente.

3. clientWindows.exe — como envia os RX (protocolo)

O envio NÃO é HTTP de upload; é Socket.IO (server.js):

  1. Conexão / auth do socket (io.use(...), server.js:823):

    • O client conecta com handshake.auth.token = machine_token (UUID gerado no cadastro do device).
    • Regex confirma UUID → busca em clinics_devices → se is_active, autoriza.
    • Opcional: se email+password também vierem, são validados (compat).
    • Resultado: socket.user = { role:'client_device', clinicId, clinicName, pcName } vindos do registro do device (não do client). Atualiza last_ip.
    • Também aceita CLIENT_API_KEY (sensor legado) e JWT (painel web / app novo).
  2. Envio da imagem — DIRECT UPLOAD pro Wasabi (fluxo REAL de produção): Confirmado pelos logs do client (🌐 Fazendo Direct Upload pro Wasabi (Bypass VPS)). O binário NÃO trafega pela VPS — vai direto pro Wasabi via URL pré-assinada:

    1. Client monitora C:\ProgramData\RF\Dental Sensor\Images (Chokidar) e gera thumb local.
    2. socket.emit('request-presigned-urls', { metadata:{guid}, patientData:{name,...} }) → servidor responde { originalUrl, thumbnailUrl, filename, thumbFilename } (storage.getUploadPresignedUrl, server.js:1218). A key do Wasabi já é fixada aqui.
    3. Client faz PUT direto no Wasabi (original + thumb) usando as presigned URLs.
    4. socket.emit('image-upload-direct', {...}) → servidor só grava os metadados em images (server.js:1258). Não recebe os bytes.
    • Em ambos (2 e 4) o eixo de clínica é o MESMO: clinicName = socket.user?.clinicName || clientInfo?.clinicName || 'Clínica não identificada' → vem do clinic_name do device (machine_token). clientName = PC; patientName = paciente.

⚙️ Existe também o caminho legado socket.on('send-image') (base64 pela VPS, server.js:1031) e POST /api/images/upload (upload manual). Mas o client Windows real usa o Direct Upload. Há ainda POST /api/client/login (JWT 2h client_device) para fluxos HTTP.

3.1. ⚠️ Estado real dos dados (2026-06-12, banco compartilhado dev+prod):

  • 1 device: clinic_name="Consultt Clinic", pc="RX".
  • images: 2552 imgs / 503 pacientes com clinic_name = NULL (subiram antes do clinic_name ser setado → Wasabi key desconhecido/RX/paciente/…) + 222 imgs / 37 pacientes sob "Consultt Clinic". → A maioria das radiografias está "órfã" de clínica.

4. Wasabi storage (storage.js) — a lógica que NÃO pode quebrar

Config: lida do banco em system_config.wasabi_config ({ wasabiAccessKey, wasabiSecretKey, wasabiBucket, wasabiRegion, wasabiEndpoint }), carregada por loadConfigFromDb() (server.js:1428, e ao salvar em routes/system.js). SDK: @aws-sdk/client-s3 com forcePathStyle:true, endpoint s3.wasabisys.com.

Chave do objeto (CRÍTICO):

key = sanitize(clinic_name) / sanitize(client_name) / sanitize(patient_name) / filename

sanitizePathSegment troca /\?#%*:"<>| por _. (storage.js:103, 120, 153, 220, 298, 340, 366, 414)

saveImage (storage.js:139):

  1. grava local em ./uploads/<filename>;
  2. se Wasabi ativo → Upload (lib-storage) para a key acima;
  3. se subiu OK → apaga o local (Wasabi vira fonte). Se falhar → mantém local (fallback).

getImageBuffer (storage.js:190): local primeiro → senão Wasabi (key vinda dos args ou de um SELECT em images por filename/thumb_filename) → fallback key na raiz do bucket (arquivos legados) → cacheia local. getImageUrl/getDownloadPresignedUrl geram URL assinada (1h).

deleteImage: apaga local (uploads + processed) e a key no Wasabi (+ key raiz, redundância legada).

Wasabi é opcional: sem config, tudo funciona só em disco local. Por isso "desligar/errar" a config do Wasabi não derruba o servidor — só muda onde o binário vive.


5. Autenticação — matriz completa

Quem Como autentica Onde
client Windows (socket) machine_token (UUID) — suficiente; email+senha opcionais io.use server.js:823
client Windows (HTTP) POST /api/client/login → JWT 2h role:client_device client-auth.js
sensor legado CLIENT_API_KEY (rf-dental-secure-key-2026 default) io.use / x-api-key
admin / painel web usuário+senha → JWT 7d (is_admin) OU api_token estático auth.js authenticateToken
público sem auth GET /api/images/:id/thumbnail

authenticateToken (auth.js:12): tenta JWT; se falhar, procura o token em api_tokens → se achar, injeta um admin mock (is_admin:true). É isso que o scoreodonto usa (token global) para chamar /api/admin/clinics e as listagens.


6. Integração scoreodonto.com (plugin RX) — estado atual

  • Config GLOBAL (só superadmin) em settings.rx_config_global = { rx_url, api_token } no backend do scoreodonto. O api_token é um token de api_tokens do RX (admin, não expira).
  • Criar PC (POST /api/rx/devices no nosso backend) → POST {rx_url}/api/admin/clinics com clinic_name = clinicaId (a clínica do scoreodonto) + pc_name + email/senha por PC. Devolve o machine_token que vai no client Windows.
  • Ler (GET /api/rx/patients, /api/rx/images) → proxeia /api/images/patients e /api/images/by-patient com ?clinic=<clinicaId> + Bearer api_token. Isolamento por vínculo.
  • Thumbnails: públicos, carregados direto de rx_url.

7. ⚠️ Pontos de atenção para NÃO quebrar o que já funciona

  1. clinic_name é o eixo de tudo (Wasabi key + filtro de listagem). Os clients Windows já em produção sobem imagens com o clinic_name que foi gravado no cadastro do device deles (provavelmente o nome legível da clínica).

  2. Descompasso scoreodonto × RX: o plugin novo cria device com clinic_name = clinicaId (ID opaco do scoreodonto). Logo:

    • Imagens antigas estão sob clinic_name = <nome legível> no Wasabi e no images.
    • Imagens novas (via plugin) ficariam sob clinic_name = <clinicaId>.
    • Filtrar por ?clinic=<clinicaId> não acha as antigas (clinic_name diferente) — mas não quebra o envio existente (cada client continua com o seu clinic_name).
    • → Para enxergar clínicas já ativas, é preciso um mapeamento clinicaId ↔ clinic_name atual (tabela/coluna de-para), NÃO renomear objetos no Wasabi (arriscado e custoso).
  3. Hierarquia desejada (4 níveis) × Wasabi (3 níveis):

    • Desejado: scoreodonto / consulttclinic@gmail.com / clinicaID / paciente.
    • Wasabi real: clinic_name / client_name / paciente / arquivo (3 níveis, sem o nível "conta").
    • A "conta-mestre" (email) é identidade de auth (api_token/users.email), não faz parte da key.
    • Realizar 4 níveis exigiria mudar o esquema de key — risco alto para objetos já gravados. Caminho seguro: manter 3 níveis e usar clinic_name = clinicaId só para o que for novo, com de-para para o legado.
  4. npm run dev do RX = node server.js (sem nodemon) → mudanças no dental-server exigem docker restart rx_dev_api. (Em prod o deploy é via deploy-prod.sh / webhook.)

  5. Não tocar: io.on('send-image'), storage.saveImage/getImageBuffer, o esquema da key, nem o cadastro de clinics_devices dos PCs que já enviam. Toda evolução deve ser aditiva (novos endpoints/colunas), preservando o caminho do client Windows.


8. Endpoints-chave (referência rápida)

Endpoint Auth Uso
WSS send-image machine_token client Windows envia RX
POST /api/admin/clinics admin/api_token cria device (clinic_name, email, pc_name) → machine_token
GET /api/admin/clinics admin/api_token lista devices
GET /api/images?clinic=&client=&search= admin lista imagens (filtros)
GET /api/images/patients?clinic=&search=&page= admin pacientes agrupados
GET /api/images/by-patient?name=&clinic= admin imagens de um paciente
GET /api/images/:id/thumbnail público miniatura
GET /api/version · /download-client público versão / instalador do client
POST /api/auth/login → JWT · POST /api/auth/tokens → api_token admin obter credenciais de API

9. Multi-tenant 2 níveis: tenant_id / clinica_id (implementado 2026-06-12, DEV)

Hierarquia (espelha o scoreodonto, que tem conta multi-clínica no plano): tenant_id (conta/dono = usuarios.id "u_…") / clinica_id (clinicas.id "c_…") / pc / paciente / arquivo. O tenant_id é resolvido pelo vínculo dono* da clínica (owner_id está vazio hoje). Para a Consultt: tenant_id=u_1780870994865_3876n (Rui Cesar) / clinica_id=c_1780871001549_koqa3o.

  • Colunas tenant_id e clinica_id em clinics_devices e images (+índices); clinic_name vira rótulo. Backfill: device + 2776 imgs → tenant+clínica da Consultt.
  • Verificado: ?tenant_id= (conta) e ?clinica_id= (clínica) isolam; tenant/clínica inexistente → 0.
  • scoreodonto: cadastro de device resolve+envia tenant_id; proxy de leitura libera conta (tenant_id = própria conta do dono) ou clínica (clinica_id com vínculo).

(Resto desta seção descreve a base por clinica_id.)

  • Schema RX: coluna clinica_id em clinics_devices e images (+ índices). clinic_name vira só rótulo legível. Backfill: device + 2776 imgs existentes → c_1780871001549_koqa3o.
  • Fluxo: POST /api/admin/clinics grava clinica_id; o socket (machine_token) resolve socket.user.clinicaId do device; image-upload-direct (e o legado send-image) gravam images.clinica_id. Wasabi key ainda é clinic_name/… (mudança física = fase de reconciliação, pendente da resposta sobre os PNGs de origem).
  • Isolamento: listagens (/api/images, /patients, /by-patient, /unique-clinics) filtram por ?clinica_id=. Verificado: tenant real retorna dados, tenant inexistente → 0.
  • scoreodonto: /api/rx/* envia/filtra por clinica_id (e clinic_name=nome_fantasia no cadastro); proxy de leitura valida vínculo (_rxClinicScopeOk) → 403 fora do tenant.
  • Pendente: reconciliação física no Wasabi (clinica_id/…) + remoção do send-image legado (após confirmar que nenhum .exe em campo usa versão pré-Direct-Upload).

TL;DR

O client Windows manda RX por Socket.IO autenticado por machine_token; o servidor grava metadados no Postgres e o arquivo no Wasabi sob clinic_name/client_name/patient_name/arquivo. O scoreodonto administra isso com um token de API global, filtrando por clinic_name = clinicaId. O risco de integração está em clinic_name: alinhar o ID do scoreodonto com o clinic_name que os PCs já usam, sem renomear nada no Wasabi nem mexer no caminho de envio.