feat: add filename and date-range filters to recordings list

Client-side filter bar with live filename search and from/to date
pickers. Rendering is split into renderFiles() + applyFilters() so
filters can be re-applied without re-fetching. Subtitle shows
'N of M shown' when a filter is active. Clear button resets all fields.
This commit is contained in:
2026-04-29 20:37:14 +02:00
parent d583620f8c
commit de667821b7
+79 -27
View File
@@ -564,6 +564,15 @@ button.chip:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.player-row td{padding:0 10px 10px;background:var(--bg);border-bottom:1px solid var(--brd)}
audio{width:100%;height:36px;border-radius:4px;display:block;
color-scheme:dark;accent-color:var(--accent)}
/* filter bar */
.filter-bar{display:flex;align-items:center;gap:10px;padding:8px 28px;
border-bottom:1px solid var(--brd);background:var(--surf);flex-wrap:wrap}
.filter-bar label{font-size:13px;color:var(--muted);white-space:nowrap}
.filter-bar input[type=text]{width:180px;background:var(--bg);border:1px solid var(--brd);
color:var(--txt);padding:3px 6px;border-radius:4px;font-size:13px}
.filter-bar input[type=date]{background:var(--bg);border:1px solid var(--brd);
color:var(--txt);padding:3px 6px;border-radius:4px;font-size:13px;color-scheme:dark}
.filter-bar input:focus{outline:2px solid var(--accent);outline-offset:1px}
</style>
</head>
<body>
@@ -580,6 +589,15 @@ audio{width:100%;height:36px;border-radius:4px;display:block;
aria-describedby="threshold-hint">
<span id="threshold-hint" class="controls-hint">RMS 01 · sections above this value are marked loud</span>
</div>
<div class="filter-bar" role="search" aria-label="Filter recordings">
<label for="filter-name">Search:</label>
<input type="text" id="filter-name" placeholder="filename…" aria-label="Filter by filename">
<label for="filter-from">From:</label>
<input type="date" id="filter-from" aria-label="From date">
<label for="filter-to">To:</label>
<input type="date" id="filter-to" aria-label="To date">
<button id="filter-clear" aria-label="Clear all filters">✕ Clear</button>
</div>
<div class="wrap" id="main">
<table aria-label="Recordings archive">
<thead>
@@ -623,6 +641,8 @@ const recMap = new Map();
// idx -> [{start,end}], populated after analysis
const sectionMap = new Map();
let activePlayerIdx = null;
// full file list from server, annotated with stable _idx
let allFiles = [];
function togglePlayer(idx, filename) {
const prow = document.getElementById('prow-'+idx);
@@ -780,10 +800,13 @@ async function deleteFile(idx, filename) {
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 = '';
allFiles = allFiles.filter(f => f._idx !== idx);
const visible = document.querySelectorAll('tr.data-row').length;
const total = allFiles.length;
document.getElementById('subtitle').textContent = total === visible
? `${total} recording${total!==1?'s':''} found`
: `${visible} of ${total} recording${total!==1?'s':''} shown`;
if (!visible) document.getElementById('empty').style.display = '';
updateStorage();
} else {
const d = await r.json().catch(()=>({}));
@@ -807,32 +830,21 @@ async function updateStorage() {
} catch(e) {}
}
async function load() {
const refreshBtn = document.getElementById('refresh-btn');
refreshBtn.disabled = true;
document.getElementById('subtitle').textContent = 'Loading…';
recMap.clear();
let files;
try {
files = await (await fetch('/api/files')).json();
} catch(e) {
document.getElementById('subtitle').textContent = 'Error loading files';
refreshBtn.disabled = false;
return;
}
function renderFiles(files) {
const tbody = document.getElementById('tbody');
tbody.innerHTML = '';
recMap.clear();
sectionMap.clear();
const n = files.length;
document.getElementById('subtitle').textContent =
`${n} recording${n!==1?'s':''} found`;
document.getElementById('empty').style.display = n ? 'none' : '';
updateStorage();
if (!n) { refreshBtn.disabled = false; return; }
const total = allFiles.length;
const visible = files.length;
document.getElementById('subtitle').textContent = total === visible
? `${total} recording${total!==1?'s':''} found`
: `${visible} of ${total} recording${total!==1?'s':''} shown`;
document.getElementById('empty').style.display = visible ? 'none' : '';
files.forEach((f, i) => {
files.forEach(f => {
const i = f._idx;
const ext = f.ext;
const canAnalyse = ext === 'wav' || ext === 'flac';
const isRec = !!f.recording;
@@ -909,10 +921,40 @@ async function load() {
.addEventListener('click', () => deleteFile(i, f.name));
}
// ---- register for live-status polling ----
recMap.set(i, f.name);
});
}
function applyFilters() {
const nameQ = document.getElementById('filter-name').value.toLowerCase().trim();
const fromD = document.getElementById('filter-from').value;
const toD = document.getElementById('filter-to').value;
const filtered = allFiles.filter(f => {
if (nameQ && !f.name.toLowerCase().includes(nameQ)) return false;
if (fromD && f.date < fromD + ' 00:00:00') return false;
if (toD && f.date > toD + ' 23:59:59') return false;
return true;
});
renderFiles(filtered);
}
async function load() {
const refreshBtn = document.getElementById('refresh-btn');
refreshBtn.disabled = true;
document.getElementById('subtitle').textContent = 'Loading…';
let files;
try {
files = await (await fetch('/api/files')).json();
} catch(e) {
document.getElementById('subtitle').textContent = 'Error loading files';
refreshBtn.disabled = false;
return;
}
allFiles = files.map((f, i) => ({...f, _idx: i}));
updateStorage();
applyFilters();
refreshBtn.disabled = false;
}
@@ -933,6 +975,16 @@ async function pollStatus() {
document.getElementById('refresh-btn').addEventListener('click', load);
document.getElementById('filter-name').addEventListener('input', applyFilters);
document.getElementById('filter-from').addEventListener('change', applyFilters);
document.getElementById('filter-to').addEventListener('change', applyFilters);
document.getElementById('filter-clear').addEventListener('click', () => {
document.getElementById('filter-name').value = '';
document.getElementById('filter-from').value = '';
document.getElementById('filter-to').value = '';
applyFilters();
});
// Seed threshold input from server config, then start
fetch('/api/config').then(r => r.json()).then(cfg => {
if (cfg.threshold != null)