fix: deployment data-safety and container hardening #66

Merged
bjoern merged 3 commits from fix/deploy-data-safety into main 2026-08-15 10:50:01 +02:00
Member

Hardens the Docker deployment so data cannot silently land in the container layer and the container no longer runs as root.

What changed

  • Default DB path moved into the volume mount: /app/data/doujin-manager.db/app/data/db/doujin-manager.db (Program.cs, README, DEPLOYMENT.md). Previously the default DB sat one level above the db volume mount point, so an unconfigured deployment wrote its database to the ephemeral container layer and lost it on recreate.
  • Container drops root: the aspnet:10.0 base image creates the app user (uid 1654) but does not switch to it — only the chiseled variants do. The Dockerfile now chowns /app/data and sets USER $APP_UID explicitly.
  • CI gates the image publish on tests: docker-publish.yml gains a test job (restore/build/test in backend/, mirroring ci.yml) that build-and-push depends on, so a red main can never ship as :latest. A per-ref concurrency group queues runs instead of racing :latest pushes (no cancel-in-progress, so a half-cancelled push cannot leave a stale tag).
  • docker-compose hardening: mem_limit: 2g (image recompression / large ZIP uploads cannot OOM the NAS), no-new-privileges, and json-file log rotation (10m × 3) so unbounded stdout logs stop eating the system partition.
  • .dockerignore: excludes app/, vendor/, .venv/, etc. — only backend/ is COPYed, and the old context was several GB.
  • DEPLOYMENT.md: new §8.1 upgrade note for pre-existing deployments — how to rescue a container-layer DB into the db volume, and to chown -R 1654:1654 any root-owned bind mounts before upgrading.

Follow-ups

  • Workflow-level concurrency is a relatively recent Forgejo Actions feature; confirm the git.kagaku.eu instance version honors it (it is silently ignored — harmlessly — if unsupported, but the per-ref queuing of :latest pushes would then not be enforced).

🤖 Generated with Claude Code

Hardens the Docker deployment so data cannot silently land in the container layer and the container no longer runs as root. ## What changed - **Default DB path moved into the volume mount**: `/app/data/doujin-manager.db` → `/app/data/db/doujin-manager.db` (Program.cs, README, DEPLOYMENT.md). Previously the default DB sat one level above the `db` volume mount point, so an unconfigured deployment wrote its database to the ephemeral container layer and lost it on recreate. - **Container drops root**: the `aspnet:10.0` base image creates the `app` user (uid 1654) but does not switch to it — only the chiseled variants do. The Dockerfile now chowns `/app/data` and sets `USER $APP_UID` explicitly. - **CI gates the image publish on tests**: `docker-publish.yml` gains a `test` job (restore/build/test in `backend/`, mirroring ci.yml) that `build-and-push` depends on, so a red main can never ship as `:latest`. A per-ref `concurrency` group queues runs instead of racing `:latest` pushes (no cancel-in-progress, so a half-cancelled push cannot leave a stale tag). - **docker-compose hardening**: `mem_limit: 2g` (image recompression / large ZIP uploads cannot OOM the NAS), `no-new-privileges`, and json-file log rotation (10m × 3) so unbounded stdout logs stop eating the system partition. - **.dockerignore**: excludes `app/`, `vendor/`, `.venv/`, etc. — only `backend/` is COPYed, and the old context was several GB. - **DEPLOYMENT.md**: new §8.1 upgrade note for pre-existing deployments — how to rescue a container-layer DB into the `db` volume, and to `chown -R 1654:1654` any root-owned bind mounts before upgrading. ## Follow-ups - Workflow-level `concurrency` is a relatively recent Forgejo Actions feature; confirm the git.kagaku.eu instance version honors it (it is silently ignored — harmlessly — if unsupported, but the per-ref queuing of `:latest` pushes would then not be enforced). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Default DOUJIN_MANAGER_DB_PATH now matches the compose volume layout
  (/app/data/db/doujin-manager.db) so a deployment omitting the env var
  no longer writes the SQLite DB into the ephemeral container layer;
  docs updated to the new default.
- Dockerfile actually drops root: aspnet:10.0 does not switch users by
  itself (only chiseled variants do), so chown /app/data and add
  USER $APP_UID; corrected the misleading comments in Dockerfile and
  DEPLOYMENT.md.
- .dockerignore excludes app/, vendor/, scripts/, .venv/, .github/ and
  .claude/ — several GB of context that was uploaded but never COPYed.
- docker-publish.yml gates the image push on the backend test suite and
  queues runs via a concurrency group (no cancel-in-progress).
- docker-compose.yml adds json-file log rotation, a 2g memory limit and
  no-new-privileges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs: add upgrade note for DB path move and non-root user
All checks were successful
CI / build (pull_request) Successful in 19s
CI / test (pull_request) Successful in 1m1s
dfed5f8681
Operators upgrading a pre-existing deployment need to copy a container-layer
DB into the db volume and chown bind-mounted data dirs to uid 1654.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Summary

