From 6da5565f89b4c7cc86a76cb71af2b4c6e24e17d0 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 May 2026 08:23:38 +0200 Subject: [PATCH] harden audio backup settings rendering --- public/js/audioBackup.js | 79 ++++++++++++++++++------ test/transcription-memory-policy.test.js | 8 +++ 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/public/js/audioBackup.js b/public/js/audioBackup.js index fb1fb3b..62573e9 100644 --- a/public/js/audioBackup.js +++ b/public/js/audioBackup.js @@ -251,28 +251,74 @@ var _db = null; var container = document.getElementById('audio-backups-list'); if (!container) return; getAudioBackups().then(function(backups) { + container.textContent = ''; if (backups.length === 0) { - container.innerHTML = '

No audio backups. Recordings are saved automatically to the server and kept for 24 hours.

'; + var empty = document.createElement('p'); + empty.style.color = 'var(--g400)'; + empty.style.fontSize = '13px'; + empty.textContent = 'No audio backups. Recordings are saved automatically to the server and kept for 24 hours.'; + container.appendChild(empty); return; } - container.innerHTML = backups.map(function(b) { + backups.forEach(function(b) { var date = new Date(b.timestamp); var sizeKb = Math.round(b.size / 1024); var age = Math.round((Date.now() - b.timestamp) / 60000); var ageStr = age < 60 ? age + 'm ago' : Math.round(age / 60) + 'h ago'; - var sourceTag = b.source === 'server' - ? 'server' - : 'local'; var compInfo = b.compressedSize ? ' (' + Math.round(b.compressedSize / 1024) + ' KB compressed)' : ''; - return '
' + - '
' + - '
' + esc(b.module) + ' recording ' + sourceTag + '
' + - '
' + date.toLocaleString() + ' · ' + sizeKb + ' KB' + compInfo + ' · ' + ageStr + '
' + - '
' + - '' + - '' + - '
'; - }).join(''); + var row = document.createElement('div'); + row.className = 'saved-enc-item'; + row.style.padding = '8px 12px'; + + var body = document.createElement('div'); + body.style.flex = '1'; + + var title = document.createElement('div'); + title.style.fontWeight = '600'; + title.style.fontSize = '13px'; + title.style.display = 'flex'; + title.style.alignItems = 'center'; + title.style.gap = '6px'; + title.appendChild(document.createTextNode((b.module || '') + ' recording ')); + + var sourceTag = document.createElement('span'); + sourceTag.style.fontSize = '10px'; + sourceTag.style.padding = '1px 5px'; + sourceTag.style.borderRadius = '3px'; + sourceTag.style.background = b.source === 'server' ? 'var(--blue-light)' : 'var(--g100)'; + sourceTag.style.color = b.source === 'server' ? 'var(--blue)' : 'var(--g500)'; + sourceTag.textContent = b.source === 'server' ? 'server' : 'local'; + title.appendChild(sourceTag); + + var meta = document.createElement('div'); + meta.style.fontSize = '11px'; + meta.style.color = 'var(--g500)'; + meta.textContent = date.toLocaleString() + ' · ' + sizeKb + ' KB' + compInfo + ' · ' + ageStr; + + body.appendChild(title); + body.appendChild(meta); + + var retryBtn = document.createElement('button'); + retryBtn.className = 'btn-sm btn-primary audio-backup-retry'; + retryBtn.dataset.id = b.id; + var retryIcon = document.createElement('i'); + retryIcon.className = 'fas fa-rotate-right'; + retryBtn.appendChild(retryIcon); + retryBtn.appendChild(document.createTextNode(' Retry')); + + var deleteBtn = document.createElement('button'); + deleteBtn.className = 'btn-sm btn-ghost audio-backup-delete'; + deleteBtn.dataset.id = b.id; + deleteBtn.style.color = 'var(--red)'; + var deleteIcon = document.createElement('i'); + deleteIcon.className = 'fas fa-trash'; + deleteBtn.appendChild(deleteIcon); + + row.appendChild(body); + row.appendChild(retryBtn); + row.appendChild(deleteBtn); + container.appendChild(row); + }); container.querySelectorAll('.audio-backup-retry').forEach(function(btn) { btn.addEventListener('click', function() { retryAudioBackup(btn.dataset.id); }); }); @@ -287,9 +333,4 @@ var _db = null; }); }; - function esc(str) { - if (!str) return ''; - return String(str).replace(/&/g, '&').replace(//g, '>'); - } - cleanupOldLocalBackups(); diff --git a/test/transcription-memory-policy.test.js b/test/transcription-memory-policy.test.js index a76f7b6..248c554 100644 --- a/test/transcription-memory-policy.test.js +++ b/test/transcription-memory-policy.test.js @@ -62,3 +62,11 @@ test('browser speech recognition is gated by explicit user setting', () => { const speechFactory = app.match(/function createSpeechRecognition\(\) \{[\s\S]*?\n\}/)[0]; assert.match(speechFactory, /window\.WebSpeechRecognition && !window\.WebSpeechRecognition\.isEnabled\(\)/); }); + +test('audio backup settings render without dynamic HTML templates', () => { + const audioBackup = read('public/js/audioBackup.js'); + const renderer = audioBackup.match(/window\.renderAudioBackups = function\(\) \{[\s\S]*?\n \};/)[0]; + assert.doesNotMatch(renderer, /innerHTML/); + assert.match(renderer, /document\.createElement\('button'\)/); + assert.match(renderer, /textContent =/); +});