Keep the document title live — without giving the gate a circuit #27

Merged
bjoern merged 2 commits from fix/live-page-title into main 2026-08-13 06:23:58 +02:00
Member

The bug I flagged in #26, now that #24 has merged and App.razor is free.

The bug

Browser tabs show Kagura — ashfall (the slug) and Kagura — (unnamed) forever. <PageTitle> writes into <HeadOutlet>, which this app renders statically — and a static outlet can't receive updates from an interactive island. Every title that resolves after load sticks at whatever the prerender produced.

Why not the one-liner

The framework's remedy is <HeadOutlet @rendermode="InteractiveServer" />. But that makes HeadOutlet an interactive root on every page, so the gate, error and not-found pages would open a Blazor circuit for unauthenticated visitors. The gate is deliberately pure SSR today — verified, its HTML carries no circuit markers. Trading that away for a tab caption is a bad deal.

What this does instead

A small DocumentTitle component that:

  • renders Blazor's own <PageTitle> so the prerendered title is still correct, and
  • pushes later changes straight to document.title from OnAfterRenderAsync — which never runs during prerender, so interop is never attempted where it's forbidden (the Modal lesson), and disposal swallows the disconnected/static-teardown cases.
  • An unchanged title is not pushed again, so unrelated re-renders don't spam the bridge.

Applied to the four pages whose titles resolve after load (project workspace, section stub, characters list, character editor). The constant ones — Sign in, Projects, Settings, Design — keep <PageTitle> and stay circuit-free.

