# FabledCurator Self-hosted media curation — gallery, ML tagging, and subscription-driven downloading in one app. Part of the FabledSword family. Combines what was [ImageRepo](https://git.fabledsword.com/bvandeusen/ImageRepo) (gallery, ML, importer) and [GallerySubscriber](https://git.fabledsword.com/bvandeusen/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. ## Versions and tags Three image tags exist, and no others: | Tag | Branch | Meaning | | --- | --- | --- | | `:latest` | `main` | Production. Moves on every merge. | | `:c-` | `main` | Immutable — the rollback unit, all three images together. | | `:dev` | `dev` | The rolling test channel. Moves on every push. | There are deliberately **no version tags**. Nothing pins one, and a per-build name nobody reads is upkeep for a model FC does not run (family rule 145; the reasoning is note #3127 §5). Rolling back is `docker pull …:c-`. Each artifact still has a version, derived rather than chosen: the commit time of the newest change to that artifact's *own* shipped files, as `YYYY.MM.DD.HHMM` UTC (rule 148). Four artifacts, four independent versions — a push touching only `agent/` re-versions the agent and leaves web and ml alone, and CI skips the builds whose content did not move. Because no registry name carries it, the running instance's own report is the only answer to "which build is this?". The foot of Settings shows `FabledCurator 2026.08.29.0201 · dev`, and `/api/health` returns the same two fields. Release tags are optional bookmarks — FC went twelve weeks without one and nothing was wrong. Pushing `v` publishes a Forgejo release listing the commits since the previous tag; it builds no image. ## 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: ```bash 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: ```bash docker compose -f docker-compose.yml up -d # (skips the dev override, so containers pull published :latest images) ``` `-f` is doing real work there: it tells Compose to use *only* that file, which skips `docker-compose.override.yml` and its local builds. What you get is the `:latest` images — the stable channel, built from `main`. This is the install path, and it is the one to use if you are running FabledCurator rather than working on it. `:dev` is the other channel: rebuilt from the `dev` branch several times a day, bleeding edge, no stability promise. Nothing in this repo points an installer at it, and nothing should. 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 Four workflows: `ci.yml` (lint, extension-version check, backend unit tests, frontend build, integration), `extension.yml` (extension lint, vitest, XPI content verification), `build.yml` (sign + publish), and `release.yml`, which runs only on a `v*` tag and publishes a changelog without building anything. **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-` 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 **both** channels and is cached per version: because the version is derived from commit time, `dev` and `main` derive the same number for the same source, so `main` finds `dev`'s signature already cached and makes no second AMO call. That cache is why signing must be one-shot — AMO rejects a re-signed version. ## License **GNU Affero General Public License v3.0** — see [LICENSE](LICENSE). You may run, study, modify and redistribute this software. The condition is reciprocity: if you distribute a modified version, or **run one as a network service that other people use**, you must offer those users the corresponding source under the same licence. That second clause (AGPL §13) is the reason this licence rather than the GPL — for a self-hosted web application, "distribution" otherwise never happens, and the obligation would never bite. Running an unmodified copy for yourself, your household or your organisation carries no obligation at all. Neither does modifying it privately. The licence asks something of you only when you hand your modified version to others. Contributions ship under the same licence — see [CONTRIBUTING](CONTRIBUTING.md). Security reports: [SECURITY.md](SECURITY.md).