Two coupled fixes for "missing thumbnails but triggering backfill found
nothing" (operator-flagged 2026-06-01):
1. Backfill UI was a black box
POST /api/thumbnails/backfill returned only {celery_task_id} and the
admin card said "Enqueued." with no counts. Impossible to tell whether
the scan found 0 candidates, 5000 candidates, or whether the worker was
even picking up the task. "Found nothing" was indistinguishable from a
broken queue.
Scan logic factored into _run_backfill_scan (shared helper).
API runs it synchronously in an executor and returns {scanned, enqueued, ok, regenerated}. The actual thumbnail
generation still goes to the thumbnail Celery queue per row.
Admin card now shows: Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok.
The magic-byte check passed for any 8+ byte file starting with a JPEG or
PNG header, including empty/truncated/zero-pad files that browsers
render as broken. Backfill counted these as ok and never regenerated.
Also require file size ≥ MIN_THUMB_BYTES (256). Real thumbnails are
minimum ~2KB even on solid-color sources; header-only corrupt files
top out around 12 bytes. 256 sits well above corrupt and well below
any legitimate thumbnail.
Test plan
CI green on 43b778a
Operator triggers backfill and reports the surfaced counts; that'll
tell us which scenario the broken-thumbnail tiles are actually in.
## Summary
Two coupled fixes for "missing thumbnails but triggering backfill found
nothing" (operator-flagged 2026-06-01):
### 1. Backfill UI was a black box
`POST /api/thumbnails/backfill` returned only `{celery_task_id}` and the
admin card said "Enqueued." with no counts. Impossible to tell whether
the scan found 0 candidates, 5000 candidates, or whether the worker was
even picking up the task. "Found nothing" was indistinguishable from a
broken queue.
- Scan logic factored into `_run_backfill_scan` (shared helper).
- API runs it synchronously in an executor and returns
`{scanned, enqueued, ok, regenerated}`. The actual thumbnail
generation still goes to the thumbnail Celery queue per row.
- Admin card now shows: `Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok`.
### 2. `_thumb_is_valid` accepted header-only corrupt files
The magic-byte check passed for any 8+ byte file starting with a JPEG or
PNG header, including empty/truncated/zero-pad files that browsers
render as broken. Backfill counted these as `ok` and never regenerated.
- Also require file size ≥ `MIN_THUMB_BYTES` (256). Real thumbnails are
minimum ~2KB even on solid-color sources; header-only corrupt files
top out around 12 bytes. 256 sits well above corrupt and well below
any legitimate thumbnail.
## Test plan
- [x] CI green on `43b778a`
- [ ] Operator triggers backfill and reports the surfaced counts; that'll
tell us which scenario the broken-thumbnail tiles are actually in.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two coupled problems, operator-flagged 2026-06-01: "missing thumbnails
but triggering backfill found nothing."
1. **Backfill UI was a black box.** `POST /api/thumbnails/backfill`
returned just `{celery_task_id}` and the admin card said
"Enqueued." with no counts. There was no way to tell whether the
scan found 0 candidates, 5000 candidates, or whether the worker
even picked up the task. "Found nothing" was indistinguishable
from a broken queue.
Fix: refactor the scan into a sync helper (`_run_backfill_scan`)
shared by the Celery task and the API endpoint. The API now runs
the scan in an executor and returns `{scanned, enqueued, ok,
regenerated}`. The actual thumbnail generation work still goes
to the thumbnail Celery queue per row via
`generate_thumbnail.delay()` — the scan itself is fast
(SELECT id+thumbnail_path + a file.stat() per row).
2. **`_thumb_is_valid` accepted header-only corrupt files.** The
magic-byte check passed for any 8-byte file starting with a JPEG
or PNG header, including empty/truncated/zero-pad files that
browsers render as broken. Backfill counted these as `ok` and
never regenerated.
Fix: also require file size ≥ MIN_THUMB_BYTES (256). Real
thumbnails are minimum ~2KB even on solid-color sources; header-
only corrupt files top out around 12 bytes. 256 is well above
the corrupt floor and well below any legitimate thumbnail.
Plus the admin card now shows the per-run counts instead of
"Enqueued.":
Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok
Same 'change shared shape, miss pinned tests' trap as cfa4fb4 — the
prior commit updated only test_backfill_mixed_aggregate; three sibling
tests in the same file also pinned the old {enqueued,ok,regenerated}
shape without 'scanned' and CI bounced.
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
Two coupled fixes for "missing thumbnails but triggering backfill found
nothing" (operator-flagged 2026-06-01):
1. Backfill UI was a black box
POST /api/thumbnails/backfillreturned only{celery_task_id}and theadmin card said "Enqueued." with no counts. Impossible to tell whether
the scan found 0 candidates, 5000 candidates, or whether the worker was
even picking up the task. "Found nothing" was indistinguishable from a
broken queue.
_run_backfill_scan(shared helper).{scanned, enqueued, ok, regenerated}. The actual thumbnailgeneration still goes to the thumbnail Celery queue per row.
Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok.2.
_thumb_is_validaccepted header-only corrupt filesThe magic-byte check passed for any 8+ byte file starting with a JPEG or
PNG header, including empty/truncated/zero-pad files that browsers
render as broken. Backfill counted these as
okand never regenerated.MIN_THUMB_BYTES(256). Real thumbnails areminimum ~2KB even on solid-color sources; header-only corrupt files
top out around 12 bytes. 256 sits well above corrupt and well below
any legitimate thumbnail.
Test plan
43b778atell us which scenario the broken-thumbnail tiles are actually in.
🤖 Generated with Claude Code
Two coupled problems, operator-flagged 2026-06-01: "missing thumbnails but triggering backfill found nothing." 1. **Backfill UI was a black box.** `POST /api/thumbnails/backfill` returned just `{celery_task_id}` and the admin card said "Enqueued." with no counts. There was no way to tell whether the scan found 0 candidates, 5000 candidates, or whether the worker even picked up the task. "Found nothing" was indistinguishable from a broken queue. Fix: refactor the scan into a sync helper (`_run_backfill_scan`) shared by the Celery task and the API endpoint. The API now runs the scan in an executor and returns `{scanned, enqueued, ok, regenerated}`. The actual thumbnail generation work still goes to the thumbnail Celery queue per row via `generate_thumbnail.delay()` — the scan itself is fast (SELECT id+thumbnail_path + a file.stat() per row). 2. **`_thumb_is_valid` accepted header-only corrupt files.** The magic-byte check passed for any 8-byte file starting with a JPEG or PNG header, including empty/truncated/zero-pad files that browsers render as broken. Backfill counted these as `ok` and never regenerated. Fix: also require file size ≥ MIN_THUMB_BYTES (256). Real thumbnails are minimum ~2KB even on solid-color sources; header- only corrupt files top out around 12 bytes. 256 is well above the corrupt floor and well below any legitimate thumbnail. Plus the admin card now shows the per-run counts instead of "Enqueued.": Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok