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 =/);
+});