Fix skipped_lock creating phantom empty genre/dedup runs; add Run now to playlist list

genre_review_service.run() and dedup_review_service._run_scan() both
unconditionally created a new run row even when the underlying job never
actually executed (skipped_lock, or any other non-success status). For
genres this was directly user-visible: the review page always shows the
most recent run, so a skipped click created an empty run that displaced
the real previous preview, making it look like every pending change had
vanished. Both now return None (persisting nothing) when the job didn't
succeed, and both routers surface a "didn't run, something else was
using the pipeline" notice instead of silently redirecting. Cleaned up
the one phantom empty genre_runs row already sitting in production,
restoring the real 20-change preview.

Also added a "Run now" button to each row on the Playlists list page
(previously only on a playlist's own detail page), for the common case
of adding new tracks and wanting to sync immediately.
This commit is contained in:
andrew
2026-07-09 12:01:53 -06:00
parent c442fb78ef
commit 2a19f84575
7 changed files with 50 additions and 10 deletions
+12 -2
View File
@@ -21,7 +21,7 @@ def _parse_json_lines(output: str) -> list[dict]:
return candidates
async def run(apply: bool, force: bool = False, triggered_by: str = "schedule") -> GenreRun:
async def run(apply: bool, force: bool = False, triggered_by: str = "schedule") -> GenreRun | None:
"""Run spotify-genre.py --json (dry-run or --apply), persisting the
diff into a fresh genre_runs/genre_candidates pair either way.
@@ -29,7 +29,14 @@ async def run(apply: bool, force: bool = False, triggered_by: str = "schedule")
(low-risk/reversible, GENRE_LOCK-protected) -- this wraps that same
scheduled run so its diff becomes reviewable after the fact, rather
than being a separate preview-only mode. scheduler_service's
maintenance:spotify_genre job calls this with apply=True."""
maintenance:spotify_genre job calls this with apply=True.
Returns None (and persists nothing) if the job never actually ran --
e.g. skipped_lock because something else was using the pipeline at
that moment. Without this check, a skipped/failed attempt still
created an empty genre_runs row, which then became "latest run" on
the review page (it's always the most recent by id) and hid the real
previous run's candidates behind a phantom empty one."""
args = ["--json"]
if apply:
args.append("--apply")
@@ -40,6 +47,9 @@ async def run(apply: bool, force: bool = False, triggered_by: str = "schedule")
job_run, output = await pipeline_runner.run_job_capture(
"genre:run", [script] + args, triggered_by=triggered_by
)
if job_run.status != "success":
return None
candidates = _parse_json_lines(output)
db = SessionLocal()