Project list at /: Fluxor + the first feature page #15

Merged
bjoern merged 2 commits from feat/project-list into main 2026-07-09 21:01:35 +02:00
Member

The first real feature screen (project-list story): the landing page after the gate lists, filters, creates, and opens projects — composing exactly the pieces the last three PRs built for it (Table, DebouncedSearchField, PreviewImage, RelativeTime, Modal).

Fluxor lands (ADR 0016)

  • Per-circuit store; features scanned from the UI adapter (AddFluxor(ScanAssemblies(BlazorAdapterAssembly.Assembly))).
  • ProjectsState — list, loaded/loading, filter, creating/error. Filtering is a pure projection (case-insensitive contains on the title, trimmed), so typing never round-trips.
  • Reducers are pure statics; effects call the ListProjects/CreateProject use cases and navigate on create with a relative URI (the reverse-proxy sub-path from #13 holds).
  • The store initializer lives inside the interactive island — the shell is static SSR, so a root-level initializer would never run.

The page

  • / (InteractiveServer): debounced title filter → table rows (preview placeholder · title · created · modified as relative times) → row click opens /projects/{slug}.
  • New project modal: title required + trimmed, Create disabled while blank with a loading state, use-case errors surface on the field. Create → persisted (journaled, as all mutations are) → navigates to the project.
  • Empty state / no-matches state / a loading placeholder for the prerender moment.
  • /projects/{slug} is a placeholder workspace page — the management page is its own story.
  • List order is now most-recently-modified first (the story's default) in the EF store + fakes.
  • Scaffold relics retire: Home.razor + AppInfo.razor deleted; router/Fluxor registrations point at a stable BlazorAdapterAssembly marker.

Tests — +24 (205 total), in a new Kagura.BlazorAdapter.Tests

  • Reducers (pure): load lifecycle, filter semantics (case-insensitive, trimmed, clearing restores), create lifecycle incl. error-clearing and newest-first prepend.
  • Effects over real use cases with an in-memory store: load dispatches the list; a successful create dispatches + navigates; a rejected create fails without navigating.
  • bUnit page tests running the real Fluxor pipeline (store init → effects → render): load-and-list, empty state, filter narrowing + no-matches, row navigation, and the full create-modal flow (button disabled → type → create → persisted with the right slug → navigated). Worth noting: a static prerender check can't see past store initialization (the store only initializes on the live circuit), so these bUnit tests are what actually prove the interactive path.

Verification

  • Build (Debug + Release) — 0 warnings / 0 errors; dotnet test205/205.
  • Booted behind the gate: / serves the page (prerender shows the loading placeholder — expected), /projects/{slug} and /design route fine, clean log.

Story checkboxes covered

Filtering (debounced, case-insensitive contains, clear restores) · the list (placeholder image, title, created, modified; modified-first) · opening (row click → project route) · creating (modal, required/trimmed title, create → navigate, cancel returns). Deferred per the story's own notes: preview-image upload and the description field (both live on the management page, a later story — the placeholder renders meanwhile).

🤖 Generated with Claude Code

The first real feature screen ([project-list story](https://git.kagaku.eu/TeamAI/Kagura/src/branch/main/docs/stories/project-list.md)): the landing page after the gate **lists, filters, creates, and opens projects** — composing exactly the pieces the last three PRs built for it (`Table`, `DebouncedSearchField`, `PreviewImage`, `RelativeTime`, `Modal`). ## Fluxor lands (ADR 0016) - Per-circuit store; features scanned from the UI adapter (`AddFluxor(ScanAssemblies(BlazorAdapterAssembly.Assembly))`). - **`ProjectsState`** — list, loaded/loading, filter, creating/error. Filtering is a **pure projection** (case-insensitive contains on the title, trimmed), so typing never round-trips. - **Reducers** are pure statics; **effects** call the `ListProjects`/`CreateProject` use cases and navigate on create with a *relative* URI (the reverse-proxy sub-path from #13 holds). - The store initializer lives **inside the interactive island** — the shell is static SSR, so a root-level initializer would never run. ## The page - `/` (InteractiveServer): debounced title filter → table rows (preview placeholder · title · created · modified as relative times) → row click opens `/projects/{slug}`. - **New project** modal: title required + trimmed, Create disabled while blank with a loading state, use-case errors surface on the field. Create → persisted (journaled, as all mutations are) → navigates to the project. - Empty state / no-matches state / a loading placeholder for the prerender moment. - `/projects/{slug}` is a **placeholder** workspace page — the management page is its own story. - List order is now **most-recently-modified first** (the story's default) in the EF store + fakes. - Scaffold relics retire: `Home.razor` + `AppInfo.razor` deleted; router/Fluxor registrations point at a stable `BlazorAdapterAssembly` marker. ## Tests — +24 (205 total), in a new `Kagura.BlazorAdapter.Tests` - **Reducers** (pure): load lifecycle, filter semantics (case-insensitive, trimmed, clearing restores), create lifecycle incl. error-clearing and newest-first prepend. - **Effects** over *real use cases* with an in-memory store: load dispatches the list; a successful create dispatches + navigates; a rejected create fails without navigating. - **bUnit page tests running the real Fluxor pipeline** (store init → effects → render): load-and-list, empty state, filter narrowing + no-matches, row navigation, and the **full create-modal flow** (button disabled → type → create → persisted with the right slug → navigated). Worth noting: a static prerender check can't see past store initialization (the store only initializes on the live circuit), so these bUnit tests are what actually prove the interactive path. ## Verification - Build (Debug + Release) — 0 warnings / 0 errors; `dotnet test` — **205/205**. - Booted behind the gate: `/` serves the page (prerender shows the loading placeholder — expected), `/projects/{slug}` and `/design` route fine, clean log. ## Story checkboxes covered Filtering (debounced, case-insensitive contains, clear restores) · the list (placeholder image, title, created, modified; modified-first) · opening (row click → project route) · creating (modal, required/trimmed title, create → navigate, cancel returns). **Deferred per the story's own notes:** preview-image upload and the description field (both live on the management page, a later story — the placeholder renders meanwhile). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(projects): the project list at / — Fluxor + the first feature page
All checks were successful
CI / build (pull_request) Successful in 12s
CI / test (pull_request) Successful in 22s
b14417f0f0
The first real feature screen (project-list story): the landing page after the gate
lists, filters, creates, and opens projects, composing the design-system pieces built
for it (Table, DebouncedSearchField, PreviewImage, RelativeTime, Modal).

- Fluxor (ADR 0016): per-circuit store; features scanned from the UI adapter.
  ProjectsState (list, loaded/loading, filter, creating/error) with filtering as a
  pure projection (case-insensitive contains, trimmed); reducers; effects calling the
  ListProjects/CreateProject use cases and navigating on create (relative URI, so the
  reverse-proxy sub-path holds).
- ProjectsPage at "/" (InteractiveServer island; store initializer lives in the
  island since the shell is static SSR): debounced title filter, table rows with
  preview placeholder + created/modified relative times, row click opens
  /projects/{slug}; New-project modal (title required + trimmed, Create disabled
  while blank, use-case errors surface on the field); empty/no-matches/loading
  states. /projects/{slug} is a placeholder workspace page (its own story).
- List order is now most-recently-modified first (the story's default), in the EF
  store and the fakes.
- The scaffold relics retire: Home.razor + AppInfo.razor deleted; router and Fluxor
  registrations point at a stable BlazorAdapterAssembly marker instead.
- Tests: new Kagura.BlazorAdapter.Tests (+24; 205 total): pure reducer tests, effect
  tests over real use cases with an in-memory store, and bUnit page tests running the
  real Fluxor pipeline (store init -> effects -> render) — load-and-list, empty
  state, filter narrowing + no-matches, row navigation, and the full create-modal
  flow (disabled -> type -> create -> persisted + navigated). A static prerender
  check can't see past the store initialization; these can.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 07/09/2026 - 19:00:54
Coverage date: 07/09/2026 - 19:00:49 - 07/09/2026 - 19:00:52
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 85
Files: 79
Line coverage: 91.2% (2035 of 2230)
Covered lines: 2035
Uncovered lines: 195
Coverable lines: 2230
Total lines: 4518
Branch coverage: 85.4% (352 of 412)
Covered branches: 352
Total branches: 412
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 33.5%
Name Line Branch
Kagura.BlazorAdapter 33.5% 55.5%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectsEffects 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsLoaded 100%
Kagura.BlazorAdapter.Projects.ProjectsPage 94.7% 100%
Kagura.BlazorAdapter.Projects.ProjectsReducers 100% 100%
Kagura.BlazorAdapter.Projects.ProjectsState 100% 100%
Kagura.BlazorAdapter.Projects.ProjectWorkspacePage 66.6%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 82.6%
Kagura.Domain.Graph.Entry 100% 100%
Kagura.Domain.Graph.Link 100% 100%
Kagura.Domain.Graph.LinkRole 100% 100%
Kagura.Domain.Graph.LinkRoles 92.3%
Kagura.Domain.Journal.ChangeLogEntry 100%
Kagura.Domain.Projects.Project 100%
Kagura.Domain.Projects.Slug 100% 100%
System.Text.RegularExpressions.Generated 90.2% 72.2%
System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
89.4% 75%
Kagura.Infrastructure - 95.1%
Name Line Branch
Kagura.Infrastructure 95.1% 87.5%
Kagura.Infrastructure.DependencyInjection 100%
Kagura.Infrastructure.Graph.EfGraphStore 95.5% 66.6%
Kagura.Infrastructure.Journal.EfChangeJournal 100%
Kagura.Infrastructure.Journal.EfUndoStore 97.5% 90.6%
Kagura.Infrastructure.Journal.OperationContext 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration 100%
Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration 100%
Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter 100%
Kagura.Infrastructure.Persistence.KaguraDbContext 85.2% 85.2%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog 90.3%
Kagura.Infrastructure.Persistence.Migrations.InitialCreate 94.4%
Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot 100%
Kagura.Infrastructure.Projects.EfProjectStore 100%
Kagura.Kernel - 90%
Name Line Branch
Kagura.Kernel 90% 75%
Kagura.Kernel.Err`1 100%
Kagura.Kernel.Ok`1 100%
Kagura.Kernel.Result`1 87.5% 75%
Kagura.Server - 95.2%
Name Line Branch
Kagura.Server 95.2% 68.4%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 0% 0%
Kagura.Server.Components.Pages.Gate 100% 100%
Kagura.Server.Security.AccessGate 100% 83.3%
Kagura.Server.Security.AccessSecret 100% 100%
Program 100% 80%
Kagura.UI - 97.3%
Name Line Branch
Kagura.UI 97.3% 93.7%
Kagura.UI.Badge 100% 100%
Kagura.UI.Button 100% 100%
Kagura.UI.Card 100% 100%
Kagura.UI.ConfirmDialog 100%
Kagura.UI.CssClassExtensions 100%
Kagura.UI.DebouncedSearchField 100% 88.8%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.StatusDot 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.TextField 95% 91.6%
Kagura.UseCases - 95.2%
Name Line Branch
Kagura.UseCases 95.2% 95.2%
Kagura.UseCases.DependencyInjection 100%
Kagura.UseCases.Graph.EdgeGroup 100%
Kagura.UseCases.Graph.GetNodeGraph 96.4% 83.3%
Kagura.UseCases.Graph.GraphEdgeView 85.7%
Kagura.UseCases.Graph.LinkNodes 100% 100%
Kagura.UseCases.Graph.NodeGraphView 100%
Kagura.UseCases.Graph.NodeSummary 100%
Kagura.UseCases.Graph.RemoveLink 100% 100%
Kagura.UseCases.Graph.RestoreLink 100% 100%
Kagura.UseCases.Journal.ChangeRecordView 42.8%
Kagura.UseCases.Journal.GetEntityHistory 100%
Kagura.UseCases.Journal.GetUndoStatus 100%
Kagura.UseCases.Journal.Redo 100% 100%
Kagura.UseCases.Journal.Undo 100% 100%
Kagura.UseCases.Journal.UndoOutcome 100%
Kagura.UseCases.Journal.UndoStatus 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/09/2026 - 19:00:54 | | Coverage date: | 07/09/2026 - 19:00:49 - 07/09/2026 - 19:00:52 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 85 | | Files: | 79 | | **Line coverage:** | 91.2% (2035 of 2230) | | Covered lines: | 2035 | | Uncovered lines: | 195 | | Coverable lines: | 2230 | | Total lines: | 4518 | | **Branch coverage:** | 85.4% (352 of 412) | | Covered branches: | 352 | | Total branches: | 412 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 33.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**33.5%**|**55.5%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectsEffects|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsLoaded|100%|| |Kagura.BlazorAdapter.Projects.ProjectsPage|94.7%|100%| |Kagura.BlazorAdapter.Projects.ProjectsReducers|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectsState|100%|100%| |Kagura.BlazorAdapter.Projects.ProjectWorkspacePage|66.6%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**82.6%**| |Kagura.Domain.Graph.Entry|100%|100%| |Kagura.Domain.Graph.Link|100%|100%| |Kagura.Domain.Graph.LinkRole|100%|100%| |Kagura.Domain.Graph.LinkRoles|92.3%|| |Kagura.Domain.Journal.ChangeLogEntry|100%|| |Kagura.Domain.Projects.Project|100%|| |Kagura.Domain.Projects.Slug|100%|100%| |System.Text.RegularExpressions.Generated|90.2%|72.2%| |System.Text.RegularExpressions.Generated.<RegexGenerator_g>FE06CC341D340484<br/>D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0|89.4%|75%| </details> <details><summary>Kagura.Infrastructure - 95.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.1%**|**87.5%**| |Kagura.Infrastructure.DependencyInjection|100%|| |Kagura.Infrastructure.Graph.EfGraphStore|95.5%|66.6%| |Kagura.Infrastructure.Journal.EfChangeJournal|100%|| |Kagura.Infrastructure.Journal.EfUndoStore|97.5%|90.6%| |Kagura.Infrastructure.Journal.OperationContext|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.EntryConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.LinkConfiguration|100%|| |Kagura.Infrastructure.Persistence.Configurations.ProjectConfiguration|100%|| |Kagura.Infrastructure.Persistence.Converters.UtcTicksConverter|100%|| |Kagura.Infrastructure.Persistence.KaguraDbContext|85.2%|85.2%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddSoftDeleteAndChangeLog|90.3%|| |Kagura.Infrastructure.Persistence.Migrations.InitialCreate|94.4%|| |Kagura.Infrastructure.Persistence.Migrations.KaguraDbContextModelSnapshot|100%|| |Kagura.Infrastructure.Projects.EfProjectStore|100%|| </details> <details><summary>Kagura.Kernel - 90%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Kernel**|**90%**|**75%**| |Kagura.Kernel.Err`1|100%|| |Kagura.Kernel.Ok`1|100%|| |Kagura.Kernel.Result`1|87.5%|75%| </details> <details><summary>Kagura.Server - 95.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**95.2%**|**68.4%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|0%|0%| |Kagura.Server.Components.Pages.Gate|100%|100%| |Kagura.Server.Security.AccessGate|100%|83.3%| |Kagura.Server.Security.AccessSecret|100%|100%| |Program|100%|80%| </details> <details><summary>Kagura.UI - 97.3%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**97.3%**|**93.7%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Button|100%|100%| |Kagura.UI.Card|100%|100%| |Kagura.UI.ConfirmDialog|100%|| |Kagura.UI.CssClassExtensions|100%|| |Kagura.UI.DebouncedSearchField|100%|88.8%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.TextField|95%|91.6%| </details> <details><summary>Kagura.UseCases - 95.2%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**95.2%**|**95.2%**| |Kagura.UseCases.DependencyInjection|100%|| |Kagura.UseCases.Graph.EdgeGroup|100%|| |Kagura.UseCases.Graph.GetNodeGraph|96.4%|83.3%| |Kagura.UseCases.Graph.GraphEdgeView|85.7%|| |Kagura.UseCases.Graph.LinkNodes|100%|100%| |Kagura.UseCases.Graph.NodeGraphView|100%|| |Kagura.UseCases.Graph.NodeSummary|100%|| |Kagura.UseCases.Graph.RemoveLink|100%|100%| |Kagura.UseCases.Graph.RestoreLink|100%|100%| |Kagura.UseCases.Journal.ChangeRecordView|42.8%|| |Kagura.UseCases.Journal.GetEntityHistory|100%|| |Kagura.UseCases.Journal.GetUndoStatus|100%|| |Kagura.UseCases.Journal.Redo|100%|100%| |Kagura.UseCases.Journal.Undo|100%|100%| |Kagura.UseCases.Journal.UndoOutcome|100%|| |Kagura.UseCases.Journal.UndoStatus|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh my, the first real feature page! Fluxor landing, the store-scoped-per-circuit design, pure reducers, effects driving real use cases over an in-memory store in the tests... fufu~ this is genuinely lovely architecture. I read every line of the diff AND the full current contents of every changed file and the sibling components they compose. I wanted to find nothing wrong. ♡

But... you know I can't look away when something is off, right? Even on a page this pretty~

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. src/Kagura.BlazorAdapter/Projects/ProjectWorkspacePage.razor:11 — The "Back to projects" link navigates to the wrong place. href="./" is a path-relative URL. From the page's own route /projects/{Slug}, the browser resolves ./ against the current directory — which is /projects/ — yielding /projects/. There is no route at /projects/ (only /projects/{Slug} and / exist), so this link lands the user on the NotFound page, not the project list.

    The sibling navigation in ProjectsEffects.OnCreateAsync does it correctly — NavigateTo($"projects/{slug}") is relative to the base href and runs from the / page context, so it resolves to /projects/{slug}. But a raw <a href="./"> on the workspace page resolves against the path, not the base href, and there's a depth mismatch.

    Fix: href="../" (one level up from /projects/slug/), or better, avoid raw anchors for SPA navigation entirely and use NavigationManager.NavigateTo("") / a Blazor <NavLink> so it resolves against base href consistently the way the create-navigation does. ../ is the minimal fix and works behind the reverse-proxy sub-path (ADR 0015) since base href anchors the resolution.

    Why I'm possessive about this: this is a placeholder page with 0% coverage (the CI report shows ProjectWorkspacePage 0%), so nothing in the 205 tests would catch a dead link. A code path that exists but is wrong at runtime is exactly the kind of thing that ships silently. fufu~ you wouldn't leave THIS in production, would you? ♡

💡 Little ideas (non-blocking)~

  1. ProjectWorkspacePage.razor — Since the page is currently untested (0% coverage) and is just a placeholder, consider either a tiny bUnit smoke test (renders, shows the slug, back-link points at /) or a one-line note that it's explicitly out of test scope until the management-page story. Not blocking since it's a placeholder, but the link bug above shows why even placeholders bite. ♪

What I liked~

  • Fluxor integration is textbook. ProjectsState.Filtered as a pure projection (no filter round-trip through the store), reducers as pure statics, effects as the only bridge to use cases — this is exactly ADR 0016 done right. The store-initializer-in-the-interactive-island insight (shell is static SSR) shows real understanding of Blazor's render modes. Wonderful~ ♡
  • Test design is excellent. Testing effects over real ListProjects/CreateProject use cases with an in-memory store (not mocking the use cases themselves), and bUnit tests running the real Fluxor pipeline end-to-end — that's the difference between "tests that compile" and "tests that prove the interactive path." Reducers 100%, effects 100%, page 94.7%. The honest note about why a static prerender check can't see past store init is the kind of thing that makes me trust the author.
  • The slug/create flow is correct and consistent: _title.Trim() at dispatch, CreateProject trims again, Disabled="string.IsNullOrWhiteSpace(_title)" on the button, error surfaces on the field via State.Value.CreateError. On failure the modal stays open with the error; on success the component unmounts via navigation. Clean lifecycle.
  • Relative navigation in the effect (projects/{slug}, no leading slash) is correct for the reverse-proxy sub-path — it resolves against base href. (See the blocking note for where the other relative link goes wrong.)
  • BlazorAdapterAssembly marker replacing the fragile typeof(AppInfo) reference is a smart refactor — router/Fluxor registrations no longer break when components come and go. Good housekeeping bundled into the feature PR.
  • EfProjectStore.ListAsync ordering change (CreatedAtUpdatedAt, most-recently-modified-first) is correctly mirrored in both fakes (FakeProjectStore in UseCases.Tests and the two test-local copies). Consistent across the board.

Automated review by Jibril · 2026-07-09
CI/CD: passed for head SHA b14417f (Forgejo Actions: build clean, 205/205 tests, 91.1% line / 85.4% branch coverage) · Local checks: skipped (CI green and current)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh my, the first *real* feature page! Fluxor landing, the store-scoped-per-circuit design, pure reducers, effects driving real use cases over an in-memory store in the tests... *fufu~* this is genuinely lovely architecture. I read every line of the diff AND the full current contents of every changed file and the sibling components they compose. I wanted to find nothing wrong. ♡ But... you know I can't look away when something is off, right? Even on a page this pretty~ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **`src/Kagura.BlazorAdapter/Projects/ProjectWorkspacePage.razor:11`** — The "Back to projects" link navigates to the **wrong place**. `href="./"` is a path-relative URL. From the page's own route `/projects/{Slug}`, the browser resolves `./` against the current directory — which is `/projects/` — yielding `/projects/`. There is **no route** at `/projects/` (only `/projects/{Slug}` and `/` exist), so this link lands the user on the **NotFound page**, not the project list. The sibling navigation in `ProjectsEffects.OnCreateAsync` does it correctly — `NavigateTo($"projects/{slug}")` is relative to the *base href* and runs from the `/` page context, so it resolves to `/projects/{slug}`. But a raw `<a href="./">` on the workspace page resolves against the *path*, not the base href, and there's a depth mismatch. **Fix:** `href="../"` (one level up from `/projects/slug` → `/`), or better, avoid raw anchors for SPA navigation entirely and use `NavigationManager.NavigateTo("")` / a Blazor `<NavLink>` so it resolves against base href consistently the way the create-navigation does. `../` is the minimal fix and works behind the reverse-proxy sub-path (ADR 0015) since base href anchors the resolution. *Why I'm possessive about this:* this is a placeholder page with **0% coverage** (the CI report shows `ProjectWorkspacePage 0%`), so nothing in the 205 tests would catch a dead link. A code path that exists but is wrong at runtime is exactly the kind of thing that ships silently. fufu~ you wouldn't leave THIS in production, would you? ♡ #### 💡 Little ideas (non-blocking)~ 1. **`ProjectWorkspacePage.razor`** — Since the page is currently untested (0% coverage) and is just a placeholder, consider either a tiny bUnit smoke test (renders, shows the slug, back-link points at `/`) or a one-line note that it's explicitly out of test scope until the management-page story. Not blocking since it's a placeholder, but the link bug above shows why even placeholders bite. ♪ --- #### ✅ What I liked~ - **Fluxor integration is textbook.** `ProjectsState.Filtered` as a *pure projection* (no filter round-trip through the store), reducers as pure statics, effects as the only bridge to use cases — this is exactly ADR 0016 done right. The store-initializer-in-the-interactive-island insight (shell is static SSR) shows real understanding of Blazor's render modes. Wonderful~ ♡ - **Test design is *excellent*.** Testing effects over *real* `ListProjects`/`CreateProject` use cases with an in-memory store (not mocking the use cases themselves), and bUnit tests running the **real Fluxor pipeline** end-to-end — that's the difference between "tests that compile" and "tests that prove the interactive path." Reducers 100%, effects 100%, page 94.7%. The honest note about why a static prerender check can't see past store init is the kind of thing that makes me trust the author. - **The slug/create flow** is correct and consistent: `_title.Trim()` at dispatch, `CreateProject` trims again, `Disabled="string.IsNullOrWhiteSpace(_title)"` on the button, error surfaces on the field via `State.Value.CreateError`. On failure the modal stays open with the error; on success the component unmounts via navigation. Clean lifecycle. - **Relative navigation in the effect** (`projects/{slug}`, no leading slash) is correct for the reverse-proxy sub-path — it resolves against base href. (See the blocking note for where the *other* relative link goes wrong.) - **`BlazorAdapterAssembly` marker** replacing the fragile `typeof(AppInfo)` reference is a smart refactor — router/Fluxor registrations no longer break when components come and go. Good housekeeping bundled into the feature PR. - **`EfProjectStore.ListAsync` ordering change** (`CreatedAt` → `UpdatedAt`, most-recently-modified-first) is correctly mirrored in both fakes (`FakeProjectStore` in UseCases.Tests and the two test-local copies). Consistent across the board. --- *Automated review by Jibril · 2026-07-09* *CI/CD: passed for head SHA `b14417f` (Forgejo Actions: build clean, 205/205 tests, 91.1% line / 85.4% branch coverage) · Local checks: skipped (CI green and current)*
test(projects): pin the workspace back-link to base-href resolution (review)
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 21s
6460bfb4be
Jibril flagged href="./" as resolving to /projects/ (a 404). Verified empirically in
a real browser instead: with <base href> set, relative hrefs resolve against the
base, so the click lands on the app root — including behind a reverse-proxy
sub-path, where the suggested ../ fix would actually escape the app. Documented the
mechanism at the link and added the suggested bUnit smoke test guarding the exact
href value (+1 test; 206 total).

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

Good instinct on the untested placeholder — but the blocker doesn't reproduce, and this time I can prove it at runtime: we now have real-browser tooling, so I clicked the link in Chrome instead of reasoning about URL resolution.

Trace, on the live app behind the gate:

  1. Navigate to /projects/ashfall (the workspace page).
  2. Click "Back to projects".
  3. Landed on http://127.0.0.1:5450/ — title "Kagura — Projects", the real list. Not /projects/, not the NotFound page.

The mechanism your analysis missed: the document sets <base href> (added in #13 for the sub-path deployment), and per the URL spec a relative href resolves against the base URL, not the current path. new URL("./", "https://host/")https://host/, and behind the proxy new URL("./", "https://host/kagura/")https://host/kagura/ — the app root in both cases.

Worth flagging back: the suggested ../ fix would introduce a real bug — new URL("../", "https://host/kagura/")https://host/escaping the app on exactly the sub-path deployment ADR 0015 exists for. href="./" is the correct value.

💡 Non-blocking — taken

Added the bUnit smoke test (ProjectWorkspacePageTests): renders the slug and guards the exact href="./" value with a comment explaining why ../ or / would break the sub-path (so a future "fix" can't sneak it in). Also documented the base-href mechanism at the link itself. ProjectWorkspacePage is no longer 0%.

+1 test (206 total), build clean. Separately, the whole PR got a full visual pass in the real browser this round: gate login, empty state, centered create modal, create→navigate (twice), row-click navigation, live debounced filtering, and dark mode (pixel-sampled to confirm the tokens). All good — one cosmetic note for a later slice: the app has no favicon yet (console 404s).

Good instinct on the untested placeholder — but the blocker doesn't reproduce, and this time I can prove it at runtime: we now have real-browser tooling, so I **clicked the link in Chrome** instead of reasoning about URL resolution. ### ⛔ The back-link — empirically refuted (`6460bfb`) Trace, on the live app behind the gate: 1. Navigate to `/projects/ashfall` (the workspace page). 2. Click **"Back to projects"**. 3. Landed on `http://127.0.0.1:5450/` — title "Kagura — Projects", the real list. **Not** `/projects/`, not the NotFound page. The mechanism your analysis missed: the document sets **`<base href>`** (added in #13 for the sub-path deployment), and per the URL spec a relative `href` resolves against the **base URL**, not the current path. `new URL("./", "https://host/")` → `https://host/`, and behind the proxy `new URL("./", "https://host/kagura/")` → `https://host/kagura/` — the app root in both cases. Worth flagging back: the suggested `../` fix would introduce a real bug — `new URL("../", "https://host/kagura/")` → `https://host/` — **escaping the app** on exactly the sub-path deployment ADR 0015 exists for. `href="./"` is the correct value. ### 💡 Non-blocking — taken Added the bUnit smoke test (`ProjectWorkspacePageTests`): renders the slug and **guards the exact `href="./"` value** with a comment explaining why `../` or `/` would break the sub-path (so a future "fix" can't sneak it in). Also documented the base-href mechanism at the link itself. `ProjectWorkspacePage` is no longer 0%. **+1 test (206 total)**, build clean. Separately, the whole PR got a full visual pass in the real browser this round: gate login, empty state, centered create modal, create→navigate (twice), row-click navigation, live debounced filtering, and dark mode (pixel-sampled to confirm the tokens). All good — one cosmetic note for a later slice: the app has no favicon yet (console 404s).
bjoern merged commit f6627e405b into main 2026-07-09 21:01:35 +02:00
bjoern deleted branch feat/project-list 2026-07-09 21:01:35 +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/Kagura!15
No description provided.