20 commits on dev (5d4f223 → fb41b90). Adds a thumbnail-backfill UI, fixes three psycopg-65535-parameter latents, race-proofs two more SELECT-then-INSERT sites with DB unique constraints, redesigns the artist detail view, and ships HTTP-friendly Copy + a context-rich ErrorDetailModal.
Features
Thumbnail backfill — new backfill_thumbnails Celery task + POST /api/thumbnails/backfill + Pinia store + ThumbnailBackfillCard wired into MaintenancePanel. Three-predicate scan (NULL / missing on disk / corrupt magic bytes) re-enqueues generate_thumbnail for repair candidates.
Artist view redesign — sticky frosted ArtistHeader (matches TopNav blur recipe, hosts name + image/post stats + tab strip). Three lazy tabs: Posts (default when post_count > 0), Gallery (fallback), Management. ?tab= URL state, cross-artist store reset, document.title per artist.
ErrorDetailModal redesign — row prop carries the failure's task name / queue / target / duration / started / retries / worker / celery-id / args alongside the error message. Pre-block contrast fix (theme background token vs auto-derived surface-variant).
Fixes
psycopg 65535-parameter ceiling, three sites — recover_interrupted_tasks, /api/import/retry-failed, /api/import/clear-stuck all folded their SELECT ids → UPDATE WHERE id IN (...) patterns into UPDATE … WHERE … RETURNING. The IN-list is impossible to overflow now regardless of row count.
ImageProvenance race + alembic 0021 — _apply_sidecar's existence-check + INSERT was racy; concurrent workers planted (image_record_id, post_id) duplicates that then broke .scalar_one_or_none() on every later deep-scan rederive (MultipleResultsFound). Migration dedupes existing rows + adds uq_image_provenance_image_post. Importer switched to savepoint + IntegrityError recovery.
Source per (artist, platform), not per post + alembic 0022 — filesystem importer was creating one Source row per imported post URL (Atole's artist page had 406 Sources where 1 was right). Migration collapses per-artist-per-platform groups to the campaign-URL canonical (or a sidecar:<platform>:<slug> synthetic with enabled=false), reparenting Posts + ImageProvenance and merging Post collisions. Importer now reuses the existing subscription Source.
Extension race-safety — ExtensionService._find_or_create_artist + _find_or_create_source switched to the same savepoint + IntegrityError pattern. Two fast clicks of the extension's "Add as source" no longer 500 on the second one.
Copy button on plain HTTP — navigator.clipboard is gated by Secure Context (HTTPS-only). New copyText() helper falls back to document.execCommand('copy') via an off-screen textarea. Applied to ErrorDetailModal, ExtensionKeyBar, BrowserExtensionCard.
## Summary
20 commits on dev (`5d4f223 → fb41b90`). Adds a thumbnail-backfill UI, fixes three psycopg-65535-parameter latents, race-proofs two more SELECT-then-INSERT sites with DB unique constraints, redesigns the artist detail view, and ships HTTP-friendly Copy + a context-rich ErrorDetailModal.
### Features
- **Thumbnail backfill** — new `backfill_thumbnails` Celery task + `POST /api/thumbnails/backfill` + Pinia store + ThumbnailBackfillCard wired into MaintenancePanel. Three-predicate scan (NULL / missing on disk / corrupt magic bytes) re-enqueues `generate_thumbnail` for repair candidates.
- **Artist view redesign** — sticky frosted `ArtistHeader` (matches TopNav blur recipe, hosts name + image/post stats + tab strip). Three lazy tabs: Posts (default when `post_count > 0`), Gallery (fallback), Management. `?tab=` URL state, cross-artist store reset, `document.title` per artist.
- **ErrorDetailModal redesign** — `row` prop carries the failure's task name / queue / target / duration / started / retries / worker / celery-id / args alongside the error message. Pre-block contrast fix (theme `background` token vs auto-derived `surface-variant`).
### Fixes
- **psycopg 65535-parameter ceiling, three sites** — `recover_interrupted_tasks`, `/api/import/retry-failed`, `/api/import/clear-stuck` all folded their `SELECT ids → UPDATE WHERE id IN (...)` patterns into `UPDATE … WHERE … RETURNING`. The IN-list is impossible to overflow now regardless of row count.
- **ImageProvenance race** + alembic 0021 — `_apply_sidecar`'s existence-check + INSERT was racy; concurrent workers planted `(image_record_id, post_id)` duplicates that then broke `.scalar_one_or_none()` on every later deep-scan rederive (MultipleResultsFound). Migration dedupes existing rows + adds `uq_image_provenance_image_post`. Importer switched to savepoint + IntegrityError recovery.
- **Source per (artist, platform), not per post** + alembic 0022 — filesystem importer was creating one Source row per imported post URL (Atole's artist page had 406 Sources where 1 was right). Migration collapses per-artist-per-platform groups to the campaign-URL canonical (or a `sidecar:<platform>:<slug>` synthetic with `enabled=false`), reparenting Posts + ImageProvenance and merging Post collisions. Importer now reuses the existing subscription Source.
- **Extension race-safety** — `ExtensionService._find_or_create_artist` + `_find_or_create_source` switched to the same savepoint + IntegrityError pattern. Two fast clicks of the extension's "Add as source" no longer 500 on the second one.
- **Copy button on plain HTTP** — `navigator.clipboard` is gated by Secure Context (HTTPS-only). New `copyText()` helper falls back to `document.execCommand('copy')` via an off-screen textarea. Applied to ErrorDetailModal, ExtensionKeyBar, BrowserExtensionCard.
### Migrations
- `0021_image_provenance_unique` — dedupe + `UNIQUE(image_record_id, post_id)`.
- `0022_source_per_artist_platform` — collapse per-post Sources, reparent Posts + ImageProvenance, merge Post collisions.
## Test plan
- [ ] Deploy `:latest`, force-update `app`, `worker`, `ml-worker`, `scheduler`, `web`. Alembic auto-applies 0021 + 0022.
- [ ] Verify Atole's artist page now shows 1 Source under Management → Subscriptions.
- [ ] Verify the new artist view: Posts tab default when posts exist, Gallery fallback when zero, frosted header continuous with TopNav.
- [ ] Settings → Maintenance → Browser extension: install button still works in Firefox.
- [ ] Settings → Maintenance: new ThumbnailBackfillCard present in the grid.
- [ ] System Activity → click an error: modal shows task context + crisp pre-block; Copy button works on plain HTTP.
- [ ] Walk through the FC-3g verification runbook (extension install → cookies push → quick-add-source).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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
20 commits on dev (
5d4f223 → fb41b90). Adds a thumbnail-backfill UI, fixes three psycopg-65535-parameter latents, race-proofs two more SELECT-then-INSERT sites with DB unique constraints, redesigns the artist detail view, and ships HTTP-friendly Copy + a context-rich ErrorDetailModal.Features
Thumbnail backfill — new
backfill_thumbnailsCelery task +POST /api/thumbnails/backfill+ Pinia store + ThumbnailBackfillCard wired into MaintenancePanel. Three-predicate scan (NULL / missing on disk / corrupt magic bytes) re-enqueuesgenerate_thumbnailfor repair candidates.Artist view redesign — sticky frosted
ArtistHeader(matches TopNav blur recipe, hosts name + image/post stats + tab strip). Three lazy tabs: Posts (default whenpost_count > 0), Gallery (fallback), Management.?tab=URL state, cross-artist store reset,document.titleper artist.ErrorDetailModal redesign —
rowprop carries the failure's task name / queue / target / duration / started / retries / worker / celery-id / args alongside the error message. Pre-block contrast fix (themebackgroundtoken vs auto-derivedsurface-variant).Fixes
psycopg 65535-parameter ceiling, three sites —
recover_interrupted_tasks,/api/import/retry-failed,/api/import/clear-stuckall folded theirSELECT ids → UPDATE WHERE id IN (...)patterns intoUPDATE … WHERE … RETURNING. The IN-list is impossible to overflow now regardless of row count.ImageProvenance race + alembic 0021 —
_apply_sidecar's existence-check + INSERT was racy; concurrent workers planted(image_record_id, post_id)duplicates that then broke.scalar_one_or_none()on every later deep-scan rederive (MultipleResultsFound). Migration dedupes existing rows + addsuq_image_provenance_image_post. Importer switched to savepoint + IntegrityError recovery.Source per (artist, platform), not per post + alembic 0022 — filesystem importer was creating one Source row per imported post URL (Atole's artist page had 406 Sources where 1 was right). Migration collapses per-artist-per-platform groups to the campaign-URL canonical (or a
sidecar:<platform>:<slug>synthetic withenabled=false), reparenting Posts + ImageProvenance and merging Post collisions. Importer now reuses the existing subscription Source.Extension race-safety —
ExtensionService._find_or_create_artist+_find_or_create_sourceswitched to the same savepoint + IntegrityError pattern. Two fast clicks of the extension's "Add as source" no longer 500 on the second one.Copy button on plain HTTP —
navigator.clipboardis gated by Secure Context (HTTPS-only). NewcopyText()helper falls back todocument.execCommand('copy')via an off-screen textarea. Applied to ErrorDetailModal, ExtensionKeyBar, BrowserExtensionCard.Migrations
0021_image_provenance_unique— dedupe +UNIQUE(image_record_id, post_id).0022_source_per_artist_platform— collapse per-post Sources, reparent Posts + ImageProvenance, merge Post collisions.Test plan
:latest, force-updateapp,worker,ml-worker,scheduler,web. Alembic auto-applies 0021 + 0022.🤖 Generated with Claude Code