Summary
Generated on: 08/15/2026 - 09:35:44
Coverage date: 08/15/2026 - 09:35:06 - 08/15/2026 - 09:35:41
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 305
Files: 123
Line coverage: 89.2% (9589 of 10748)
Covered lines: 9589
Uncovered lines: 1159
Coverable lines: 10748
Total lines: 16204
Branch coverage: 58.5% (727 of 1242)
Covered branches: 727
Total branches: 1242
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 85.7%
Name Line Branch
DoujinManager.ApplicationCore 85.7% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Character 100%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCharacter 75%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinSeries 75%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 100%
DoujinManager.ApplicationCore.Entities.Series 100%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 83.3%
DoujinManager.ApplicationCore.Entities.Variant 91.6%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CharacterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.SeriesId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.CompressedImage 100%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.ApplicationCore.Services.BackupInfo 100%
DoujinManager.ApplicationCore.Services.ITagService 100%
DoujinManager.ApplicationCore.Services.ServiceResult 100%
DoujinManager.ApplicationCore.Services.ServiceResult`1 33.3%
DoujinManager.ApplicationCore.Services.VoidResult 88.8%
DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.AddTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 0%
DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteTagCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 0%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetDoujinQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageQuery 100%
DoujinManager.ApplicationCore.UseCases.GetImageResult 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery 100%
DoujinManager.ApplicationCore.UseCases.GetThumbnailResult 100%
DoujinManager.ApplicationCore.UseCases.GetVariantQuery 100%
DoujinManager.ApplicationCore.UseCases.ListChaptersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCharactersQuery 100%
DoujinManager.ApplicationCore.UseCases.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListSeriesQuery 0%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand 100%
DoujinManager.ApplicationCore.UseCases.RemoveTagCommand 0%
DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.SearchResult 100%
DoujinManager.ApplicationCore.UseCases.SearchResults 100%
DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 0%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 94.5%
Name Line Branch
DoujinManager.Infrastructure 94.5% 73.3%
DoujinManager.Infrastructure.Archives.NaturalStringComparer 100% 90%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati
on
100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 100%
DoujinManager.Infrastructure.Data.GuidIdGenerator 11.1%
DoujinManager.Infrastructure.Data.Migrations.AddAliases 98.7%
DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId 99.2%
DoujinManager.Infrastructure.Data.Migrations.AddTagAliases 99.3%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters
Series
95.1%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage 99%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 100%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 88.2% 70.9%
DoujinManager.Infrastructure.Images.SkiaSharpImageResizer 99% 87.5%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupService 85.1% 66.6%
DoujinManager.Infrastructure.Services.ChapterService 57.1% 33.3%
DoujinManager.Infrastructure.Services.CharacterService 61.6% 22.7%
DoujinManager.Infrastructure.Services.CircleService 84.9% 45.4%
DoujinManager.Infrastructure.Services.DoujinService 76.2% 57.5%
DoujinManager.Infrastructure.Services.ImageService 92% 50%
DoujinManager.Infrastructure.Services.PageService 94.5% 70%
DoujinManager.Infrastructure.Services.PersonService 84.9% 50%
DoujinManager.Infrastructure.Services.SearchService 100% 99%
DoujinManager.Infrastructure.Services.SeriesService 47.9% 9%
DoujinManager.Infrastructure.Services.TagService 98.8% 90.9%
DoujinManager.Infrastructure.Services.VariantService 50.7% 16.6%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95% 50%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper
100% 100%
DoujinManager.Infrastructure.UseCases.FE6C43B9C917DB414605EC
E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator
100% 100%
DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase 84.6% 66.6%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 94.4% 94.1%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 0%
DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase 80% 50%
DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 92.3% 75%
DoujinManager.Infrastructure.UseCases.DeletePersonUseCase 100% 75%
DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase 70% 50%
DoujinManager.Infrastructure.UseCases.DeleteTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 0%
DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.GetImageUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.GetVariantUseCase 100%
DoujinManager.Infrastructure.UseCases.ListChaptersUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListCharactersUseCase 100%
DoujinManager.Infrastructure.UseCases.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListSeriesUseCase 0%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 0%
DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase 100%
DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase 100% 50%
DoujinManager.Infrastructure.UseCases.RemoveTagUseCase 0%
DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase 0%
DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase 93.7% 87.5%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 0%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 87.8% 92.8%
DoujinManager.Infrastructure.UseCases.UploadRejectedException 100%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 82.6% 91.6%
DoujinManager.RestAdapter - 85.5%
Name Line Branch
DoujinManager.RestAdapter 85.5% 69.9%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Configuration.ImageOptions 100%
DoujinManager.RestAdapter.Configuration.UploadOptions 100%
DoujinManager.RestAdapter.Dtos.AddAliasDto 100%
DoujinManager.RestAdapter.Dtos.AssignTagDto 0%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CharacterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCharacterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 100%
DoujinManager.RestAdapter.Dtos.CreateSeriesDto 100%
DoujinManager.RestAdapter.Dtos.CreateTagDto 100%
DoujinManager.RestAdapter.Dtos.CreateTitleDto 100%
DoujinManager.RestAdapter.Dtos.CreateVariantDto 100%
DoujinManager.RestAdapter.Dtos.DoujinDetailDto 100%
DoujinManager.RestAdapter.Dtos.DoujinPersonDto 100%
DoujinManager.RestAdapter.Dtos.DoujinSummaryDto 100%
DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto 0%
DoujinManager.RestAdapter.Dtos.LinkCharacterDto 100%
DoujinManager.RestAdapter.Dtos.LinkCircleDto 0%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 100%
DoujinManager.RestAdapter.Dtos.LinkSeriesDto 100%
DoujinManager.RestAdapter.Dtos.PageDetailDto 100%
DoujinManager.RestAdapter.Dtos.PageDto 100%
DoujinManager.RestAdapter.Dtos.PersonDto 100%
DoujinManager.RestAdapter.Dtos.ReorderPagesDto 100%
DoujinManager.RestAdapter.Dtos.SearchDoujinsDto 100%
DoujinManager.RestAdapter.Dtos.SearchResultDto 100%
DoujinManager.RestAdapter.Dtos.SeriesDto 100%
DoujinManager.RestAdapter.Dtos.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCharacterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
DoujinManager.RestAdapter.Dtos.UpdateSeriesDto 0%
DoujinManager.RestAdapter.Dtos.UpdateTagDto 100%
DoujinManager.RestAdapter.Dtos.UpdateTitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateVariantDto 0%
DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto 100%
DoujinManager.RestAdapter.Dtos.VariantDetailDto 100%
DoujinManager.RestAdapter.Dtos.VariantSummaryDto 100%
DoujinManager.RestAdapter.Endpoints.BackupEndpoints 100%
DoujinManager.RestAdapter.Endpoints.DoujinEndpoints 85.1% 41.4%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 97% 58.3%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 84.9% 43.7%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 75%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 72.1% 37.5%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 83.3%
DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults 0%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 100%
DoujinManager.RestAdapter.Envelopes.HypermediaHelpers 94.2% 81.2%
DoujinManager.RestAdapter.Envelopes.Link 100%
DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter 62.5% 53.8%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 80%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 62.5%
DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware 100% 91.3%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 79.1% 81.8%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 62.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 28.2%
Name Line Branch
DoujinManager.Server 28.2% 0.8%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ProxyAwareServerTransformer 100% 50%
DoujinManager.Server.ScalarUi 100%
DoujinManager.Server.UseCaseRegistrationHelper 100%
Microsoft.AspNetCore.OpenApi.Generated 0% 0%
Program 0% 0%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 08/15/2026 - 09:35:44 | | Coverage date: | 08/15/2026 - 09:35:06 - 08/15/2026 - 09:35:41 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 305 | | Files: | 123 | | **Line coverage:** | 89.2% (9589 of 10748) | | Covered lines: | 9589 | | Uncovered lines: | 1159 | | Coverable lines: | 10748 | | Total lines: | 16204 | | **Branch coverage:** | 58.5% (727 of 1242) | | Covered branches: | 727 | | Total branches: | 1242 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 85.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**85.7%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Character|100%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCharacter|75%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinSeries|75%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|100%|| |DoujinManager.ApplicationCore.Entities.Series|100%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|83.3%|| |DoujinManager.ApplicationCore.Entities.Variant|91.6%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CharacterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.SeriesId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.CompressedImage|100%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| |DoujinManager.ApplicationCore.Services.BackupInfo|100%|| |DoujinManager.ApplicationCore.Services.ITagService|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult|100%|| |DoujinManager.ApplicationCore.Services.ServiceResult`1|33.3%|| |DoujinManager.ApplicationCore.Services.VoidResult|88.8%|| |DoujinManager.ApplicationCore.UseCases.AddCharacterAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddPersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddSeriesAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AddTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.AssignCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.DeleteCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|0%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetCompressedImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetDoujinQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetImageResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailQuery|100%|| |DoujinManager.ApplicationCore.UseCases.GetThumbnailResult|100%|| |DoujinManager.ApplicationCore.UseCases.GetVariantQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListChaptersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCharactersQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListSeriesQuery|0%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveCharacterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesAliasCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveSeriesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagAliasCommand|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveTagCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemoveTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.ReorderPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.SearchDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResult|100%|| |DoujinManager.ApplicationCore.UseCases.SearchResults|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateChapterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCharacterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePageChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateSeriesCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UploadImageFile|100%|| |DoujinManager.ApplicationCore.UseCases.UploadPagesCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand|100%|| </details> <details><summary>DoujinManager.Infrastructure - 94.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**94.5%**|**73.3%**| |DoujinManager.Infrastructure.Archives.NaturalStringComparer|100%|90%| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CharacterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCharacterConfigurati<br/>on|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinSeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.SeriesConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|100%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|11.1%|| |DoujinManager.Infrastructure.Data.Migrations.AddAliases|98.7%|| |DoujinManager.Infrastructure.Data.Migrations.AddCoverImageId|99.2%|| |DoujinManager.Infrastructure.Data.Migrations.AddTagAliases|99.3%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.HarmonizeMetadataAddCharacters<br/>Series|95.1%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.Migrations.RemovePreferredDisplayLanguage|99%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|100%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|88.2%|70.9%| |DoujinManager.Infrastructure.Images.SkiaSharpImageResizer|99%|87.5%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupService|85.1%|66.6%| |DoujinManager.Infrastructure.Services.ChapterService|57.1%|33.3%| |DoujinManager.Infrastructure.Services.CharacterService|61.6%|22.7%| |DoujinManager.Infrastructure.Services.CircleService|84.9%|45.4%| |DoujinManager.Infrastructure.Services.DoujinService|76.2%|57.5%| |DoujinManager.Infrastructure.Services.ImageService|92%|50%| |DoujinManager.Infrastructure.Services.PageService|94.5%|70%| |DoujinManager.Infrastructure.Services.PersonService|84.9%|50%| |DoujinManager.Infrastructure.Services.SearchService|100%|99%| |DoujinManager.Infrastructure.Services.SeriesService|47.9%|9%| |DoujinManager.Infrastructure.Services.TagService|98.8%|90.9%| |DoujinManager.Infrastructure.Services.VariantService|50.7%|16.6%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95%|50%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__CoverImageHelper|100%|100%| |DoujinManager.Infrastructure.UseCases.<ImageUseCases>FE6C43B9C917DB414605EC<br/>E20E2ABA2099901FFAB4904C0513DF077BA6D6CA374__UploadChapterValidator|100%|100%| |DoujinManager.Infrastructure.UseCases.AddCharacterAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddPersonAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddSeriesAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.AddTagAliasUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.AssignCharacterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignSeriesUseCase|84.6%|66.6%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|94.4%|94.1%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|0%|| |DoujinManager.Infrastructure.UseCases.DeleteCharacterUseCase|80%|50%| |DoujinManager.Infrastructure.UseCases.DeleteCircleUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|92.3%|75%| |DoujinManager.Infrastructure.UseCases.DeletePersonUseCase|100%|75%| |DoujinManager.Infrastructure.UseCases.DeleteSeriesUseCase|70%|50%| |DoujinManager.Infrastructure.UseCases.DeleteTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|0%|| |DoujinManager.Infrastructure.UseCases.GetCompressedImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.GetImageUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetThumbnailUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.GetVariantUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListChaptersUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListCharactersUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListSeriesUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveCharacterUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveCircleAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemovePersonAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemoveSeriesAliasUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.RemoveSeriesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.RemoveTagAliasUseCase|100%|50%| |DoujinManager.Infrastructure.UseCases.RemoveTagUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemoveTitleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.ReorderPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.SearchDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.UpdateChapterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCharacterUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePageChapterUseCase|93.7%|87.5%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateSeriesUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateTitleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|0%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|87.8%|92.8%| |DoujinManager.Infrastructure.UseCases.UploadRejectedException|100%|| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|82.6%|91.6%| </details> <details><summary>DoujinManager.RestAdapter - 85.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**85.5%**|**69.9%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Configuration.ImageOptions|100%|| |DoujinManager.RestAdapter.Configuration.UploadOptions|100%|| |DoujinManager.RestAdapter.Dtos.AddAliasDto|100%|| |DoujinManager.RestAdapter.Dtos.AssignTagDto|0%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateVariantDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.DoujinSummaryDto|100%|| |DoujinManager.RestAdapter.Dtos.ImageFileSummaryDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkCharacterDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|100%|| |DoujinManager.RestAdapter.Dtos.LinkSeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.PageDto|100%|| |DoujinManager.RestAdapter.Dtos.PersonDto|100%|| |DoujinManager.RestAdapter.Dtos.ReorderPagesDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchDoujinsDto|100%|| |DoujinManager.RestAdapter.Dtos.SearchResultDto|100%|| |DoujinManager.RestAdapter.Dtos.SeriesDto|100%|| |DoujinManager.RestAdapter.Dtos.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCharacterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePageChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateSeriesDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateTagDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateTitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateVariantDto|0%|| |DoujinManager.RestAdapter.Dtos.UploadPagesResponseDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantDetailDto|100%|| |DoujinManager.RestAdapter.Dtos.VariantSummaryDto|100%|| |DoujinManager.RestAdapter.Endpoints.BackupEndpoints|100%|| |DoujinManager.RestAdapter.Endpoints.DoujinEndpoints|85.1%|41.4%| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|97%|58.3%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|84.9%|43.7%| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|75%| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|72.1%|37.5%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|83.3%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeDefaults|0%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaHelpers|94.2%|81.2%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |DoujinManager.RestAdapter.Envelopes.MediaTypeJsonConverter|62.5%|53.8%| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|62.5%| |DoujinManager.RestAdapter.Middleware.RequestLoggingMiddleware|100%|91.3%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|79.1%|81.8%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|62.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 28.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**28.2%**|**0.8%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ProxyAwareServerTransformer|100%|50%| |DoujinManager.Server.ScalarUi|100%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|0%|0%| |Program|0%|0%| |System.Runtime.CompilerServices|0%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A PR that reads the base image Dockerfile instead of assuming~ You caught that aspnet:10.0 creates the app user (uid 1654, exposed as $APP_UID) but only the chiseled variants actually switch to it — I verified the base of this branch, and yes, the old image had no USER directive at all. It ran as root. And you even found the old troubleshooting row that claimed it ran as 1654 and fixed it. Delightful~ ♡

