Audio Backups: - New audio_backups table in PostgreSQL (bytea, gzip compressed) - POST /api/audio-backups — upload with gzip compression (level 6) - GET /api/audio-backups — list user's backups - GET /api/audio-backups/:id/audio — download decompressed audio - DELETE /api/audio-backups/:id — delete backup - Auto-cleanup every hour (24h expiry) - Frontend saves to server first, falls back to IndexedDB - Settings shows source badge (server/local) per backup AI Corrections: - Corrections list is now expandable — click to view original vs corrected - Shows red "Original" and green "Corrected to" sections - Click arrow to expand/collapse each correction - Date shown on each correction
93 lines
4.1 KiB
JavaScript
93 lines
4.1 KiB
JavaScript
// ============================================================
|
|
// AUDIO BACKUPS ROUTES — Server-side audio backup storage
|
|
// Stores compressed audio in PostgreSQL, auto-deletes after 24h
|
|
// ============================================================
|
|
|
|
var express = require('express');
|
|
var router = express.Router();
|
|
var zlib = require('zlib');
|
|
var multer = require('multer');
|
|
var db = require('../db/database');
|
|
var { authMiddleware } = require('../middleware/auth');
|
|
|
|
// 25MB upload limit (same as transcribe)
|
|
var upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 25 * 1024 * 1024 } });
|
|
|
|
router.use(authMiddleware);
|
|
|
|
// ── POST save audio backup (compressed) ──────────────────────────────────
|
|
router.post('/audio-backups', upload.single('audio'), async function(req, res) {
|
|
try {
|
|
if (!req.file) return res.status(400).json({ error: 'No audio file' });
|
|
|
|
var module = req.body.module || 'encounter';
|
|
var originalSize = req.file.size;
|
|
|
|
// Gzip compress the audio data
|
|
var compressed = await new Promise(function(resolve, reject) {
|
|
zlib.gzip(req.file.buffer, { level: 6 }, function(err, result) {
|
|
if (err) reject(err); else resolve(result);
|
|
});
|
|
});
|
|
|
|
var result = await db.run(
|
|
'INSERT INTO audio_backups (user_id, module, mime_type, size_bytes, compressed_bytes, audio_data) VALUES ($1,$2,$3,$4,$5,$6)',
|
|
[req.user.id, module, req.file.mimetype || 'audio/webm', originalSize, compressed.length, compressed]
|
|
);
|
|
|
|
var ratio = originalSize > 0 ? Math.round((1 - compressed.length / originalSize) * 100) : 0;
|
|
console.log('[AudioBackup] Saved ' + (originalSize / 1024).toFixed(0) + 'KB → ' + (compressed.length / 1024).toFixed(0) + 'KB (' + ratio + '% compression)');
|
|
|
|
res.json({ success: true, id: result.lastInsertRowid, originalSize: originalSize, compressedSize: compressed.length });
|
|
} catch (e) {
|
|
console.error('[AudioBackup] Save error:', e.message);
|
|
res.status(500).json({ error: e.message });
|
|
}
|
|
});
|
|
|
|
// ── GET list audio backups ───────────────────────────────────────────────
|
|
router.get('/audio-backups', async function(req, res) {
|
|
try {
|
|
var rows = await db.all(
|
|
"SELECT id, module, mime_type, size_bytes, compressed_bytes, created_at, expires_at FROM audio_backups WHERE user_id = $1 AND expires_at > NOW() ORDER BY created_at DESC",
|
|
[req.user.id]
|
|
);
|
|
res.json({ success: true, backups: rows });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// ── GET download audio backup (decompressed) ─────────────────────────────
|
|
router.get('/audio-backups/:id/audio', async function(req, res) {
|
|
try {
|
|
var row = await db.get(
|
|
'SELECT audio_data, mime_type, size_bytes FROM audio_backups WHERE id = $1 AND user_id = $2 AND expires_at > NOW()',
|
|
[req.params.id, req.user.id]
|
|
);
|
|
if (!row) return res.status(404).json({ error: 'Backup not found or expired' });
|
|
|
|
// Decompress
|
|
var decompressed = await new Promise(function(resolve, reject) {
|
|
zlib.gunzip(row.audio_data, function(err, result) {
|
|
if (err) reject(err); else resolve(result);
|
|
});
|
|
});
|
|
|
|
res.setHeader('Content-Type', row.mime_type || 'audio/webm');
|
|
res.setHeader('Content-Length', decompressed.length);
|
|
res.send(decompressed);
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
// ── DELETE audio backup ──────────────────────────────────────────────────
|
|
router.delete('/audio-backups/:id', async function(req, res) {
|
|
try {
|
|
var result = await db.run(
|
|
'DELETE FROM audio_backups WHERE id = $1 AND user_id = $2',
|
|
[req.params.id, req.user.id]
|
|
);
|
|
if (result.changes === 0) return res.status(404).json({ error: 'Not found' });
|
|
res.json({ success: true });
|
|
} catch (e) { res.status(500).json({ error: e.message }); }
|
|
});
|
|
|
|
module.exports = router;
|