- 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>
15 KiB
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.exedo 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:
- clientWindows.exe — instalado no PC da clínica; captura a radiografia e envia via Socket.IO.
- rx.scoreodonto.com / dental-server — recebe, persiste metadados no Postgres e o binário no Wasabi; serve listagens e imagens.
- Wasabi — storage S3-compatível (objeto =
clinic/client/patient/arquivo). - 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):
-
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→ seis_active, autoriza. - Opcional: se
email+passwordtambém vierem, são validados (compat). - Resultado:
socket.user = { role:'client_device', clinicId, clinicName, pcName }vindos do registro do device (não do client). Atualizalast_ip. - Também aceita
CLIENT_API_KEY(sensor legado) e JWT (painel web / app novo).
- O client conecta com
-
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:- Client monitora
C:\ProgramData\RF\Dental Sensor\Images(Chokidar) e gera thumb local. 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.- Client faz PUT direto no Wasabi (original + thumb) usando as presigned URLs.
socket.emit('image-upload-direct', {...})→ servidor só grava os metadados emimages(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 doclinic_namedo device (machine_token).clientName= PC;patientName= paciente.
- Client monitora
⚙️ Existe também o caminho legado
socket.on('send-image')(base64 pela VPS, server.js:1031) ePOST /api/images/upload(upload manual). Mas o client Windows real usa o Direct Upload. Há aindaPOST /api/client/login(JWT 2hclient_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 comclinic_name = NULL(subiram antes do clinic_name ser setado → Wasabi keydesconhecido/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):
- grava local em
./uploads/<filename>; - se Wasabi ativo →
Upload(lib-storage) para a key acima; - 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 | só 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. Oapi_tokené um token deapi_tokensdo RX (admin, não expira). - Criar PC (
POST /api/rx/devicesno nosso backend) →POST {rx_url}/api/admin/clinicscomclinic_name = clinicaId(a clínica do scoreodonto) +pc_name+ email/senha por PC. Devolve omachine_tokenque vai no client Windows. - Ler (
GET /api/rx/patients,/api/rx/images) → proxeia/api/images/patientse/api/images/by-patientcom?clinic=<clinicaId>+ Bearerapi_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
-
clinic_nameé o eixo de tudo (Wasabi key + filtro de listagem). Os clients Windows já em produção sobem imagens com oclinic_nameque foi gravado no cadastro do device deles (provavelmente o nome legível da clínica). -
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 noimages. - 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).
- Imagens antigas estão sob
-
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 = clinicaIdsó para o que for novo, com de-para para o legado.
- Desejado:
-
npm run devdo RX =node server.js(sem nodemon) → mudanças no dental-server exigemdocker restart rx_dev_api. (Em prod o deploy é viadeploy-prod.sh/ webhook.) -
Não tocar:
io.on('send-image'),storage.saveImage/getImageBuffer, o esquema da key, nem o cadastro declinics_devicesdos 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_ideclinica_idemclinics_deviceseimages(+índices);clinic_namevira 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_idcom vínculo).
(Resto desta seção descreve a base por clinica_id.)
- Schema RX: coluna
clinica_idemclinics_deviceseimages(+ índices).clinic_namevira só rótulo legível. Backfill: device + 2776 imgs existentes →c_1780871001549_koqa3o. - Fluxo:
POST /api/admin/clinicsgravaclinica_id; o socket (machine_token) resolvesocket.user.clinicaIddo device;image-upload-direct(e o legadosend-image) gravamimages.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 porclinica_id(eclinic_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 dosend-imagelegado (após confirmar que nenhum.exeem 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.