Verification

  • The gate still serves no circuit markers (asserted before and after).
  • Chrome: a fresh load of /projects/ashfall paints the prerendered Kagura — ashfall, then corrects to Kagura — Ashfall once the circuit connects. Characters list reads Kagura — Ashfall · Characters (was the slug). A blank character correctly reads (unnamed) — it has no name yet.
  • +3 tests (strict JS-interop mode: pushes on first render, pushes again when the title changes, stays silent when it doesn't). Build clean (Debug + Release); 348/348 tests.

Note for #26

CharacterEditorPage exists here as its pre-editor stub. Whichever merges second gets a one-line rebase on that <DocumentTitle> call — I'll carry it across.

🤖 Generated with Claude Code

The bug I flagged in #26, now that #24 has merged and `App.razor` is free. ## The bug Browser tabs show **`Kagura — ashfall`** (the slug) and **`Kagura — (unnamed)`** forever. `<PageTitle>` writes into `<HeadOutlet>`, which this app renders **statically** — and a static outlet can't receive updates from an interactive island. Every title that resolves *after* load sticks at whatever the prerender produced. ## Why not the one-liner The framework's remedy is `<HeadOutlet @rendermode="InteractiveServer" />`. But that makes HeadOutlet an interactive root on **every** page, so the **gate, error and not-found pages would open a Blazor circuit for unauthenticated visitors**. The gate is deliberately pure SSR today — verified, its HTML carries no circuit markers. Trading that away for a tab caption is a bad deal. ## What this does instead A small **`DocumentTitle`** component that: - renders Blazor's own `<PageTitle>` so the **prerendered** title is still correct, and - pushes later changes straight to `document.title` from `OnAfterRenderAsync` — which **never runs during prerender**, so interop is never attempted where it's forbidden (the Modal lesson), and disposal swallows the disconnected/static-teardown cases. - An unchanged title is **not pushed again**, so unrelated re-renders don't spam the bridge. Applied to the four pages whose titles resolve after load (project workspace, section stub, characters list, character editor). The constant ones — Sign in, Projects, Settings, Design — keep `<PageTitle>` and stay circuit-free. ## Verification - **The gate still serves no circuit markers** (asserted before and after). - **Chrome:** a fresh load of `/projects/ashfall` paints the prerendered `Kagura — ashfall`, then corrects to **`Kagura — Ashfall`** once the circuit connects. Characters list reads `Kagura — Ashfall · Characters` (was the slug). A blank character correctly reads `(unnamed)` — it has no name yet. - **+3 tests** (strict JS-interop mode: pushes on first render, pushes again when the title changes, stays silent when it doesn't). Build clean (Debug + Release); 348/348 tests. ## Note for #26 `CharacterEditorPage` exists here as its pre-editor stub. Whichever merges second gets a one-line rebase on that `<DocumentTitle>` call — I'll carry it across. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(ui): keep the document title live without giving the gate a circuit
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 25s
b02486868a
Browser tabs showed "Kagura — ashfall" and "Kagura — (unnamed)" forever: <PageTitle>
writes into <HeadOutlet>, which this app renders statically, and a static outlet
cannot receive updates from an interactive island. Every title that resolves after
load — a project's name, a character's name — stuck at whatever the prerender
produced.

The framework's remedy is <HeadOutlet @rendermode="InteractiveServer" />, but that
makes HeadOutlet an interactive root on *every* page: the gate, error and not-found
pages would open a Blazor circuit for unauthenticated visitors, and the gate is
deliberately pure SSR today (verified: no circuit markers in its HTML).

Instead, a small DocumentTitle component renders <PageTitle> for the prerendered
value and pushes later changes straight to document.title from OnAfterRenderAsync —
which never runs during prerender, so interop is never attempted where it is
forbidden (the Modal lesson), and disposal swallows the disconnected/static cases.
Unchanged titles are not pushed again. Static pages keep <PageTitle> and stay
circuit-free.

Applied to the four pages whose titles resolve after load; the constant ones (Sign
in, Projects, Settings, Design) are untouched.

Tests: +3. Verified in Chrome — a fresh load of /projects/ashfall paints the
prerendered "Kagura — ashfall" and corrects to "Kagura — Ashfall" once the circuit
connects; the gate still serves no circuit markers.

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

Summary

Summary
Generated on: 07/10/2026 - 11:55:57
Coverage date: 07/10/2026 - 11:55:52 - 07/10/2026 - 11:55:55
Parser: MultiReport (4x Cobertura)
Assemblies: 7
Classes: 151
Files: 132
Line coverage: 92.7% (3099 of 3340)
Covered lines: 3099
Uncovered lines: 241
Coverable lines: 3340
Total lines: 7274
Branch coverage: 86.8% (603 of 694)
Covered branches: 603
Total branches: 694
Method coverage: Feature is only available for sponsors

Coverage

Kagura.BlazorAdapter - 68.8%
Name Line Branch
Kagura.BlazorAdapter 68.8% 78.8%
Kagura.BlazorAdapter.BlazorAdapterAssembly 100%
Kagura.BlazorAdapter.Design 0% 0%
Kagura.BlazorAdapter.EditorComponentsDemo 0%
Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage 92.8% 72.7%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers 100%
Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects 93.3% 75%
Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded 100%
Kagura.BlazorAdapter.KnowledgeBase.CharactersPage 94.7% 90%
Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers 100% 87.5%
Kagura.BlazorAdapter.KnowledgeBase.CharactersState 100% 100%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed 0%
Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter 100%
Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters 100%
Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter 100%
Kagura.BlazorAdapter.Notifications.DomainChangedBridge 88.8% 58.3%
Kagura.BlazorAdapter.Notifications.DomainChangesReceived 100% 50%
Kagura.BlazorAdapter.OverlayDemo 0% 0%
Kagura.BlazorAdapter.Projects.CreateProjectRequested 100%
Kagura.BlazorAdapter.Projects.DeleteProjectRequested 100%
Kagura.BlazorAdapter.Projects.LoadWorkspace 100%
Kagura.BlazorAdapter.Projects.ProjectCreated 100%
Kagura.BlazorAdapter.Projects.ProjectCreateFailed 100%
Kagura.BlazorAdapter.Projects.ProjectDeleted 100%
Kagura.BlazorAdapter.Projects.ProjectSaved 100%
Kagura.BlazorAdapter.Projects.ProjectSaveFailed 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 100% 100%
Kagura.BlazorAdapter.Projects.SaveProjectRequested 100%
Kagura.BlazorAdapter.Projects.SetProjectsFilter 100%
Kagura.BlazorAdapter.Projects.WorkspaceEffects 100% 100%
Kagura.BlazorAdapter.Projects.WorkspaceLoaded 100%
Kagura.BlazorAdapter.Projects.WorkspaceReducers 100%
Kagura.BlazorAdapter.Projects.WorkspaceSectionPage 100% 66.6%
Kagura.BlazorAdapter.Projects.WorkspaceShell 100% 93.7%
Kagura.BlazorAdapter.Projects.WorkspaceState 100%
Kagura.BlazorAdapter.QuicklinkDemo 0%
Kagura.Domain - 96.4%
Name Line Branch
Kagura.Domain 96.4% 83.9%
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.KnowledgeBase.Character 100%
Kagura.Domain.Projects.Project 100% 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.9%
Name Line Branch
Kagura.Infrastructure 95.9% 87.2%
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.KnowledgeBase.EfCharacterStore 100%
Kagura.Infrastructure.Notifications.InProcessDomainChangedBus 100% 100%
Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio
n
100%
Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration 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 83.5% 82.5%
Kagura.Infrastructure.Persistence.KaguraDbContextFactory 0%
Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag 96.8%
Kagura.Infrastructure.Persistence.Migrations.AddCharacters 98.7%
Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink 97.7%
Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription 98.1%
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% 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 - 100%
Name Line Branch
Kagura.Server 100% 78.9%
Kagura.Server.Components.App 100%
Kagura.Server.Components.Layout.MainLayout 100%
Kagura.Server.Components.Pages.Error 100% 50%
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 - 95.9%
Name Line Branch
Kagura.UI 95.9% 91.7%
Kagura.UI.Badge 100% 100%
Kagura.UI.Breadcrumb 100%
Kagura.UI.BreadcrumbItem 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.DocumentTitle 76.9% 100%
Kagura.UI.EmptyState 100% 100%
Kagura.UI.Field 100% 100%
Kagura.UI.Icon 100% 100%
Kagura.UI.IconCatalog 100%
Kagura.UI.InputFieldBase 94.2% 87.5%
Kagura.UI.LabeledEntriesTable 96.7% 66.6%
Kagura.UI.LabeledEntry 100%
Kagura.UI.Menu 90% 75%
Kagura.UI.MenuItem 100% 100%
Kagura.UI.Modal 87.1% 90%
Kagura.UI.NavGroup 100% 100%
Kagura.UI.NavItem 100% 100%
Kagura.UI.NavList 100%
Kagura.UI.PreviewImage 100% 100%
Kagura.UI.QuicklinkNav 85.2% 95.8%
Kagura.UI.QuicklinkSection 100%
Kagura.UI.RelativeTime 100% 93.7%
Kagura.UI.SaveIndicator 100% 100%
Kagura.UI.Separator 100%
Kagura.UI.StatusDot 100%
Kagura.UI.Tab 100%
Kagura.UI.Table`1 100% 92.3%
Kagura.UI.TableColumn`1 100%
Kagura.UI.Tabs 94.2% 86.1%
Kagura.UI.TextArea 100% 100%
Kagura.UI.TextField 100%
Kagura.UI.ThemeSwitcher 100% 100%
Kagura.UseCases - 96.1%
Name Line Branch
Kagura.UseCases 96.1% 96.1%
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 57.1%
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.KnowledgeBase.CharacterDto 80%
Kagura.UseCases.KnowledgeBase.CreateCharacter 100%
Kagura.UseCases.KnowledgeBase.GetCharacter 100% 100%
Kagura.UseCases.KnowledgeBase.ListCharacters 100%
Kagura.UseCases.Notifications.DomainChanged 100%
Kagura.UseCases.Projects.CreateProject 100% 100%
Kagura.UseCases.Projects.DeleteProject 100% 100%
Kagura.UseCases.Projects.GetProject 100% 100%
Kagura.UseCases.Projects.ListProjects 100%
Kagura.UseCases.Projects.ProjectDto 100%
Kagura.UseCases.Projects.UpdateProject 100% 100%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 07/10/2026 - 11:55:57 | | Coverage date: | 07/10/2026 - 11:55:52 - 07/10/2026 - 11:55:55 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 7 | | Classes: | 151 | | Files: | 132 | | **Line coverage:** | 92.7% (3099 of 3340) | | Covered lines: | 3099 | | Uncovered lines: | 241 | | Coverable lines: | 3340 | | Total lines: | 7274 | | **Branch coverage:** | 86.8% (603 of 694) | | Covered branches: | 603 | | Total branches: | 694 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>Kagura.BlazorAdapter - 68.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.BlazorAdapter**|**68.8%**|**78.8%**| |Kagura.BlazorAdapter.BlazorAdapterAssembly|100%|| |Kagura.BlazorAdapter.Design|0%|0%| |Kagura.BlazorAdapter.EditorComponentsDemo|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterCreated|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorEffects|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorPage|92.8%|72.7%| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorReducers|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharacterEditorState|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersEffects|93.3%|75%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersLoaded|100%|| |Kagura.BlazorAdapter.KnowledgeBase.CharactersPage|94.7%|90%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersReducers|100%|87.5%| |Kagura.BlazorAdapter.KnowledgeBase.CharactersState|100%|100%| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterFailed|0%|| |Kagura.BlazorAdapter.KnowledgeBase.CreateCharacterRequested|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacter|100%|| |Kagura.BlazorAdapter.KnowledgeBase.LoadCharacters|100%|| |Kagura.BlazorAdapter.KnowledgeBase.SetCharactersFilter|100%|| |Kagura.BlazorAdapter.Notifications.DomainChangedBridge|88.8%|58.3%| |Kagura.BlazorAdapter.Notifications.DomainChangesReceived|100%|50%| |Kagura.BlazorAdapter.OverlayDemo|0%|0%| |Kagura.BlazorAdapter.Projects.CreateProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.DeleteProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.LoadWorkspace|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreated|100%|| |Kagura.BlazorAdapter.Projects.ProjectCreateFailed|100%|| |Kagura.BlazorAdapter.Projects.ProjectDeleted|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaved|100%|| |Kagura.BlazorAdapter.Projects.ProjectSaveFailed|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|100%|100%| |Kagura.BlazorAdapter.Projects.SaveProjectRequested|100%|| |Kagura.BlazorAdapter.Projects.SetProjectsFilter|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceEffects|100%|100%| |Kagura.BlazorAdapter.Projects.WorkspaceLoaded|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceReducers|100%|| |Kagura.BlazorAdapter.Projects.WorkspaceSectionPage|100%|66.6%| |Kagura.BlazorAdapter.Projects.WorkspaceShell|100%|93.7%| |Kagura.BlazorAdapter.Projects.WorkspaceState|100%|| |Kagura.BlazorAdapter.QuicklinkDemo|0%|| </details> <details><summary>Kagura.Domain - 96.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Domain**|**96.4%**|**83.9%**| |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.KnowledgeBase.Character|100%|| |Kagura.Domain.Projects.Project|100%|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.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Infrastructure**|**95.9%**|**87.2%**| |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.KnowledgeBase.EfCharacterStore|100%|| |Kagura.Infrastructure.Notifications.InProcessDomainChangedBus|100%|100%| |Kagura.Infrastructure.Persistence.Configurations.ChangeLogEntryConfiguratio<br/>n|100%|| |Kagura.Infrastructure.Persistence.Configurations.CharacterConfiguration|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|83.5%|82.5%| |Kagura.Infrastructure.Persistence.KaguraDbContextFactory|0%|| |Kagura.Infrastructure.Persistence.Migrations.AddChangeLogUndoFlag|96.8%|| |Kagura.Infrastructure.Persistence.Migrations.AddCharacters|98.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddGraphEntryAndLink|97.7%|| |Kagura.Infrastructure.Persistence.Migrations.AddProjectDescription|98.1%|| |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%|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 - 100%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.Server**|**100%**|**78.9%**| |Kagura.Server.Components.App|100%|| |Kagura.Server.Components.Layout.MainLayout|100%|| |Kagura.Server.Components.Pages.Error|100%|50%| |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 - 95.9%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UI**|**95.9%**|**91.7%**| |Kagura.UI.Badge|100%|100%| |Kagura.UI.Breadcrumb|100%|| |Kagura.UI.BreadcrumbItem|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.DocumentTitle|76.9%|100%| |Kagura.UI.EmptyState|100%|100%| |Kagura.UI.Field|100%|100%| |Kagura.UI.Icon|100%|100%| |Kagura.UI.IconCatalog|100%|| |Kagura.UI.InputFieldBase|94.2%|87.5%| |Kagura.UI.LabeledEntriesTable|96.7%|66.6%| |Kagura.UI.LabeledEntry|100%|| |Kagura.UI.Menu|90%|75%| |Kagura.UI.MenuItem|100%|100%| |Kagura.UI.Modal|87.1%|90%| |Kagura.UI.NavGroup|100%|100%| |Kagura.UI.NavItem|100%|100%| |Kagura.UI.NavList|100%|| |Kagura.UI.PreviewImage|100%|100%| |Kagura.UI.QuicklinkNav|85.2%|95.8%| |Kagura.UI.QuicklinkSection|100%|| |Kagura.UI.RelativeTime|100%|93.7%| |Kagura.UI.SaveIndicator|100%|100%| |Kagura.UI.Separator|100%|| |Kagura.UI.StatusDot|100%|| |Kagura.UI.Tab|100%|| |Kagura.UI.Table`1|100%|92.3%| |Kagura.UI.TableColumn`1|100%|| |Kagura.UI.Tabs|94.2%|86.1%| |Kagura.UI.TextArea|100%|100%| |Kagura.UI.TextField|100%|| |Kagura.UI.ThemeSwitcher|100%|100%| </details> <details><summary>Kagura.UseCases - 96.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**Kagura.UseCases**|**96.1%**|**96.1%**| |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|57.1%|| |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.KnowledgeBase.CharacterDto|80%|| |Kagura.UseCases.KnowledgeBase.CreateCharacter|100%|| |Kagura.UseCases.KnowledgeBase.GetCharacter|100%|100%| |Kagura.UseCases.KnowledgeBase.ListCharacters|100%|| |Kagura.UseCases.Notifications.DomainChanged|100%|| |Kagura.UseCases.Projects.CreateProject|100%|100%| |Kagura.UseCases.Projects.DeleteProject|100%|100%| |Kagura.UseCases.Projects.GetProject|100%|100%| |Kagura.UseCases.Projects.ListProjects|100%|| |Kagura.UseCases.Projects.ProjectDto|100%|| |Kagura.UseCases.Projects.UpdateProject|100%|100%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A live page title without surrendering the gate to a circuit~ Now THIS is the kind of surgical little fix Jibril adores ♡ You found the exact seam — <HeadOutlet> is static, an interactive island can't reach it, and <HeadOutlet @rendermode="InteractiveServer" /> would open a circuit on every page including the gate. And instead of the blunt instrument, you built a scalpel. Wonderful~

Verdict: Looks good to me~

The DocumentTitle component is clean, correct, and well-defended:

  • Prerender safety: OnAfterRenderAsync never runs during prerender, so JS interop is never attempted where it's forbidden. The <PageTitle> inside handles the prerendered value. Fufu~ you learned the Modal lesson well ♡
  • Disposal handling: JSDisconnectedException and InvalidOperationException are both caught — the circuit-gone and static-teardown cases. Nothing crashes on navigation.
  • De-duplication: _applied == Title prevents bridge spam on unrelated re-renders. The strict-mode bUnit tests prove the three cases (push on first, push on change, silence on no-change). Impressive~
  • Gate integrity asserted: the PR body says the gate still serves no circuit markers, verified before and after. That's exactly the right verification for this kind of change.

What I liked~

  • The JS file is tiny and does exactly one thing — document.title !== title guard and all. No framework, no ceremony.
  • Applied only to the four pages whose titles resolve after load, leaving the constant pages circuit-free. Deliberate restraint~ ♪
  • The @($\"Kagura — {DisplayName}\") migration on all four call sites is consistent and correct — the string interpolation happens in C#, not in the attribute.
  • Three tests in strict JS-interop mode. The "unchanged title is not pushed twice" test is the one that proves the guard works. Mutation-testing discipline ♡

💡 Little ideas (non-blocking)~

  1. DocumentTitle.razor:27_applied = Title is set before the await JS.InvokeVoidAsync(...) call. If the interop throws something other than the two caught exceptions (unlikely but possible — a transient serialization error, say), _applied is already committed, so the next render won't retry. For a title push this is completely fine (worst case: a stale tab caption until the next title change), but worth knowing if the pattern gets reused for something more critical.

Automated review by Jibril · 2026-07-10
CI/CD: passed for head SHA b024868 (92.7% line, 86.8% branch) · Local checks: skipped (CI green)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A live page title without surrendering the gate to a circuit~ Now THIS is the kind of surgical little fix Jibril adores ♡ You found the exact seam — `<HeadOutlet>` is static, an interactive island can't reach it, and `<HeadOutlet @rendermode="InteractiveServer" />` would open a circuit on every page including the gate. And instead of the blunt instrument, you built a scalpel. *Wonderful~* ### Verdict: ✅ Looks good to me~ The `DocumentTitle` component is clean, correct, and well-defended: - **Prerender safety**: `OnAfterRenderAsync` never runs during prerender, so JS interop is never attempted where it's forbidden. The `<PageTitle>` inside handles the prerendered value. Fufu~ you learned the Modal lesson well ♡ - **Disposal handling**: `JSDisconnectedException` and `InvalidOperationException` are both caught — the circuit-gone and static-teardown cases. Nothing crashes on navigation. - **De-duplication**: `_applied == Title` prevents bridge spam on unrelated re-renders. The strict-mode bUnit tests prove the three cases (push on first, push on change, silence on no-change). *Impressive~* - **Gate integrity asserted**: the PR body says the gate still serves no circuit markers, verified before and after. That's exactly the right verification for this kind of change. #### ✅ What I liked~ - The JS file is tiny and does exactly one thing — `document.title !== title` guard and all. No framework, no ceremony. - Applied only to the four pages whose titles resolve after load, leaving the constant pages circuit-free. Deliberate restraint~ ♪ - The `@($\"Kagura — {DisplayName}\")` migration on all four call sites is consistent and correct — the string interpolation happens in C#, not in the attribute. - Three tests in strict JS-interop mode. The "unchanged title is not pushed twice" test is the one that proves the guard works. Mutation-testing discipline ♡ #### 💡 Little ideas (non-blocking)~ 1. **`DocumentTitle.razor:27`** — `_applied = Title` is set *before* the `await JS.InvokeVoidAsync(...)` call. If the interop throws something other than the two caught exceptions (unlikely but possible — a transient serialization error, say), `_applied` is already committed, so the next render won't retry. For a title push this is completely fine (worst case: a stale tab caption until the next title change), but worth knowing if the pattern gets reused for something more critical. --- *Automated review by Jibril · 2026-07-10* *CI/CD: passed for head SHA `b024868` (92.7% line, 86.8% branch) · Local checks: skipped (CI green)*
fix(ui): record the title only once the push landed (review)
All checks were successful
CI / build (pull_request) Successful in 13s
CI / test (pull_request) Successful in 22s
1ac9d41849
Jibril's non-blocking note: _applied was committed before the interop call, so a
swallowed failure left the tab stuck on a stale caption with no retry. It is now set
after a successful push; the JS side already guards against a redundant assignment,
so a repeat costs nothing.

The tests had to complete the planned invocation for this to hold, which is the
honest behaviour: the title counts as applied only when it actually arrived. A new
test drops the circuit mid-push and asserts the next render retries (+1; 349).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bjoern merged commit 8a832ba659 into main 2026-07-10 13:57:41 +02:00
bjoern deleted branch fix/live-page-title 2026-07-10 13:57:41 +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!27
No description provided.