-
released this
2026-05-26 20:15:36 -04:00 | 573 commits to dev since this releaseFour commits. PR #27 → main, merge commit
4e82208, pinned head85b640fverified as second parent.Highlights
Extension CORS unblock (
c7001f4)/api/credentialsPOSTs from the Firefox extension hit a genericNetworkErroron first use because the backend had zero CORS handling — the browser's preflightOPTIONSrequest (triggered by theX-Extension-Keyheader) 405'd before the actual request could run. Two new app-level hooks:before_requestshort-circuitsOPTIONSfrommoz-extension:///chrome-extension://origins with 204after_requeststampsAccess-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>hadpy-6, pushing first content well below where TopNav's fade-out gradient lands. Switched topt-2 pb-6(8px top, 24px bottom unchanged). PlaceholderView keptpt-3 pb-8. - ArtistHeader's
top: 64pxleft a visible gap (TopNav is actually ~48px tall). Nowtop: 48pxso 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: droppedpull_request:trigger (push-on-dev already covered the same code).build.yml: droppeddevfrom 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.5image 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 connectionshould 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
:latestand:v26.05.26.5tags
Downloads
-
released this
2026-05-26 18:06:40 -04:00 | 578 commits to dev since this releaseThird hotfix for v26.05.26.1's alembic 0022 migration. PR #26 → main, merge commit
52fff00, pinned headf3e8f30verified.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
ImageProvenancerows 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 — theUPDATE image_provenance SET post_id = keep WHERE post_id = dropwould try to plant a second (image, keep) row, trippinguq_image_provenance_image_postfrom alembic 0021. The after-the-fact dedupe-DELETE never ran because the UPDATE fires UNIQUE row-by-row.Fix
Pre-DELETE
image_provenancerows under the drop Post whoseimage_record_idalready 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:
- Two Posts under same/different sources sharing
external_post_id, canonical has it → handled by v26.05.26.2's pre-merge case A - Two non-canonical Posts sharing
external_post_id, canonical doesn't → handled by v26.05.26.3's full-group pre-merge - 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
- Two Posts under same/different sources sharing
-
released this
2026-05-26 17:53:21 -04:00 | 581 commits to dev since this releaseSecond 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 head7a64730verified.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 fourCache pip wheelssteps 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 as1803a09; rides along here.Deploy
Pull
:latest, force-updateapp,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
-
released this
2026-05-26 16:51:11 -04:00 | 584 commits to dev since this releaseHotfix for v26.05.26.1. PR #24 → main. Pinned head SHA
0f7cd3cverified as second parent of8c36dd2.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_idSame gallery-dl post was ingested via two different sidecar paths in the operator's history, planting two Post rows with identical
external_post_idunder different per-post Sources. The original 0022 tried to handle this by merging Post collisions AFTER the bulkUPDATE 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 keepwithpost AS drop_on identicalexternal_post_idwherekeep.source_id = canonicalanddrop_.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: trueonCache 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-updateapp,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
- JOIN
-
released this
2026-05-26 16:41:34 -04:00 | 587 commits to dev since this releasePR #23 → main. Pinned head SHA
fb41b90; verified merge commit88cfb3dsecond parent matches.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. -
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 + 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, reparents Posts + ImageProvenance, merges 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. -
Copy button on plain HTTP —
navigator.clipboardis Secure-Context-gated. NewcopyText()helper falls back todocument.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-updateapp,worker,ml-worker,scheduler,web. Alembic auto-applies 0021 + 0022.Downloads
-
-
released this
2026-05-26 08:28:01 -04:00 | 608 commits to dev since this releaseFirst 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 fromImportSettings.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_tagsflow unchanged.
Backend: 9 endpoints under
/api/cleanup/*, newcleanup_service.start_audit_run / apply_audit_run / cancel_audit_run, newscan_library_for_ruleCelery task onmaintenancequeue (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 trippeduq_source_artist_platform_url, session poisoned, every subsequent op failed with "transaction has been rolled back". New_find_or_create_source+_find_or_create_posthelpers inImporteruse SQLAlchemy savepoints (begin_nested()); onIntegrityError, the savepoint rolls back (outer transaction preserved) and a re-select picks up the row the concurrent op committed.Error display flat-text + copyable
SystemActivityTabandImportTaskListpreviously used:title="..."to surface full SQLAlchemy traceback content as a browser-native tooltip — uncopyable, unscrollable, cramped. Replaced withErrorDetailModal: 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 atapplication/x-xpinstall. Also: manifest endpoint now excludes the-latest.xpialias from version-detection (was tying mtime sort, yielding cosmetic· Firefox · v latest).Memory banked
reference-forgejo-auto-merge-pins-commit—merge_when_checks_succeedpins 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 useljust(64,...)[:64].reference-calver-today-date— CalVer tag is based on CUT-DAY's date;.Nresets each day. Don't carry over.Nfrom yesterday's tag.
Schema changes
None new this release. Migration
0020_library_audit_runwas already in v26.05.25.6; FC-Cleanup just wires API + UI to consume it.Verification post-deploy
- Settings → Cleanup tab present between Import and Maintenance, four cards render
- Min-dimension Preview returns a count; Delete with token works
- Transparency audit Scan kicks off, polling shows scanned/matched counters tick up, Apply with token deletes
- Import worker no longer crashes on concurrent Source insertions (verify by triggering a deep scan of /import that touches a multi-file post)
- Click an error in System Activity → modal opens with full text + Copy button
- Settings → Overview → "Install Firefox extension" → Firefox shows the standard install prompt; card displays
· Firefox · v1.0.3
🤖 Generated with Claude Code
Downloads
- Minimum dimensions — instant SQL preview + Tier-C
-
released this
2026-05-26 01:42:21 -04:00 | 623 commits to dev since this releaseWhat's new
Worker no longer hangs on animated WebPs
_transparency_pctandcompute_phashnow short-circuitis_animatedimages. Previously PIL'sgetchannel("A")iterated every frame of a multi-frame WebP, exceeding Celery's 300s soft / 360s hard time limits →SIGKILLand stuck-in-processingrows. Operator-flagged 2026-05-26.
Gallery-dl post-level sidecars are no longer invisible (BIG)
- gallery-dl writes
01_HOLLOW-ICHIGO.pngbut the sidecar isHOLLOW-ICHIGO.json(post stem, noNN_prefix). FC'sfind_sidecaronly checked<media>.jsonand<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/importagainst the GS download dir and yourposttable should finally populate from your existing 57k images.
Browser extension XPI now actually bundles into
:latestbuild.yml's sign-extension step now usesbrowser_download_url(the canonical Forgejo asset binary endpoint) instead of/releases/assets/<id>(which returns JSON metadata).curl -fadded 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. LibraryAuditRunmodel + registered in models package.backend/app/services/audits/transparency.py+single_color.py— pure-function rule modules.cleanup_service.pygainsproject_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 onmaintenancequeue, 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 missreference-forgejo-actions-no-artifact-v4— Forgejo doesn't support upload-artifact@v4; use Release Assets for cross-job handoffreference-image-record-sha256-fixture— image_record.sha256 is varchar(64); fake fixtures must useljust(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
:latestextension XPI should land in this image — if it doesn't, see the diagnostic dump inbuild.yml's sign-extension step.
🤖 Generated with Claude Code
Downloads
-
released this
2026-05-25 23:21:12 -04:00 | 833 commits to main since this releaseInternal cache for the signed XPI consumed by build.yml's build-web job. Not a user-facing FC release.
Downloads
-
released this
2026-05-25 23:01:04 -04:00 | 630 commits to dev since this releaseWhat's new
Extension publish flow re-architected —
:latestalways carries the XPIbuild.ymlnow has asign-extensionjob that runs BEFOREbuild-webas a dependency. No more race with a separateextension.ymlleaving:latestwithout 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.ymlshrinks to lint-only.web-ext-config.cjsremoved — web-ext v8 mis-parses.cjsconfigs as ifmodule.exportswere a config option (UsageError: The config option "module.exports" must be specified in camel case). Ignore-files moved inline to eachpackage.jsonscript 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
refreshedImportResult status +ImportBatch.refreshedcounter (migration 0019). UI banner showsrefreshed Nalongside 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 ALLImageRecord.phash IS NOT NULLrows. 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
ArtistViewnow 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"statustest_phash_dedup.py::test_import_task_maps_superseded_to_complete_and_requeues: added"refreshed"assertiontest_scan_deep_requeue.py(new): pins the quick-vs-deep skip-set divergence
Migration
0019_import_batch_refreshed: addsrefreshed: int NOT NULL DEFAULT 0toimport_batch. Backfills in place viaserver_default.
🤖 Generated with Claude Code
Downloads
-
released this
2026-05-25 21:26:53 -04:00 | 637 commits to dev since this releaseWhat's new
Worker-blocking import bug fixed
- gallery-dl produces filenames with URL-encoded basenames (
...https___www.patreon.com_media-u_Z0F...);Path.suffixreturned 50+ chars of base64-ish junk and blew thePostAttachment.ext varchar(32)column, crashing the import worker on every such file. - New
_safe_ext()helper inbackend/app/services/importer.pyreturns empty string for anything >16 chars or containing non-alphanumeric characters; bounded column stays as load-bearing safety net for future drift. Pinned bytest_mangled_filename_extension_is_sanitized. - Memory
reference_path_suffix_sanitize.mdbanked so future bounded-string columns populated fromPath.suffixdon'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_corenames but act_runner strips underscores in container labels — switched to no-separator names + added a diagnosticdocker ps -adump so any future naming-convention shift surfaces in the log instead of a guess-and-push cycle. - Approximate split — rebalance once
--durations=15output 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.ymlandextension.ymlfire in parallel on the merge commit, so the docker image tagged:latestdoesn't carry the XPI; if extension.yml does commit the XPI back, a second build.yml run overwrites:latestcorrectly 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.ymlas a dependency ofbuild-webso:latestis always built post-XPI. Cache strategy under discussion (see PR notes for v26.05.25.5).
🤖 Generated with Claude Code
Downloads
- gallery-dl produces filenames with URL-encoded basenames (