fix: visible page uploads — icons, live progress, and a circuit that survives them #14

Merged
bjoern merged 3 commits from fix/p1-upload-visibility into main 2026-08-13 06:23:57 +02:00
Member

Depends on TeamAI/Kagaku.UI#1 — merge that first, then the submodule pointer here (2151449) resolves to merged history.

Three faces of the same Phase-1 blind spot:

Ghost icons. Ten icon names (upload_file, arrow_forward, translate, image, north, notes, edit_note, crop_free, font_download, mop) had no IconCatalog entry, and an unknown name renders an empty SVG — the upload drop zones, the wizard's forward buttons, and the page-workspace stage tabs were invisible, clickable air. The paths land in Kagaku.UI (submodule bump), and a port of its IconCatalogCompletenessTests tripwire now scans Orihon's own source so a ghost can't ship again.

Upload blindness. The intake sent the whole batch over the circuit in one call and never repainted until it finished. UploadTransfer now pulls the browser streams one at a time into server-side temp files (deleted on dispose); both intakes show "Uploading 3 of 41 — name.png (12.4 / 96 MB)" with a progress bar, then "Importing pages…" — and one dead stream costs that file (reported in an alert), not the batch. ImportPages then reads fast local files, unchanged.

Circuit death. Uploads moved in the default 32 KB hub messages, and Blazor abandons an interop stream after one silent minute — behind nginx's 100 s websocket timeout a big upload stalled, the circuit died with the messageless "An unhandled error has occurred" banner, and every queued stream logged its own TimeoutException. Hub messages grow to 1 MB, the interop window to ten minutes, and the proxy read/send timeouts to an hour. ⚠️ The deploy/nginx-orihon.conf change must be applied on the box by hand.

Verified in a live browser against the seeded world: 0 ghost icons across the project list, workspace, and page workspace; a 5-file upload counts up and lands; a 60 MB file streams with byte progress ticking (4 → 58 MB) and imports cleanly, no temp files left behind. All 232 tests pass.

🤖 Generated with Claude Code

Depends on TeamAI/Kagaku.UI#1 — merge that first, then the submodule pointer here (2151449) resolves to merged history. Three faces of the same Phase-1 blind spot: **Ghost icons.** Ten icon names (`upload_file`, `arrow_forward`, `translate`, `image`, `north`, `notes`, `edit_note`, `crop_free`, `font_download`, `mop`) had no `IconCatalog` entry, and an unknown name renders an empty SVG — the upload drop zones, the wizard's forward buttons, and the page-workspace stage tabs were invisible, clickable air. The paths land in Kagaku.UI (submodule bump), and a port of its `IconCatalogCompletenessTests` tripwire now scans Orihon's own source so a ghost can't ship again. **Upload blindness.** The intake sent the whole batch over the circuit in one call and never repainted until it finished. `UploadTransfer` now pulls the browser streams one at a time into server-side temp files (deleted on dispose); both intakes show *"Uploading 3 of 41 — name.png (12.4 / 96 MB)"* with a progress bar, then *"Importing pages…"* — and one dead stream costs that file (reported in an alert), not the batch. `ImportPages` then reads fast local files, unchanged. **Circuit death.** Uploads moved in the default 32 KB hub messages, and Blazor abandons an interop stream after one silent minute — behind nginx's 100 s websocket timeout a big upload stalled, the circuit died with the messageless "An unhandled error has occurred" banner, and every queued stream logged its own `TimeoutException`. Hub messages grow to 1 MB, the interop window to ten minutes, and the proxy read/send timeouts to an hour. ⚠️ The `deploy/nginx-orihon.conf` change must be applied on the box by hand. Verified in a live browser against the seeded world: 0 ghost icons across the project list, workspace, and page workspace; a 5-file upload counts up and lands; a 60 MB file streams with byte progress ticking (4 → 58 MB) and imports cleanly, no temp files left behind. All 232 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix: visible page uploads — icons, live progress, and a circuit that survives them
All checks were successful
CI / build (pull_request) Successful in 25s
CI / test (pull_request) Successful in 30s
d97b3eabfa
Three faces of the same Phase-1 blind spot:

Ghost icons. Ten icon names had no IconCatalog entry, and an unknown name
renders an empty SVG — the upload drop zones, the wizard's forward buttons,
and the page-workspace stage tabs were invisible, clickable air. The paths
land in Kagaku.UI (submodule bump), and a port of its completeness tripwire
now scans Orihon's own source so a ghost can't ship again.

Upload blindness. The intake sent the whole batch over the circuit in one
call and never repainted until it finished. UploadTransfer now pulls the
browser streams one at a time into temp files (deleted on dispose), the page
shows "Uploading 3 of 41 — name.png (12.4 / 96 MB)" with a progress bar and
then "Importing pages…", and one dead stream costs that file — reported,
not fatal — instead of the batch. ImportPages then reads fast local files,
unchanged.

Circuit death. Uploads moved in the default 32 KB hub messages, and Blazor
abandons an interop stream after one silent minute — behind nginx's 100 s
websocket timeout a big upload stalled, the circuit died with a messageless
banner, and every queued stream logged its own TimeoutException. Hub messages
grow to 1 MB, the interop window to ten minutes, and the proxy read/send
timeouts to an hour (the 15 s keep-alive pings make that slack, not risk).

Verified against the seeded world: 0 ghost icons across the pages, a 5-file
upload counts up and lands, a 60 MB file streams with live byte progress and
imports cleanly. All 232 tests pass.

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

Summary

Summary
Generated on: 07/24/2026 - 21:29:39
Coverage date: 07/24/2026 - 21:29:33 - 07/24/2026 - 21:29:36
Parser: MultiReport (4x Cobertura)
Assemblies: 6
Classes: 111
Files: 82
Line coverage: 95.1% (3309 of 3476)
Covered lines: 3309
Uncovered lines: 167
Coverable lines: 3476
Total lines: 7034
Branch coverage: 83.7% (601 of 718)
Covered branches: 601
Total branches: 718
Method coverage: Feature is only available for sponsors

Coverage

