diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md
index 504f31b..37245c7 100644
--- a/docs/tables_frontend_overhaul_implementation_plan.md
+++ b/docs/tables_frontend_overhaul_implementation_plan.md
@@ -54,6 +54,7 @@ It is intentionally implementation-focused:
| 2026-03-21 | P2.2 | Completed | Replaced the old `/diagnostics` and `/api` pages with lightweight compatibility routes that reuse a shared redirect component, preserve query strings and fragments, and replace browser history while forwarding into the canonical `Tools` routes. |
| 2026-03-21 | P2.3 | Completed | Added a shared `RecentTablesState` service backed by browser storage, centralized the recents storage key, and started recording successful table visits from the `Tables` page so later omnibox and rail work has real shared data. |
| 2026-03-21 | P2.4 | Completed | Added a shared `PinnedTablesState` service with browser persistence, centralized the pin storage key, initialized pin state in the `Tables` page, and added a first live pin/unpin action plus pinned status chips so later omnibox and navigation work have real saved pins to consume. |
+| 2026-03-21 | P2.5 | Completed | Added shared table-context URL types and a serializer, registered the serializer centrally, and started round-tripping table context through `/tables` and `/tools/diagnostics` so direct links restore the selected table and diagnostic cell context instead of relying only on local storage defaults. |
### Lessons Learned
@@ -77,6 +78,7 @@ It is intentionally implementation-focused:
- Compatibility routes should stay declarative and shared. A single redirect component is enough for route forwarding and avoids copy-pasted `NavigationManager` lifecycle logic in page files.
- Shared browser-backed state becomes more useful once one real page writes to it immediately. Recording recents from `Tables` now keeps later omnibox work from being blocked on synthetic placeholder data.
- Shared pinned-state services also need one live writer early. A minimal pin/unpin affordance in the current `Tables` page is enough to validate persistence before the larger navigation surfaces consume pins.
+- URL serializers only pay off when wired into real pages. Using the shared serializer in both `Tables` and diagnostics now gives the project one verified round-trip path before the larger table-context service lands.
## Target Outcomes
@@ -368,7 +370,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest
| `P2.2` | Completed | Old `/diagnostics` and `/api` routes now forward into the canonical `Tools` routes with shared redirect behavior. |
| `P2.3` | Completed | Recent critical-table visits now persist through a shared app-state service and are recorded from the `Tables` page. |
| `P2.4` | Completed | Pinned tables now persist through a shared app-state service and can already be toggled from the `Tables` page. |
-| `P2.5` | Pending | Table-context URL parsing and serialization still needs a shared model. |
+| `P2.5` | Completed | Shared table-context URLs now parse and serialize through one helper and are consumed by `/tables` and `/tools/diagnostics`. |
| `P2.6` | Pending | The shell omnibox is still a placeholder trigger. |
| `P2.7` | Pending | Shared primitives for chips, tabs, drawers, and inspector sections are not extracted yet. |
| `P2.8` | Pending | Table-selection logic still lives inside individual pages. |
diff --git a/src/RolemasterDb.App/Components/Pages/Tables.razor b/src/RolemasterDb.App/Components/Pages/Tables.razor
index 543c525..ae09846 100644
--- a/src/RolemasterDb.App/Components/Pages/Tables.razor
+++ b/src/RolemasterDb.App/Components/Pages/Tables.razor
@@ -4,10 +4,12 @@
@using System.Collections.Generic
@using System.Diagnostics.CodeAnalysis
@using System.Linq
+@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject LookupService LookupService
@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
+@inject RolemasterDb.App.Frontend.AppState.TableContextUrlSerializer TableContextUrlSerializer
Critical Tables
@@ -285,6 +287,7 @@
isTableMenuOpen = false;
await PersistSelectedTableAsync(tableSlug);
await LoadTableDetailAsync();
+ SyncTableContextUrl();
}
private async Task LoadTableDetailAsync()
@@ -335,16 +338,18 @@
{
try
{
+ var routeContext = TableContextUrlSerializer.Parse(NavigationManager.Uri);
var storedTableSlug = await JSRuntime.InvokeAsync("localStorage.getItem", SelectedTableStorageKey);
hasResolvedStoredTableSelection = true;
- var resolvedTableSlug = ResolveSelectedTableSlug(storedTableSlug);
+ var resolvedTableSlug = ResolveSelectedTableSlug(routeContext.TableSlug ?? storedTableSlug);
if (string.IsNullOrWhiteSpace(selectedTableSlug) ||
!string.Equals(resolvedTableSlug, selectedTableSlug, StringComparison.OrdinalIgnoreCase))
{
selectedTableSlug = resolvedTableSlug;
await LoadTableDetailAsync();
await PersistSelectedTableAsync(selectedTableSlug);
+ SyncTableContextUrl();
await InvokeAsync(StateHasChanged);
return;
}
@@ -353,6 +358,8 @@
{
await PersistSelectedTableAsync(selectedTableSlug);
}
+
+ SyncTableContextUrl();
}
catch (InvalidOperationException)
{
@@ -793,6 +800,27 @@
private Task PersistSelectedTableAsync(string tableSlug) =>
JSRuntime.InvokeVoidAsync("localStorage.setItem", SelectedTableStorageKey, tableSlug).AsTask();
+ private void SyncTableContextUrl()
+ {
+ if (string.IsNullOrWhiteSpace(selectedTableSlug))
+ {
+ return;
+ }
+
+ var targetUri = TableContextUrlSerializer.BuildRelativeUri(
+ "/tables",
+ new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
+ TableSlug: selectedTableSlug,
+ Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference));
+
+ if (string.Equals(NavigationManager.ToBaseRelativePath(NavigationManager.Uri), targetUri.TrimStart('/'), StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ NavigationManager.NavigateTo(targetUri, replace: true);
+ }
+
private Task TogglePinnedTableAsync()
{
if (SelectedTableReference is not { } selectedTable)
diff --git a/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor b/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor
index f89d924..b8a58bf 100644
--- a/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor
+++ b/src/RolemasterDb.App/Components/Tools/DiagnosticsPageContent.razor
@@ -1,7 +1,9 @@
@using System
@using System.Collections.Generic
@using System.Linq
+@inject NavigationManager NavigationManager
@inject LookupService LookupService
+@inject RolemasterDb.App.Frontend.AppState.TableContextUrlSerializer TableContextUrlSerializer