bvandeusen 0c43fa3eb2
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 21s
extension / lint (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m44s
ci: shadow the per-artifact derived versions (milestone 313 step 2)
Every build job now logs the tag, version and revision its artifact would
get. Nothing reads them; no `set -e`, and each derivation falls back to
UNAVAILABLE, so a broken script cannot fail a build. Same discipline as
milestone 271 step 2, which is what made that cutover safe to do in one
commit.

Also fixes a landmine the plan named but had not checked: build-ml and
build-agent were checking out at depth 1. Both now use fetch-depth: 0.

That mattered more than it looks. A depth-1 clone sees one commit, so
`git log HEAD -- <shipped paths>` either returns that commit's timestamp —
plausible, and wrong — or returns nothing. For build-ml on this push it
would have returned today's date, because HEAD touches backend/, and
nothing downstream would have questioned it. For build-agent it would have
returned nothing at all, since no single commit here touches agent/, and
artifacts.sh exits non-zero rather than guessing. One direction is silent
and one is loud; only the loud one was ever going to get noticed.

What to read from the shadow lines over the next few pushes, in order of
how badly each would bite:

  * a push touching the extension must move BOTH the extension and web,
    because build-web bakes the XPI in. If web does not move, its path set
    is too narrow and a pinned web image will serve an extension it does
    not name.
  * a push touching only agent/ must leave web and ml still. If they move,
    their sets are too wide and they will rebuild for changes they do not
    ship.
  * a docs-only push must move nothing.
  * dev and main must derive the same values for the same source.

Step 3 only lets these values name a tag once those hold.
2026-08-27 21:41:14 -04:00

FabledCurator

Self-hosted media curation — gallery, ML tagging, and subscription-driven downloading in one app. Part of the FabledSword family.

Combines what was ImageRepo (gallery, ML, importer) and GallerySubscriber (gallery-dl wrapper, subscriptions, credential capture) into a single product.

Status

In production. main is continuously deployed — every merge to main builds and publishes :latest images, so whatever is on main is what is running. Day-to-day work happens on dev, which publishes :dev images.

What's in here

Five deployable pieces, built by .forgejo/workflows/build.yml:

Piece Built from Image Role
Web / workers Dockerfile fabledcurator Quart API + the built Vue SPA in one image. entrypoint.sh picks the role: web, worker, scheduler. The maintenance-long service is a second worker pinned to the long-running maintenance queue.
ML worker Dockerfile.ml fabledcurator-ml Same app, plus requirements-ml.txt — tagging and embedding models that run in-container.
GPU agent agent/Dockerfile fabledcurator-agent Optional desktop-GPU worker (agent/). Leases jobs over HTTP only — never touches the database or Redis. Run it for a burst, stop it to reclaim the card. See agent/README.md.
Firefox extension extension/ signed XPI MV3 extension: pushes platform session cookies into FC and adds a creator as a Source in one click. AMO-signed on both dev and main (one signature per extension change, shared by the two channels), bundled into that channel's web image and served from Settings → Maintenance. See extension/README.md.
Data pgvector/pgvector:pg16, redis:7-alpine Postgres with pgvector for embeddings; Redis as the Celery broker.

Quick start

For local development and testing, just:

docker compose up -d
# UI: http://localhost:8080

That uses sane dev defaults baked into docker-compose.yml and the dev override (docker-compose.override.yml, auto-merged) — local builds, DEBUG logging, exposed Postgres + Redis ports on the host. No .env required.

For a production-like deployment, override the dev defaults via shell env or a .env file (see .env.example for the variable names) and use:

docker compose -f docker-compose.yml up -d
# (skips the override so containers pull registry images)

The GPU agent is deployed separately, on the machine with the card — agent/docker-compose.yml, not this stack.

Deployment posture

FabledCurator is designed to run inside a self-hosted homelab environment over plain HTTP. If you want TLS, terminate it at your reverse proxy. The app does not generate certificates, redirect to HTTPS, or set HSTS.

CI / Forgejo setup

Three workflows: ci.yml (lint, extension-version check, backend unit tests, frontend build, integration), extension.yml (extension lint, vitest, XPI content verification), and build.yml (sign + publish).

The toolchain each job runs in is its container.image, not its runs-on label. runs-on: python-ci only schedules the job onto a runner; every job then names the image it actually wants. ci-requirements.md is the current, authoritative list of images and per-job installs — read that rather than a copy here, so the two can't drift.

The repo expects one secret:

  • RELEASE_TOKEN — a Forgejo PAT with:

    • write:package + read:package — for docker push to git.fabledsword.com
    • write:release — for the ext-<version> releases that cache the signed XPI
    • write:issue — for issue-management automation

    Generate at https://git.fabledsword.com/user/settings/applications. The injected GITHUB_TOKEN cannot be used because it lacks write:package.

AMO signing additionally needs MOZILLA_AMO_JWT_KEY / MOZILLA_AMO_JWT_SECRET; it runs on main only and is cached per version, since AMO rejects a re-signed version.

License

Personal project; use at your own discretion.

S
Description
Self-hosted media curation — gallery, ML tagging, and subscription-driven downloads. Part of the FabledSword family. (Merge of ImageRepo + GallerySubscriber.)
Readme AGPL-3.0
8.6 MiB
2026-08-29 13:46:09 -04:00
Languages
Python 73.1%
Vue 17.8%
JavaScript 8%
Shell 0.5%
CSS 0.3%
Other 0.2%