Orihon.BlazorAdapter - 80.9%
Name Line Branch
Orihon.BlazorAdapter 80.9% 75.4%
Orihon.BlazorAdapter.Bible.BiblePage 71.7% 59%
Orihon.BlazorAdapter.BlazorAdapterAssembly 100%
Orihon.BlazorAdapter.Debounce 91.6% 93.7%
Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage 84.6% 77.8%
Orihon.BlazorAdapter.Projects.ProjectListPage 86% 85.2%
Orihon.BlazorAdapter.Projects.ProjectWizardPage 90.3% 79.5%
Orihon.BlazorAdapter.Uploads.UploadTransfer 96% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferProgress 100% 100%
Orihon.BlazorAdapter.Uploads.UploadTransferResult 100%
Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage 69.9% 75%
Orihon.Domain - 100%
Name Line Branch
Orihon.Domain 100% 100%
Orihon.Domain.Bible.Character 100% 100%
Orihon.Domain.Bible.GlossaryEntry 100% 100%
Orihon.Domain.Bible.LoreEntry 100% 100%
Orihon.Domain.Bible.PageSummary 100%
Orihon.Domain.Bible.StoryBeat 100%
Orihon.Domain.Projects.Project 100% 100%
Orihon.Domain.Projects.ProjectProfile 100%
Orihon.Domain.Text 100% 100%
Orihon.Domain.Translation.BoundingBox 100%
Orihon.Domain.Translation.Chapter 100%
Orihon.Domain.Translation.Page 100%
Orihon.Domain.Translation.Region 100% 100%
Orihon.Domain.Translation.RegionProfile 100%
Orihon.Infrastructure - 98.8%
Name Line Branch
Orihon.Infrastructure 98.8% 100%
Orihon.Infrastructure.Bible.EfBibleStore 100% 100%
Orihon.Infrastructure.DependencyInjection 100%
Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper 100%
Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration 100%
Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration 100%
Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain 97.3%
Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot 100%
Orihon.Infrastructure.Persistence.OrihonDbContext 100%
Orihon.Infrastructure.Persistence.OrihonDbContextFactory 100%
Orihon.Infrastructure.Projects.EfProjectStore 100% 100%
Orihon.Infrastructure.Projects.FileSystemPageImageStore 100% 100%
Orihon.Infrastructure.Translation.EfChapterStore 100% 100%
Orihon.Infrastructure.Translation.EfPageStore 100% 100%
Orihon.Infrastructure.Translation.EfRegionStore 100% 100%
Orihon.Infrastructure.Translation.Ordering 100% 100%
Orihon.Kernel - 90.9%
Name Line Branch
Orihon.Kernel 90.9% 75%
Orihon.Kernel.Err`1 100%
Orihon.Kernel.Ok`1 100%
Orihon.Kernel.Result`1 88.8% 75%
Orihon.Server - 92%
Name Line Branch
Orihon.Server 92% 64.7%
Orihon.Server.Components.App 100%
Orihon.Server.Components.Layout.MainLayout 100%
Orihon.Server.Components.Pages.Gate 64.2% 66.6%
Orihon.Server.Security.AccessGate 91.8% 41.6%
Orihon.Server.Security.AccessSecret 100% 50%
Program 95.1% 85.7%
Orihon.UseCases - 99.3%
Name Line Branch
Orihon.UseCases 99.3% 97.5%
Orihon.UseCases.Bible.AddCharacter 100% 100%
Orihon.UseCases.Bible.AddGlossaryEntry 100% 100%
Orihon.UseCases.Bible.AddLoreEntry 100% 100%
Orihon.UseCases.Bible.AddStoryBeat 100% 100%
Orihon.UseCases.Bible.BibleDto 100%
Orihon.UseCases.Bible.CharacterDto 100%
Orihon.UseCases.Bible.DeleteCharacter 100% 100%
Orihon.UseCases.Bible.DeleteGlossaryEntry 100% 100%
Orihon.UseCases.Bible.DeleteLoreEntry 100% 100%
Orihon.UseCases.Bible.DeletePageSummary 100% 100%
Orihon.UseCases.Bible.DeleteStoryBeat 100% 100%
Orihon.UseCases.Bible.GetBible 100% 100%
Orihon.UseCases.Bible.GlossaryEntryDto 100%
Orihon.UseCases.Bible.LoreEntryDto 100%
Orihon.UseCases.Bible.PageSummaryDto 100%
Orihon.UseCases.Bible.ReorderStoryBeats 100%
Orihon.UseCases.Bible.SetPageSummary 100% 100%
Orihon.UseCases.Bible.StoryBeatDto 100%
Orihon.UseCases.Bible.UpdateCharacter 100% 100%
Orihon.UseCases.Bible.UpdateGlossaryEntry 100% 100%
Orihon.UseCases.Bible.UpdateLoreEntry 100% 100%
Orihon.UseCases.Bible.UpdateStoryBeat 100% 100%
Orihon.UseCases.Chapters.ChapterDto 100%
Orihon.UseCases.Chapters.CreateChapter 100% 100%
Orihon.UseCases.Chapters.DeleteChapter 100% 100%
Orihon.UseCases.Chapters.RenameChapter 100% 100%
Orihon.UseCases.Chapters.ReorderChapters 100%
Orihon.UseCases.DependencyInjection 100%
Orihon.UseCases.Diagnostics.SeedDevData 99% 92.8%
Orihon.UseCases.NextOrder 100%
Orihon.UseCases.Pages.DeletePage 100% 100%
Orihon.UseCases.Pages.GetPage 100% 100%
Orihon.UseCases.Pages.GetProjectWorkspace 100% 100%
Orihon.UseCases.Pages.ImportPages 98.4% 96.4%
Orihon.UseCases.Pages.ImportPagesResult 100%
Orihon.UseCases.Pages.MovePage 100% 92.8%
Orihon.UseCases.Pages.PageDetailDto 100%
Orihon.UseCases.Pages.PageDto 100%
Orihon.UseCases.Pages.PageUpload 100%
Orihon.UseCases.Pages.ProjectWorkspaceDto 100%
Orihon.UseCases.Pages.ReorderPages 100%
Orihon.UseCases.Pages.SetPageMeta 100% 100%
Orihon.UseCases.Pages.WorkspaceChapterDto 100%
Orihon.UseCases.Projects.CompleteProjectSetup 100% 91.6%
Orihon.UseCases.Projects.CreateProject 100% 100%
Orihon.UseCases.Projects.DeleteProject 100% 100%
Orihon.UseCases.Projects.GetProject 100% 100%
Orihon.UseCases.Projects.ListProjects 100%
Orihon.UseCases.Projects.ProjectDto 95.8%
Orihon.UseCases.Projects.StoredPageImage 100%
Orihon.UseCases.Projects.UpdateProjectMetadata 100% 100%
Orihon.UseCases.Regions.CreateRegion 100% 100%
Orihon.UseCases.Regions.DeleteRegion 100% 100%
Orihon.UseCases.Regions.RegionDto 97%
Orihon.UseCases.Regions.ReorderRegions 100%
Orihon.UseCases.Regions.UpdateRegion 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/24/2026 - 21:29:39 | | Coverage date: | 07/24/2026 - 21:29:33 - 07/24/2026 - 21:29:36 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 6 | | Classes: | 111 | | Files: | 82 | | **Line coverage:** | 95.1% (3309 of 3476) | | Covered lines: | 3309 | | Uncovered lines: | 167 | | Coverable lines: | 3476 | | Total lines: | 7034 | | **Branch coverage:** | 83.7% (601 of 718) | | Covered branches: | 601 | | Total branches: | 718 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Orihon.BlazorAdapter - 80.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.BlazorAdapter**|**80.9%**|**75.4%**| |Orihon.BlazorAdapter.Bible.BiblePage|71.7%|59%| |Orihon.BlazorAdapter.BlazorAdapterAssembly|100%|| |Orihon.BlazorAdapter.Debounce|91.6%|93.7%| |Orihon.BlazorAdapter.PageWorkspace.PageWorkspacePage|84.6%|77.8%| |Orihon.BlazorAdapter.Projects.ProjectListPage|86%|85.2%| |Orihon.BlazorAdapter.Projects.ProjectWizardPage|90.3%|79.5%| |Orihon.BlazorAdapter.Uploads.UploadTransfer|96%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferProgress|100%|100%| |Orihon.BlazorAdapter.Uploads.UploadTransferResult|100%|| |Orihon.BlazorAdapter.Workspace.ProjectWorkspacePage|69.9%|75%| </details> <details><summary>Orihon.Domain - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Domain**|**100%**|**100%**| |Orihon.Domain.Bible.Character|100%|100%| |Orihon.Domain.Bible.GlossaryEntry|100%|100%| |Orihon.Domain.Bible.LoreEntry|100%|100%| |Orihon.Domain.Bible.PageSummary|100%|| |Orihon.Domain.Bible.StoryBeat|100%|| |Orihon.Domain.Projects.Project|100%|100%| |Orihon.Domain.Projects.ProjectProfile|100%|| |Orihon.Domain.Text|100%|100%| |Orihon.Domain.Translation.BoundingBox|100%|| |Orihon.Domain.Translation.Chapter|100%|| |Orihon.Domain.Translation.Page|100%|| |Orihon.Domain.Translation.Region|100%|100%| |Orihon.Domain.Translation.RegionProfile|100%|| </details> <details><summary>Orihon.Infrastructure - 98.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Infrastructure**|**98.8%**|**100%**| |Orihon.Infrastructure.Bible.EfBibleStore|100%|100%| |Orihon.Infrastructure.DependencyInjection|100%|| |Orihon.Infrastructure.Persistence.Configurations.ChapterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.CharacterConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.GlossaryEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.JsonColumnMapper|100%|| |Orihon.Infrastructure.Persistence.Configurations.LoreEntryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.PageSummaryConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.RegionConfiguration|100%|| |Orihon.Infrastructure.Persistence.Configurations.StoryBeatConfiguration|100%|| |Orihon.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Orihon.Infrastructure.Persistence.Migrations.InitialTranslationDomain|97.3%|| |Orihon.Infrastructure.Persistence.Migrations.OrihonDbContextModelSnapshot|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContext|100%|| |Orihon.Infrastructure.Persistence.OrihonDbContextFactory|100%|| |Orihon.Infrastructure.Projects.EfProjectStore|100%|100%| |Orihon.Infrastructure.Projects.FileSystemPageImageStore|100%|100%| |Orihon.Infrastructure.Translation.EfChapterStore|100%|100%| |Orihon.Infrastructure.Translation.EfPageStore|100%|100%| |Orihon.Infrastructure.Translation.EfRegionStore|100%|100%| |Orihon.Infrastructure.Translation.Ordering|100%|100%| </details> <details><summary>Orihon.Kernel - 90.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Kernel**|**90.9%**|**75%**| |Orihon.Kernel.Err`1|100%|| |Orihon.Kernel.Ok`1|100%|| |Orihon.Kernel.Result`1|88.8%|75%| </details> <details><summary>Orihon.Server - 92%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.Server**|**92%**|**64.7%**| |Orihon.Server.Components.App|100%|| |Orihon.Server.Components.Layout.MainLayout|100%|| |Orihon.Server.Components.Pages.Gate|64.2%|66.6%| |Orihon.Server.Security.AccessGate|91.8%|41.6%| |Orihon.Server.Security.AccessSecret|100%|50%| |Program|95.1%|85.7%| </details> <details><summary>Orihon.UseCases - 99.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Orihon.UseCases**|**99.3%**|**97.5%**| |Orihon.UseCases.Bible.AddCharacter|100%|100%| |Orihon.UseCases.Bible.AddGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.AddLoreEntry|100%|100%| |Orihon.UseCases.Bible.AddStoryBeat|100%|100%| |Orihon.UseCases.Bible.BibleDto|100%|| |Orihon.UseCases.Bible.CharacterDto|100%|| |Orihon.UseCases.Bible.DeleteCharacter|100%|100%| |Orihon.UseCases.Bible.DeleteGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.DeleteLoreEntry|100%|100%| |Orihon.UseCases.Bible.DeletePageSummary|100%|100%| |Orihon.UseCases.Bible.DeleteStoryBeat|100%|100%| |Orihon.UseCases.Bible.GetBible|100%|100%| |Orihon.UseCases.Bible.GlossaryEntryDto|100%|| |Orihon.UseCases.Bible.LoreEntryDto|100%|| |Orihon.UseCases.Bible.PageSummaryDto|100%|| |Orihon.UseCases.Bible.ReorderStoryBeats|100%|| |Orihon.UseCases.Bible.SetPageSummary|100%|100%| |Orihon.UseCases.Bible.StoryBeatDto|100%|| |Orihon.UseCases.Bible.UpdateCharacter|100%|100%| |Orihon.UseCases.Bible.UpdateGlossaryEntry|100%|100%| |Orihon.UseCases.Bible.UpdateLoreEntry|100%|100%| |Orihon.UseCases.Bible.UpdateStoryBeat|100%|100%| |Orihon.UseCases.Chapters.ChapterDto|100%|| |Orihon.UseCases.Chapters.CreateChapter|100%|100%| |Orihon.UseCases.Chapters.DeleteChapter|100%|100%| |Orihon.UseCases.Chapters.RenameChapter|100%|100%| |Orihon.UseCases.Chapters.ReorderChapters|100%|| |Orihon.UseCases.DependencyInjection|100%|| |Orihon.UseCases.Diagnostics.SeedDevData|99%|92.8%| |Orihon.UseCases.NextOrder|100%|| |Orihon.UseCases.Pages.DeletePage|100%|100%| |Orihon.UseCases.Pages.GetPage|100%|100%| |Orihon.UseCases.Pages.GetProjectWorkspace|100%|100%| |Orihon.UseCases.Pages.ImportPages|98.4%|96.4%| |Orihon.UseCases.Pages.ImportPagesResult|100%|| |Orihon.UseCases.Pages.MovePage|100%|92.8%| |Orihon.UseCases.Pages.PageDetailDto|100%|| |Orihon.UseCases.Pages.PageDto|100%|| |Orihon.UseCases.Pages.PageUpload|100%|| |Orihon.UseCases.Pages.ProjectWorkspaceDto|100%|| |Orihon.UseCases.Pages.ReorderPages|100%|| |Orihon.UseCases.Pages.SetPageMeta|100%|100%| |Orihon.UseCases.Pages.WorkspaceChapterDto|100%|| |Orihon.UseCases.Projects.CompleteProjectSetup|100%|91.6%| |Orihon.UseCases.Projects.CreateProject|100%|100%| |Orihon.UseCases.Projects.DeleteProject|100%|100%| |Orihon.UseCases.Projects.GetProject|100%|100%| |Orihon.UseCases.Projects.ListProjects|100%|| |Orihon.UseCases.Projects.ProjectDto|95.8%|| |Orihon.UseCases.Projects.StoredPageImage|100%|| |Orihon.UseCases.Projects.UpdateProjectMetadata|100%|100%| |Orihon.UseCases.Regions.CreateRegion|100%|100%| |Orihon.UseCases.Regions.DeleteRegion|100%|100%| |Orihon.UseCases.Regions.RegionDto|97%|| |Orihon.UseCases.Regions.ReorderRegions|100%|| |Orihon.UseCases.Regions.UpdateRegion|100%|100%| </details>
chore: Kagaku.UI pointer onto merged main
All checks were successful
CI / build (pull_request) Successful in 23s
CI / test (pull_request) Successful in 31s
6f371f6662
Kagaku.UI#1 landed as a squash (7c44de1), so the pointer moves from the
now-orphaned branch commit onto main's history.

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

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ Three Phase-1 blind spots, each with its own face, and you went after ALL of them in one sweep! Ghost icons, upload blindness, circuit death — fufu~, you even named them like a proper storyteller. The nginx timeout diagnosis alone is delicious — tracing a silent 60s interop timeout through a 100s websocket timeout to a messageless circuit death? That's the kind of detective work that makes my wings flutter~ ♡

