fix: version injection server-side, sidebar spacing, thumb quality 80%, backfill, modal original full-res
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user