fix: version injection server-side, sidebar spacing, thumb quality 80%, backfill, modal original full-res
continuous-integration/webhook Deploy concluído (VPS4)

This commit is contained in:
VPS 4 Deploy Agent
2026-05-30 15:51:48 +02:00
parent 0c31ed7670
commit dcfc221e9a
5 changed files with 107 additions and 19 deletions
+4 -4
View File
@@ -104,15 +104,15 @@ class ImageProcessor {
// MÉTODOS DE UTILIDADE
// ================================================================
async getThumbnail(imageBuffer, width = 300, height = 300) {
async getThumbnail(imageBuffer, width = 500, height = 500) {
try {
const sharp = require('sharp');
return await sharp(imageBuffer)
.resize(width, height, {
.resize(width, height, {
fit: 'inside',
withoutEnlargement: true
withoutEnlargement: true
})
.jpeg({ quality: 50, force: true })
.jpeg({ quality: 80, force: true })
.toBuffer();
} catch (error) {
console.error('Erro ao gerar thumbnail:', error);
+13 -5
View File
@@ -655,20 +655,28 @@ async function showTransformModal(imageId) {
throw new Error('Imagem não encontrada na lista carregada.');
}
// Painel "Preview" sempre mostra a versão atual (com transformações aplicadas)
const imageUrl = image.filename.startsWith('http') ? image.filename : `/uploads/${image.filename}`;
// Atribui a imagem às tags do DOM para exibição no painel
document.getElementById('originalImgPreview').src = imageUrl;
document.getElementById('transformedImgPreview').src = imageUrl;
// Check if the image has been modified (has original_image_id)
// Painel "Original": se a imagem foi transformada, busca a versão anterior ao processamento
const btnResetImage = document.getElementById('btnResetImage');
if (image.original_image_id) {
btnResetImage.style.display = 'flex';
btnResetImage.setAttribute('data-modified', 'true');
// Carrega a imagem-mãe (full-res, sem transformações) no painel Original
fetchWithAuth(`${API_URL}/images/${image.original_image_id}/file-url`)
.then(r => r.ok ? r.json() : null)
.then(data => {
const origUrl = data && data.url ? data.url : imageUrl;
document.getElementById('originalImgPreview').src = origUrl;
})
.catch(() => { document.getElementById('originalImgPreview').src = imageUrl; });
} else {
btnResetImage.style.display = 'none';
btnResetImage.setAttribute('data-modified', 'false');
// Sem transformações: original = atual
document.getElementById('originalImgPreview').src = imageUrl;
}
document.getElementById('transformModalTitle').innerText = `${escapeHtml(image.patient_name || 'Desconhecido')} - ${formatDate(image.created_at)}`;
+9 -10
View File
@@ -97,11 +97,12 @@ body {
}
.sidebar-header {
padding: 0 24px 32px;
padding: 0 20px 16px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: none;
flex-shrink: 0;
}
.logo-icon {
@@ -127,8 +128,8 @@ body {
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 8px;
padding: 0 16px;
gap: 2px;
padding: 0 12px;
flex: 1;
overflow-y: auto;
}
@@ -170,8 +171,10 @@ body {
}
.sidebar-footer {
padding: 0 16px;
margin-bottom: 12px;
padding: 12px 16px;
border-top: 1px solid var(--border-color);
margin-top: auto;
flex-shrink: 0;
}
.logout-btn {
@@ -841,11 +844,7 @@ body {
padding: 10px 16px;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid var(--border-color);
margin-top: auto;
}
/* sidebar-footer definida acima, perto do .sidebar */
.sidebar-brand {
display: flex;
+64
View File
@@ -456,6 +456,23 @@ router.delete('/:id', async (req, res) => {
}
});
// ================================================================
// GET /api/images/:id/file-url — Retorna a URL de visualização
// full-res de uma imagem (para o painel Original do modal)
// ================================================================
router.get('/:id/file-url', async (req, res) => {
try {
const image = await db.get('SELECT filename FROM images WHERE id = $1', [req.params.id]);
if (!image) return res.status(404).json({ error: 'Imagem não encontrada' });
const url = image.filename.startsWith('http')
? image.filename
: `/uploads/${image.filename}`;
res.json({ url });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
// ================================================================
// PUT /api/images/:id/toggle-enabled - Habilitar/Desabilitar
// ================================================================
@@ -475,4 +492,51 @@ router.put('/:id/toggle-enabled', async (req, res) => {
}
});
// ================================================================
// POST /api/images/backfill-thumbs — Gera thumbnails para imagens
// que ainda não têm thumb_filename. Roda em background; retorna
// imediatamente com o total de imagens na fila.
// ================================================================
router.post('/backfill-thumbs', async (req, res) => {
try {
const rows = await db.all(
`SELECT id, filename, client_name, patient_name
FROM images
WHERE thumb_filename IS NULL AND enabled = true
ORDER BY created_at DESC`
);
res.json({ queued: rows.length, message: 'Backfill iniciado em background' });
// Processa sem bloquear a resposta
setImmediate(() => runThumbBackfill(rows));
} catch (e) {
res.status(500).json({ error: e.message });
}
});
async function runThumbBackfill(rows) {
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', '..', 'uploads');
const PROCESSED_DIR = process.env.PROCESSED_DIR || path.join(__dirname, '..', '..', 'processed');
let done = 0, failed = 0;
for (const row of rows) {
try {
// Tenta carregar a imagem do disco local
let imgBuffer = null;
for (const dir of [UPLOAD_DIR, PROCESSED_DIR]) {
try { imgBuffer = await fs.readFile(path.join(dir, row.filename)); break; } catch (_) {}
}
if (!imgBuffer) { failed++; continue; }
const thumbBuffer = await imageProcessor.getThumbnail(imgBuffer);
const thumbFilename = `thumb_${row.filename.replace(/\.[^.]+$/, '')}.jpg`;
await storage.saveImage(thumbFilename, thumbBuffer, row.client_name, row.patient_name, false);
await db.run('UPDATE images SET thumb_filename = $1 WHERE id = $2', [thumbFilename, row.id]);
done++;
} catch (e) {
failed++;
}
}
console.log(`[backfill-thumbs] concluído: ${done} geradas, ${failed} falhas de ${rows.length} total`);
}
// Exporta a função para ser chamada no boot do servidor
module.exports = router;
module.exports.runThumbBackfill = runThumbBackfill;
+17
View File
@@ -507,6 +507,8 @@ async function serveHtmlWithCacheBuster(res, filePath) {
// Injeta versão em tags de script e css
content = content.replace(/src="([^"]+\.js)(?:\?v=[^"]*)?"/g, `src="$1?v=${APP_VERSION}"`);
content = content.replace(/href="([^"]+\.css)(?:\?v=[^"]*)?"/g, `href="$1?v=${APP_VERSION}"`);
// Substitui o número de versão exibido na sidebar (elimina hardcode no HTML)
content = content.replace(/(<span[^>]*>)\s*v\d+\.\d+\.\d+\s*(<\/span>)/g, `$1v${APP_VERSION}$2`);
res.set('Content-Type', 'text/html');
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
@@ -1233,6 +1235,21 @@ async function start() {
redis.initRedis();
// Iniciar servidor
// Backfill de thumbnails ausentes: roda em background após o boot
setTimeout(async () => {
try {
const { runThumbBackfill } = require('./routes/images');
const missing = await db.all(
`SELECT id, filename, client_name, patient_name FROM images
WHERE thumb_filename IS NULL AND enabled = true ORDER BY created_at DESC`
);
if (missing.length > 0) {
console.log(`[boot] Iniciando backfill de thumbnails para ${missing.length} imagem(ns) sem thumb...`);
runThumbBackfill(missing);
}
} catch (e) { /* silencia erro de backfill para não afetar o boot */ }
}, 5000);
server.listen(PORT, () => {
console.log('═══════════════════════════════════════════════════════');
console.log('🚀 DENTAL IMAGE SERVER STARTED');