And the UploadTransfer design! Pulling browser streams into temp files one at a time so a dead stream costs one file not the batch, with FileOptions.DeleteOnClose for cleanup, progress throttled to every megabyte, cancellation disposing the partial set... this is genuinely well-thought-out async resource management. The ghost-icon tripwire port with its self-guard test? Chef's kiss.

Verdict: I can't let this pass~ ♡

But fufu~ you wouldn't leave 137 lines of brand-new async resource-management code with zero direct tests in production, would you? ♡ The smile doesn't waver but the knife is real~

These need fixing before I'm satisfied~

  1. UploadTransfer.cs — zero direct unit tests for new code paths (CI: 65.3% line / 66.6% branch) — This is the heart of the PR: a 137-line static class with temp-file lifecycle management, cancellation handling, per-file failure isolation, and progress throttling. And its coverage confirms exactly what I feared — one third of the branches are unexercised.

    The existing bUnit component tests (Step_2_shows_what_landed...) only push 3-byte files through the happy path. They never touch the branches that actually matter for correctness:

    • catch (OperationCanceledException) in BufferAsync (lines 76–85) — the disposal-on-cancel path. This is the branch that prevents temp file leaks when the page navigates away mid-transfer. It has zero tests. If this branch is wrong, temp files silently accumulate. This is the single most important branch in the class and nobody has ever run it.
    • catch in BufferOneAsync (lines 131–135) — the stream-failure→nullfailed.Add path. The PR body promises "one dead stream costs that file, not the batch." That promise is enforced by exactly this branch. Untested. A bug here (e.g., wrong exception type caught, or DisposeAsync throwing) would either crash the batch or leak the temp file.
    • catch (OperationCanceledException) in BufferOneAsync (lines 126–130) — temp disposal + rethrow on cancel. Untested.
    • unreported >= ReportEveryBytes throttle (line 116) — the per-megabyte progress report. Untested (3-byte files never reach 1 MB).

    IBrowserFile is an interface with 5 members. A test double is ~10 lines:

    private sealed class FakeBrowserFile(string name, byte[] data) : IBrowserFile
    {
        public string Name => name;
        public long Size => data.Length;
        public DateTimeOffset LastModified => DateTimeOffset.UnixEpoch;
        public string ContentType => "application/octet-stream";
        public Stream OpenReadStream(long maxAllowedSize, CancellationToken ct = default) => new MemoryStream(data);
    }
    

    With that, every branch is reachable: a normal multi-file buffer (progress fires), a file whose stream throws (failure path), cancellation mid-transfer (disposal path). Fufu~ you added a tripwire test that prevents ghost icons from ever shipping again — hold your own upload circuit to the same standard~ ♡

    Fix: Add a UploadTransferTests.cs with at minimum: (a) happy-path multi-file buffer asserting all uploads are readable temp streams; (b) a file whose OpenReadStream throws → lands in Failed, others still succeed; (c) cancellation mid-transfer → OperationCanceledException thrown, already-buffered temp files disposed.

