• bvandeusen released this 2026-05-26 20:15:36 -04:00 | 573 commits to dev since this release

    Four commits. PR #27 → main, merge commit 4e82208, pinned head 85b640f verified as second parent.

    Highlights

    Extension CORS unblock (c7001f4)

    /api/credentials POSTs from the Firefox extension hit a generic NetworkError on first use because the backend had zero CORS handling — the browser's preflight OPTIONS request (triggered by the X-Extension-Key header) 405'd before the actual request could run. Two new app-level hooks:

    • before_request short-circuits OPTIONS from moz-extension:// / chrome-extension:// origins with 204
    • after_request stamps Access-Control-Allow-Origin + Methods + Headers (Content-Type, X-Extension-Key) + Max-Age 86400 on responses to extension-origin requests

    Whitelist intentionally narrow — normal browser usage stays no-CORS. Five integration tests pin the contract.

    UI gap closures (85b640f, f827612)

    • All 12 views' root <v-container> had py-6, pushing first content well below where TopNav's fade-out gradient lands. Switched to pt-2 pb-6 (8px top, 24px bottom unchanged). PlaceholderView kept pt-3 pb-8.
    • ArtistHeader's top: 64px left a visible gap (TopNav is actually ~48px tall). Now top: 48px so the two bars sit flush. Added a right-side spacer cell so the tab strip is geometrically centered (1fr | auto | 1fr).

    CI workflow cleanup (3f0153c)

    • ci.yml: dropped pull_request: trigger (push-on-dev already covered the same code).
    • build.yml: dropped dev from triggers (operator doesn't use :dev), added tag-push trigger + Determine-tag logic for immutable per-version images.

    This is the first release that publishes a versioned :v26.05.26.5 image alongside the floating :latest. Going forward, every release tag will produce an immutable image you can roll back to.

    Per hotfix cycle: 5 CI runs → 3-4.

    Deploy

    Pull :latest, force-update services. After deploy:

    • Firefox extension Test connection should return green "Connected" instead of NetworkError
    • Top-of-page gaps should be tight (~8px) instead of the previous ~24-32px
    • Artist detail header should sit flush against the main nav with centered tabs
    • Registry should show both :latest and :v26.05.26.5 tags
    Downloads
  • bvandeusen released this 2026-05-26 18:06:40 -04:00 | 578 commits to dev since this release

    Third hotfix for v26.05.26.1's alembic 0022 migration. PR #26 → main, merge commit 52fff00, pinned head f3e8f30 verified.

    What still broke after v26.05.26.3

    IntegrityError: duplicate key (image_record_id, post_id)=(82045, 5411) already exists
    [parameters: {'keep': 5411, 'drop_': 14147}]
    

    When the same image legitimately had ImageProvenance rows under BOTH a keep Post (5411) and a drop Post (14147) — e.g. the same image attached to two different gallery-dl posts that we're now merging — the UPDATE image_provenance SET post_id = keep WHERE post_id = drop would try to plant a second (image, keep) row, tripping uq_image_provenance_image_post from alembic 0021. The after-the-fact dedupe-DELETE never ran because the UPDATE fires UNIQUE row-by-row.

    Fix

    Pre-DELETE image_provenance rows under the drop Post whose image_record_id already has a provenance under the keep Post (those rows are redundant — the keep-side row already represents that image-to-post link). Then the remaining drop-side provenance rows get UPDATEd safely with no collisions possible.

    This now handles all three collision patterns the migration has hit:

    1. Two Posts under same/different sources sharing external_post_id, canonical has it → handled by v26.05.26.2's pre-merge case A
    2. Two non-canonical Posts sharing external_post_id, canonical doesn't → handled by v26.05.26.3's full-group pre-merge
    3. Image already has provenance under both keep and drop → handled by THIS pre-DELETE

    Deploy

    Pull :latest, force-update services. Alembic 0022 should apply cleanly.

    If it errors again, paste the new traceback — there may be yet another pattern in the migration's nested foreign-key web that needs the same row-by-row treatment.

    Downloads
  • bvandeusen released this 2026-05-26 17:53:21 -04:00 | 581 commits to dev since this release

    Second hotfix for v26.05.26.1's alembic 0022 migration. v26.05.26.2 fixed case (A); this fixes case (B). PR #25 → main, merge commit c14338c, pinned head 7a64730 verified.

    What still broke after v26.05.26.2

    IntegrityError: duplicate key (source_id, external_post_id)=(42, 6166997)
    

    v26.05.26.2's pre-merge only handled the case where canonical already had a Post with the colliding epid. It missed: two non-canonical Sources both have Posts with the same external_post_id, canonical has none. The bulk UPDATE moves the first cleanly onto canonical, then collides on the second.

    Fix

    Group ALL Posts in the (artist, platform) by external_post_id (across canonical AND all others). For any group with count > 1:

    • Keep = a Post under canonical (if any) or the lowest-id Post in the group
    • Merge the rest into keep (repoint ImageProvenance + ImageRecord.primary_post_id, dedupe ImageProvenance against alembic 0021's UNIQUE, delete drop Posts)

    Now handles all three patterns:

    • canonical has Post + "other" has duplicate → keep canonical's
    • two "others" both have duplicates (canonical has none) → keep lowest-id one
    • canonical + multiple "others" all share epid → keep canonical's, drop all others

    Also included

    ci.yml: the four Cache pip wheels steps were removed entirely (act_runner cache backend has been broken for 11+ days; cached path wasn't uv's actual cache anyway). Was on dev as 1803a09; rides along here.

    Deploy

    Pull :latest, force-update app, worker, ml-worker, scheduler, web. Alembic should apply 0022 cleanly this time.

    Verify Atole:

    SELECT id, platform, url, enabled FROM source
    WHERE artist_id = (SELECT id FROM artist WHERE slug='atole');
    

    Expected: 1 row, url=https://www.patreon.com/cw/Atole, enabled=true.

    Downloads
  • bvandeusen released this 2026-05-26 16:51:11 -04:00 | 584 commits to dev since this release

    Hotfix for v26.05.26.1. PR #24 → main. Pinned head SHA 0f7cd3c verified as second parent of 8c36dd2.

    What broke in v26.05.26.1

    Alembic 0022 aborted on deploy with:

    IntegrityError: duplicate key (source_id, external_post_id)=(42, 6166997)
    violates uq_post_source_external_id
    

    Same gallery-dl post was ingested via two different sidecar paths in the operator's history, planting two Post rows with identical external_post_id under different per-post Sources. The original 0022 tried to handle this by merging Post collisions AFTER the bulk UPDATE post SET source_id = canonical, but Postgres fires UNIQUE constraints row-by-row during the UPDATE — the merge step never got to run.

    Fix

    Pre-merge colliding (keep, drop) Post pairs BEFORE the bulk reparent:

    • JOIN post AS keep with post AS drop_ on identical external_post_id where keep.source_id = canonical and drop_.source_id ∈ others.
    • For each pair: repoint ImageProvenance + ImageRecord.primary_post_id from drop to keep, dedupe ImageProvenance against alembic 0021's uq_image_provenance_image_post, delete the drop Post.
    • Then the bulk reparent runs cleanly (no collisions possible).

    The original migration runs in a transaction; operator's failed v26.05.26.1 deploy rolled back to the post-0021 state cleanly. No partial-state damage.

    Also included

    • ci.yml: continue-on-error: true on Cache pip wheels (4 sites). act_runner's cache backend is hard-failing the action JS load itself ("Cannot find module .../dist/restore/index.js"); the install step handles cold caches natively. Workaround until the runner-side cache backend is fixed.

    Deploy

    Pull :latest, force-update app, worker, ml-worker, scheduler, web. Alembic should apply 0022 cleanly this time.

    Verify Atole-side:

    SELECT id, platform, url, enabled FROM source
    WHERE artist_id = (SELECT id FROM artist WHERE slug='atole');
    

    Expected: 1 row, url=https://www.patreon.com/cw/Atole, enabled=true.

    Downloads
  • bvandeusen released this 2026-05-26 16:41:34 -04:00 | 587 commits to dev since this release

    PR #23 → main. Pinned head SHA fb41b90; verified merge commit 88cfb3d second parent matches.

    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 redesignrow 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 sitesrecover_interrupted_tasks, /api/import/retry-failed, /api/import/clear-stuck all folded their SELECT ids → UPDATE WHERE id IN (...) patterns into UPDATE … WHERE … RETURNING.

    • ImageProvenance race + alembic 0021 — _apply_sidecar's existence-check + INSERT planted (image_record_id, post_id) duplicates under concurrent workers, breaking .scalar_one_or_none() on every later deep-scan rederive (MultipleResultsFound). Migration dedupes + 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, reparents Posts + ImageProvenance, merges Post collisions. Importer now reuses the existing subscription Source.

    • Extension race-safetyExtensionService._find_or_create_artist + _find_or_create_source switched to the same savepoint + IntegrityError pattern.

    • Copy button on plain HTTPnavigator.clipboard is Secure-Context-gated. New copyText() helper falls back to document.execCommand('copy') via 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.

    Deploy

    Pull :latest, force-update app, worker, ml-worker, scheduler, web. Alembic auto-applies 0021 + 0022.

    Downloads
  • bvandeusen released this 2026-05-26 08:28:01 -04:00 | 608 commits to dev since this release

    First release of 2026-05-26 (.0). Wraps up FC-Cleanup (Settings → Cleanup tab) end-to-end plus three production-impacting fixes flagged during the session.

    What's new

    FC-Cleanup tab — full feature

    New top-level Settings → Cleanup tab between Import and Maintenance:

    • Minimum dimensions — instant SQL preview + Tier-C delete-min-dim-<sha8> confirm. Defaults pre-fill from ImportSettings.min_width / min_height.
    • Transparency audit — async Celery scan with 5s polling; Tier-C delete-audit-<id> confirm to delete matched images. Animated WebP/GIF short-circuit (no multi-frame PIL decode).
    • Single-color audit — same async pattern; threshold + tolerance inputs. First canonical implementation of the rule (import-side filter was unimplemented).
    • Tag maintenance — moved as-is from Maintenance tab. prune_unused_tags flow unchanged.

    Backend: 9 endpoints under /api/cleanup/*, new cleanup_service.start_audit_run / apply_audit_run / cancel_audit_run, new scan_library_for_rule Celery task on maintenance queue (soft_time_limit=7200, time_limit=7500, 50k matched-IDs cap, cancellation check between batches). Inherits FC-3i task_run lifecycle.

    Defaults pre-fill from ImportSettings; operator can override per-audit.

    Worker UniqueViolation crash fix

    Concurrent workers processing different files of the same post both did check-then-INSERT on Source, second one tripped uq_source_artist_platform_url, session poisoned, every subsequent op failed with "transaction has been rolled back". New _find_or_create_source + _find_or_create_post helpers in Importer use SQLAlchemy savepoints (begin_nested()); on IntegrityError, the savepoint rolls back (outer transaction preserved) and a re-select picks up the row the concurrent op committed.

    Error display flat-text + copyable

    SystemActivityTab and ImportTaskList previously used :title="..." to surface full SQLAlchemy traceback content as a browser-native tooltip — uncopyable, unscrollable, cramped. Replaced with ErrorDetailModal: click error → modal with <pre> block + copy-to-clipboard button + scroll cap.

    Firefox extension install button now works

    Install button switched from window.location.assign(url) (programmatic) to :href="manifest.latest_url" (real anchor click). Firefox's XPI-install gesture requires a directly user-clicked anchor pointing at application/x-xpinstall. Also: manifest endpoint now excludes the -latest.xpi alias from version-detection (was tying mtime sort, yielding cosmetic · Firefox · v latest).

    Memory banked

    • reference-forgejo-auto-merge-pins-commitmerge_when_checks_succeed pins to PR head SHA, not whatever dev HEAD is at CI-pass. Don't push to dev while a PR is queued.
    • reference-image-record-sha256-fixture — sha256 column is varchar(64); fake fixtures use ljust(64,...)[:64].
    • reference-calver-today-date — CalVer tag is based on CUT-DAY's date; .N resets each day. Don't carry over .N from yesterday's tag.

    Schema changes

    None new this release. Migration 0020_library_audit_run was already in v26.05.25.6; FC-Cleanup just wires API + UI to consume it.

    Verification post-deploy

    1. Settings → Cleanup tab present between Import and Maintenance, four cards render
    2. Min-dimension Preview returns a count; Delete with token works
    3. Transparency audit Scan kicks off, polling shows scanned/matched counters tick up, Apply with token deletes
    4. Import worker no longer crashes on concurrent Source insertions (verify by triggering a deep scan of /import that touches a multi-file post)
    5. Click an error in System Activity → modal opens with full text + Copy button
    6. Settings → Overview → "Install Firefox extension" → Firefox shows the standard install prompt; card displays · Firefox · v1.0.3

    🤖 Generated with Claude Code

    Downloads
  • bvandeusen released this 2026-05-26 01:42:21 -04:00 | 623 commits to dev since this release

    What's new

    Worker no longer hangs on animated WebPs

    • _transparency_pct and compute_phash now short-circuit is_animated images. Previously PIL's getchannel("A") iterated every frame of a multi-frame WebP, exceeding Celery's 300s soft / 360s hard time limits → SIGKILL and stuck-in-processing rows. Operator-flagged 2026-05-26.

    Gallery-dl post-level sidecars are no longer invisible (BIG)

    • gallery-dl writes 01_HOLLOW-ICHIGO.png but the sidecar is HOLLOW-ICHIGO.json (post stem, no NN_ prefix). FC's find_sidecar only checked <media>.json and <media-stem>.json; it never stripped the prefix. Since FC-3 shipped, every gallery-dl post-level sidecar has been silently invisible. Now strips ^\d+_ from the media stem before lookup. After deploy: run a deep scan of /import against the GS download dir and your post table should finally populate from your existing 57k images.

    Browser extension XPI now actually bundles into :latest

    • build.yml's sign-extension step now uses browser_download_url (the canonical Forgejo asset binary endpoint) instead of /releases/assets/<id> (which returns JSON metadata).
    • curl -f added so HTTP errors fail the build instead of silently writing 404-text-as-XPI.
    • ZIP magic-byte check (PK) on the downloaded file as a defense-in-depth guard.
    • Extension version bumped to 1.0.3 to escape the AMO "version already exists" lock from the earlier partial-failure cycle.

    FC-Cleanup backend (partial — Tasks 1-6 of 17)

    • New migration 0020_library_audit_run — backs async transparency / single_color audits.
    • LibraryAuditRun model + registered in models package.
    • backend/app/services/audits/transparency.py + single_color.py — pure-function rule modules.
    • cleanup_service.py gains project_min_dimension_violations / delete_min_dimension_violations / start_audit_run / apply_audit_run / cancel_audit_run (+ 3 exception classes).
    • backend/app/tasks/library_audit.py::scan_library_for_rule — keyset-paginated PIL inspection task on maintenance queue, soft_time_limit=7200, time_limit=7500. Inherits FC-3i task_run lifecycle.

    This backend is currently dead code from a user perspective — no API or UI exposes it yet. Tasks 7-16 (API blueprint + Pinia store + 4 cards + SettingsView integration) ship in v26.05.25.7.

    Memory banked

    • reference-gallerydl-sidecar-layout — don't repeat the prefix-strip miss
    • reference-forgejo-actions-no-artifact-v4 — Forgejo doesn't support upload-artifact@v4; use Release Assets for cross-job handoff
    • reference-image-record-sha256-fixture — image_record.sha256 is varchar(64); fake fixtures must use ljust(64,...)[:64]
    • reference-path-suffix-sanitize — Path.suffix on URL-encoded basenames returns garbage; sanitize before bounded VARCHAR

    Schema changes

    • 0020_library_audit_run — new table, additive, no backfill needed

    What's still pending

    • FC-Cleanup frontend (Pinia store, 4 cards, CleanupView, SettingsView tab) → v26.05.25.7
    • The :latest extension XPI should land in this image — if it doesn't, see the diagnostic dump in build.yml's sign-extension step.

    🤖 Generated with Claude Code

    Downloads
  • bvandeusen released this 2026-05-25 23:21:12 -04:00 | 833 commits to main since this release

    Internal cache for the signed XPI consumed by build.yml's build-web job. Not a user-facing FC release.

    Downloads
  • bvandeusen released this 2026-05-25 23:01:04 -04:00 | 630 commits to dev since this release

    What's new

    Extension publish flow re-architected — :latest always carries the XPI

    • build.yml now has a sign-extension job that runs BEFORE build-web as a dependency. No more race with a separate extension.yml leaving :latest without the XPI for ~5min until a side-commit triggered a second build.
    • Signed XPIs cached as assets on ext-<version> Forgejo releases. Future builds hit the cache and skip AMO entirely unless the extension version bumps. AMO blocks re-signing the same version (returns 409), so this cache is load-bearing.
    • Extension bumped 1.0.1 → 1.0.2.
    • extension.yml shrinks to lint-only.
    • web-ext-config.cjs removed — web-ext v8 mis-parses .cjs configs as if module.exports were a config option (UsageError: The config option "module.exports" must be specified in camel case). Ignore-files moved inline to each package.json script with --no-config-discovery.

    Deep scan now does what users expect (IR-parity)

    • Re-applies sidecar metadata, fills NULL phash/artist on already-imported rows. Previously was a near-no-op that just chained backfill_phash.
    • New refreshed ImportResult status + ImportBatch.refreshed counter (migration 0019). UI banner shows refreshed N alongside imported/skipped.
    • scan_directory(mode=deep) skip-set excludes only in-flight statuses (completed/skipped paths get re-queued in deep mode). Quick scan keeps the original "any non-failed" semantics.
    • Misleading "Scan complete — no new files" toast replaced with honest workload-aware messaging.

    Archive imports no longer soft-timeout on large libraries

    • Operator hit SoftTimeLimitExceeded() because each archive member re-queried ALL ImageRecord.phash IS NOT NULL rows. Fixed by caching the candidates list on the Importer instance, appending on each imported/superseded outcome, invalidating on supersede/deep-rephash. Same one-cache-per-archive-import pattern IR uses.

    Artist DangerZone reachable in one click

    • ArtistView now has Overview / Settings tabs (sticky at top under TopNav). The cascade-delete UI moved from below the infinite-scroll image grid (unreachable without exhausting the whole gallery) into the Settings tab.

    Test changes

    • test_importer_deep.py: updated for the new "refreshed" status
    • test_phash_dedup.py::test_import_task_maps_superseded_to_complete_and_requeues: added "refreshed" assertion
    • test_scan_deep_requeue.py (new): pins the quick-vs-deep skip-set divergence

    Migration

    • 0019_import_batch_refreshed: adds refreshed: int NOT NULL DEFAULT 0 to import_batch. Backfills in place via server_default.

    🤖 Generated with Claude Code

    Downloads
  • bvandeusen released this 2026-05-25 21:26:53 -04:00 | 637 commits to dev since this release

    What's new

    Worker-blocking import bug fixed

    • gallery-dl produces filenames with URL-encoded basenames (...https___www.patreon.com_media-u_Z0F...); Path.suffix returned 50+ chars of base64-ish junk and blew the PostAttachment.ext varchar(32) column, crashing the import worker on every such file.
    • New _safe_ext() helper in backend/app/services/importer.py returns empty string for anything >16 chars or containing non-alphanumeric characters; bounded column stays as load-bearing safety net for future drift. Pinned by test_mangled_filename_extension_is_sanitized.
    • Memory reference_path_suffix_sanitize.md banked so future bounded-string columns populated from Path.suffix don't repeat the pattern.

    CI: integration suite now runs in 3 parallel shards

    • intapi (api tests) + intimp (importer/migration/phash/sidecar/scan) + intcore (everything else). Each shard gets its own pgvector + redis service containers. Newly feasible after act_runner capacity bump 2→6.
    • First push used int_api/int_imp/int_core names but act_runner strips underscores in container labels — switched to no-separator names + added a diagnostic docker ps -a dump so any future naming-convention shift surfaces in the log instead of a guess-and-push cycle.
    • Approximate split — rebalance once --durations=15 output reveals which shard is the long pole.

    UI: BrowserExtensionCard moved from Maintenance → Overview

    • Overview is the discovery surface for "things to set up"; Maintenance is housekeeping for already-set-up systems.

    Extension publish diagnostic in place

    • set -x + targeted echos on the commit step so the next run reveals why run #309 silent-failed to commit the XPI back to main.

    Known follow-up: extension XPI publish race (v26.05.25.5 candidate)

    • build.yml and extension.yml fire in parallel on the merge commit, so the docker image tagged :latest doesn't carry the XPI; if extension.yml does commit the XPI back, a second build.yml run overwrites :latest correctly within ~5 min.
    • AMO additionally now rejects re-signing the same extension version (Version 1.0.1 already exists), so subsequent sign attempts need a version bump.
    • Architectural fix coming: move extension sign into build.yml as a dependency of build-web so :latest is always built post-XPI. Cache strategy under discussion (see PR notes for v26.05.25.5).

    🤖 Generated with Claude Code

    Downloads