No description
  • Dart 57.9%
  • C# 41.1%
  • CMake 0.4%
  • C++ 0.3%
  • Dockerfile 0.2%
Find a file
matikane cc5e220478
All checks were successful
Build and Push Docker Image / test (push) Successful in 1m5s
Build and Push Docker Image / build-and-push (push) Successful in 51s
docs: pin CIFS volume ownership at mount time (uid=1654) (#82)
Fixes #81.

Docs-only outcome of the 2026-08-16 production incident (uploads failing with `UnauthorizedAccessException` while reads worked, see issue for the full timeline).

Production's three external media volumes are CIFS-backed (`type=cifs` local volumes mounting `//192.168.68.80/Doujinshi/…`) and were created without `uid=`/`gid=` — so everything presented as `root:root` with fake 0755 modes, `chown` was a silent no-op, and every write under uid 1654 failed. The docs never mentioned this layout or its ownership rules.

- **§4.1**: document the CIFS volume layout production actually uses, with `uid=1654,gid=1654,file_mode=0644,dir_mode=0755` pinned in the mount options; the bind-mount variant stays as the chown-repairable alternative.
- **§8.1 item 4**: CIFS-backed volumes cannot be chowned — ownership is fixed at mount time. Fingerprint (reads OK, writes fail, chown exits 0 and changes nothing) + the recreate-and-redeploy fix, cross-linked to §4.1.
- **Troubleshooting**: new row for the read-works-write-fails fingerprint pointing at §8.1 item 4.

The fix procedure itself was executed and verified in production earlier today (volume recreation + stack redeploy; upload, thumbnails, and backup paths confirmed working) — this PR writes down what actually works.

Co-authored-by: bjoern <bjoern@kagaku.eu>
Reviewed-on: #82
2026-08-16 18:17:03 +02:00
.github ci: serialize coverage suites and retry once on runner hang (#71) 2026-08-15 11:51:52 +02:00
app fix: vision-mode main model keeps pixels; captioner is blind-mode only (#80) 2026-08-16 15:07:56 +02:00
backend fix: split-query multi-collection reads + dind volume repair docs (#78) 2026-08-16 10:39:35 +02:00
deploy docs: pin CIFS volume ownership at mount time (uid=1654) (#82) 2026-08-16 18:17:03 +02:00
docs fix: deployment data-safety and container hardening (#66) 2026-08-15 10:50:00 +02:00
vendor chore: re-pin openrouter_dart to 436b956 (web ProcessSignal fix) 2026-08-13 12:14:56 +02:00
.dockerignore fix: deployment data-safety and container hardening (#66) 2026-08-15 10:50:00 +02:00
.gitattributes fix: normalize CRLF to LF and add .gitattributes policy 2026-06-27 21:09:02 +00:00
.gitignore feat: scaffold .NET backend solution with Clean Architecture 2026-06-27 20:49:38 +00:00
.gitmodules feat(app): openrouter_dart submodule + AI assistant settings 2026-07-02 18:12:37 +02:00
README.md docs: pin CIFS volume ownership at mount time (uid=1654) (#82) 2026-08-16 18:17:03 +02:00

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 is feature-complete for the MVP scope: library with filtered search 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), and an immersive reader.

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)
  • 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.json with Scalar API reference UI at /scalar
  • Database backup/restore via API (POST /api/backups)
  • 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, plus rating
  • Doujin detail view: variant tabs with page thumbnails (collapsible chapters), editable user rating, associations, cover
  • Immersive reader (the only rail-less screen): single full-res page on black, reading-direction-aware keyboard/mouse/click navigation, pan/zoom, auto-hiding overlay with chapter jump, page slider, and rating; adjacent pages precached
  • 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
    • 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, 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 (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 the library, read doujins, navigate the app, research on the web (search → fetch → budgeted headless browser), look at covers/pages (vision or blind-mode captioning), and edit metadata — every database write requires inline user approval. Recurring workflows are packaged as loadable skills (built-in + user markdown files under ~/.config/doujinmanager/skills/)

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
    presentation/                  Redux state, epics, feature-first pages
  test/
deploy/
  Dockerfile, docker-compose.yml, DEPLOYMENT.md
docs/
  ARCHITECTURE.md, PROJECT_PLAN.md, adr/

API overview

All endpoints under /api require Authorization: Bearer *** GET /health` is anonymous.

Resource Endpoints
Doujins GET/POST /api/doujins, GET/PUT/DELETE /api/doujins/{id}
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/DELETE /api/variants/{id}/chapters/{chapterId}
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/thumbnails/{id}
Metadata GET/POST + GET/PUT/DELETE /{id} + alias endpoints for /api/tags, /api/people, /api/circles, /api/characters, /api/series
Search POST /api/doujins/search (structured include/exclude query)
Backups GET/POST /api/backups, DELETE /api/backups/{filename}

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.

Building and running locally

Prerequisites

  • .NET 10 SDK
  • SQLite (included via EF Core bundle)

Run

cd backend
dotnet restore
export DOUJIN_MANAGER_AUTH_TOKEN="dev-token-change-me"
dotnet run --project src/DoujinManager.Server

The server starts on http://localhost:8080. EF Core migrations apply automatically on startup.

Run tests

cd backend
dotnet test

Flutter app

cd app
flutter pub get
dart run build_runner build --delete-conflicting-outputs
flutter run -d linux        # or windows / macos
flutter test

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_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

Variable Description Default
DOUJIN_MANAGER_AUTH_TOKEN Bearer token for /api/* (required)
DOUJIN_MANAGER_DB_PATH SQLite database file path /app/data/db/doujin-manager.db
DOUJIN_MANAGER_IMAGE_DIR Uploaded images directory /app/data/images
DOUJIN_MANAGER_THUMBNAIL_DIR Generated thumbnails directory /app/data/thumbnails
DOUJIN_MANAGER_BACKUP_DIR Database backup directory /app/data/backups

Documentation

  • Project Plan — full scope, domain rules, MVP/non-MVP, phases
  • Architecture Overview — Clean Architecture, dependency direction, request flow
  • Decision Records — 23 ADRs covering tech choices, auth, domain model, storage, and implementation decisions
  • Deployment Guide — Docker, volumes, backup/restore, troubleshooting

CI/CD

Two Forgejo Actions workflows:

  • CI (ci.yml) — builds and runs all tests on every push/PR to main
  • Docker publish (docker-publish.yml) — builds and pushes the container image to the Forgejo registry on every push to main