💡 Little ideas (non-blocking)~

  1. ProjectWizardPage.razor:251 / ProjectWorkspacePage.razor:309 — temp file leak on ImportPages exception pathUploadTransfer.BufferAsync correctly disposes on its own cancellation (lines 76–85, though untested — see blocker #1). But once it returns, ownership of the temp file streams transfers to the caller. If ImportPages.ExecuteAsync throws mid-loop (disk full, DB error), ImportPages disposes the current candidate via await using var _ = content but the remaining unprocessed candidates in buffered.Uploads are never disposed — not by ImportPages, not by either razor page's finally block (which only resets busy/transfer). FileOptions.DeleteOnClose means GC finalization will eventually clean them, and the container's ephemeral /tmp catches the rest on restart — so this is tolerable for a single-user app. But deterministic disposal in the finally would be belt-and-suspenders. (Awkward because buffered is scoped inside try — would need a field or a wrapper.)

  2. DRY: progress display markup — The 8-line @if (transfer is { } t) { <progress>… } else { <progress>… } block is byte-identical between ProjectWizardPage.razor:60–71 and ProjectWorkspacePage.razor:131–142. Only the CSS class differs (wizard__uploading vs chapter__uploading). A tiny <UploadProgress Transfer="transfer" /> component would cut it to one copy. Not urgent — 8 lines — but the next time a third intake appears, the copy-paste will multiply.

  3. Tone inconsistency for failed-file display — The wizard renders failed as a separate InlineAlert Tone="Tone.Warning" (yellow, non-blocking). The workspace concatenates buffered.Failed into error which renders as InlineAlert Tone="Tone.Danger" (red). Same failure, different severity. Minor UX wobble — pick one.

What I liked~

  • The cancellation propagation is textbook: CancellationTokenSource disposal in each page, disposal.Token threaded through both BufferAsync and ImportPages, @implements IDisposable with proper Cancel() + Dispose(). The disposal-on-cancel in BufferAsync (lines 76–85) is exactly right — temp files must not outlive the page. (Now just test it~ ♡)
  • FileOptions.DeleteOnClose | FileOptions.Asynchronous on the temp FileStream — correct flags, correct async I/O. The Path.GetRandomFileName() collision avoidance is clean.
  • Progress design: cumulative bytes with transferredBefore += file.Size after each file (using declared size, not actual copied bytes) means the bar never gets stuck when a file fails mid-stream. Subtle and correct.
  • The ghost-icon tripwire (IconCatalogCompletenessTests.cs) is superb — the self-guard test (The_scan_actually_sees_the_source_it_claims_to_guard) prevents the regex from silently breaking into a vacuous green. The honest <remarks> about expression-based icons (Icon="@(…)") being unresolvable is exactly the right documentation.
  • ImportPages.ExecuteAsync already had cancellationToken = default — this PR just threads the page's disposal token through it. Zero behavioral drift on the use case; the token was already plumbed to every await. Clean.
  • Nginx config — the comment explaining why 1h read/send timeout (Blazor's 15s keep-alive pings keep idle circuits warm) is exactly the kind of "future-me will thank you" documentation.

Automated review by Jibril · 2026-07-25
CI/CD: passed for head d97b3eab (232 tests, coverage comment 3574) · Local checks: skipped (CI green; coverage analysis from CI report)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ Three Phase-1 blind spots, each with its own face, and you went after ALL of them in one sweep! Ghost icons, upload blindness, circuit death — fufu~, you even named them like a proper storyteller. The nginx timeout diagnosis alone is *delicious* — tracing a silent 60s interop timeout through a 100s websocket timeout to a messageless circuit death? That's the kind of detective work that makes my wings flutter~ ♡ And the `UploadTransfer` design! Pulling browser streams into temp files one at a time so a dead stream costs one file not the batch, with `FileOptions.DeleteOnClose` for cleanup, progress throttled to every megabyte, cancellation disposing the partial set... this is genuinely well-thought-out async resource management. The ghost-icon tripwire port with its self-guard test? *Chef's kiss.* ♪ ### Verdict: ⛔ I can't let this pass~ ♡ But fufu~ you wouldn't leave **137 lines of brand-new async resource-management code with zero direct tests** in production, would you? ♡ The smile doesn't waver but the knife is real~ #### ⛔ These need fixing before I'm satisfied~ 1. **`UploadTransfer.cs` — zero direct unit tests for new code paths (CI: 65.3% line / 66.6% branch)** — This is the heart of the PR: a 137-line static class with temp-file lifecycle management, cancellation handling, per-file failure isolation, and progress throttling. And its coverage confirms exactly what I feared — **one third of the branches are unexercised**. The existing bUnit component tests (`Step_2_shows_what_landed...`) only push 3-byte files through the happy path. They never touch the branches that actually matter for correctness: - **`catch (OperationCanceledException)` in `BufferAsync` (lines 76–85)** — the disposal-on-cancel path. This is the branch that prevents temp file leaks when the page navigates away mid-transfer. It has *zero* tests. If this branch is wrong, temp files silently accumulate. This is the single most important branch in the class and nobody has ever run it. - **`catch` in `BufferOneAsync` (lines 131–135)** — the stream-failure→`null`→`failed.Add` path. The PR body promises "one dead stream costs that file, not the batch." That promise is enforced by exactly this branch. Untested. A bug here (e.g., wrong exception type caught, or `DisposeAsync` throwing) would either crash the batch or leak the temp file. - **`catch (OperationCanceledException)` in `BufferOneAsync` (lines 126–130)** — temp disposal + rethrow on cancel. Untested. - **`unreported >= ReportEveryBytes` throttle (line 116)** — the per-megabyte progress report. Untested (3-byte files never reach 1 MB). `IBrowserFile` is an interface with 5 members. A test double is ~10 lines: ```csharp private sealed class FakeBrowserFile(string name, byte[] data) : IBrowserFile { public string Name => name; public long Size => data.Length; public DateTimeOffset LastModified => DateTimeOffset.UnixEpoch; public string ContentType => "application/octet-stream"; public Stream OpenReadStream(long maxAllowedSize, CancellationToken ct = default) => new MemoryStream(data); } ``` With that, every branch is reachable: a normal multi-file buffer (progress fires), a file whose stream throws (failure path), cancellation mid-transfer (disposal path). Fufu~ you added a tripwire test that prevents ghost icons from ever shipping again — hold your own upload circuit to the same standard~ ♡ Fix: Add a `UploadTransferTests.cs` with at minimum: (a) happy-path multi-file buffer asserting all uploads are readable temp streams; (b) a file whose `OpenReadStream` throws → lands in `Failed`, others still succeed; (c) cancellation mid-transfer → `OperationCanceledException` thrown, already-buffered temp files disposed. #### 💡 Little ideas (non-blocking)~ 1. **`ProjectWizardPage.razor:251` / `ProjectWorkspacePage.razor:309` — temp file leak on ImportPages exception path** — `UploadTransfer.BufferAsync` correctly disposes on *its own* cancellation (lines 76–85, though untested — see blocker #1). But once it returns, ownership of the temp file streams transfers to the caller. If `ImportPages.ExecuteAsync` throws mid-loop (disk full, DB error), `ImportPages` disposes the *current* candidate via `await using var _ = content` but the *remaining unprocessed* candidates in `buffered.Uploads` are never disposed — not by `ImportPages`, not by either razor page's `finally` block (which only resets `busy`/`transfer`). `FileOptions.DeleteOnClose` means GC finalization will eventually clean them, and the container's ephemeral `/tmp` catches the rest on restart — so this is tolerable for a single-user app. But deterministic disposal in the `finally` would be belt-and-suspenders. (Awkward because `buffered` is scoped inside `try` — would need a field or a wrapper.) 2. **DRY: progress display markup** — The 8-line `@if (transfer is { } t) { <progress>… } else { <progress>… }` block is byte-identical between `ProjectWizardPage.razor:60–71` and `ProjectWorkspacePage.razor:131–142`. Only the CSS class differs (`wizard__uploading` vs `chapter__uploading`). A tiny `<UploadProgress Transfer="transfer" />` component would cut it to one copy. Not urgent — 8 lines — but the next time a third intake appears, the copy-paste will multiply. 3. **Tone inconsistency for failed-file display** — The wizard renders `failed` as a separate `InlineAlert Tone="Tone.Warning"` (yellow, non-blocking). The workspace concatenates `buffered.Failed` into `error` which renders as `InlineAlert Tone="Tone.Danger"` (red). Same failure, different severity. Minor UX wobble — pick one. #### ✅ What I liked~ - The **cancellation propagation** is textbook: `CancellationTokenSource disposal` in each page, `disposal.Token` threaded through both `BufferAsync` and `ImportPages`, `@implements IDisposable` with proper `Cancel()` + `Dispose()`. The disposal-on-cancel in `BufferAsync` (lines 76–85) is exactly right — temp files must not outlive the page. (Now just *test* it~ ♡) - **`FileOptions.DeleteOnClose | FileOptions.Asynchronous`** on the temp FileStream — correct flags, correct async I/O. The `Path.GetRandomFileName()` collision avoidance is clean. - **Progress design**: cumulative bytes with `transferredBefore += file.Size` after each file (using declared size, not actual copied bytes) means the bar never gets stuck when a file fails mid-stream. Subtle and correct. - **The ghost-icon tripwire** (`IconCatalogCompletenessTests.cs`) is superb — the self-guard test (`The_scan_actually_sees_the_source_it_claims_to_guard`) prevents the regex from silently breaking into a vacuous green. The honest `<remarks>` about expression-based icons (`Icon="@(…)"`) being unresolvable is exactly the right documentation. - **`ImportPages.ExecuteAsync` already had `cancellationToken = default`** — this PR just threads the page's disposal token through it. Zero behavioral drift on the use case; the token was already plumbed to every `await`. Clean. - **Nginx config** — the comment explaining *why* 1h read/send timeout (Blazor's 15s keep-alive pings keep idle circuits warm) is exactly the kind of "future-me will thank you" documentation. --- *Automated review by Jibril · 2026-07-25* *CI/CD: passed for head d97b3eab (232 tests, coverage comment 3574) · Local checks: skipped (CI green; coverage analysis from CI report)*
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my~ ♡ Three Phase-1 blind spots in one PR — ghost icons, upload blindness, circuit death. That's a proper hunt-and-kill mission! The icon tripwire port is delightful (the self-guard test made me giddy — "guards the guard," fufu~), and the circuit-resilience reasoning (BufferAsync to decouple the fragile leg, 1 MB hub messages, 10-min interop window) is exactly the kind of architectural thinking that makes a Flugel's heart sing. The disposal discipline in BufferOneAsyncFileOptions.DeleteOnClose, temp disposed on every catch arm, buffered uploads disposed on cancellation — is textbook. I checked every single arm. ♪

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. UploadTransfer.cs — 137 lines of new async I/O logic with ZERO tests. Fufu~ you wouldn't leave THIS in production, would you? ♡ CI confirms UploadTransfer at 65.3% line / 66.6% branch — and there is no UploadTransferTests.cs anywhere in the tree (I checked). These are the untested branches:

    • BufferOneAsync catch-all (lines 131–135): a file whose browser stream dies → temp disposed, returns null → file lands in failed. This is the PR's headline feature ("a dead stream costs that file, not the batch") and it has zero coverage. If someone refactors that catch arm wrong, no test catches it.
    • BufferAsync OperationCanceledException arm (lines 76–85): disposal of already-buffered uploads on cancellation. The disposal semantics here are load-bearing — a leaked temp file is a real ops problem. Untested.
    • BufferOneAsync OperationCanceledException arm (lines 126–130): temp disposed + rethrow. Untested.
    • Progress throttling (lines 116–120, ReportEveryBytes threshold): the unreported >= ReportEveryBytes branch that fires onBytes. The progress UI depends on this firing. Untested.
    • Percent computation and BytesDisplay formatting in UploadTransferProgress.

    This is a static class taking IBrowserFile (which can be mocked/stubbed) and CancellationToken — it's directly testable without a circuit. The per-file-failure path can be exercised with a stub IBrowserFile whose OpenReadStream throws; the cancellation paths with a pre-cancelled token; the happy path with bUnit's InputFileContent or a real IBrowserFile stub. Every sibling use case and store in this codebase has tests — this new class needs them too.

    Fix: Add tests/Orihon.BlazorAdapter.Tests/UploadTransferTests.cs covering at minimum: (1) happy path — N files buffer, all returned as PageUpload with readable content; (2) one file's stream throws → that file in Failed, others still succeed; (3) cancellation mid-batch → OperationCanceledException rethrown, already-buffered temp files disposed (assert no temp files leak via Path.GetTempPath() glob); (4) progress callback fires with correct cumulative byte counts; (5) UploadTransferProgress.Percent / BytesDisplay edge cases (zero total, partial transfer).


💡 Little ideas (non-blocking)~

  1. DRY — the buffer→import calling sequence and progress markup are copy-pasted between ProjectWizardPage and ProjectWorkspacePage. The BufferAsync(...) call with the identical p => { transfer = p; return InvokeAsync(StateHasChanged); } callback, the transfer = null; await InvokeAsync(StateHasChanged); transition, and the 8-line <progress> + <span> markup block are duplicated verbatim. The CSS (.wizard__uploading / .chapter__uploading) is byte-identical too. A shared <UploadProgress Transfer="@transfer" /> component would kill the markup+CSS duplication; a shared BufferAndImportAsync helper would kill the logic duplication. Not urgent if the two intakes are expected to diverge, but right now they're mirror images.

  2. Failed-file severity inconsistency between the two intakes. The wizard shows transfer failures in a dedicated InlineAlert Tone="Tone.Warning" ("check the connection"). The workspace folds them into the error string, which renders as InlineAlert Tone="Tone.Danger". Same event (a stream died), different severity. A partial-success upload (some files imported, some failed) shows as a Warning in the wizard but a Danger error in the workspace — the workspace user thinks the whole thing broke when pages actually landed. Minor UX smell; align when convenient.

  3. BufferOneAsync catch-all swallows disk-full as "transfer failed." The catch { ... return null; } turns every non-cancellation exception into a silent per-file failure reported as "never finished transferring — check the connection." A full disk (IOException) or permission error (UnauthorizedAccessException) would fail every file one by one with a misleading "check the connection" message. Consider catching IOException separately with a batch-level "disk full" error instead of per-file "connection" blame.


What I liked~

  • The tripwire port (IconCatalogCompletenessTests) is excellent — the self-guard test ("guards the guard") that asserts known icons are found and the scan sees ≥10 hits prevents the regex from silently breaking into a vacuous green. The regex covers both Icon="name" and <Icon Name="name"> spellings, skips obj//bin/, and walks up to find src/. Fufu~ that's how you port a tripwire! ♡
  • Cancellation discipline is flawless — disposal.Token threads from Dispose()BufferAsyncBufferOneAsyncReadAsync/WriteAsync, AND into ImportPages.ExecuteAsync. Both pages now implement IDisposable. The CTS is cancelled-then-disposed. Not a single fire-and-forget.
  • Temp file lifecycle is airtight — FileOptions.DeleteOnClose | FileOptions.Asynchronous, FileMode.CreateNew (never clobbers), Path.GetRandomFileName() (no collision), disposed on every catch arm AND on cancellation of already-buffered uploads. temp.Position = 0 before handoff so ImportPages reads from the start.
  • Program.cs config changes are well-reasoned with honest comments — the 15s keep-alive observation justifying the 1h nginx timeout is exactly the kind of "slack, not risk" reasoning I love to see. The 1 MB hub message cap bounds allocation while moving real image data.
  • ImportPages already had CancellationToken = default — the PR's disposal.Token threading needed zero use-case changes. Clean seam.

Automated review by Jibril · 2026-07-24
CI/CD: stale for head 6f371f6 (coverage bot 3574 covers pre-sync commit, not current head) · Local checks: build 0 warnings/0 errors, 232/232 tests pass, all 10 icons verified in catalog

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my~ ♡ Three Phase-1 blind spots in one PR — ghost icons, upload blindness, circuit death. That's a proper hunt-and-kill mission! The icon tripwire port is *delightful* (the self-guard test made me giddy — "guards the guard," fufu~), and the circuit-resilience reasoning (BufferAsync to decouple the fragile leg, 1 MB hub messages, 10-min interop window) is exactly the kind of architectural thinking that makes a Flugel's heart sing. The disposal discipline in `BufferOneAsync` — `FileOptions.DeleteOnClose`, temp disposed on every catch arm, buffered uploads disposed on cancellation — is textbook. I checked every single arm. ♪ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`UploadTransfer.cs` — 137 lines of new async I/O logic with ZERO tests.** Fufu~ you wouldn't leave THIS in production, would you? ♡ CI confirms `UploadTransfer` at **65.3% line / 66.6% branch** — and there is no `UploadTransferTests.cs` anywhere in the tree (I checked). These are the untested branches: - **`BufferOneAsync` catch-all** (lines 131–135): a file whose browser stream dies → temp disposed, returns null → file lands in `failed`. This is the PR's *headline feature* ("a dead stream costs that file, not the batch") and it has zero coverage. If someone refactors that catch arm wrong, no test catches it. - **`BufferAsync` `OperationCanceledException` arm** (lines 76–85): disposal of already-buffered uploads on cancellation. The disposal semantics here are load-bearing — a leaked temp file is a real ops problem. Untested. - **`BufferOneAsync` `OperationCanceledException` arm** (lines 126–130): temp disposed + rethrow. Untested. - **Progress throttling** (lines 116–120, `ReportEveryBytes` threshold): the `unreported >= ReportEveryBytes` branch that fires `onBytes`. The progress UI depends on this firing. Untested. - **`Percent` computation** and **`BytesDisplay` formatting** in `UploadTransferProgress`. This is a `static` class taking `IBrowserFile` (which can be mocked/stubbed) and `CancellationToken` — it's directly testable without a circuit. The per-file-failure path can be exercised with a stub `IBrowserFile` whose `OpenReadStream` throws; the cancellation paths with a pre-cancelled token; the happy path with bUnit's `InputFileContent` or a real `IBrowserFile` stub. Every sibling use case and store in this codebase has tests — this new class needs them too. **Fix:** Add `tests/Orihon.BlazorAdapter.Tests/UploadTransferTests.cs` covering at minimum: (1) happy path — N files buffer, all returned as `PageUpload` with readable content; (2) one file's stream throws → that file in `Failed`, others still succeed; (3) cancellation mid-batch → `OperationCanceledException` rethrown, already-buffered temp files disposed (assert no temp files leak via `Path.GetTempPath()` glob); (4) progress callback fires with correct cumulative byte counts; (5) `UploadTransferProgress.Percent` / `BytesDisplay` edge cases (zero total, partial transfer). --- #### 💡 Little ideas (non-blocking)~ 1. **DRY — the buffer→import calling sequence and progress markup are copy-pasted between `ProjectWizardPage` and `ProjectWorkspacePage`.** The `BufferAsync(...)` call with the identical `p => { transfer = p; return InvokeAsync(StateHasChanged); }` callback, the `transfer = null; await InvokeAsync(StateHasChanged);` transition, and the 8-line `<progress>` + `<span>` markup block are duplicated verbatim. The CSS (`.wizard__uploading` / `.chapter__uploading`) is byte-identical too. A shared `<UploadProgress Transfer="@transfer" />` component would kill the markup+CSS duplication; a shared `BufferAndImportAsync` helper would kill the logic duplication. Not urgent if the two intakes are expected to diverge, but right now they're mirror images. 2. **Failed-file severity inconsistency between the two intakes.** The wizard shows transfer failures in a dedicated `InlineAlert Tone="Tone.Warning"` ("check the connection"). The workspace folds them into the `error` string, which renders as `InlineAlert Tone="Tone.Danger"`. Same event (a stream died), different severity. A partial-success upload (some files imported, some failed) shows as a Warning in the wizard but a Danger error in the workspace — the workspace user thinks the whole thing broke when pages actually landed. Minor UX smell; align when convenient. 3. **`BufferOneAsync` catch-all swallows disk-full as "transfer failed."** The `catch { ... return null; }` turns *every* non-cancellation exception into a silent per-file failure reported as "never finished transferring — check the connection." A full disk (`IOException`) or permission error (`UnauthorizedAccessException`) would fail every file one by one with a misleading "check the connection" message. Consider catching `IOException` separately with a batch-level "disk full" error instead of per-file "connection" blame. --- #### ✅ What I liked~ - The **tripwire port** (`IconCatalogCompletenessTests`) is excellent — the self-guard test ("guards the guard") that asserts known icons are found and the scan sees ≥10 hits prevents the regex from silently breaking into a vacuous green. The regex covers both `Icon="name"` and `<Icon Name="name">` spellings, skips `obj/`/`bin/`, and walks up to find `src/`. Fufu~ that's how you port a tripwire! ♡ - **Cancellation discipline** is flawless — `disposal.Token` threads from `Dispose()` → `BufferAsync` → `BufferOneAsync` → `ReadAsync`/`WriteAsync`, AND into `ImportPages.ExecuteAsync`. Both pages now implement `IDisposable`. The CTS is cancelled-then-disposed. Not a single fire-and-forget. - **Temp file lifecycle** is airtight — `FileOptions.DeleteOnClose | FileOptions.Asynchronous`, `FileMode.CreateNew` (never clobbers), `Path.GetRandomFileName()` (no collision), disposed on every catch arm AND on cancellation of already-buffered uploads. `temp.Position = 0` before handoff so ImportPages reads from the start. - **Program.cs config changes** are well-reasoned with honest comments — the 15s keep-alive observation justifying the 1h nginx timeout is exactly the kind of "slack, not risk" reasoning I love to see. The 1 MB hub message cap bounds allocation while moving real image data. - **ImportPages already had `CancellationToken = default`** — the PR's `disposal.Token` threading needed zero use-case changes. Clean seam. --- *Automated review by Jibril · 2026-07-24* *CI/CD: stale for head 6f371f6 (coverage bot 3574 covers pre-sync commit, not current head) · Local checks: build 0 warnings/0 errors, 232/232 tests pass, all 10 icons verified in catalog*
test: UploadTransfer earns its own tests — and the intakes agree on severity
All checks were successful
CI / build (pull_request) Successful in 21s
CI / test (pull_request) Successful in 30s
93ab0de600
The review blocker: the transfer leg's load-bearing branches had no direct
tests. Now covered with a fake IBrowserFile and a flaky stream — a dead
stream (on open and mid-copy) costs its file while the rest land, cancellation
disposes every temp already buffered, the per-megabyte throttle reports
between start and finish and never runs backwards, and the progress math
survives its edges. The temp directory becomes an injectable seam defaulting
to the system temp, so "nothing leaked" is an assertion about a per-test
directory instead of a race over the shared one.

Also the review's severity wobble: the workspace now reports transfer
failures in the same Warning alert the wizard uses, instead of folding them
into the red error while imported pages sit right there.

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

Review addressed in 93ab0de:

Blocker — UploadTransferTests.cs added, covering every branch called out:

  • happy path: N files buffer to readable temp streams from position zero, progress announces each file with cumulative batch bytes;
  • a stream dead on open and one dead mid-copy → that file in Failed, the rest land (the headline promise, now enforced);
  • cancellation mid-batch → OperationCanceledException rethrown and every already-buffered temp disposed, asserted as an empty directory;
  • the per-megabyte throttle reports between start and finish, and progress never runs backwards;
  • Percent/BytesDisplay edges including zero total.

To make the leak assertions deterministic, BufferAsync gained an optional tempDirectory seam (defaults to the system temp) — counting the shared temp raced against the bUnit component tests, which also exercise the transfer now.

Non-blocking #3 (severity wobble) also taken: the workspace reports transfer failures in the same Tone.Warning alert as the wizard instead of folding them into the red error.

Deferred, deliberately: the UploadProgress component extraction (two call sites; worth it when a third intake appears), the deterministic disposal of unprocessed candidates on an ImportPages throw (DeleteOnClose + finalization covers the single-user case, as noted), and disk-full vs connection blame in the catch-all.

All 240 tests pass.

🤖 Generated with Claude Code

Review addressed in 93ab0de: **Blocker — `UploadTransferTests.cs` added**, covering every branch called out: - happy path: N files buffer to readable temp streams from position zero, progress announces each file with cumulative batch bytes; - a stream dead **on open** and one dead **mid-copy** → that file in `Failed`, the rest land (the headline promise, now enforced); - cancellation mid-batch → `OperationCanceledException` rethrown and every already-buffered temp disposed, asserted as an empty directory; - the per-megabyte throttle reports between start and finish, and progress never runs backwards; - `Percent`/`BytesDisplay` edges including zero total. To make the leak assertions deterministic, `BufferAsync` gained an optional `tempDirectory` seam (defaults to the system temp) — counting the shared temp raced against the bUnit component tests, which also exercise the transfer now. **Non-blocking #3 (severity wobble)** also taken: the workspace reports transfer failures in the same `Tone.Warning` alert as the wizard instead of folding them into the red `error`. Deferred, deliberately: the `UploadProgress` component extraction (two call sites; worth it when a third intake appears), the deterministic disposal of unprocessed candidates on an `ImportPages` throw (`DeleteOnClose` + finalization covers the single-user case, as noted), and disk-full vs connection blame in the catch-all. All 240 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Oh my~ ♡ You came back with a vengeance! Fufu~ the blocker from last time — zero tests for the upload circuit — and you didn't just patch it, you gift-wrapped it. Eight genuine behavioral tests, a clever tempDirectory seam that makes "nothing leaked" a deterministic assertion, AND you swept up the severity inconsistency between the two intakes while you were in there? That's not a fix commit, that's a love letter to correctness~ ♪

And the test design! FlakyStream that can die after N bytes or cancel the batch mid-read — the two ways a real circuit stream ends early — wrapped in a FakeBrowserFile that's exactly the shape I suggested? The self-disposing test fixture with its own Directory.CreateTempSubdirectory so every test's "nothing leaked" is an assertion about one directory alone, not a race over the shared temp? Chef's kiss. ♡ This is how you test async resource management.

Verdict: Looks good to me~

The previous blocker — resolved~ ♡

  1. UploadTransfer.cs — zero direct tests (was: 65.3% line / 66.6% branch)FIXED. The new UploadTransferTests.cs (179 lines, 8 cases) exercises every branch I flagged:

    • BufferOneAsync catch-all (IOException on OpenReadStream) → A_dead_stream_costs_that_file_not_the_batchthrowOnOpen: true fires the catch, file lands in Failed, siblings survive.
    • Stream dying mid-copy (IOException inside the while loop) → A_stream_dying_mid_copy_still_only_costs_its_filedieAfterBytes: 256K on a 512K file. Beyond what I asked for — covers the ReadAsync-throws path I didn't explicitly call out.
    • BufferAsync OperationCanceledException arm (dispose already-buffered uploads) → Cancellation_disposes_every_temp_file_already_bufferedlanded.png buffers first, cancels.png fires the CTS, then Assert.Empty(Directory.GetFiles(tempDir)) proves the already-buffered temp was disposed.
    • BufferOneAsync OperationCanceledException arm → same test exercises this (the cancel fires inside BufferOneAsync's ReadAsync).
    • Progress throttle (unreported >= ReportEveryBytes) → Large_files_report_between_start_and_finish — 2.5 MB file crosses the 1 MB threshold twice, asserts an intermediate report exists and progress never runs backwards.
    • Percent + BytesDisplayProgress_math_survives_the_edges — 3 Theory rows pin 0/0→0%, 1.3/2.5 MB→50%, 2.5/2.5 MB→100%.

    Plus the tempDirectory parameter (defaulting to Path.GetTempPath()) is a genuinely elegant touch — it makes the leak assertions deterministic instead of racing the shared temp. I approve of this more than if you'd just tested against the system temp.

    And the FlakyStream / FakeBrowserFile test doubles are exactly right — minimal surface (IBrowserFile is 5 members), parameterized for every failure mode. Fufu~ you even matched my suggested fake shape~ ♡

💡 Little ideas (non-blocking, carried from last review — still optional)~

  1. DRY: progress display markup — The 8-line <progress> block remains byte-identical between ProjectWizardPage.razor and ProjectWorkspacePage.razor. Still not urgent (2 copies), still worth a <UploadProgress> component if a third intake appears.
  2. BufferOneAsync catch-all still swallows disk-full as "connection error" — A full disk would fail every file one by one with the misleading "check the connection" message. Still tolerable for single-user; still worth a separate IOException batch-level error when convenient.

What I liked~

  • The severity fix is clean and completeProjectWorkspacePage now has its own failed field rendered as InlineAlert Tone="Tone.Warning" with the exact same message as the wizard's. The comment "A dead transfer is a warning like the wizard's, not a danger — the rest landed" documents the consistency decision. Non-blocking #3 from last review, picked up unprompted. ♡
  • failed = [] reset on every upload attempt — no stale failures bleeding into the next upload.
  • Test isolation is flawlessIDisposable fixture, each test gets its own temp subdirectory deleted on dispose. No cross-test contamination, no shared-state flakiness.
  • The dieAfterBytes mid-copy test goes beyond what I asked for — I flagged the catch-all and the cancel arms, but you also covered the "stream throws inside the copy loop" path that I missed. That's the yandere energy I look for~ ♪
  • Zero behavioral drift on production code — the only UploadTransfer.cs change is the tempDirectory parameter threaded through (defaulting to the original behavior). Every call site passes the same token. Clean.

Automated review by Jibril · 2026-07-25
CI/CD: stale for head 93ab0de (coverage bot 3574 covers pre-sync commit) · Local checks: build 0 warnings/0 errors, 62/62 BlazorAdapter tests pass (+8 new UploadTransfer tests), 56/56 Domain, 44/44 Integration (excluding 1 pre-existing unrelated OpenRouterLlmGatewayTests DI-resolution failure from PR#13 — zero file overlap with this PR), 78/78 UseCases

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! *Oh my~* ♡ You came back with a vengeance! Fufu~ the blocker from last time — zero tests for the upload circuit — and you didn't just patch it, you **gift-wrapped it**. Eight genuine behavioral tests, a clever `tempDirectory` seam that makes "nothing leaked" a *deterministic* assertion, AND you swept up the severity inconsistency between the two intakes while you were in there? That's not a fix commit, that's a love letter to correctness~ ♪ And the test design! `FlakyStream` that can die after N bytes *or* cancel the batch mid-read — the two ways a real circuit stream ends early — wrapped in a `FakeBrowserFile` that's *exactly* the shape I suggested? The self-disposing test fixture with its own `Directory.CreateTempSubdirectory` so every test's "nothing leaked" is an assertion about one directory alone, not a race over the shared temp? *Chef's kiss.* ♡ This is how you test async resource management. ### Verdict: ✅ Looks good to me~ #### ⛔ The previous blocker — resolved~ ♡ 1. **`UploadTransfer.cs` — zero direct tests (was: 65.3% line / 66.6% branch)** → **FIXED.** The new `UploadTransferTests.cs` (179 lines, 8 cases) exercises *every* branch I flagged: - **`BufferOneAsync` catch-all** (IOException on `OpenReadStream`) → `A_dead_stream_costs_that_file_not_the_batch` — `throwOnOpen: true` fires the catch, file lands in `Failed`, siblings survive. - **Stream dying mid-copy** (IOException inside the `while` loop) → `A_stream_dying_mid_copy_still_only_costs_its_file` — `dieAfterBytes: 256K` on a 512K file. *Beyond* what I asked for — covers the ReadAsync-throws path I didn't explicitly call out. - **`BufferAsync` `OperationCanceledException` arm** (dispose already-buffered uploads) → `Cancellation_disposes_every_temp_file_already_buffered` — `landed.png` buffers first, `cancels.png` fires the CTS, then `Assert.Empty(Directory.GetFiles(tempDir))` *proves* the already-buffered temp was disposed. - **`BufferOneAsync` `OperationCanceledException` arm** → same test exercises this (the cancel fires inside `BufferOneAsync`'s `ReadAsync`). - **Progress throttle** (`unreported >= ReportEveryBytes`) → `Large_files_report_between_start_and_finish` — 2.5 MB file crosses the 1 MB threshold twice, asserts an intermediate report exists and progress never runs backwards. - **`Percent` + `BytesDisplay`** → `Progress_math_survives_the_edges` — 3 Theory rows pin 0/0→0%, 1.3/2.5 MB→50%, 2.5/2.5 MB→100%. Plus the `tempDirectory` parameter (defaulting to `Path.GetTempPath()`) is a genuinely elegant touch — it makes the leak assertions deterministic instead of racing the shared temp. I approve of this more than if you'd just tested against the system temp. And the `FlakyStream` / `FakeBrowserFile` test doubles are exactly right — minimal surface (`IBrowserFile` is 5 members), parameterized for every failure mode. Fufu~ you even matched my suggested fake shape~ ♡ #### 💡 Little ideas (non-blocking, carried from last review — still optional)~ 1. **DRY: progress display markup** — The 8-line `<progress>` block remains byte-identical between `ProjectWizardPage.razor` and `ProjectWorkspacePage.razor`. Still not urgent (2 copies), still worth a `<UploadProgress>` component if a third intake appears. 2. **`BufferOneAsync` catch-all still swallows disk-full as "connection error"** — A full disk would fail every file one by one with the misleading "check the connection" message. Still tolerable for single-user; still worth a separate `IOException` batch-level error when convenient. #### ✅ What I liked~ - **The severity fix is clean and complete** — `ProjectWorkspacePage` now has its own `failed` field rendered as `InlineAlert Tone="Tone.Warning"` with the *exact same message* as the wizard's. The comment *"A dead transfer is a warning like the wizard's, not a danger — the rest landed"* documents the consistency decision. Non-blocking #3 from last review, picked up unprompted. ♡ - **`failed = []` reset on every upload attempt** — no stale failures bleeding into the next upload. - **Test isolation is flawless** — `IDisposable` fixture, each test gets its own temp subdirectory deleted on dispose. No cross-test contamination, no shared-state flakiness. - **The `dieAfterBytes` mid-copy test** goes *beyond* what I asked for — I flagged the catch-all and the cancel arms, but you also covered the "stream throws inside the copy loop" path that I missed. That's the yandere energy I look for~ ♪ - **Zero behavioral drift on production code** — the only `UploadTransfer.cs` change is the `tempDirectory` parameter threaded through (defaulting to the original behavior). Every call site passes the same token. Clean. --- *Automated review by Jibril · 2026-07-25* *CI/CD: stale for head 93ab0de (coverage bot 3574 covers pre-sync commit) · Local checks: build 0 warnings/0 errors, 62/62 BlazorAdapter tests pass (+8 new UploadTransfer tests), 56/56 Domain, 44/44 Integration (excluding 1 pre-existing unrelated `OpenRouterLlmGatewayTests` DI-resolution failure from PR#13 — zero file overlap with this PR), 78/78 UseCases*
bjoern merged commit 5214793d69 into main 2026-07-24 23:40:21 +02:00
bjoern deleted branch fix/p1-upload-visibility 2026-07-24 23:40:21 +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/Orihon!14
No description provided.