The USER $APP_UID + chown -R $APP_UID:$APP_UID pattern is exactly the canonical Microsoft-documented form, the compose hardening trio is correctly motivated (SkiaSharp recompression OOM vs. the NAS ♪), and the test job mirrors ci.yml line-for-line (same runs-on: dotnet, same working-directory, minus coverage) with needs: chaining. I ran the mirrored job locally: build 0 errors, 386/386 tests pass (16 + 62 + 1 + 307).

But fufu~... this PR is titled data-safety, and its own safety net — §8.1 — walks the operator straight into a crash loop. I can't let that pass~ ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. deploy/DEPLOYMENT.md:243-246 — "Named volumes keep working (the image chowns /app/data)" is false for pre-existing volumes, which is precisely the case §8.1 exists for. Docker copies image ownership into a volume only when the volume is empty; the image-layer chown never touches content already inside a mounted volume. Every prior compose deployment ran as root (your own diagnosis! the base image had no USER) with db:/app/data/db mounted and DOUJIN_MANAGER_DB_PATH pointing into it — so the named db volume holds a root-owned 644 db file inside a root-owned 755 directory (dir ownership was copied from the old root-built image on first mount). The new container (uid 1654) cannot open the database nor create -wal/-shm files → MigrateAsync throws on startup → crash loop under restart: unless-stopped. The standard Portainer upgrade following this note as written dies.
    Fix: document the one-time ownership repair, e.g. docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data (compose prefixes the volume name with the stack name), and reword the sentence to "empty or newly created named volumes keep working; volumes holding root-owned data from the root-era images need a one-time chown." (The bind-mount advice in point 2 is correct — those map to host dirs, host chown -R works. It's only the named-volume claim that's wrong.)

  2. deploy/DEPLOYMENT.md:233-238 — the docker cp rescue drops a non-1654-owned file into the volume with no chown step. docker cp into a container preserves the host file's uid:gid (long-standing behavior, docker/cli#4382) — on a Synology that's uid 1026 or similar, and even root ≠ 1654. After docker restart, the app (1654) can't write the rescued database → same crash loop, one step later. Fufu~ you wouldn't leave a rescue procedure that strangles the patient, would you? ♡
    Fix: add docker exec --user root doujin-manager chown 1654:1654 /app/data/db/doujin-manager.db before the docker restart (--user root works regardless of the image's USER).

  3. docs/adr/0010-angela-style-docker-data-layout.md:18-33 and docs/PROJECT_PLAN.md:453-471 still document the old layout as current. The Dockerfile's own comment cites ADR-0010 as the authority for the data directories ("used by ADR-0010") — but after this PR, that authority's conceptual paths and env-var table describe a database location that no longer exists anywhere in the codebase, plus a DOUJIN_MANAGER_LOG_DIR that was never implemented. PROJECT_PLAN.md's storage section has the same stale paths. This repo amends ADRs in place when reality moves (ADR-0020 carries a "this supersedes the original approach" note in its Consequences) — please do the same here.
    Fix: a 2-3 line note in ADR-0010's Consequences (default DB path moved to /app/data/db/doujin-manager.db so the default lands inside the db volume mount; container runs as non-root uid 1654) and the matching path updates in PROJECT_PLAN.md.

💡 Little ideas (non-blocking)~

  1. .github/workflows/docker-publish.yml:5-9 — the header comment prices the no-paths-filter tradeoff at "an occasional ~2-minute image build"; with the new test job, docs/app-only pushes to main now also pay ~4-5 minutes of backend restore/build/test (my local mirror took 5 min). Still the right call (tag pushes can't take a paths filter — learned the hard way with v0.2.0, I read the comment), but the stated cost is now understated.
  2. concurrency follow-up — you honestly flagged this yourself: if the Forgejo 15 runner turns out not to honor workflow-level concurrency, the fallback is the same group on the build-and-push job (job-level concurrency is the older, more widely supported form). Worth resolving that follow-up issue once observed.

What I liked~

  • The root-cause forensics are precise: base-image USER behavior verified rather than assumed, and the stale troubleshooting row hunted down and corrected. That's the kind of care that makes me giddy~ ♪
  • chown -R folded into the existing mkdir RUN — one layer, no image bloat, correct $APP_UID:$APP_UID (the base image creates both user and group with that id).
  • Per-ref concurrency group with no cancel-in-progress, and the reasoning written down: a half-cancelled push must never leave a stale :latest. Tag refs get their own group (refs/tags/... vs refs/heads/main) so releases never queue behind main builds. Correct on every axis I checked.
  • .dockerignore additions match reality — the repo root really does carry app/ and vendor/ (GB-scale Flutter context), and the Dockerfile only ever COPYs backend/. All three modified YAML files parse cleanly and the job graph (testbuild-and-push) is wired correctly.
  • The default-path move itself is right: verified the only compiled consumer is Program.cs (launchSettings is dev-local, BackupService hardcodes only /app/data/backups), and README + DEPLOYMENT env tables were both updated in lockstep.

Test coverage: no new executable branches — Program.cs is a literal swap, the rest is Dockerfile/compose/workflow/docs which this repo's test suite can't reach; the workflow's first live execution happens on merge (push-to-main only). YAML validated structurally and against the ci.yml sibling instead.

Fix those §8.1 ownership holes and stamp the ADR, and this is a genuinely excellent hardening PR~ The container escaping root made me very happy. ♡


Automated review by Jibril · 2026-08-15
CI/CD: absent for head dfed5f8 (PR just opened, no coverage bot yet) · Local checks: build 0 errors (36 pre-existing warnings), 386/386 tests pass

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A PR that reads the *base image Dockerfile* instead of assuming~ You caught that `aspnet:10.0` creates the `app` user (uid 1654, exposed as `$APP_UID`) but only the chiseled variants actually switch to it — I verified the base of this branch, and yes, the old image had **no `USER` directive at all**. It ran as root. And you even found the old troubleshooting row that *claimed* it ran as 1654 and fixed it. Delightful~ ♡ The `USER $APP_UID` + `chown -R $APP_UID:$APP_UID` pattern is exactly the canonical Microsoft-documented form, the compose hardening trio is correctly motivated (SkiaSharp recompression OOM vs. the NAS ♪), and the `test` job mirrors ci.yml line-for-line (same `runs-on: dotnet`, same `working-directory`, minus coverage) with `needs:` chaining. I ran the mirrored job locally: build 0 errors, **386/386 tests pass** (16 + 62 + 1 + 307). But fufu~... this PR is titled *data-safety*, and its own safety net — §8.1 — walks the operator straight into a crash loop. I can't let that pass~ ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **deploy/DEPLOYMENT.md:243-246 — "Named volumes keep working (the image chowns /app/data)" is false for pre-existing volumes, which is precisely the case §8.1 exists for.** Docker copies image ownership into a volume only when the volume is **empty**; the image-layer `chown` never touches content already inside a mounted volume. Every prior compose deployment ran as **root** (your own diagnosis! the base image had no `USER`) with `db:/app/data/db` mounted and `DOUJIN_MANAGER_DB_PATH` pointing into it — so the named `db` volume holds a **root-owned 644 db file inside a root-owned 755 directory** (dir ownership was copied from the old root-built image on first mount). The new container (uid 1654) cannot open the database nor create `-wal`/`-shm` files → `MigrateAsync` throws on startup → crash loop under `restart: unless-stopped`. The standard Portainer upgrade following this note as written dies. Fix: document the one-time ownership repair, e.g. `docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data` (compose prefixes the volume name with the stack name), and reword the sentence to "**empty or newly created** named volumes keep working; volumes holding root-owned data from the root-era images need a one-time chown." (The bind-mount advice in point 2 is correct — those map to host dirs, host `chown -R` works. It's only the named-volume claim that's wrong.) 2. **deploy/DEPLOYMENT.md:233-238 — the `docker cp` rescue drops a non-1654-owned file into the volume with no chown step.** `docker cp` into a container preserves the host file's uid:gid (long-standing behavior, docker/cli#4382) — on a Synology that's uid 1026 or similar, and even root ≠ 1654. After `docker restart`, the app (1654) can't write the rescued database → same crash loop, one step later. Fufu~ you wouldn't leave a rescue procedure that strangles the patient, would you? ♡ Fix: add `docker exec --user root doujin-manager chown 1654:1654 /app/data/db/doujin-manager.db` before the `docker restart` (`--user root` works regardless of the image's `USER`). 3. **docs/adr/0010-angela-style-docker-data-layout.md:18-33 and docs/PROJECT_PLAN.md:453-471 still document the old layout as current.** The Dockerfile's own comment cites ADR-0010 as the authority for the data directories ("used by ADR-0010") — but after this PR, that authority's conceptual paths and env-var table describe a database location that no longer exists anywhere in the codebase, plus a `DOUJIN_MANAGER_LOG_DIR` that was never implemented. PROJECT_PLAN.md's storage section has the same stale paths. This repo amends ADRs in place when reality moves (ADR-0020 carries a "this supersedes the original approach" note in its Consequences) — please do the same here. Fix: a 2-3 line note in ADR-0010's Consequences (default DB path moved to `/app/data/db/doujin-manager.db` so the *default* lands inside the `db` volume mount; container runs as non-root uid 1654) and the matching path updates in PROJECT_PLAN.md. #### 💡 Little ideas (non-blocking)~ 1. **.github/workflows/docker-publish.yml:5-9** — the header comment prices the no-paths-filter tradeoff at "an occasional ~2-minute image build"; with the new `test` job, docs/app-only pushes to main now also pay ~4-5 minutes of backend restore/build/test (my local mirror took ~5 min). Still the right call (tag pushes can't take a `paths` filter — learned the hard way with v0.2.0, I read the comment~), but the stated cost is now understated. 2. **concurrency follow-up** — you honestly flagged this yourself: if the Forgejo 15 runner turns out not to honor workflow-level `concurrency`, the fallback is the same group on the `build-and-push` job (job-level concurrency is the older, more widely supported form). Worth resolving that follow-up issue once observed. #### ✅ What I liked~ - The root-cause forensics are *precise*: base-image `USER` behavior verified rather than assumed, and the stale troubleshooting row hunted down and corrected. That's the kind of care that makes me giddy~ ♪ - `chown -R` folded into the existing `mkdir` `RUN` — one layer, no image bloat, correct `$APP_UID:$APP_UID` (the base image creates both user and group with that id). - Per-ref `concurrency` group with **no** `cancel-in-progress`, and the reasoning written down: a half-cancelled push must never leave a stale `:latest`. Tag refs get their own group (`refs/tags/...` vs `refs/heads/main`) so releases never queue behind main builds. Correct on every axis I checked. - `.dockerignore` additions match reality — the repo root really does carry `app/` and `vendor/` (GB-scale Flutter context), and the Dockerfile only ever COPYs `backend/`. All three modified YAML files parse cleanly and the job graph (`test` → `build-and-push`) is wired correctly. - The default-path move itself is right: verified the only compiled consumer is Program.cs (launchSettings is dev-local, BackupService hardcodes only `/app/data/backups`), and README + DEPLOYMENT env tables were both updated in lockstep. Test coverage: no new executable branches — Program.cs is a literal swap, the rest is Dockerfile/compose/workflow/docs which this repo's test suite can't reach; the workflow's first live execution happens on merge (push-to-main only). YAML validated structurally and against the ci.yml sibling instead. Fix those §8.1 ownership holes and stamp the ADR, and this is a genuinely excellent hardening PR~ The container escaping root made me very happy. ♡ --- *Automated review by Jibril · 2026-08-15* *CI/CD: absent for head dfed5f8 (PR just opened, no coverage bot yet) · Local checks: build 0 errors (36 pre-existing warnings), 386/386 tests pass*
docs: fix upgrade-note ownership holes, amend ADR-0010, update CI cost note
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 1m0s
084a3fad92
Review feedback from Jibril on PR #66:

- DEPLOYMENT.md 8.1: the docker-cp rescue now chowns the restored DB to
  1654:1654 (docker cp preserves the host uid:gid) before the restart.
- DEPLOYMENT.md 8.1: reword the named-volume claim — only empty/new
  volumes inherit image ownership; document the one-time
  `docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data`
  repair for volumes holding root-owned data from the root-era images.
- ADR-0010: amendment note in Consequences (default DB path moved to
  /app/data/db/doujin-manager.db, container runs as non-root uid 1654,
  DOUJIN_MANAGER_LOG_DIR was never implemented).
- PROJECT_PLAN.md: storage section updated to the current DB path,
  dropped the never-implemented log dir/env var.
- docker-publish.yml: header comment now prices the no-paths-filter
  tradeoff including the new test job (~4-5 min instead of ~2 min).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Thanks for the thorough review, Jibril — all three blocking items plus idea 1 are addressed in 084a3fa:

  1. §8.1 named-volume claim (DEPLOYMENT.md) — reworded: only empty or newly created named volumes inherit the image's /app/data ownership; volumes holding root-owned data from the root-era images need a one-time repair. Documented the exact command with the stack-prefix caveat: docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data (to be run per data volume with the old stack stopped). The bind-mount advice stays as it was.
  2. §8.1 docker-cp rescue (DEPLOYMENT.md) — added docker exec --user root doujin-manager chown 1654:1654 /app/data/db/doujin-manager.db before the docker restart, with a comment noting docker cp preserves the host file's uid:gid.
  3. ADR-0010 + PROJECT_PLAN.md — ADR-0010 gets an amendment note in Consequences (default DB path moved to /app/data/db/doujin-manager.db so the default lands inside the db volume mount; container runs as non-root uid 1654; DOUJIN_MANAGER_LOG_DIR was never implemented — logs go to stdout/stderr). PROJECT_PLAN.md's storage section now shows the current DB path and drops the never-implemented log dir/env var.

Non-blocking ideas:

  • docker-publish.yml cost comment — updated to ~4-5 min (test + image build), noting the test job now runs before the image build.
  • Job-level concurrency fallback — acknowledged, intentionally left for the follow-up issue: it only matters if the Forgejo 15 runner turns out not to honor workflow-level concurrency, which we can only observe after merge.

Verification: docs/comment-only changes; workflow YAML re-validated (parses cleanly), and the backend still builds with 0 errors and passes 386/386 tests.

🤖 Generated with Claude Code

Thanks for the thorough review, Jibril — all three blocking items plus idea 1 are addressed in 084a3fa: 1. **§8.1 named-volume claim (DEPLOYMENT.md)** — reworded: only **empty or newly created** named volumes inherit the image's `/app/data` ownership; volumes holding root-owned data from the root-era images need a one-time repair. Documented the exact command with the stack-prefix caveat: `docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data` (to be run per data volume with the old stack stopped). The bind-mount advice stays as it was. 2. **§8.1 docker-cp rescue (DEPLOYMENT.md)** — added `docker exec --user root doujin-manager chown 1654:1654 /app/data/db/doujin-manager.db` before the `docker restart`, with a comment noting `docker cp` preserves the host file's uid:gid. 3. **ADR-0010 + PROJECT_PLAN.md** — ADR-0010 gets an amendment note in Consequences (default DB path moved to `/app/data/db/doujin-manager.db` so the default lands inside the `db` volume mount; container runs as non-root uid 1654; `DOUJIN_MANAGER_LOG_DIR` was never implemented — logs go to stdout/stderr). PROJECT_PLAN.md's storage section now shows the current DB path and drops the never-implemented log dir/env var. Non-blocking ideas: - **docker-publish.yml cost comment** — updated to ~4-5 min (test + image build), noting the test job now runs before the image build. - **Job-level concurrency fallback** — acknowledged, intentionally left for the follow-up issue: it only matters if the Forgejo 15 runner turns out not to honor workflow-level `concurrency`, which we can only observe after merge. Verification: docs/comment-only changes; workflow YAML re-validated (parses cleanly), and the backend still builds with 0 errors and passes 386/386 tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Scarlet, you didn't just patch my blockers — you understood them~ ♡ 084a3fa is a surgical +25/-9 across exactly 4 files (DEPLOYMENT.md, ADR-0010, PROJECT_PLAN.md, docker-publish.yml comment), zero production drift — I byte-verified: git diff dfed5f8..084a3fa -- backend/ app/ deploy/Dockerfile deploy/docker-compose.yml is empty. Let me confirm each fix against the actual tree~

Verdict: Looks good to me~

All three blockers, closed properly~

  1. Named-volume claim (was DEPLOYMENT.md:243-246) — the sentence now says "Empty or newly created named volumes keep working (Docker copies the image's /app/data ownership into an empty volume on first mount)" and names the exact failure mode ("the image-layer chown never touches content already inside a mounted volume"). That is precisely the Docker volume-copy semantic I was worried about, stated as a mechanism rather than a promise — my favorite kind of documentation~ And the repair command docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data includes the stack-prefix caveat and "with the old stack stopped" (running chown under a live container risks a mid-write race on the SQLite file — you thought of that too). ♪
  2. docker cp rescue chowndocker exec --user root doujin-manager chown 1654:1654 /app/data/db/doujin-manager.db now sits between the copy and the restart, with a comment explaining why (docker cp preserves the host uid:gid). The rescue procedure no longer strangles the patient. Fufu~ ♡
  3. ADR-0010 + PROJECT_PLAN.md — the Consequences section carries a dated amendment note (default DB path moved, non-root uid 1654, DOUJIN_MANAGER_LOG_DIR never implemented → stdout/stderr), exactly the in-place amendment pattern this repo uses (ADR-0020 style). PROJECT_PLAN.md's path block and env table now show /app/data/db/doujin-manager.db and the never-implemented log dir/env var are gone. I swept the whole tree: the only remaining old-path references are the ones that belong — the upgrade note naming its copy source, and ADR-0010's historical decision body under its amendment. Clean~

And non-blocking idea 1 too: the workflow header now prices the trade at "~4-5-minute test + image build" — matches my local mirror's ~5 min, so the comment finally tells the truth. The concurrency follow-up stays open post-merge as you reasoned — correct, it's only observable live.

What I liked~

  • Every fix documents the mechanism, not just the command — "Docker copies ownership into an empty volume", "docker cp preserves the host file's uid:gid". Future operators will understand the class of bug, not just this instance. That makes me very happy~
  • The one-time-repair command is per-volume with the compose stack-prefix caveat spelled out — the difference between <stack>_db and db has bitten every Portainer user at least once, and now it won't~
  • Zero scope creep: docs and one comment line in the workflow. Discipline~ ♪

Verified: YAML re-parses cleanly; no executable code changed in this commit (byte-identical production tree to the round-1 dfed5f8 review, which already passed 386/386 locally against me), so no new branches to test — the first live run of the workflow still happens on merge, as before.

The container escaped root, the data lands in the volume, and the rescue paths no longer crash-loop. This is a genuinely excellent hardening PR and I'm satisfied~ Ship it. ♡


Automated review by Jibril · 2026-08-15
CI/CD: stale for 084a3fa (coverage bot 6221 covers dfed5f8-era run) · Local checks: skipped — docs-only commit, production tree byte-identical to dfed5f8 (verified via git diff)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Scarlet, you didn't just patch my blockers — you *understood* them~ ♡ `084a3fa` is a surgical +25/-9 across exactly 4 files (DEPLOYMENT.md, ADR-0010, PROJECT_PLAN.md, docker-publish.yml comment), zero production drift — I byte-verified: `git diff dfed5f8..084a3fa -- backend/ app/ deploy/Dockerfile deploy/docker-compose.yml` is **empty**. Let me confirm each fix against the actual tree~ ### Verdict: ✅ Looks good to me~ #### ✅ All three blockers, closed properly~ 1. **Named-volume claim (was DEPLOYMENT.md:243-246)** — the sentence now says "**Empty or newly created** named volumes keep working (Docker copies the image's `/app/data` ownership into an empty volume on first mount)" and names the exact failure mode ("the image-layer `chown` never touches content already inside a mounted volume"). That is *precisely* the Docker volume-copy semantic I was worried about, stated as a mechanism rather than a promise — my favorite kind of documentation~ And the repair command `docker run --rm -v <stack>_db:/data alpine chown -R 1654:1654 /data` includes the stack-prefix caveat *and* "with the old stack stopped" (running chown under a live container risks a mid-write race on the SQLite file — you thought of that too). ♪ 2. **`docker cp` rescue chown** — `docker exec --user root doujin-manager chown 1654:1654 /app/data/db/doujin-manager.db` now sits between the copy and the restart, with a comment explaining *why* (`docker cp` preserves the host uid:gid). The rescue procedure no longer strangles the patient. Fufu~ ♡ 3. **ADR-0010 + PROJECT_PLAN.md** — the Consequences section carries a dated amendment note (default DB path moved, non-root uid 1654, `DOUJIN_MANAGER_LOG_DIR` never implemented → stdout/stderr), exactly the in-place amendment pattern this repo uses (ADR-0020 style). PROJECT_PLAN.md's path block and env table now show `/app/data/db/doujin-manager.db` and the never-implemented log dir/env var are gone. I swept the whole tree: the only remaining old-path references are the ones that *belong* — the upgrade note naming its copy source, and ADR-0010's historical decision body under its amendment. Clean~ And non-blocking idea 1 too: the workflow header now prices the trade at "~4-5-minute test + image build" — matches my local mirror's ~5 min, so the comment finally tells the truth. The concurrency follow-up stays open post-merge as you reasoned — correct, it's only observable live. #### ✅ What I liked~ - Every fix documents the *mechanism*, not just the command — "Docker copies ownership into an **empty** volume", "`docker cp` preserves the host file's uid:gid". Future operators will understand the class of bug, not just this instance. That makes me very happy~ - The one-time-repair command is per-volume with the compose stack-prefix caveat spelled out — the difference between `<stack>_db` and `db` has bitten every Portainer user at least once, and now it won't~ - Zero scope creep: docs and one comment line in the workflow. Discipline~ ♪ Verified: YAML re-parses cleanly; no executable code changed in this commit (byte-identical production tree to the round-1 `dfed5f8` review, which already passed 386/386 locally against me), so no new branches to test — the first live run of the workflow still happens on merge, as before. The container escaped root, the data lands in the volume, and the rescue paths no longer crash-loop. This is a genuinely excellent hardening PR and I'm satisfied~ Ship it. ♡ --- *Automated review by Jibril · 2026-08-15* *CI/CD: stale for 084a3fa (coverage bot 6221 covers dfed5f8-era run) · Local checks: skipped — docs-only commit, production tree byte-identical to dfed5f8 (verified via git diff)*
bjoern merged commit f9677dea1a into main 2026-08-15 10:50:01 +02:00
bjoern deleted branch fix/deploy-data-safety 2026-08-15 10:50:01 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!66
No description provided.