- Dart 50.9%
- C# 47.7%
- Shell 0.7%
- Python 0.3%
- CMake 0.2%
- Other 0.1%
|
Some checks failed
CI / build (push) Successful in 30s
Deploy Smoke Test / smoke (push) Failing after 3m2s
Build and Push Docker Image / test (push) Successful in 2m31s
Flutter CI / analyze-and-test (push) Failing after 24m7s
CI / test (push) Successful in 2m43s
Build and Push Docker Image / build-and-push (push) Successful in 1m8s
## What's in The owner paged through the library with the overlay on — the deliverable phase 1 existed to produce — and reported the shape of the error: **few false positives, many missing detections.** Measured against that, and two things came out of it. ### The floor was leaving text behind 120 random pages of the real library (seed 23, the same draw as ADR 0027's tables), one inference pass each at a 0.02 floor so every candidate threshold is counted from the same detections: | Floor | Text | Balloon | Pages carrying text | |---|---:|---:|---:| | 0.25 (was) | 171 | 239 | 43 | | **0.15 (is)** | **206** | **257** | **43** | | 0.10 | 251 | 268 | 43 | | 0.05 | 324 | 289 | 44 | **The useful column is the last one.** Lowering the floor almost never finds a new *page* — 43 of 120 carry text at 0.25 and still 43 at 0.07. What it finds is more text *on pages already known to have some*: sound effects, margin notes, narration over art. Exactly the class a balloon outline does not contain, and the one phase 3 needs most. What the old floor was dropping, by band: | Band | Text | Balloon | |---|---:|---:| | 0.20–0.25 | 15 | 9 | | 0.15–0.20 | 20 | 9 | | 0.10–0.15 | 45 | 11 | | **0.15–0.25 — what moving to 0.15 recovers** | **35** | **18** | | 0.10–0.25 — what moving to 0.10 would | 80 | 29 | 0.15 rather than 0.10 is the owner's call — the conservative step, taken with the numbers in view. Below 0.05 a long tail appears (151 further text detections between 0.02 and 0.05) that looks like noise. ### The change would have done nothing This is the part worth reading. The detector version was the model file's fingerprint alone: ``` Version = FingerprintOf(options.ModelPath); ``` So lowering the floor and restarting would have changed **nothing**: every image still carried an analysis at the unchanged version, the sweep correctly concluded it had finished, and the new floor never ran. No error, no log line, no regions. The same was true of `DOUJIN_MANAGER_DETECTION_IMAGE_SIZE`. The regions on disk are a function of the model **and** the settings that shape what comes out of it, so the version now says so: ``` f3bf7701aebb0185@1024/0.15 ``` Composite and readable rather than a second hash over the three, so a row in `image_analyses` and a line in the log say *why* a re-sweep happened. Formatted `:R` — round-trippable — because a rounded format puts the same defect back one edge out: `0.123456` and `0.123457` both render as `"0.1235"`, and two different floors sharing a version is exactly the silence this fixes. **`DOUJIN_MANAGER_DETECTION_THREADS` is deliberately excluded.** It decides how fast a page is examined, never what is found on it, and folding it in would re-sweep twelve thousand images because an operator gave the sweep another core. ## Verification `scripts/verify.sh` green (`== verify OK (all) ==`) — backend 1,526 passed / 5 skipped, app 1,562, analyze clean. New directional tests, all reachable **without a model on disk** — which matters, see below: | Test | Pins | |---|---| | `A_Different_Confidence_Floor_Is_A_Different_Version` | the silent gate | | `A_Different_Input_Size_Is_A_Different_Version` | its twin | | `Two_Floors_That_Agree_To_Four_Places_Are_Still_Two_Versions` | the format collision | | `A_Floor_Near_One_Is_Not_Rendered_As_One` | `0.99996` rendering as a value the config refuses | | `A_Different_Thread_Count_Is_The_Same_Version` | the exclusion — without it, folding everything in looks like an improvement | | `A_Different_Model_Is_A_Different_Version` | ADR 0027's original promise | | `A_Built_Detector_Reports_The_Model_And_The_Settings_As_Its_Version` | the wiring the whole PR turns on — on a **constructed** detector, see below | | `The_Settings_That_Shape_A_Detection_Default_To_What_Is_Documented` | the floor itself: that the default *is* 0.15 | | `The_Version_Says_Which_Settings_Produced_It` / `..._Fits_The_Column_It_Is_Stored_In` | the format and `HasMaxLength(100)` | **Mutation-probed: 9 mutations, all killed — re-run with `DOUJIN_MANAGER_DETECTOR_MODEL` unset, the way CI runs.** > **Correcting this PR's verification claims, twice.** > > It first read "6 mutations, all killed", true only on a machine with the > weights present: the constructor's wiring was pinned solely by a > `RealModelFact`, skipped everywhere CI runs. Caught in review. > > The fix for that — "7 mutations" — was still wrong. The replacement test > compared `VersionOf` to `VersionFor` and **never constructed a detector**, so > reverting `OnnxRegionDetector.cs:102` to the pre-PR `FingerprintOf(path)`, > which is the silent-gate bug itself, stayed green. Also caught in review. It > now builds one from the synthetic model #195 committed for exactly this, which > loads in CI with no weights in the tree. > > Probing *that* with the variable unset turned up a hole neither review named: > **the default floor.** Every test set `ConfidenceThreshold` explicitly, so > putting `0.25` back — the entire subject of this PR — changed no test at all. > That is the ninth mutation, and it is now pinned. ## Consequence: the first start re-sweeps About an hour for 12,468 pages at the measured 309 ms each. That is the mechanism ADR 0027 designed for a model swap, doing its job for the first time. The Detection section shows it working through the library, and the old regions are replaced per image as it goes — nothing is deleted up front. Recorded as ADR 0027 **Amendment 2**, with the measurement tables and the silent-gate story, alongside `matikane`'s Amendment 1. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: schattenan <schattenan@kagaku.eu> Reviewed-on: #196 |
||
|---|---|---|
| .github | ||
| app | ||
| backend | ||
| deploy | ||
| docs | ||
| scripts | ||
| vendor | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .gitmodules | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| README.md | ||
Doujin Manager
A self-hosted doujinshi collection manager with a .NET backend and a Flutter desktop app.
The backend owns canonical storage, metadata, search, and image handling. It exposes a resource-oriented REST API with lightweight hypermedia, backed by SQLite and UUID-based image storage. The Flutter app is the rich desktop client for browsing, managing, and editing the collection.
Current status
The backend server is complete and deployed. It provides a fully functional REST API covering doujin management, language variants, page/chapter organization, image upload (individual files and ZIPs), structured search, metadata entities (tags, people, circles, characters, series), and database backups.
The Flutter desktop app covers the MVP scope and more: library with filtered search, sorting and autocomplete, doujin detail view with variant tabs and user rating, metadata entity management for all five types, a full Editor for doujins (metadata, associations, variants, chapters, uploads, page grid), an immersive reader with reading progress, an offline library for downloaded variants, and an in-app AI assistant.
What works — backend
- Full CRUD for doujins, titles, variants, chapters, pages, and all five metadata entity types
- Doujin associations: tags, people (with role), circles, characters, series
- Image upload via individual files or ZIP archives, optionally targeted at a chapter
- Page management: reorder (variant-global), move between chapters, delete, set as cover
- Automatic thumbnail generation (SkiaSharp)
- UUID-based sharded image storage on the filesystem
- Structured boolean search (include/exclude tags, language OR, person, circle, character, series, rating)
- SHA-256 content dedup on upload; duplicate listings per image hash and per doujin set (
/api/doujins/duplicates) - SQLite persistence via EF Core with auto-migrating startup
- Static bearer token authentication
- REST API with hypermedia response envelope (
data,links,actions) - OpenAPI spec at
/openapi/v1.jsonwith Scalar API reference UI at/scalar - Database backups via API: a
VACUUM INTOsnapshot the server opens and verifies before listing it (POST /api/backups), downloads (GET /api/backups/{fileName}) and accepts back from another instance (POST /api/backups/upload, checked before it is accepted) — database only, no images. Restore is two steps the server performs itself:POST /api/backups/{fileName}/restoremarks a backup, and the next start swaps it in before the database is opened, moving the old one aside. A backup from before an upgrade (an older schema, verdictneedsMigration) is restored and migrated up on that same start, with the applied migrations in the log; a backup from a newer version is refused (deploy/DEPLOYMENT.md §6.6) - Optional daily backup taken by the server itself
(
DOUJIN_MANAGER_BACKUP_SCHEDULE=03:30), verified like any other and rotated down toDOUJIN_MANAGER_BACKUP_KEEP;GET /api/backups/schedulereports when the next one is due and what the last one did (deploy/DEPLOYMENT.md §6.7) - Library integrity via API:
GET /api/integrityruns the database-only invariant checks,GET /api/integrity/image-sweepwalks the image tree a bounded run of storage shards per request to find the two drifts the database cannot see (an image row whose file is gone, and a file no row names — both reported for information only), andPOST /api/integrity/findings/{kind}/{subjectId}/repairputs right the findings with one correct outcome (a drifted page count, a missing or duplicated default variant, a cover that dangles or is unowned), re-running the check first and answering 409 when it no longer reproduces - Docker deployment with health checks
- CI/CD pipeline: build, test, publish to Forgejo container registry
What works — Flutter app
- Library page with smart filter bar: token-based filtering with autocomplete over names and aliases of all five metadata types, a
kind:variant-kind token (kind:translated— one kind at a time, as the server takes one), plus rating - Doujin detail view: variant tabs with page thumbnails (collapsible chapters), editable user rating, associations as chips that open the library filtered to that tag, person, circle, character or series, cover
- Immersive reader (the only rail-less screen): three reading modes on black — a single full-res page, a two-page spread (the earlier page on the side the reading direction starts from; a pair never crosses a chapter boundary), or continuous vertical scroll (each page whose stored dimensions are known reserves the box they give it, so it holds its place while it loads and nothing below it shifts when the bitmap arrives) — with pages fitted to the window (to half of it in a spread, to its width in the strip), reading-direction-aware keyboard/mouse/click navigation, pan/zoom (Ctrl, or ⌘ on macOS, with the wheel zooms; the wheel alone turns pages, or scrolls the strip), auto-hiding overlay with chapter jump, page slider, and rating; page numbers, deep links (
?page=N) and reading progress are the same in every mode, and adjacent pages are precached. A toggle in the overlay outlines the regions a detector found on the page (ADR 0027), colour-coded by kind and off by default — and it says which of three things is true rather than leaving an empty page ambiguous: nothing has looked at this page, something looked and there is nothing on it, or it could not be read - Reading progress per variant (client-side, ADR 0023): "Continue reading" on the detail page, read/in-progress badges in the library
- Library sort options (created, updated, rating, title; either direction) and an unrated filter; manual refresh and stale-result auto-refresh
- Offline library: download a variant for offline reading (quality follows the reader's image-size setting); Downloads/Library tabs on the Offline page; a downloaded variant always reads from disk
- Metadata management page: master-detail editors for tags, people, circles, characters, and series (name, description, aliases)
- Editor page (master-detail): searchable doujin list, tabbed editing per doujin; open the detail page or the reader from the header
- Metadata tab: titles (add/edit/remove with language + kind), description, reading direction, cover preview, Ctrl+S save
- Associations tab: attach/detach all five types with alias-aware autocomplete; people carry a role
- Variants & Pages tab: variant CRUD with default flag, chapter chips (rename, delete, reorder chapters), upload panel (multi-image picker, folder picker with subfolders→chapters mapping, ZIP), sequential upload queue with progress/retry, and a virtualized page grid with collapsible chapter sections, multi-select, drag reorder, context menu (new chapter from the selection, move to chapter, set as cover, delete)
- AI assistant (docked side panel, ADR 0024): chat agent over the user's own OpenRouter account that can search and list the library (newest first, by title or rating), read doujins including their chapters (pages numbered in the reader's order), navigate the app (including the reader and the offline library), research on the web (search → fetch → budgeted headless browser), look at covers/pages (vision or blind-mode captioning), edit metadata and covers, and edit structure (create, rename, delete and reorder chapters, move pages between them, delete and reorder pages, add, edit and delete variants; uploading pages stays in the Editor, which is where the files are picked) — every database write requires inline user approval, with a whole set of associations (tags, creators, characters, series) approved on one card. A run in progress can be stopped (Stop replaces Send in the panel): the model step already in flight still finishes, then no further tool runs, any pending approval is refused, and the conversation stays usable for the next message. Recurring workflows are packaged as loadable skills (built-in + user markdown files under
~/.config/doujinmanager/skills/) - Possible-duplicates page (Library toolbar → Find duplicates): sets of doujins whose pages share identical images, one card per set with a preview of a shared image and a numbered chip per member (oldest first) that opens its detail, plus a per-member delete (after a confirmation) that removes that doujin with all its variants and pages from the server and refreshes the list. Merge metadata on a card copies the other members' tags, people, circles, characters, series and titles onto the member you pick as the survivor — without opening each doujin — and reports what was added and what was dropped (a person whose role differs keeps the survivor's, and the discarded role is named). It copies only: the other members stay until you delete them with the same per-member delete
- Settings page (desktop settings shell): a category sidebar separates this client's preferences — server connection, AI assistant, reader (image-size budget and reading mode: single page, two-page spread, or continuous scroll), display (preferred title language: when a work has a title in that language, the library, detail, editor and reader show it — and new offline downloads capture it — otherwise the original title as before) — from what the configured server holds — statistics, library health, backups, and detection (whether the region sweep is off, idle or working, which model is loaded, and how many images it has examined out of how many; the count includes the pages nothing was found on, because most of a doujin library is illustration and a count of pages with regions would show a finished sweep as a broken one). One action bar under every section carries Test Connection (which, when the server answers but cannot store data, names the volume that refused and why) and a Save that stays disabled until a field actually differs from what is stored, and the sidebar marks the sections holding unsaved edits
- Settings server sections (once a server is configured): statistics (version, row counts, storage usage with a total), library health (the server's integrity report, grouped by check, each expandable into the findings behind it, with a Fix on exactly the findings the server advertises as repairable — the rest say they are reported for information only; a repair refreshes the report, and one the server re-checks and finds already put right reads as "Already fixed" rather than as an error — plus Scan image files, which walks the server's image tree shard by shard with its own progress and folds what it finds, read-only, into the same list of failed checks without holding up the report beside it), and database backups
- Settings → Backups: every backup carries the server's verdict on it (Verified — the server opened the file and it loaded; Not checked; Will not load), with what was checked in its tooltip, so a file that is merely present is never mistaken for one that would restore, and Check now promotes Not checked to a verdict without scheduling anything (a Will not load file offers Check again instead of restore). Create (which says it can take a while, because the server copies the database and then reads the copy back), download to this machine, upload a database file, delete, and restore. Restore replaces the whole live library: it needs the word
restoretyped into a dialog that states what is replaced, compares the library's current doujin count with the backup's, and says that images and thumbnails are not part of a backup. It takes effect only when the server is restarted, and until then a card on the page says so and offers to cancel it. Above the list, the section shows the server's own backup schedule — daily time in the server's time zone, retention, next run, and what the last scheduled run did — with a failed run as a red banner saying the nightly backup is not working (read-only: the schedule is server configuration)
Architecture
Clean Architecture / Ports and Adapters with strict dependency direction:
DoujinManager.Server Composition root, ASP.NET host, Scalar UI
DoujinManager.RestAdapter Primary adapter: REST endpoints, DTOs, hypermedia mapping
DoujinManager.ApplicationCore Domain model, use cases, ports/interfaces, business rules
DoujinManager.Infrastructure EF Core SQLite, filesystem storage, SkiaSharp, ZIP, search
Dependency rule: ApplicationCore depends on nothing. RestAdapter and Infrastructure depend on ApplicationCore. Server depends on all three. The REST adapter may only call use cases — never DbContext, repositories, or filesystem directly.
See Architecture Overview for the full design.
Domain model
Doujin logical work — titles, associations, rating, reading direction, cover, variants
Variant readable edition/language/version — mandatory language code, pages, optional chapters
Chapter ordered grouping inside a variant
Page stable logical page — belongs to a variant (optionally a chapter), points to an ImageFile
ImageFile physical stored image — UUID-based sharded path, hash/dimensions/media type
Tag metadata entity — name, description, aliases
Person metadata entity — display name, description, aliases; role lives on the doujin link
Circle metadata entity — display name, description, aliases
Character metadata entity — display name, description, aliases
Series metadata entity — display name, description, aliases
All five metadata types are first-class categories with the same harmonized shape (ADR 0022) and are usable in search and library filtering.
Key rules: a doujin requires at least one title. Variant language is mandatory (none and unknown are valid). Page order lives in the database, not filenames. Replacing a page image changes the referenced image file, not the page ID. The doujin model stores work data only — client/user preferences stay client-side (ADR 0023).
Technology
| Concern | Choice |
|---|---|
| Backend | .NET 10 / modern C# |
| Persistence | SQLite via EF Core |
| Image processing | SkiaSharp |
| API docs | OpenAPI + Scalar |
| Auth | Static bearer token |
| IDs | UUID (strongly typed value objects) |
| Desktop app | Flutter (Redux, dio, freezed, go_router) |
| Deployment | Docker (Synology NAS / Portainer) |
| CI/CD | Forgejo Actions |
Repository structure
backend/
src/
DoujinManager.Server/ ASP.NET host, composition root
DoujinManager.RestAdapter/ REST endpoints, DTOs, middleware
DoujinManager.ApplicationCore/ Domain entities, use cases, ports
DoujinManager.Infrastructure/ EF Core, filesystem, SkiaSharp, ZIP
tests/
DoujinManager.ApplicationCore.Tests/
DoujinManager.RestAdapter.Tests/
DoujinManager.Infrastructure.Tests/
DoujinManager.IntegrationTests/
app/
lib/
domain/ Repository ports, entities (ADR 0018)
data/ dio API client, freezed models, repo impls, offline store
presentation/ Redux state, epics, feature-first pages, layout
agent/ AI assistant: tools, skills, system prompt (ADR 0024)
app/ Composition root: DI, store, router
core/ Shared helpers: constants, theme, URLs, titles
test/ Tests + shared stubs in test/support/
deploy/
Dockerfile, docker-compose.yml, DEPLOYMENT.md
docs/
ARCHITECTURE.md, DEVELOPMENT.md, BACKLOG.md, PROJECT_PLAN.md, adr/
scripts/
verify.sh The CI checks, runnable locally
AGENTS.md Working agreements (CLAUDE.md points here)
API overview
All endpoints under /api require Authorization: Bearer <token>. GET /health, GET /version, the Scalar UI at /scalar, and /openapi/v1.json are anonymous. GET /health is a readiness probe: 200 only when all four data directories accept writes and the db directory can host a SQLite database, 503 naming the one that cannot and why (ADR 0010, deploy/DEPLOYMENT.md §7).
| Resource | Endpoints |
|---|---|
| Doujins | GET/POST /api/doujins, GET/PUT/DELETE /api/doujins/{id}, GET /api/doujins/duplicates (sets of doujins sharing identical page images), POST /api/doujins/{id}/merge (copies the listed sources' metadata onto this doujin and answers it with a report of what was added and what was dropped; the sources are left intact, and no variant or page moves) |
| Titles | POST /api/doujins/{id}/titles, PUT/DELETE /api/doujins/{id}/titles/{titleId} |
| Associations | POST/DELETE /api/doujins/{id}/tags, .../people (role-aware), .../circles, .../characters, .../series |
| Variants | GET/POST /api/doujins/{id}/variants, GET/PUT/DELETE /api/variants/{id} |
| Chapters | GET/POST /api/variants/{id}/chapters, PUT /api/variants/{id}/chapters/reorder (listed chapters first in that order, the rest keep their relative order), PUT/DELETE /api/variants/{id}/chapters/{chapterId} |
| Pages | GET/POST /api/variants/{id}/pages, POST /api/variants/{id}/pages/zip (?chapterId= optional), PUT /api/variants/{id}/pages/reorder, PUT /api/pages/{id}/chapter, DELETE /api/pages/{id} |
| Images | GET /api/images/{id}, GET /api/images/{id}/compressed?maxKb=, GET /api/thumbnails/{id}, GET /api/images?sha256= (content lookup: page references plus coverOf, the doujins using it only as their cover), GET /api/images/duplicates (per-hash cross-doujin duplicates) |
| Metadata | GET/POST + GET/PUT/DELETE /{id} + alias endpoints for /api/tags, /api/people, /api/circles, /api/characters, /api/series; creating or renaming an entry onto a name one of them already carries is a 409. POST /api/{people|circles|characters|series}/{id}/merge joins duplicate entries: the one in the route survives and takes over every doujin link (one link per doujin; for a person under two roles the survivor's stands and the dropped one is reported) plus the sources' names as aliases, and the sources are deleted |
| Search | POST /api/doujins/search (structured include/exclude query); titleLanguage — like the ?titleLanguage= on GET /api/doujins, a per-request BCP-47 hint (never stored) that makes sortBy=title order by each doujin's title in that language, falling back to its primary title where it has none |
| Backups | GET/POST /api/backups, GET/DELETE /api/backups/{fileName} (download / delete), POST /api/backups/upload (multipart, verified before it is accepted), POST /api/backups/{fileName}/verify (check a backup already on the volume and record the verdict; changes nothing else), POST /api/backups/{fileName}/restore (202, applied on the next start), GET/DELETE /api/backups/restore-request, GET /api/backups/schedule (the daily schedule, when it next fires, and what the last run did) — the database only (images and thumbnails are not in a backup); each entry carries a verification verdict (verified/unverified/failed, or needsMigration for a backup at an older schema — restorable, and migrated up by the restart that restores it) |
| Stats | GET /api/stats (server version, doujin/variant/page/image counts, image/thumbnail/database bytes) |
| Integrity | GET /api/integrity (report: every database-only check the server knows, with an exact count and the first few findings per check; no filesystem walk), GET /api/integrity/image-sweep (the checks that do read the tree — an image row whose file is gone, and a file in the image or thumbnail tree no row names — walked a bounded run of storage shards per request and resumed with the cursor the previous slice handed back; both are reported for information only), POST /api/integrity/findings/{kind}/{subjectId}/repair (repairs one finding, re-running that check for that row first: 409 when it no longer reproduces, 400 for a check reported for information only — the findings the server can repair say so with a repair action of their own) |
| Detection | GET /api/detection (region detection: whether the sweep is off, idle or working, which detector and version are loaded — null when the deployment has none — and how many images are analysed out of how many), GET /api/images/{id}/regions (what was found on one image: state is notAnalysed, analysed or failed, and reading the region list without the state cannot tell "nothing has looked at this page" from "there is nothing on this page"; coordinates are fractions of the image, never pixels) — off by default, see ADR 0027 |
Responses use a hypermedia envelope:
{
"data": {},
"links": { "self": "/api/doujins/abc-123" },
"actions": { "delete": { "method": "DELETE", "href": "/api/doujins/abc-123" } }
}
Collections include pagination:
{
"data": [],
"page": { "page": 1, "pageSize": 50, "totalItems": 123, "totalPages": 3 },
"links": {}, "actions": {}
}
Interactive API docs are available at /scalar when the server is running. The
document behind them is committed at backend/openapi/v1.json
and regenerated by a test, so a change to the wire is a diff in that file
(see docs/DEVELOPMENT.md).
Building and running locally
Prerequisites: .NET 10 SDK, Flutter 3.47 stable, and the vendored submodule
(git clone --recursive, or git submodule update --init in an existing clone).
cd backend && dotnet run --project src/DoujinManager.Server # http://localhost:5132, token dev-local-token
cd app && flutter pub get && dart run build_runner build && flutter run -d linux # or windows / macos
scripts/verify.sh # what CI runs: build, analyze, all tests
dotnet run uses the launch profile in launchSettings.json: port 5132, the
dev token, and a data/ directory under the Server project (the container
listens on 8080 and reads the DOUJIN_MANAGER_* variables instead). The full
recipe, EF migrations, test-harness conventions and the checklists for adding
an endpoint or an app page are in docs/DEVELOPMENT.md;
the working agreements for humans and coding agents are in AGENTS.md.
On first launch, configure the server URL and bearer token on the Settings page.
Optional app environment variables:
| Variable | Description | Default |
|---|---|---|
DOUJIN_MANAGER_AGENT_MAX_TOOL_ROUNDS |
Tool-round cap per AI-assistant turn (runaway guard) | 64 |
DOUJIN_MANAGER_AGENT_HISTORY_BUDGET_CHARS |
Character budget for the assistant's replayed conversation history; past it, old tool results are trimmed to placeholders and then whole oldest turns are dropped (never the newest one) | 200000 |
DOUJIN_MANAGER_AGENT_IMAGE_BUDGET_BYTES |
Byte budget (base64-encoded, as sent) for the images replayed with that history; past it, the oldest images are detached and replaced by a placeholder naming their id so the assistant can re-fetch them. Keep it well under OpenRouter's 30 MB request cap | 16777216 |
DOUJIN_MANAGER_AGENT_REFLECTION |
Set to off to disable the assistant's self-improvement pass (memories/skills) |
on |
DOUJIN_MANAGER_AGENT_REFLECTION_DELAY |
Idle seconds after a run before the self-improvement pass starts (a new message cancels it) | 180 |
PUPPETEER_EXECUTABLE_PATH |
Browser executable for the assistant's web_browser tool (also settable in Settings) |
— |
Docker deployment
The container image is built and pushed to the Forgejo container registry on every push to main. Deploy via Portainer or docker-compose.
See the Deployment Guide for full instructions including volume setup, environment variables, backup/restore, and troubleshooting.
Quick start:
docker volume create --driver local --opt type=none --opt o=bind \
--opt device=/your/storage/path/images doujinshi_images
(Mounting a CIFS/SMB share instead? See the deployment guide §4.1 — ownership must be pinned in the mount options.)
docker run -d \
-e DOUJIN_MANAGER_AUTH_TOKEN=$(openssl rand -hex 32) \
-p 8080:8080 \
git.kagaku.eu/teamai/doujin-manager:latest
Environment variables
The container is configured through DOUJIN_MANAGER_* variables: the bearer
token (required), the database path and image/thumbnail/backup directories,
the upload size limit, and the default compressed-image budget. The complete
table with defaults lives in deploy/DEPLOYMENT.md §2;
deploy/docker-compose.yml mirrors it.
Documentation
- AGENTS.md — where things are, the boundaries reviews enforce, how to verify, working agreements (for humans and coding agents)
- Development Guide — local run, verification, codegen, migrations, feature checklists, test-harness conventions
- Architecture Overview — Clean Architecture, dependency direction, request flow, client architecture
- Decision Records — ADRs covering tech choices, auth, domain model, storage, and implementation decisions (binding)
- Backlog — deferred work, in one place
- Deployment Guide — Docker, volumes, environment variables, backup/restore, troubleshooting
- Project Plan — the original (June 2026) plan; historical, scope changes since live in the ADRs and this README
CI/CD
Three Forgejo Actions workflows; the first two run scripts/verify.sh, so CI and local verification cannot drift:
- CI (
ci.yml) — backend build and tests with coverage, on pushes and PRs that touchbackend/ - Flutter CI (
flutter-ci.yml) — codegen, analyze, and tests with coverage, on pushes and PRs that touchapp/orvendor/ - Docker publish (
docker-publish.yml) — backend tests, then builds and pushes the container image on every push tomainand onv*tags