Adds two related features to the web UI, requested as a follow-up after the Spotify-style theming PR merged:
Rerun — re-enqueue a finished or failed job with its exact original input as a new linked job (POST /api/jobs/{id}/rerun).
Edit & rebuild — resubmit a job with edited fields (title/artist/lyrics/source/genre/year/language/bpm/cover) and automatically redo only the pipeline stages the edit invalidates, via a new FIELD_STAGE policy map (src/karaoke_maker/rebuild.py) that feeds the existing run_pipeline(..., force_from=...) argument. Everything before the invalidated stage is served from cache.
Supporting changes: uploads are now retained on job failure (previously deleted unconditionally, which made a job that died in acquire permanently un-rerunnable) and covers are always kept since a rebuild inherits cover_path; a retitled song's work directory is relocated rather than duplicated; a superseded output directory (when a retitle changes the output folder's slug) is removed after a successful rebuild; and the frontend gained RERUN / EDIT & REBUILD controls that reuse the existing single song form in a "rebuild mode" rather than duplicating it.
Design doc: ~/specs/2026-07-26-karaoke-maker-rerun-rebuild-design.md. Job persistence across server restarts was scoped out and explicitly deferred (documented in the design doc) — jobs remain in-memory only, same as before this branch.
How this was built
Implemented via Subagent-Driven Development against a 7-task plan, one task per commit, each independently spec- and quality-reviewed:
rebuild.py — field-to-stage mapping, relocate/superseded-output helpers (pure, no web dependency)
Upload retention fix (delete-on-success-only, video only, covers always kept)
rebuilt_from job link + POST /rerun, with job creation factored into a shared _enqueue helper
GET /api/jobs/{id}/input — echoes a job's inputs keyed by form field name, for prefilling the rebuild form
POST /rebuild + worker force_from wiring — the endpoint and the worker land together since one is meaningless without the other
Superseded output directory removal after a successful rebuild
Frontend: action row, rebuild-mode reuse of the single song form, lineage marker on rebuilt jobs
A final whole-branch review (independent from the per-task reviews) caught two real test-coverage gaps — cover inheritance across a rebuild and an accessibility-label test that didn't pin its own literal — both were mutation-verified (the code they're meant to protect was temporarily deleted/broken to confirm the new tests actually catch it) and fixed in the closing commit.
Test plan
Full suite passes: 227 passed, 1 skipped (up from the pre-branch baseline of 163 passed, 1 skipped)
Every new/changed test was mutation-verified where feasible (temporarily break the code under test, confirm the test fails; restore, confirm it passes) rather than merely asserted to pass
Manual browser check of the new UI: RERUN/EDIT & REBUILD control visibility per job status, form-mode swap (heading/button label/CANCEL/prefill), keep-note visibility for inherited files, and the rebuilt-job lineage marker
No innerHTML/outerHTML/insertAdjacentHTML introduced (existing guard test enforces this); no external network requests added; focus-preservation on the 3-second poll verified for the new action row and lineage link
No changes to cli.py or anything under src/karaoke_maker/stages/
**Summary generated by LLM agent**
## Summary
Adds two related features to the web UI, requested as a follow-up after the Spotify-style theming PR merged:
- **Rerun** — re-enqueue a finished or failed job with its exact original input as a new linked job (`POST /api/jobs/{id}/rerun`).
- **Edit & rebuild** — resubmit a job with edited fields (title/artist/lyrics/source/genre/year/language/bpm/cover) and automatically redo only the pipeline stages the edit invalidates, via a new `FIELD_STAGE` policy map (`src/karaoke_maker/rebuild.py`) that feeds the existing `run_pipeline(..., force_from=...)` argument. Everything before the invalidated stage is served from cache.
Supporting changes: uploads are now retained on job failure (previously deleted unconditionally, which made a job that died in `acquire` permanently un-rerunnable) and covers are always kept since a rebuild inherits `cover_path`; a retitled song's work directory is relocated rather than duplicated; a superseded output directory (when a retitle changes the output folder's slug) is removed after a successful rebuild; and the frontend gained RERUN / EDIT & REBUILD controls that reuse the existing single song form in a "rebuild mode" rather than duplicating it.
Design doc: `~/specs/2026-07-26-karaoke-maker-rerun-rebuild-design.md`. Job persistence across server restarts was scoped out and explicitly deferred (documented in the design doc) — jobs remain in-memory only, same as before this branch.
## How this was built
Implemented via Subagent-Driven Development against a 7-task plan, one task per commit, each independently spec- and quality-reviewed:
1. `rebuild.py` — field-to-stage mapping, relocate/superseded-output helpers (pure, no web dependency)
2. Upload retention fix (delete-on-success-only, video only, covers always kept)
3. `rebuilt_from` job link + `POST /rerun`, with job creation factored into a shared `_enqueue` helper
4. `GET /api/jobs/{id}/input` — echoes a job's inputs keyed by form field name, for prefilling the rebuild form
5. `POST /rebuild` + worker `force_from` wiring — the endpoint and the worker land together since one is meaningless without the other
6. Superseded output directory removal after a successful rebuild
7. Frontend: action row, rebuild-mode reuse of the single song form, lineage marker on rebuilt jobs
A final whole-branch review (independent from the per-task reviews) caught two real test-coverage gaps — cover inheritance across a rebuild and an accessibility-label test that didn't pin its own literal — both were mutation-verified (the code they're meant to protect was temporarily deleted/broken to confirm the new tests actually catch it) and fixed in the closing commit.
## Test plan
- [x] Full suite passes: `227 passed, 1 skipped` (up from the pre-branch baseline of `163 passed, 1 skipped`)
- [x] Every new/changed test was mutation-verified where feasible (temporarily break the code under test, confirm the test fails; restore, confirm it passes) rather than merely asserted to pass
- [x] Manual browser check of the new UI: RERUN/EDIT & REBUILD control visibility per job status, form-mode swap (heading/button label/CANCEL/prefill), keep-note visibility for inherited files, and the rebuilt-job lineage marker
- [x] No `innerHTML`/`outerHTML`/`insertAdjacentHTML` introduced (existing guard test enforces this); no external network requests added; focus-preservation on the 3-second poll verified for the new action row and lineage link
- [x] No changes to `cli.py` or anything under `src/karaoke_maker/stages/`
Directly assert STAGE_LINE does not match the [rebuild] line, rather than
inferring it from job.stage, which ok_runner's later [acquire] running
line would overwrite either way.
- add test_rebuild_inherits_the_parents_cover: covers the cover_path
inheritance mechanism in the rebuild endpoint (Task 2's upload-retention
design relied on it, but nothing exercised it)
- tighten test_lineage_marker_survives_in_the_accessibility_tree to assert
the literal " rebuilt" screen-reader text, not just that rebuilt_from
is referenced somewhere in paintCard
- normalize submit's youtube_url/genre/language the same way rebuild does,
for symmetry, plus a pinning test
- pin acquire's stage outputs to [meta.json, audio.wav] in test_acquire.py,
since web.py's _require_source hardcodes those same filenames
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
Summary
Adds two related features to the web UI, requested as a follow-up after the Spotify-style theming PR merged:
POST /api/jobs/{id}/rerun).FIELD_STAGEpolicy map (src/karaoke_maker/rebuild.py) that feeds the existingrun_pipeline(..., force_from=...)argument. Everything before the invalidated stage is served from cache.Supporting changes: uploads are now retained on job failure (previously deleted unconditionally, which made a job that died in
acquirepermanently un-rerunnable) and covers are always kept since a rebuild inheritscover_path; a retitled song's work directory is relocated rather than duplicated; a superseded output directory (when a retitle changes the output folder's slug) is removed after a successful rebuild; and the frontend gained RERUN / EDIT & REBUILD controls that reuse the existing single song form in a "rebuild mode" rather than duplicating it.Design doc:
~/specs/2026-07-26-karaoke-maker-rerun-rebuild-design.md. Job persistence across server restarts was scoped out and explicitly deferred (documented in the design doc) — jobs remain in-memory only, same as before this branch.How this was built
Implemented via Subagent-Driven Development against a 7-task plan, one task per commit, each independently spec- and quality-reviewed:
rebuild.py— field-to-stage mapping, relocate/superseded-output helpers (pure, no web dependency)rebuilt_fromjob link +POST /rerun, with job creation factored into a shared_enqueuehelperGET /api/jobs/{id}/input— echoes a job's inputs keyed by form field name, for prefilling the rebuild formPOST /rebuild+ workerforce_fromwiring — the endpoint and the worker land together since one is meaningless without the otherA final whole-branch review (independent from the per-task reviews) caught two real test-coverage gaps — cover inheritance across a rebuild and an accessibility-label test that didn't pin its own literal — both were mutation-verified (the code they're meant to protect was temporarily deleted/broken to confirm the new tests actually catch it) and fixed in the closing commit.
Test plan
227 passed, 1 skipped(up from the pre-branch baseline of163 passed, 1 skipped)innerHTML/outerHTML/insertAdjacentHTMLintroduced (existing guard test enforces this); no external network requests added; focus-preservation on the 3-second poll verified for the new action row and lineage linkcli.pyor anything undersrc/karaoke_maker/stages/