feat: split the web UI into modules and rework it for screen readers
webui.html becomes webui/ (index.html, style.css, ES modules in js/, no
build step); web.py serves it under / and /ui/<path> with the same
containment check as recordings.
How it feels with a screen reader:
- Days read "Today, Saturday 17 October"; recordings are named by their
wall-clock span ("22:00 to 23:00"), which is the row header and what
every button says. The filename moves to the last column.
- Focus is never dropped: busy buttons stay focused (aria-disabled),
finished analyses focus their first section, delete moves to the next
row, closing the clip bar (x or Escape) returns to where you were.
- Reloads rebuild only the days whose content changed; DOM ids are stable
per filename, so a rollover no longer resets the reading position.
- Only user-initiated actions are announced; highlights completion is
spoken (the progress area was stuck aria-busy before).
- Media Session previous/next track step sections like J/K (AirPods,
lock screen, media keys).
- Row layout: Time | Length | Loudness | Player | Options | File. The
player opens inside its cell; Download/Cut/Delete are always there,
and Cut toggles its start/end fields in place.
- Settings tucked behind a <details>; pre-roll and auto-advance are
remembered per browser; no CSS uppercase on headers or badges.
Fixes the REC badge showing on every row: its inline-flex display beat
the UA [hidden] rule. One global [hidden]{display:none !important} now
covers every case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -245,7 +245,9 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
p = parsed.path
|
||||
|
||||
if p == '/':
|
||||
self._html()
|
||||
self._ui('index.html')
|
||||
elif p.startswith('/ui/'):
|
||||
self._ui(unquote(p[len('/ui/'):]))
|
||||
elif p == '/api/files':
|
||||
self._api_files()
|
||||
elif p == '/api/analyze':
|
||||
@@ -267,8 +269,21 @@ class _Handler(BaseHTTPRequestHandler):
|
||||
else:
|
||||
self._send(404, b'Not found', 'text/plain')
|
||||
|
||||
def _html(self):
|
||||
self._send(200, _HTML.encode('utf-8'), 'text/html; charset=utf-8')
|
||||
def _ui(self, name: str):
|
||||
"""Serve a file of the browser UI from webui/. Read per request: it is
|
||||
a handful of small files, and edits show up on reload without a server
|
||||
restart. Only the listed types are served, and the path must stay
|
||||
inside webui/ (same guard as _safe_path, but for the UI folder)."""
|
||||
try:
|
||||
path = (WEBUI_DIR / name).resolve()
|
||||
path.relative_to(WEBUI_DIR)
|
||||
except ValueError:
|
||||
path = None
|
||||
ctype = _UI_TYPES.get(path.suffix) if path else None
|
||||
if ctype is None or not path.is_file():
|
||||
self._send(404, b'Not found', 'text/plain')
|
||||
return
|
||||
self._send(200, path.read_bytes(), ctype)
|
||||
|
||||
def _api_files(self):
|
||||
files = list_files(self.recordings_dir)
|
||||
@@ -738,10 +753,15 @@ class _Server(ThreadingHTTPServer):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UI page — single-page HTML/CSS/JS, loaded once at startup
|
||||
# Browser UI — static files in webui/ (index.html, style.css, js/*.js modules)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_HTML = (Path(__file__).resolve().parent / 'webui.html').read_text(encoding='utf-8')
|
||||
WEBUI_DIR = Path(__file__).resolve().parent / 'webui'
|
||||
_UI_TYPES = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -773,6 +793,8 @@ def main():
|
||||
|
||||
if not rec_dir.exists():
|
||||
print(f"Warning: recordings directory '{rec_dir}' does not exist yet.")
|
||||
if not (WEBUI_DIR / 'index.html').is_file():
|
||||
sys.exit(f"Error: browser UI not found at {WEBUI_DIR} — keep webui/ next to web.py.")
|
||||
|
||||
prune_orphan_analyses(analyses_dir, rec_dir)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user