diff --git a/public/app.js b/public/app.js index e85d79a..3c2e169 100644 --- a/public/app.js +++ b/public/app.js @@ -4112,7 +4112,7 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` document.querySelectorAll('.btn-ver-estudio').forEach(btn => { btn.addEventListener('click', async (e) => { - const id = e.target.dataset.id; + const id = e.currentTarget.dataset.id; try { const res = await fetch(`/api/music/${id}`); const itemData = await res.json(); @@ -4130,10 +4130,10 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` document.querySelectorAll('.btn-del-estudio').forEach(btn => { btn.addEventListener('click', async (e) => { if(!confirm('Tem certeza que deseja apagar esta canção?')) return; - const id = e.target.dataset.id; + const id = e.currentTarget.dataset.id; try { await fetch(`/api/music/${id}`, { method: 'DELETE' }); - e.target.closest('div[style*="var(--bg-tertiary)"]').remove(); + e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove(); } catch (err) { alert('Erro ao deletar.'); } }); }); @@ -6800,7 +6800,7 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.btn-ver-mindlab').forEach(btn => { btn.addEventListener('click', async (e) => { - const id = e.target.dataset.id; + const id = e.currentTarget.dataset.id; try { const res = await fetch(`/api/mindlab/${id}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } @@ -6820,13 +6820,13 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.btn-del-mindlab').forEach(btn => { btn.addEventListener('click', async (e) => { if(!confirm('Tem certeza que deseja apagar este planejamento?')) return; - const id = e.target.dataset.id; + const id = e.currentTarget.dataset.id; try { await fetch(`/api/mindlab/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); - e.target.closest('div[style*="var(--bg-tertiary)"]').remove(); + e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove(); } catch (err) { alert('Erro ao deletar.'); } }); }); @@ -7188,7 +7188,7 @@ document.addEventListener('DOMContentLoaded', () => { // Bind Ver/Editar Buttons document.querySelectorAll('.btn-ver-cartaz').forEach(btn => { btn.addEventListener('click', async (e) => { - const id = e.target.dataset.id; + const id = e.currentTarget.dataset.id; try { const r = await fetch(`/api/cartazes/${id}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } @@ -7223,7 +7223,7 @@ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.btn-del-cartaz').forEach(btn => { btn.addEventListener('click', async (e) => { if (!confirm('Tem certeza que deseja apagar este cartaz?')) return; - const id = e.target.dataset.id; + const id = e.currentTarget.dataset.id; try { const r = await fetch(`/api/cartazes/${id}`, { method: 'DELETE', diff --git a/server.js b/server.js index 34614e8..fffc1b1 100644 --- a/server.js +++ b/server.js @@ -82,6 +82,30 @@ const dbPool = new Pool({ connectionString: process.env.DATABASE_URL }); +async function backupMediaFile(filePath, buffer) { + try { + const filename = path.basename(filePath); + let contentType = 'application/octet-stream'; + const ext = path.extname(filename).toLowerCase(); + if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'; + else if (ext === '.png') contentType = 'image/png'; + else if (ext === '.gif') contentType = 'image/gif'; + else if (ext === '.mp3') contentType = 'audio/mpeg'; + else if (ext === '.mp4') contentType = 'video/mp4'; + else if (ext === '.wav') contentType = 'audio/wav'; + + await dbPool.query( + `INSERT INTO escola.arquivos (filename, content_type, dados) + VALUES ($1, $2, $3) + ON CONFLICT (filename) DO UPDATE SET dados = $3, content_type = $2`, + [filename, contentType, buffer] + ); + console.log(`[Media Backup] Backup do arquivo ${filename} salvo no banco escola.arquivos.`); + } catch (err) { + console.error(`[Media Backup] Erro ao fazer backup do arquivo ${filePath}:`, err); + } +} + const CONFIG_FILE_PATH = path.join(__dirname, 'agent_config.json'); const KNOWLEDGE_FILE_PATH = path.join(__dirname, 'conhecimento_adquirido.json'); const TEMPERATURE_PRESETS = { @@ -302,7 +326,9 @@ Retorne APENAS a letra formatada em versos com quebras de linha. Não adicione n } const fileName = `estudio_musica_${Date.now()}.mp3`; const filePath = path.join(mediaDir, fileName); - fs.writeFileSync(filePath, Buffer.from(audioHex, 'hex')); + const audioBuffer = Buffer.from(audioHex, 'hex'); + fs.writeFileSync(filePath, audioBuffer); + await backupMediaFile(filePath, audioBuffer); const audioUrl = `/generated-media/${fileName}`; // 6. Retornar dados para o frontend @@ -526,6 +552,7 @@ app.post('/api/comics/generate-frame', requireAuth, async (req, res) => { const fileName = `quadrinho_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`; const filePath = path.join(mediaDir, fileName); fs.writeFileSync(filePath, imgBuffer); + await backupMediaFile(filePath, imgBuffer); res.json({ imageUrl: `/generated-media/${fileName}` }); } catch (error) { @@ -600,6 +627,16 @@ app.post('/api/comics/generate-video', requireAuth, async (req, res) => { return res.status(500).json({ error: 'Erro ao gerar o vídeo da apresentação. Como o servidor foi atualizado e reiniciado recentemente, por favor crie/gere a história em quadrinhos novamente para recriar as imagens temporárias antes de exportar o vídeo.' }); } + // Backup do vídeo gerado no banco de dados + (async () => { + try { + const videoBuffer = fs.readFileSync(outputFilePath); + await backupMediaFile(outputFilePath, videoBuffer); + } catch (backupErr) { + console.error('[Media Backup] Erro ao fazer backup do vídeo do FFmpeg:', backupErr); + } + })(); + res.json({ videoUrl: `/generated-media/${outputFileName}` }); }); @@ -909,6 +946,7 @@ app.post('/api/garatujas/analyze', requireAuth, multerUpload.single('image'), as const fileName = `garatuja_${Date.now()}${ext}`; const filePath = path.join(mediaDir, fileName); fs.writeFileSync(filePath, file.buffer); + await backupMediaFile(filePath, file.buffer); const imageUrl = `/generated-media/${fileName}`; const base64Image = file.buffer.toString('base64'); @@ -1448,7 +1486,9 @@ REGRAS: if (imgFetch.ok) { const imgBuffer = Buffer.from(await imgFetch.arrayBuffer()); const fileName = `cartaz_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`; - fs.writeFileSync(path.join(mediaDir, fileName), imgBuffer); + const filePath = path.join(mediaDir, fileName); + fs.writeFileSync(filePath, imgBuffer); + await backupMediaFile(filePath, imgBuffer); generatedImageUrl = `/generated-media/${fileName}`; } } @@ -2063,6 +2103,15 @@ async function initDatabase() { imagem_url TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); + + -- Arquivos persistentes (Mídia gerada por IA) + CREATE TABLE IF NOT EXISTS escola.arquivos ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + filename VARCHAR(255) UNIQUE NOT NULL, + content_type VARCHAR(100) NOT NULL, + dados BYTEA NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); `); // Garantir que a coluna character_description_english exista na tabela projetos_comics @@ -3229,13 +3278,51 @@ app.use((req, res, next) => { req.path === '/manifest.json' || req.path === '/sw.js' || req.path === '/favicon.ico' || - req.path.startsWith('/assets/') + req.path.startsWith('/assets/') || + req.path.startsWith('/generated-media/') ) { return next(); } requireAuth(req, res, next); }); +// Interceptar requisições de mídia gerada para restaurar do banco se sumir do disco +app.get('/generated-media/:filename', async (req, res, next) => { + const { filename } = req.params; + const localPath = path.join(__dirname, 'public', 'generated-media', filename); + + if (fs.existsSync(localPath)) { + return next(); + } + + try { + const dbRes = await dbPool.query( + `SELECT content_type, dados FROM escola.arquivos WHERE filename = $1`, + [filename] + ); + + if (dbRes.rowCount === 0) { + console.log(`[Media Server] Arquivo não encontrado no banco: ${filename}`); + return res.status(404).send('Arquivo não encontrado'); + } + + const { content_type, dados } = dbRes.rows[0]; + + const mediaDir = path.join(__dirname, 'public', 'generated-media'); + if (!fs.existsSync(mediaDir)) { + fs.mkdirSync(mediaDir, { recursive: true }); + } + fs.writeFileSync(localPath, dados); + console.log(`[Media Server] Arquivo ${filename} carregado do banco de dados e gravado em cache.`); + + res.setHeader('Content-Type', content_type); + return res.send(dados); + } catch (err) { + console.error(`[Media Server] Erro ao carregar ${filename} do banco:`, err); + return res.status(500).send('Erro ao carregar arquivo'); + } +}); + // Servir arquivos estáticos do diretório public app.use(express.static(path.join(__dirname, 'public'))); @@ -3897,8 +3984,9 @@ app.post('/api/chat', requireAuth, async (req, res) => { const fileName = `imagem_${Date.now()}.jpg`; const filePath = path.join(mediaDir, fileName); fs.writeFileSync(filePath, imgBuffer); + await backupMediaFile(filePath, imgBuffer); const localUrl = `/generated-media/${fileName}`; - console.log(`[Smart Router] Imagem salva em: ${localUrl}`); + console.log(`[Smart Router] Imagem salva em e backup no banco: ${localUrl}`); res.write(`data: ${JSON.stringify({ type: 'image', url: localUrl, originalPrompt: promptText })}\n\n`); res.write('data: [DONE]\n\n'); @@ -3996,9 +4084,11 @@ app.post('/api/chat', requireAuth, async (req, res) => { } const fileName = `musica_${Date.now()}.mp3`; const filePath = path.join(mediaDir, fileName); - fs.writeFileSync(filePath, Buffer.from(audioHex, 'hex')); + const audioBuffer = Buffer.from(audioHex, 'hex'); + fs.writeFileSync(filePath, audioBuffer); + await backupMediaFile(filePath, audioBuffer); const audioUrl = `/generated-media/${fileName}`; - console.log(`[Smart Router] Música salva em: ${audioUrl}`); + console.log(`[Smart Router] Música salva em e backup no banco: ${audioUrl}`); res.write(`data: ${JSON.stringify({ type: 'audio', url: audioUrl, originalPrompt: promptText })}\n\n`); res.write('data: [DONE]\n\n'); @@ -4108,10 +4198,12 @@ app.post('/api/chat', requireAuth, async (req, res) => { const fileName = `video_${Date.now()}.mp4`; const filePath = path.join(mediaDir, fileName); - fs.writeFileSync(filePath, Buffer.from(videoBuffer)); + const vBuf = Buffer.from(videoBuffer); + fs.writeFileSync(filePath, vBuf); + await backupMediaFile(filePath, vBuf); const localVideoUrl = `/generated-media/${fileName}`; - console.log(`[Smart Router] Vídeo salvo localmente em: ${localVideoUrl}`); + console.log(`[Smart Router] Vídeo salvo em e backup no banco: ${localVideoUrl}`); res.write(`data: ${JSON.stringify({ type: 'video', url: localVideoUrl, originalPrompt: promptText })}\n\n`); res.write('data: [DONE]\n\n');