The retry badge stops offering jobs the human retired

Marking the D&D blue box "local" removed its job from the queue — but
the badge still counted its old failure and the checkbox still offered
to retry it, because the count read only upload_log.csv. A failure is
retryable only if the job is still queued AND still endorsed by
matches.csv; on Eric's data that's the difference between 2 and 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-05 23:06:40 -04:00
co-authored by Claude Fable 5
parent 99c1fbf02d
commit 74ecd5059b
5 changed files with 73 additions and 9 deletions
+22 -7
View File
@@ -117,14 +117,29 @@ def stale_jobs(queue_rows: list[dict], match_rows: list[dict]) -> dict[str, str]
return stale
def outstanding_failures(log_rows: list[dict]) -> int:
"""Jobs whose LATEST attempt failed. upload_log.csv is an append-only
audit trail, so counting every 'failed' row ever written would keep
growing after a successful retry."""
last: dict[tuple[str, str, str], str] = {}
def outstanding_failures(
log_rows: list[dict],
queue_rows: list[dict] | None = None,
match_rows: list[dict] | None = None,
) -> int:
"""Failures still worth retrying: the job's LATEST attempt failed AND
it is still queued and still endorsed by matches.csv.
Two ways to over-count. upload_log.csv is append-only, so a successful
retry leaves its old 'failed' line behind; and a failed job whose
review decision has since changed (marked local, rejected) is not
pending work at all — it will never run again, so offering to retry it
is a lie."""
last: dict[tuple[str, str, str], tuple[str, str]] = {}
for row in log_rows:
last[_job_key(row)] = row["status"]
return sum(1 for status in last.values() if status == "failed")
last[_job_key(row)] = (row["status"], row.get("bgg_id", ""))
failed = [bgg_id for status, bgg_id in last.values() if status == "failed"]
if queue_rows is None:
return len(failed)
live = {r.get("bgg_id", "") for r in queue_rows} - set(
stale_jobs(queue_rows, match_rows or [])
)
return sum(1 for bgg_id in failed if bgg_id in live)
def _read_csv(path: Path) -> list[dict]:
+12 -1
View File
@@ -793,7 +793,18 @@ def create_app(
with log_path.open(newline="") as f:
log_rows = list(csv.DictReader(f))
log_counts = Counter(row["status"] for row in log_rows)
failed_now = outstanding_failures(log_rows)
def _csv_rows(path: Path) -> list[dict]:
if not path.exists():
return []
with path.open(newline="") as handle:
return list(csv.DictReader(handle))
failed_now = outstanding_failures(
log_rows,
_csv_rows(cfg.to_add_path) + _csv_rows(cfg.to_update_path),
session.rows,
)
games = len(read_games())
return {
"boot": boot,