feat: add delete button for recordings in web UI

Adds a DELETE /api/files/<name> endpoint that refuses to remove files
currently being recorded (409). The UI shows a red '✕ Delete' button per
row (disabled while REC), confirms before proceeding, and removes both
the data row and the hidden player row from the DOM on success without
a full page reload. README updated accordingly.
This commit is contained in:
2026-04-27 00:14:56 +02:00
parent 8121564e8c
commit c3575c712e
2 changed files with 70 additions and 1 deletions
+68
View File
@@ -249,6 +249,14 @@ class _Handler(BaseHTTPRequestHandler):
recordings_dir: str = 'recordings'
threshold: float = LOUD_THRESHOLD
def do_DELETE(self):
parsed = urlparse(self.path)
p = parsed.path
if p.startswith('/api/files/'):
self._api_delete(unquote(p[len('/api/files/'):]))
else:
self._send(404, b'Not found', 'text/plain')
def do_GET(self):
parsed = urlparse(self.path)
qs = parse_qs(parsed.query)
@@ -419,6 +427,28 @@ class _Handler(BaseHTTPRequestHandler):
data = json.dumps({'threshold': self.threshold})
self._send(200, data.encode(), 'application/json')
def _api_delete(self, filename: str):
status_path = Path(self.recordings_dir) / 'status.json'
try:
with open(status_path) as fh:
if filename in set(json.load(fh).get('active', [])):
self._json_err(409, 'Cannot delete a file that is currently being recorded')
return
except Exception:
pass
path = self._safe_path(filename)
if path is None:
return
try:
path.unlink()
except Exception as e:
self._json_err(500, f'Failed to delete: {e}')
return
self._send(200, json.dumps({'deleted': filename}).encode(), 'application/json')
def _safe_path(self, filename: str):
base = Path(self.recordings_dir).resolve()
try:
@@ -512,6 +542,8 @@ a.dl{color:var(--accent);text-decoration:none;font-size:13px}
a.dl:hover{text-decoration:underline}
a.dl:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
.actions{display:flex;gap:6px;align-items:center}
button.del{color:var(--red);border-color:#7f1d1d}
button.del:hover:not(:disabled){background:#2d0808}
/* waveform */
.wbox{background:var(--surf);border:1px solid var(--brd);border-radius:6px;padding:10px 12px}
svg.wave{display:block;width:100%;height:56px}
@@ -686,6 +718,33 @@ async function analyse(filename, cell, btn) {
}
}
async function deleteFile(idx, filename) {
if (!confirm(`Delete "${filename}"?\nThis cannot be undone.`)) return;
const btn = document.getElementById('delbtn-'+idx);
btn.disabled = true;
btn.textContent = '';
try {
const r = await fetch('/api/files/'+encodeURIComponent(filename), {method:'DELETE'});
if (r.ok) {
document.getElementById('row-'+idx)?.remove();
document.getElementById('prow-'+idx)?.remove();
recMap.delete(idx);
const remaining = document.querySelectorAll('tr.data-row').length;
document.getElementById('subtitle').textContent =
`${remaining} recording${remaining!==1?'s':''} found`;
if (!remaining) document.getElementById('empty').style.display = '';
updateStorage();
} else {
const d = await r.json().catch(()=>({}));
alert('Delete failed: '+(d.error||r.statusText));
btn.disabled = false; btn.textContent = '✕ Delete';
}
} catch(e) {
alert('Delete failed: '+e.message);
btn.disabled = false; btn.textContent = '✕ Delete';
}
}
async function updateStorage() {
try {
const s = await (await fetch('/api/storage')).json();
@@ -753,6 +812,9 @@ async function load() {
aria-label="Play ${esc(f.name)}">▶ Play</button>
<a class="dl" href="/download/${encodeURIComponent(f.name)}"
aria-label="Download ${esc(f.name)}">↓ Download</a>
<button id="delbtn-${i}" class="del"
aria-label="Delete ${esc(f.name)}"
${isRec ? 'disabled title="Cannot delete while recording"' : ''}>✕ Delete</button>
</div>
</td>`;
tbody.appendChild(tr);
@@ -787,6 +849,12 @@ async function load() {
document.getElementById('pbtn-'+i)
.addEventListener('click', () => togglePlayer(i, f.name));
// ---- attach delete button handler ----
if (!isRec) {
document.getElementById('delbtn-'+i)
.addEventListener('click', () => deleteFile(i, f.name));
}
// ---- register for live-status polling ----
recMap.set(i, f.name);
});