Skip to content

Review 2026-08-14: CI, task safety, config validation, opt-in auth, History paging, ref-cache TTL - #1

Merged
leoli-dev merged 11 commits into
mainfrom
improve/review-2026-08-14
Aug 15, 2026
Merged

Review 2026-08-14: CI, task safety, config validation, opt-in auth, History paging, ref-cache TTL#1
leoli-dev merged 11 commits into
mainfrom
improve/review-2026-08-14

Conversation

@leoli-dev

Copy link
Copy Markdown
Owner

Implements the six findings from the 2026-08-14 repo review, one commit each, on top of 5b51173.

What's in it

P Commit Change
P1 f9f9cec GitHub Actions CI + a reproducible OpenAPI exporter with a drift guard
P2 e51387e spawn() helper so detached lifecycle tasks can't be GC'd mid-flight
P3 06109f2 PUT /api/config validates before applying; 422 instead of 500
P4 06944bd Opt-in auth_token gating /api/* (default behaviour unchanged)
P5 55c9023 History pages past the first 100 rows
P6 e6c34b0 Worker reference-clip cache expires after a week of disuse

Notes for review

P1 — adapters import their model libraries lazily, so scripts/export_openapi.py regenerates all eight worker documents from the hub venv alone, no engine venv needed. It reproduces the committed docs/api/ byte for byte; pytest asserts they stay in sync. CI installs the fake engine venv so the existing worker integration test runs instead of skipping.

P4 is the only change with a behavioural surface, and it is inert unless [hub] auth_token is set. Two details worth a look: EventSource and <audio src> cannot send a header, so those URLs fall back to ?token= (which lands in the access log of the machine that already holds the plaintext token — documented in both READMEs); and save_config now preserves a token the WebUI never sees, so saving from Settings cannot silently drop it. The token cannot be rotated through the API it protects.

P5 changes GET /api/history from a bare array to {items, total, offset, limit}. The WebUI is the only consumer — audio-book-maker uses discovery/lifecycle plus direct worker generation. New generations now fold into the top of the list rather than resetting it, so a finished job no longer discards loaded pages.

Verification

All run locally on macOS; CI runs the first three on every push.

  • pytest — 56 passed (9 pre-existing + 47 new)
  • ruff check — clean across hub, sdk, engines, scripts
  • node --test — 21 passed (4 pre-existing + 17 new)
  • playwright test — 8 passed in Chromium against a real hub process, covering the token gate, reload persistence, recovery from a stale token, paging 230 seeded rows to the oldest without duplicates, and every page loading without console errors
  • scripts/export_openapi.py --check — docs current

🤖 Generated with Claude Code

https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s

leoli-dev and others added 11 commits August 14, 2026 22:56
The repo shipped pytest, Node and ruff guards that only ever ran by hand,
and docs/api/ could drift from the adapters unnoticed.

scripts/export_openapi.py regenerates every worker document from one entry
point; adapters import their model libraries lazily, so it needs no engine
venv. --check turns it into a drift gate, and pytest asserts the committed
documents still match. CI runs ruff + pytest (with the fake engine venv
installed so the worker integration test runs instead of skipping) and the
Node WebUI tests on push and pull request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
Engine start/stop/restart, rescan-disable, autostart and idle-unload all
ran through a bare asyncio.create_task. The loop holds only a weak
reference to a task, so work nobody awaits can be collected mid-flight,
and a failure inside it was swallowed entirely.

spawn() holds a strong reference until completion, names the task for the
logs, and reports exceptions. Tasks already stored on an object keep their
own reference and are unchanged; a pytest walks the AST to make sure no
new detached create_task creeps back in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
PUT /api/config coerced with a bare int(), so a non-numeric port answered
500, and nothing checked ranges — port 0 or a negative retention was
accepted and written to config.toml. Worse, fields were applied one by one
as they were parsed, so a later failure left the hub half-configured.

Everything is now validated up front and rejected with a 422 the WebUI
already surfaces as a toast; cfg is mutated only once the whole payload is
known good. Bounds live in config.HUB_INT_BOUNDS beside the defaults.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
The hub binds 0.0.0.0 by default with no authentication, so any device on
the LAN could rewrite config.toml through PUT /api/config — including
data_dir and per-engine env vars, which land in a spawned worker process.

Setting [hub] auth_token now gates every /api/* call behind a bearer token
compared with compare_digest. Leaving it unset changes nothing. Streams
that cannot send a header (SSE, <audio>, download links) accept ?token=;
the static shell stays open so the browser can load the page that asks for
the token. The secret is never echoed by GET /api/config, cannot be rotated
through the API, and save_config preserves it so saving from Settings can
never drop it.

Covered by pytest (both credential paths, 401s, config round-trip), node
tests for the URL/retry logic, and a Playwright suite that drives a real
browser against a real hub for the gate, reload persistence and recovery
from a stale token. CI gains an e2e job; the WebUI strings ship in en/fr/zh
with a new locale parity test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
The History page asked for a fixed limit=100 and the endpoint returned a
bare array, so with retention off (the default) everything older than the
hundredth generation was unreachable from the UI.

GET /api/history now returns {items, total, offset, limit} with the limit
clamped to 1–200, and the page appends with a "Load N more" button. New
generations are folded into the top of the list rather than resetting it,
so a finished job no longer discards the pages already loaded, and deleting
a row adjusts the count in place.

Covered by pytest for paging, clamping and totals, node tests for the merge
rules, and Playwright walking 230 seeded rows to the oldest one while
asserting no row is served twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
Workers cache every uploaded reference clip under /tmp by content hash and
never removed one, so a long-lived worker accumulated each voice it had
ever cloned.

Startup now drops clips untouched for a week, and a cache hit refreshes the
mtime so a voice still in regular use is never collected. Failures while
scanning are ignored — a temp-dir problem must not stop a worker booting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
The token work rewired imports across six modules; a broken specifier or a
missing translation key would still render a shell and pass the existing
assertions. Walking all five pages with the console watched catches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G6DQkv8vHsu7CotHM7Pa8s
loadMore asked the server for offset=rows.length, but a job finishing while
that request was in flight ran load(), which merges a fresh first page and
inserts rows above that offset. The page then came back shifted and half of
it was dropped as already-seen: no data was lost and the next click healed
it, but the "Load N more" count wobbled and the user paid an extra click.

A refresh arriving during a page request now queues itself and runs once
the page has landed, so the offset a request was built from stays valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
The middleware matched paths starting with "/api", which also covers static
WebUI files whose names merely begin with those letters. None exist today,
but a future one would answer 401 to a browser that cannot yet ask for the
token. No route is a bare /api, so the slash costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
withAuthRetry replayed a request once and then gave up. If the token was
rotated in config.toml mid-session, or the user mistyped it at the prompt,
the replay was still unauthorised and the call failed with a toast, leaving
no way back except a page reload.

The retry is now a loop, matching ensureAuthenticated: each 401 re-opens the
prompt. It only ever awaits promptForToken(), so it is paced by the user and
cannot spin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
PUT /api/config ran str() and int() over the incoming values, so via curl
host = null was saved as the string "None", port = 3.9 became 3 and
retention_days = true became 1. The WebUI only ever sends numbers and
strings, so none of this was reachable from the UI.

Values are now type-checked: host and data_dir must be strings, and the
numeric settings must be whole numbers — bools and floats are refused
rather than truncated. Digit strings stay accepted, since hand-written
clients send them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GNkSG7RYPAaYkEBxczycC1
@leoli-dev
leoli-dev merged commit d7c666a into main Aug 15, 2026
3 checks passed
@leoli-dev
leoli-dev deleted the improve/review-2026-08-14 branch August 15, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant