- .gitea/workflows/build.yml: push->build rx-scoreodonto:<sha> no registry; tag->promote+deploy
- docker-compose.prod.yml: image do registry, APP_VERSION/GIT_COMMIT em runtime
- deploy-prod.sh: VPS1 pull + up --no-build (antigo vira deploy-prod-legado-webhook.sh)
- /api/health usa APP_VERSION (tag) com fallback version.txt; badge no canto
- webhook 9003 aposentado (gatilho agora e CI/tag)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /api/admin/rx-health (admin): contagens por (tenant_id, clinica_id) + último
resultado do cron de storage; ?clinica_id=&checkLimit=N faz HEAD-check bounded de
objetos ausentes no Wasabi. Read-only, aditivo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Antes do INSERT, remove linha existente com mesmo original_filename. Sem isso, o
reenvio (sync-request/Plan B) de um arquivo cujo objeto Wasabi sumiu mas a linha
existia criava DUPLICATA. Descoberto no teste de resync de 3 radiografias.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
storage.js: escrita usa key nova (tenant/clinica/paciente) quando o device tem
tenant+clinica; leitura testa existência e cai para a key legada (clinic_name/
client_name/...) e raiz — não quebra objetos antigos antes do resync. deleteImage
apaga todas as keys. getUploadPresignedUrl/saveImage recebem tenantId/clinicaId.
Verificado em dev: presigned URL -> u_…/c_…/paciente/arquivo; leitura de imagem
existente (key legada) -> HTTP 200 via fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hierarquia tenant_id (usuarios.id do dono) / clinica_id / pc / paciente, espelhando
o scoreodonto (conta multi-clínica planejada: um dono gerencia várias clínicas).
- Colunas tenant_id em clinics_devices e images (+índices); backfill device + 2776 imgs.
- Cadastro de device, socket (machine_token), JWT de client e uploads gravam tenant_id.
- Listagens (/images, /patients, /by-patient, /unique-clinics) filtram por tenant_id e/ou clinica_id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- clinica_id (tenant do scoreodonto, clinicas.id) como eixo de isolamento em
clinics_devices e images (+índices); clinic_name vira rótulo legível.
- Cadastro de device, socket (machine_token) e uploads (direct + legado) gravam clinica_id.
- Listagens (/images, /patients, /by-patient, /unique-clinics) filtram por clinica_id.
- /clients: lista clientes Windows ativos (inclui autenticados sem client-identify),
tempo real + polling, exibe a clínica.
- send-image marcado como legado (client real usa Direct Upload).
Inclui trabalho de dev acumulado no working tree (não havia commits desde origin/main).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. admin-clinics.js — bloqueia cadastro de clinic_name + pc_name duplicados:
Antes só validava email único. Agora verifica se já existe dispositivo com
a mesma combinação clinic_name + pc_name (case-insensitive). Sem isso, dois
PCs com mesmo nome na mesma clínica sobrescreveriam arquivos um do outro
no Wasabi (caminho idêntico).
2. admin-clinics.js — wipe usava client_name ao invés de clinic_name no banco:
'DELETE FROM images WHERE client_name = clinic_name' deletava o conjunto
errado de imagens (client_name é o nome do PC, não da clínica). Corrigido
para 'WHERE clinic_name = $1'. Wasabi continua usando sanitize(clinic_name)/
como prefixo (correto).
3. server.js — verificação de duplicata no connect incluía clinicName:
O check usava só clientName (pc_name), causando falso positivo: dois PCs
de clínicas diferentes com o mesmo nome (ex: ambos "PC-SALA-1") — o segundo
era desconectado. Agora verifica clinicName + clientName juntos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /api/auth/device-info?token=xxx: valida machine_token e retorna
clinic_name e pc_name sem exigir email/senha. Usado pelo client para
preview antes de salvar e para validação no save.
- client-identify: quando autenticado via machine_token, usa
socket.user.pcName como nome autoritativo (ignora o que o client enviou).
- server-identified: clientInfo agora inclui 'pcName' para o client
manter o nome em sincronia com o cadastro do servidor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bugs corrigidos:
1. Condição bloqueante: 'if (email && password && token.includes("-"))' fazia
o machine_token ser ignorado se o client conectasse só com o token (sem
email/senha). A conexão caía para o check de JWT, falhava e era rejeitada.
Agora o machine_token é verificado sempre que o token contém hífens —
email+senha são opcionais (validados se presentes, ignorados se ausentes).
2. Comparação de email case-sensitive: 'device.email === email' falhava
se houver diferença de maiúsculas/minúsculas entre o cadastro e o config
do client. Corrigido para .toLowerCase() em ambos os lados.
3. Log de autenticação: emite mensagem clara no console quando machine_token
é aceito, incluindo nome da clínica e do PC, facilitando diagnóstico.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
O servidor emitia 'force-sync' mas o client-monitor.js só ouve
'sync-request'. O botão Sincronizar do painel nunca chegava ao cliente.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Causa das gambiarras identificadas e corrigidas:
1. clinicName inconsistente: `(clientInfo ? clientInfo.clinicName : fallback)`
retornava undefined quando clientInfo.clinicName não estava definido,
gerando pasta 'desconhecido' no Wasabi para clientes sem machine_token.
Corrigido para `clientInfo?.clinicName || 'Clínica não identificada'`
nos 3 handlers: send-image, request-presigned-urls, image-upload-direct.
2. patientName inconsistente: banco gravava 'Paciente não identificado'
mas Wasabi usava 'Desconhecido' quando o nome estava vazio, quebrando
a recuperação por key. Ambos agora usam 'Desconhecido' como fallback.
O getImageBufferWithFallback com tentativa em 'Desconhecido' permanece
como safety net para imagens antigas já gravadas no Wasabi, mas não
será mais necessário para novos uploads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Novo cron 'verifyStorage' (30min): percorre as 200 imagens mais
recentes, tenta buscar no storage com fallback legado; para cada
arquivo ausente aciona scheduleReupload() (Plan B) imediatamente.
Primeira execução 15s após o boot para cobrir os IDs 404 sem esperar
o usuário abrir o card.
- cronState agora tem dois jobs; /api/cron/status retorna ambos.
- POST /api/cron/run/:key usa mapa de funções; suporta 'thumbBackfill'
e 'verifyStorage' (botão "Executar agora" na tela de Configurações).
- lastResult do verifyStorage inclui: checked, missing, requested.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fix:
- server.js e routes/images.js: 'enabled = true' → 'enabled = 1'
(PostgreSQL rejeita smallint = boolean — causava 'operator does not exist')
Plan B — imagens ausentes no Wasabi:
- server.js: pendingReupload (Map clientName → Set<original_filename>)
quando getImageBufferWithFallback retorna null no endpoint /thumbnail,
scheduleReupload() registra o original_filename e emite 'reupload-files'
imediatamente se o cliente estiver online, ou na próxima conexão.
- server.js: client-identify agora drena a fila pendente do cliente ao
reconectar, antes de emitir clients-list-updated.
- original_filename adicionado ao SELECT do endpoint /thumbnail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- server.js: cron thumb-backfill a cada 10min com rastreio de estado
(lastRun, nextRun, running, lastResult). GET /api/cron/status e
POST /api/cron/run/:key para consulta e disparo manual via painel.
- routes/images.js: PUT /api/images/patients/:name — edita patient_name,
doctor e remark em todas as imagens do paciente de uma vez.
Frontend:
- PatientsPage.jsx (/patients): tabela com colunas Nome (+ miniatura),
Dentista, Imagens, Última atualização, Observações, Ações. Suporta
busca, paginação infinita, editar e excluir por linha.
- SettingsPage.jsx (/settings): seção de Cron Jobs com status, resultado
e botão "Executar agora"; seção de informações do servidor.
- EditPatientModal.jsx: edita nome, dentista e observações do paciente.
- Sidebar.jsx: add "Pacientes" (todos usuários) e "Configurações" (admin).
- App.jsx: rotas /patients (protegida) e /settings (admin).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Causa raiz: request-presigned-urls usava firstName/lastName para montar
o patientName do caminho no Wasabi, mas o client envia apenas name.
Resultado: toda imagem era salva em '.../Desconhecido/arquivo.png' no
Wasabi, mas o banco guardava o nome real do paciente — na recuperação,
a key ficava diferente e resultava em NoSuchKey → 500.
Correções:
1. server.js: getImageBufferWithFallback() — tenta o path correto (nome
real) e, se falhar, tenta o path legado 'Desconhecido'. Usado tanto
para ler thumb existente quanto para gerar do original.
2. server.js: patientName corrigido em send-image e request-presigned-urls
para usar data.patientData.name (o campo que o client realmente envia),
garantindo consistência entre Wasabi e banco em novos uploads.
3. routes/images.js: runThumbBackfill agora usa storage.getImageBuffer
com fallback 'Desconhecido' em vez de leitura apenas do disco local
— o boot backfill passa a funcionar para imagens no Wasabi.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- storage.js: adiciona fileExists() com HeadObject (sem baixar o arquivo)
- check-image-exists: verifica existência física no storage, não só no banco;
se registro existe mas arquivo não, apaga o registro órfão e retorna exists=false
- send-image: mesma lógica — registro órfão é removido e re-upload prossegue
- Remove migrate-to-wasabi.js do repositório
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Adiciona coluna clinic_name em images (ALTER TABLE IF NOT EXISTS)
- Todas as funções de storage recebem clinicName como parâmetro: saveImage,
getImageBuffer, getImageUrl, deleteImage, getDownloadPresignedUrl, getUploadPresignedUrl
- Chave Wasabi passa de pc_name/patient/arquivo → clinic_name/pc_name/patient/arquivo
- server.js: usa socket.user.clinicName (preenchido via machine_token auth do DB) como
fonte primária do nome da clínica nos 3 handlers de upload e na rota thumbnail
- Todos os INSERTs em images passam a gravar clinic_name
- Corrige bug oculto: wipe de clínica agora apaga o prefixo correto no Wasabi
- Scripts utilitários atualizados: backfill-wasabi-thumbs, recreate-all-thumbs, migrate-to-wasabi
- client-monitor.js: envia clinicName no client-identify (fallback para clientes legados)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
O endpoint /api/images/:id/thumbnail retornava JSON {url}, mas o React aponta
<img src> direto para ele, causando placeholders cinza (sem erro no devtools).
Agora serve image/jpeg diretamente. Quando thumb_filename é null, puxa a original,
gera a thumb na hora, serve imediatamente e faz backfill no Wasabi + atualiza o banco.