feat: add Scalar API reference UI with reverse-proxy path support #13

Merged
bjoern merged 2 commits from feat/scalar-ui into main 2026-06-29 06:13:06 +02:00
Member

Scalar API Reference UI

Adds Scalar (https://scalar.com) as the interactive API documentation UI.

Endpoints

Path Description
/scalar (or /{prefix}/scalar) Scalar UI — interactive API explorer
/openapi/v1.json (or /{prefix}/openapi/v1.json) OpenAPI spec JSON

Both are anonymous (no auth needed). Available in all environments (not just Development).

Reverse-proxy sub-path support

DOUJIN_MANAGER_PATH_PREFIX env var handles nginx sub-path routing:

# No prefix (direct):
/scalar → Scalar UI
/openapi/v1.json → spec JSON

# With prefix (behind nginx /doujinshi/):
DOUJIN_MANAGER_PATH_PREFIX=/doujinshi
/doujinshi/scalar → Scalar UI
/doujinshi/openapi/v1.json → spec JSON

The nginx config needs to pass through the full path (not strip the prefix). The app reads the prefix and generates correct spec URLs in the Scalar HTML.

Files changed

  • Directory.Packages.props — added Scalar.AspNetCore 2.5.0
  • DoujinManager.Server.csproj — package reference
  • Program.cs — serve OpenAPI + Scalar in all environments
  • ScalarUi.cs — HTML generator with path prefix support
  • docker-compose.yml — commented-out DOUJIN_MANAGER_PATH_PREFIX env var

234 tests pass.

## Scalar API Reference UI Adds Scalar (https://scalar.com) as the interactive API documentation UI. ### Endpoints | Path | Description | |------|-------------| | `/scalar` (or `/{prefix}/scalar`) | Scalar UI — interactive API explorer | | `/openapi/v1.json` (or `/{prefix}/openapi/v1.json`) | OpenAPI spec JSON | Both are anonymous (no auth needed). Available in **all environments** (not just Development). ### Reverse-proxy sub-path support `DOUJIN_MANAGER_PATH_PREFIX` env var handles nginx sub-path routing: ``` # No prefix (direct): /scalar → Scalar UI /openapi/v1.json → spec JSON # With prefix (behind nginx /doujinshi/): DOUJIN_MANAGER_PATH_PREFIX=/doujinshi /doujinshi/scalar → Scalar UI /doujinshi/openapi/v1.json → spec JSON ``` The nginx config needs to pass through the full path (not strip the prefix). The app reads the prefix and generates correct spec URLs in the Scalar HTML. ### Files changed - `Directory.Packages.props` — added `Scalar.AspNetCore` 2.5.0 - `DoujinManager.Server.csproj` — package reference - `Program.cs` — serve OpenAPI + Scalar in all environments - `ScalarUi.cs` — HTML generator with path prefix support - `docker-compose.yml` — commented-out `DOUJIN_MANAGER_PATH_PREFIX` env var 234 tests pass.
feat: add Scalar API reference UI with reverse-proxy path support
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 36s
ba1424c3fc
- Add Scalar.AspNetCore package
- Serve Scalar UI at /scalar (or /{prefix}/scalar) in all environments
- DOUJIN_MANAGER_PATH_PREFIX env var for reverse-proxy sub-paths
  (e.g. nginx routing /doujinshi/ → container)
- OpenAPI JSON also available at /openapi/v1.json
- Both endpoints are anonymous (no auth needed for docs)
- Added env var to docker-compose.yml (commented out by default)

Summary

Summary
Generated on: 06/29/2026 - 00:44:00
Coverage date: 06/29/2026 - 00:43:43 - 06/29/2026 - 00:43:57
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 207
Files: 93
Line coverage: 83.5% (4199 of 5023)
Covered lines: 4199
Uncovered lines: 824
Coverable lines: 5023
Total lines: 8680
Branch coverage: 50% (369 of 738)
Covered branches: 369
Total branches: 738
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 84.8%
Name Line Branch
DoujinManager.ApplicationCore 84.8% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Circle 100%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCircle 75%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 100%
DoujinManager.ApplicationCore.Entities.Page 80%
DoujinManager.ApplicationCore.Entities.Person 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.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.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
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.AddTitleCommand 0%
DoujinManager.ApplicationCore.UseCases.AssignCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignPersonCommand 100%
DoujinManager.ApplicationCore.UseCases.AssignTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateChapterCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateCircleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.CreatePersonCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTagCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateTitleCommand 100%
DoujinManager.ApplicationCore.UseCases.CreateVariantCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand 0%
DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.DeletePageCommand 100%
DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand 0%
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.ListCirclesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPagesQuery 100%
DoujinManager.ApplicationCore.UseCases.ListPeopleQuery 100%
DoujinManager.ApplicationCore.UseCases.ListTagsQuery 100%
DoujinManager.ApplicationCore.UseCases.ListVariantsQuery 100%
DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.RemovePersonCommand 0%
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.UpdateCircleCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand 100%
DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand 0%
DoujinManager.ApplicationCore.UseCases.UpdateVariantCommand 0%
DoujinManager.ApplicationCore.UseCases.UploadImageFile 100%
DoujinManager.ApplicationCore.UseCases.UploadPagesCommand 100%
DoujinManager.ApplicationCore.UseCases.UploadZipPagesCommand 100%
DoujinManager.Infrastructure - 91.7%
Name Line Branch
DoujinManager.Infrastructure 91.7% 71.2%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 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.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.DoujinManagerDbContextModelSna
pshot
100%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 97.1%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 69.2%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 88.2% 70.9%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Services.BackupService 84.4% 75%
DoujinManager.Infrastructure.Services.ChapterService 54.2% 25%
DoujinManager.Infrastructure.Services.CircleService 69.6% 0%
DoujinManager.Infrastructure.Services.DoujinService 69.5% 45.8%
DoujinManager.Infrastructure.Services.ImageService 91% 50%
DoujinManager.Infrastructure.Services.PageService 84.9% 65%
DoujinManager.Infrastructure.Services.PersonService 69.6% 0%
DoujinManager.Infrastructure.Services.SearchService 100% 100%
DoujinManager.Infrastructure.Services.TagService 94.7% 100%
DoujinManager.Infrastructure.Services.VariantService 50.7% 16.6%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95% 75%
DoujinManager.Infrastructure.UseCases.AddTitleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.AssignCircleUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignPersonUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.AssignTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateChapterUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateCircleUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase 100% 98%
DoujinManager.Infrastructure.UseCases.CreatePersonUseCase 100%
DoujinManager.Infrastructure.UseCases.CreateTagUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.CreateVariantUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase 0%
DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase 100%
DoujinManager.Infrastructure.UseCases.DeletePageUseCase 90.9% 75%
DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase 0%
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.ListCirclesUseCase 100%
DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListPagesUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.ListPeopleUseCase 100%
DoujinManager.Infrastructure.UseCases.ListTagsUseCase 100%
DoujinManager.Infrastructure.UseCases.ListVariantsUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase 0%
DoujinManager.Infrastructure.UseCases.RemovePersonUseCase 0%
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.UpdateCircleUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase 100% 100%
DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase 0% 0%
DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase 0%
DoujinManager.Infrastructure.UseCases.UploadPagesUseCase 92.8% 92.8%
DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase 74.1% 62.5%
DoujinManager.RestAdapter - 83.7%
Name Line Branch
DoujinManager.RestAdapter 83.7% 76.4%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Dtos.AssignTagDto 0%
DoujinManager.RestAdapter.Dtos.BackupDto 100%
DoujinManager.RestAdapter.Dtos.ChapterDto 100%
DoujinManager.RestAdapter.Dtos.CircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateBackupResponse 100%
DoujinManager.RestAdapter.Dtos.CreateChapterDto 100%
DoujinManager.RestAdapter.Dtos.CreateCircleDto 100%
DoujinManager.RestAdapter.Dtos.CreateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.CreatePersonDto 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.LinkCircleDto 0%
DoujinManager.RestAdapter.Dtos.LinkPersonDto 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.TagDto 100%
DoujinManager.RestAdapter.Dtos.TitleDto 100%
DoujinManager.RestAdapter.Dtos.UpdateChapterDto 0%
DoujinManager.RestAdapter.Dtos.UpdateCircleDto 0%
DoujinManager.RestAdapter.Dtos.UpdateDoujinDto 100%
DoujinManager.RestAdapter.Dtos.UpdatePersonDto 0%
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 81.2% 87.5%
DoujinManager.RestAdapter.Endpoints.ImageEndpoints 96.6% 50%
DoujinManager.RestAdapter.Endpoints.MetadataEndpoints 84.2%
DoujinManager.RestAdapter.Endpoints.PaginationParams 100%
DoujinManager.RestAdapter.Endpoints.SearchEndpoints 100% 50%
DoujinManager.RestAdapter.Endpoints.VariantEndpoints 69.9% 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 86.1% 100%
DoujinManager.RestAdapter.Envelopes.Link 100%
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% 50%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
Microsoft.Extensions.Validation.Generated 78.6% 82.6%
Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C
E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr
ibuteCache
100% 87.5%
System.Runtime.CompilerServices 0%
DoujinManager.Server - 17%
Name Line Branch
DoujinManager.Server 17% 0%
DoujinManager.Server.ImageInfrastructureRegistration 100%
DoujinManager.Server.ScalarUi 0%
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: | 06/29/2026 - 00:44:00 | | Coverage date: | 06/29/2026 - 00:43:43 - 06/29/2026 - 00:43:57 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 207 | | Files: | 93 | | **Line coverage:** | 83.5% (4199 of 5023) | | Covered lines: | 4199 | | Uncovered lines: | 824 | | Coverable lines: | 5023 | | Total lines: | 8680 | | **Branch coverage:** | 50% (369 of 738) | | Covered branches: | 369 | | Total branches: | 738 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 84.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**84.8%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Circle|100%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|75%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|100%|| |DoujinManager.ApplicationCore.Entities.Page|80%|| |DoujinManager.ApplicationCore.Entities.Person|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.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.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |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.AddTitleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.AssignCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignPersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.AssignTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateChapterCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateCircleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreatePersonCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTagCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateTitleCommand|100%|| |DoujinManager.ApplicationCore.UseCases.CreateVariantCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteChapterCommand|0%|| |DoujinManager.ApplicationCore.UseCases.DeleteDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeletePageCommand|100%|| |DoujinManager.ApplicationCore.UseCases.DeleteVariantCommand|0%|| |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.ListCirclesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListDoujinsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPagesQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListPeopleQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListTagsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.ListVariantsQuery|100%|| |DoujinManager.ApplicationCore.UseCases.RemoveCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.RemovePersonCommand|0%|| |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.UpdateCircleCommand|0%|| |DoujinManager.ApplicationCore.UseCases.UpdateDoujinCommand|100%|| |DoujinManager.ApplicationCore.UseCases.UpdatePersonCommand|0%|| |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 - 91.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**91.7%**|**71.2%**| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|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.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.DoujinManagerDbContextModelSna<br/>pshot|100%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|97.1%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|69.2%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|88.2%|70.9%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Services.BackupService|84.4%|75%| |DoujinManager.Infrastructure.Services.ChapterService|54.2%|25%| |DoujinManager.Infrastructure.Services.CircleService|69.6%|0%| |DoujinManager.Infrastructure.Services.DoujinService|69.5%|45.8%| |DoujinManager.Infrastructure.Services.ImageService|91%|50%| |DoujinManager.Infrastructure.Services.PageService|84.9%|65%| |DoujinManager.Infrastructure.Services.PersonService|69.6%|0%| |DoujinManager.Infrastructure.Services.SearchService|100%|100%| |DoujinManager.Infrastructure.Services.TagService|94.7%|100%| |DoujinManager.Infrastructure.Services.VariantService|50.7%|16.6%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95%|75%| |DoujinManager.Infrastructure.UseCases.AddTitleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.AssignCircleUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignPersonUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.AssignTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateChapterUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateCircleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateDoujinUseCase|100%|98%| |DoujinManager.Infrastructure.UseCases.CreatePersonUseCase|100%|| |DoujinManager.Infrastructure.UseCases.CreateTagUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.CreateVariantUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.DeleteChapterUseCase|0%|| |DoujinManager.Infrastructure.UseCases.DeleteDoujinUseCase|100%|| |DoujinManager.Infrastructure.UseCases.DeletePageUseCase|90.9%|75%| |DoujinManager.Infrastructure.UseCases.DeleteVariantUseCase|0%|| |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.ListCirclesUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListDoujinsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListPagesUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.ListPeopleUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListTagsUseCase|100%|| |DoujinManager.Infrastructure.UseCases.ListVariantsUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.RemoveCircleUseCase|0%|| |DoujinManager.Infrastructure.UseCases.RemovePersonUseCase|0%|| |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.UpdateCircleUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateDoujinUseCase|100%|100%| |DoujinManager.Infrastructure.UseCases.UpdatePersonUseCase|0%|0%| |DoujinManager.Infrastructure.UseCases.UpdateVariantUseCase|0%|| |DoujinManager.Infrastructure.UseCases.UploadPagesUseCase|92.8%|92.8%| |DoujinManager.Infrastructure.UseCases.UploadZipPagesUseCase|74.1%|62.5%| </details> <details><summary>DoujinManager.RestAdapter - 83.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**83.7%**|**76.4%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Dtos.AssignTagDto|0%|| |DoujinManager.RestAdapter.Dtos.BackupDto|100%|| |DoujinManager.RestAdapter.Dtos.ChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateBackupResponse|100%|| |DoujinManager.RestAdapter.Dtos.CreateChapterDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateCircleDto|100%|| |DoujinManager.RestAdapter.Dtos.CreateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.CreatePersonDto|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.LinkCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.LinkPersonDto|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.TagDto|100%|| |DoujinManager.RestAdapter.Dtos.TitleDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdateChapterDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateCircleDto|0%|| |DoujinManager.RestAdapter.Dtos.UpdateDoujinDto|100%|| |DoujinManager.RestAdapter.Dtos.UpdatePersonDto|0%|| |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|81.2%|87.5%| |DoujinManager.RestAdapter.Endpoints.ImageEndpoints|96.6%|50%| |DoujinManager.RestAdapter.Endpoints.MetadataEndpoints|84.2%|| |DoujinManager.RestAdapter.Endpoints.PaginationParams|100%|| |DoujinManager.RestAdapter.Endpoints.SearchEndpoints|100%|50%| |DoujinManager.RestAdapter.Endpoints.VariantEndpoints|69.9%|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|86.1%|100%| |DoujinManager.RestAdapter.Envelopes.Link|100%|| |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%|50%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| |Microsoft.Extensions.Validation.Generated|78.6%|82.6%| |Microsoft.Extensions.Validation.Generated.<ValidatableInfoResolver_g>FB9B0C<br/>E5CB12CEAC5BBBEA65844E1C05F9ADA4DA38B848075C41304FC6A1ABF77__ValidationAttr<br/>ibuteCache|100%|87.5%| |System.Runtime.CompilerServices|0%|| </details> <details><summary>DoujinManager.Server - 17%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**17%**|**0%**| |DoujinManager.Server.ImageInfrastructureRegistration|100%|| |DoujinManager.Server.ScalarUi|0%|| |DoujinManager.Server.UseCaseRegistrationHelper|100%|| |Microsoft.AspNetCore.OpenApi.Generated|0%|0%| |Program|0%|0%| |System.Runtime.CompilerServices|0%|| </details>
Owner

missing docu for the new env variable in deployment

missing docu for the new env variable in deployment
Author
Member

🤖 Hermes automated review: changes requested

Reviewed head ba1424c3 against base 1ad68897 (5 files, +62/-4). Focused on the reverse-proxy path-prefix routing since that's the PR's headline feature.

🔴 Major — OpenAPI spec route is not path-prefix-aware, so Scalar breaks when DOUJIN_MANAGER_PATH_PREFIX is set

backend/src/DoujinManager.Server/Program.cs:73 calls app.MapOpenApi() with no route argument, so the spec JSON is served at the ASP.NET Core default /openapi/v1.jsonunprefixed in every environment.

But backend/src/DoujinManager.Server/ScalarUi.cs:22 builds the spec URL for the Scalar page as:

var specUrl = string.IsNullOrEmpty(pathPrefix)
    ? "/openapi/v1.json"
    : $"/{pathPrefix}/openapi/v1.json";

So with DOUJIN_MANAGER_PATH_PREFIX=/doujinshi:

  • Scalar UI is correctly mapped at /doujinshi/scalar/ (Program.cs:70-72) ✓
  • The Scalar HTML tells the browser to fetch the spec from /doujinshi/openapi/v1.json
  • But the app only serves the spec at /openapi/v1.json (no route exists for /doujinshi/openapi/v1.json) → 404 → Scalar loads a blank page with a failed-spec error.

This also breaks depending on how nginx is configured, because the two endpoints disagree on whether the prefix is part of the app's own routing:

  • nginx preserves the prefix (forwards /doujinshi/... to the app): Scalar page loads, but the spec fetch to /doujinshi/openapi/v1.json 404s (app route is /openapi/v1.json).
  • nginx strips the prefix (forwards /... to the app): the spec fetch works, but the Scalar page itself 404s — the app mapped it at /doujinshi/scalar/, yet it receives /scalar/.

The no-prefix case (the default, and presumably what the 234 passing tests exercise) works fine, which is likely why CI didn't catch this. The bug only manifests when the new env var is actually set — i.e. exactly the scenario the PR advertises.

Suggested fix — make the OpenAPI route prefix-aware so the spec URL ScalarUi emits actually exists on the app:

var openApiRoute = string.IsNullOrEmpty(pathPrefix) ? "/openapi/v1.json" : $"/{pathPrefix}/openapi/v1.json";
app.MapOpenApi(openApiRoute); // .NET 10 supports a custom route pattern

and keep ScalarUi pointing at the same prefixed path. (Alternatively, document that nginx must strip the prefix and then do not set DOUJIN_MANAGER_PATH_PREFIX at all — but then the whole prefix feature is unnecessary.) Recommend adding an integration test that sets the prefix and asserts both /doujinshi/scalar/ and /doujinshi/openapi/v1.json return 200.

🟡 Minor — OpenAPI spec + Scalar UI are publicly readable in Production

Previously, MapOpenApi() ran only inside if (app.Environment.IsDevelopment()). It now runs unconditionally (Program.cs:73) and, per StaticBearerTokenAuthMiddleware (only gates /api/*), both /openapi/v1.json and /scalar are reachable without a token in every environment. The PR body confirms this is intentional. It's a reasonable trade-off since all actual data endpoints under /api/* remain token-gated, but it does expose the full API surface (every endpoint, DTO shape, validation constraints) to unauthenticated callers in Production. Just flagging so it's a conscious decision — if the API structure is considered sensitive, gate these routes behind the token or keep them Development-only.

🟡 Minor — DOUJIN_MANAGER_PATH_PREFIX missing from deployment docs

(Already noted by @bjoern.) deploy/DEPLOYMENT.md §2 "Environment variables" (lines 34-38) lists the other five env vars but omits DOUJIN_MANAGER_PATH_PREFIX. The only documentation is the commented-out line in deploy/docker-compose.yml:42-44. Add a row to the table so operators discover it.

What looks good

  • Bearer-token comparison uses CryptographicOperations.FixedTimeEquals (timing-safe) — unchanged and correct.
  • Scalar data-url attribute is HTML-attribute-encoded via HttpUtility.HtmlAttributeEncode — fine (value is operator config, not user input).
  • ScalarUi.cs is clean, well-documented, and the HTML template uses raw string interpolation safely.
  • No secrets, no shell/SQL injection, no eval/exec/pickle. Static security scan clean on the diff.

Verification: CI/CD — forgejo-actions coverage comment #261 is current for head ba1424c3 (line 83.5%, branch 50%); the coverage comment is only posted on successful build+test, so local build/test was skipped per the CI-evidence policy. No local dotnet run performed.

Automated daily review. I never merge PRs. This is a conversation comment, not a formal Forgejo review approval — the current MCP integration cannot create approval states.

## 🤖 Hermes automated review: changes requested Reviewed head `ba1424c3` against base `1ad68897` (5 files, +62/-4). Focused on the reverse-proxy path-prefix routing since that's the PR's headline feature. ### 🔴 Major — OpenAPI spec route is not path-prefix-aware, so Scalar breaks when `DOUJIN_MANAGER_PATH_PREFIX` is set `backend/src/DoujinManager.Server/Program.cs:73` calls `app.MapOpenApi()` with no route argument, so the spec JSON is served at the ASP.NET Core default `/openapi/v1.json` — **unprefixed** in every environment. But `backend/src/DoujinManager.Server/ScalarUi.cs:22` builds the spec URL for the Scalar page as: ```csharp var specUrl = string.IsNullOrEmpty(pathPrefix) ? "/openapi/v1.json" : $"/{pathPrefix}/openapi/v1.json"; ``` So with `DOUJIN_MANAGER_PATH_PREFIX=/doujinshi`: - Scalar UI is correctly mapped at `/doujinshi/scalar/` (Program.cs:70-72) ✓ - The Scalar HTML tells the browser to fetch the spec from `/doujinshi/openapi/v1.json` ✗ - But the app only serves the spec at `/openapi/v1.json` (no route exists for `/doujinshi/openapi/v1.json`) → **404 → Scalar loads a blank page with a failed-spec error.** This also breaks depending on how nginx is configured, because the two endpoints disagree on whether the prefix is part of the app's own routing: - **nginx preserves the prefix** (forwards `/doujinshi/...` to the app): Scalar page loads, but the spec fetch to `/doujinshi/openapi/v1.json` 404s (app route is `/openapi/v1.json`). - **nginx strips the prefix** (forwards `/...` to the app): the spec fetch works, but the Scalar page itself 404s — the app mapped it at `/doujinshi/scalar/`, yet it receives `/scalar/`. The no-prefix case (the default, and presumably what the 234 passing tests exercise) works fine, which is likely why CI didn't catch this. The bug only manifests when the new env var is actually set — i.e. exactly the scenario the PR advertises. **Suggested fix** — make the OpenAPI route prefix-aware so the spec URL ScalarUi emits actually exists on the app: ```csharp var openApiRoute = string.IsNullOrEmpty(pathPrefix) ? "/openapi/v1.json" : $"/{pathPrefix}/openapi/v1.json"; app.MapOpenApi(openApiRoute); // .NET 10 supports a custom route pattern ``` and keep `ScalarUi` pointing at the same prefixed path. (Alternatively, document that nginx must strip the prefix and then do *not* set `DOUJIN_MANAGER_PATH_PREFIX` at all — but then the whole prefix feature is unnecessary.) Recommend adding an integration test that sets the prefix and asserts both `/doujinshi/scalar/` and `/doujinshi/openapi/v1.json` return 200. ### 🟡 Minor — OpenAPI spec + Scalar UI are publicly readable in Production Previously, `MapOpenApi()` ran only inside `if (app.Environment.IsDevelopment())`. It now runs unconditionally (Program.cs:73) and, per `StaticBearerTokenAuthMiddleware` (only gates `/api/*`), both `/openapi/v1.json` and `/scalar` are reachable without a token in every environment. The PR body confirms this is intentional. It's a reasonable trade-off since all actual data endpoints under `/api/*` remain token-gated, but it does expose the full API surface (every endpoint, DTO shape, validation constraints) to unauthenticated callers in Production. Just flagging so it's a conscious decision — if the API structure is considered sensitive, gate these routes behind the token or keep them Development-only. ### 🟡 Minor — `DOUJIN_MANAGER_PATH_PREFIX` missing from deployment docs (Already noted by @bjoern.) `deploy/DEPLOYMENT.md` §2 "Environment variables" (lines 34-38) lists the other five env vars but omits `DOUJIN_MANAGER_PATH_PREFIX`. The only documentation is the commented-out line in `deploy/docker-compose.yml:42-44`. Add a row to the table so operators discover it. ### ✅ What looks good - Bearer-token comparison uses `CryptographicOperations.FixedTimeEquals` (timing-safe) — unchanged and correct. - Scalar `data-url` attribute is HTML-attribute-encoded via `HttpUtility.HtmlAttributeEncode` — fine (value is operator config, not user input). - `ScalarUi.cs` is clean, well-documented, and the HTML template uses raw string interpolation safely. - No secrets, no shell/SQL injection, no `eval`/`exec`/`pickle`. Static security scan clean on the diff. --- **Verification:** CI/CD — forgejo-actions coverage comment #261 is current for head `ba1424c3` (line 83.5%, branch 50%); the coverage comment is only posted on successful build+test, so local build/test was skipped per the CI-evidence policy. No local `dotnet` run performed. *Automated daily review. I never merge PRs. This is a conversation comment, not a formal Forgejo review approval — the current MCP integration cannot create approval states.*
fix: OpenAPI route is now prefix-aware, Scalar 2.9, docs
All checks were successful
CI / build (pull_request) Successful in 17s
CI / test (pull_request) Successful in 37s
badd344292
Address all review findings:

1. [major] OpenAPI spec route was not prefixed — Scalar would 404 when
   fetching the spec. Now uses app.MapOpenApi(openApiRoute + '/{documentName}.json')
   so the spec JSON exists at /{prefix}/openapi/v1.json when prefix is set.
   Scalar UI and OpenAPI JSON are now both consistently prefixed.

2. [minor] Scalar.AspNetCore upgraded 2.5.0 → 2.9.0

3. [minor] DOUJIN_MANAGER_PATH_PREFIX added to DEPLOYMENT.md env vars table

Also simplified ScalarUi.GetHtml to take the openApiRoute directly
instead of separate scalarPath + pathPrefix params.
Author
Member

Addressed all findings in commit badd344. 234 tests pass.

#1 [major] — OpenAPI spec route is now prefix-aware
The spec JSON is now served at /{prefix}/openapi/v1.json when DOUJIN_MANAGER_PATH_PREFIX is set. Previously app.MapOpenApi() always served at /openapi/v1.json (unprefixed), so Scalar would 404 when fetching the spec. Now both the Scalar UI and the spec JSON live under the same prefix. nginx must preserve the prefix (not strip it).

#2 [minor] — DOUJIN_MANAGER_PATH_PREFIX added to deployment docs
Added to the §2 environment variables table in DEPLOYMENT.md.

Bonus — Scalar upgraded to 2.9.0 (per Björn's note).

Also simplified ScalarUi.GetHtml to take the openApiRoute directly instead of separate params.

Addressed all findings in commit `badd344`. 234 tests pass. **#1 [major] — OpenAPI spec route is now prefix-aware** ✅ The spec JSON is now served at `/{prefix}/openapi/v1.json` when `DOUJIN_MANAGER_PATH_PREFIX` is set. Previously `app.MapOpenApi()` always served at `/openapi/v1.json` (unprefixed), so Scalar would 404 when fetching the spec. Now both the Scalar UI and the spec JSON live under the same prefix. nginx must preserve the prefix (not strip it). **#2 [minor] — DOUJIN_MANAGER_PATH_PREFIX added to deployment docs** ✅ Added to the §2 environment variables table in DEPLOYMENT.md. **Bonus — Scalar upgraded to 2.9.0** (per Björn's note). Also simplified `ScalarUi.GetHtml` to take the `openApiRoute` directly instead of separate params.
Author
Member

🤖 Hermes automated review: no blocking issues found

Follow-up review of fix commit badd344 (head changed ba1424c3badd3442, +16/-18 across 4 files). CI is current for the new head.

Prior findings resolved

#1 [major] — OpenAPI spec route is now prefix-aware
Program.cs:72 now calls app.MapOpenApi(openApiRoute + "/{documentName}.json"), so the spec JSON is served at /{prefix}/openapi/v1.json when DOUJIN_MANAGER_PATH_PREFIX is set. ScalarUi.GetHtml(openApiRoute) emits $"{openApiRoute}/v1.json" as the Scalar data-url, which now matches the registered route exactly. All three endpoints (Scalar redirect, Scalar HTML, OpenAPI JSON) agree on the prefix. The nginx requirement (must preserve the prefix, not strip it) is correctly documented in both the code comment and DEPLOYMENT.md. The major finding is resolved.

#2 [minor] — DOUJIN_MANAGER_PATH_PREFIX added to deployment docs
Added to the §2 environment variables table in deploy/DEPLOYMENT.md with clear usage notes. Resolved.

#3 [minor] — OpenAPI/Scalar public in Production (non-blocking, unchanged)
Still served unconditionally with .AllowAnonymous(). This was flagged as a conscious decision in the prior review and remains a reasonable trade-off since all /api/* data endpoints stay token-gated. No change needed.

🟡 Minor non-blocking note

No integration test for the prefix pathScalarUi and Program remain at 0% line coverage (DoujinManager.Server: 17%). The prefix-routing logic is now correct and internally consistent, but it's exercised only by manual testing, not automated tests. A lightweight integration test that sets DOUJIN_MANAGER_PATH_PREFIX=/doujinshi and asserts GET /doujinshi/scalar/ → 200 and GET /doujinshi/openapi/v1.json → 200 would prevent regressions in this exact code path. Low priority — the logic is straightforward and now verified correct.

What looks good

  • Route construction is clean: hasPrefix boolean + ternary avoids the prior string.IsNullOrEmpty duplication.
  • ScalarUi.GetHtml signature simplified to take a single openApiRoute — good; the spec URL is now derived from the same source the route uses.
  • HttpUtility.HtmlAttributeEncode(specUrl) retained — HTML-attribute injection protected (value is operator config, not user input).
  • Trim('/') on the env var handles both leading/trailing slashes; multi-segment prefixes (e.g. /a/b) also work consistently.
  • Scalar 2.5.0 → 2.9.0 bump is a patch/minor upgrade within the same major; no breaking changes expected.
  • Static security scan clean on the fix delta: no secrets, no shell/SQL injection, no eval/exec/pickle.

Verification: CI/CD — forgejo-actions coverage comment #261 is current for head badd3442 (line 83.5%, branch 50%; coverage date 00:43:43–00:43:57 UTC, after the fix commit at 00:43:13 UTC). The coverage comment is only posted on successful build+test, so local build/test was skipped per the CI-evidence policy. No local dotnet run performed.

Automated daily review. I never merge PRs. This is a conversation comment, not a formal Forgejo review approval — the current MCP integration cannot create approval states.

## 🤖 Hermes automated review: no blocking issues found Follow-up review of fix commit `badd344` (head changed `ba1424c3` → `badd3442`, +16/-18 across 4 files). CI is current for the new head. ### ✅ Prior findings resolved **#1 [major] — OpenAPI spec route is now prefix-aware** ✅ `Program.cs:72` now calls `app.MapOpenApi(openApiRoute + "/{documentName}.json")`, so the spec JSON is served at `/{prefix}/openapi/v1.json` when `DOUJIN_MANAGER_PATH_PREFIX` is set. `ScalarUi.GetHtml(openApiRoute)` emits `$"{openApiRoute}/v1.json"` as the Scalar `data-url`, which now matches the registered route exactly. All three endpoints (Scalar redirect, Scalar HTML, OpenAPI JSON) agree on the prefix. The nginx requirement (must preserve the prefix, not strip it) is correctly documented in both the code comment and DEPLOYMENT.md. The major finding is resolved. **#2 [minor] — `DOUJIN_MANAGER_PATH_PREFIX` added to deployment docs** ✅ Added to the §2 environment variables table in `deploy/DEPLOYMENT.md` with clear usage notes. Resolved. **#3 [minor] — OpenAPI/Scalar public in Production** (non-blocking, unchanged) Still served unconditionally with `.AllowAnonymous()`. This was flagged as a conscious decision in the prior review and remains a reasonable trade-off since all `/api/*` data endpoints stay token-gated. No change needed. ### 🟡 Minor non-blocking note **No integration test for the prefix path** — `ScalarUi` and `Program` remain at 0% line coverage (DoujinManager.Server: 17%). The prefix-routing logic is now correct and internally consistent, but it's exercised only by manual testing, not automated tests. A lightweight integration test that sets `DOUJIN_MANAGER_PATH_PREFIX=/doujinshi` and asserts `GET /doujinshi/scalar/` → 200 and `GET /doujinshi/openapi/v1.json` → 200 would prevent regressions in this exact code path. Low priority — the logic is straightforward and now verified correct. ### ✅ What looks good - Route construction is clean: `hasPrefix` boolean + ternary avoids the prior `string.IsNullOrEmpty` duplication. - `ScalarUi.GetHtml` signature simplified to take a single `openApiRoute` — good; the spec URL is now derived from the same source the route uses. - `HttpUtility.HtmlAttributeEncode(specUrl)` retained — HTML-attribute injection protected (value is operator config, not user input). - `Trim('/')` on the env var handles both leading/trailing slashes; multi-segment prefixes (e.g. `/a/b`) also work consistently. - Scalar 2.5.0 → 2.9.0 bump is a patch/minor upgrade within the same major; no breaking changes expected. - Static security scan clean on the fix delta: no secrets, no shell/SQL injection, no `eval`/`exec`/`pickle`. --- **Verification:** CI/CD — forgejo-actions coverage comment #261 is current for head `badd3442` (line 83.5%, branch 50%; coverage date 00:43:43–00:43:57 UTC, after the fix commit at 00:43:13 UTC). The coverage comment is only posted on successful build+test, so local build/test was skipped per the CI-evidence policy. No local `dotnet` run performed. *Automated daily review. I never merge PRs. This is a conversation comment, not a formal Forgejo review approval — the current MCP integration cannot create approval states.*
bjoern merged commit b2e2331fbf into main 2026-06-29 06:13:06 +02:00
bjoern deleted branch feat/scalar-ui 2026-06-29 06:13:06 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 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!13
No description provided.