diff --git a/docs/tables_frontend_overhaul_implementation_plan.md b/docs/tables_frontend_overhaul_implementation_plan.md
index 7774256..cfdd4d1 100644
--- a/docs/tables_frontend_overhaul_implementation_plan.md
+++ b/docs/tables_frontend_overhaul_implementation_plan.md
@@ -51,6 +51,7 @@ It is intentionally implementation-focused:
| 2026-03-21 | Post-P1 fix 3 | Completed | Restored layout-level shell interactivity by rendering routed content in `InteractiveServer` mode, which re-enabled shell event handlers such as the hamburger menu and theme selector. |
| 2026-03-21 | Post-P1 fix 4 | Completed | Added early theme bootstrapping in `App.razor` and `theme.js` so the stored mode is applied before hydration and remains visible after refresh. |
| 2026-03-21 | P2.1 | Completed | Added canonical `/tools/diagnostics` and `/tools/api` routes with dedicated tooling page components, extracted the diagnostics and API content into shared tool components, and updated the `Tools` landing page to link to the new route structure. |
+| 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. |
### Lessons Learned
@@ -71,6 +72,7 @@ It is intentionally implementation-focused:
- In Blazor Web Apps, page-level render modes do not automatically make layout-level controls interactive in the way this shell expects. The routed shell itself needs an interactive render boundary.
- Persisted theme state should be applied before Blazor hydrates, not only after layout initialization. Otherwise refresh can look broken even when storage writes succeed.
- For route migration in Blazor, extracting the destination UI into shared components keeps canonical routes and temporary compatibility routes from drifting while the redirect phase is still pending.
+- 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.
## Target Outcomes
@@ -359,7 +361,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest
| Task | Status | Notes |
| --- | --- | --- |
| `P2.1` | Completed | Canonical `Tools` child routes now exist, backed by dedicated route pages and shared tooling content components. |
-| `P2.2` | Pending | Old `/diagnostics` and `/api` routes still need to become compatibility forwards. |
+| `P2.2` | Completed | Old `/diagnostics` and `/api` routes now forward into the canonical `Tools` routes with shared redirect behavior. |
| `P2.3` | Pending | Recent tables state has not been introduced yet. |
| `P2.4` | Pending | Pinned tables state has not been introduced yet. |
| `P2.5` | Pending | Table-context URL parsing and serialization still needs a shared model. |
diff --git a/src/RolemasterDb.App/Components/Pages/Api.razor b/src/RolemasterDb.App/Components/Pages/Api.razor
index 3126bcb..61ab01d 100644
--- a/src/RolemasterDb.App/Components/Pages/Api.razor
+++ b/src/RolemasterDb.App/Components/Pages/Api.razor
@@ -2,146 +2,4 @@
GET /api/reference-data
{
- "attackTables": [
- {
- "key": "broadsword",
- "label": "Broadsword",
- "attackKind": "melee",
- "fumbleMinRoll": 1,
- "fumbleMaxRoll": 2
- }
- ],
- "criticalTables": [
- {
- "key": "mana",
- "label": "Mana Critical Strike Table",
- "family": "standard",
- "sourceDocument": "Mana.pdf",
- "notes": "Imported from PDF XML extraction."
- }
- ]
-}
- POST /api/lookup/attack
{
- "attackTable": "broadsword",
- "armorType": "AT10",
- "roll": 111,
- "criticalRoll": 72
-}
- POST /api/lookup/critical
{
- "criticalType": "mana",
- "column": "E",
- "roll": 100,
- "group": null
-}
- Response now includes table metadata, roll-band bounds, raw imported cell text, parse status, and parsed JSON alongside the gameplay description.
-GET /api/tables/critical/{slug}/cells/{resultId}
{
- "resultId": 412,
- "tableSlug": "slash",
- "tableName": "Slash Critical Strike Table",
- "rollBand": "66-70",
- "groupKey": null,
- "columnKey": "C",
- "isCurated": false,
- "sourcePageNumber": 1,
- "sourceImageUrl": "/api/tables/critical/slash/cells/412/source-image",
- "rawCellText": "Original imported full cell text",
- "descriptionText": "Current curated prose",
- "rawAffixText": "+8H - 2S",
- "parseStatus": "verified",
- "parsedJson": "{\"version\":1,\"isDescriptionOverridden\":false,\"isRawAffixTextOverridden\":false,\"areEffectsOverridden\":false,\"areBranchesOverridden\":false,\"effects\":[],\"branches\":[]}",
- "isDescriptionOverridden": false,
- "isRawAffixTextOverridden": false,
- "areEffectsOverridden": false,
- "areBranchesOverridden": false,
- "validationMessages": [],
- "effects": [],
- "branches": []
-}
- Use this to retrieve the full editable result graph for one critical-table cell, including nested branches, normalized effects, and review notes for unresolved quick-parse tokens.
-GET /api/tables/critical/{slug}/cells/{resultId}/source-image
Streams the importer-generated PNG crop for the current critical cell. Returns 404 when the row has no stored crop or the artifact is missing.
POST /api/tables/critical/{slug}/cells/{resultId}/reparse
{
- "currentState": {
- "rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside.",
- "descriptionText": "Curated prose",
- "rawAffixText": "+8H",
- "parseStatus": "partial",
- "parsedJson": "{}",
- "isCurated": false,
- "isDescriptionOverridden": true,
- "isRawAffixTextOverridden": false,
- "areEffectsOverridden": false,
- "areBranchesOverridden": false,
- "effects": [],
- "branches": []
- }
-}
- Re-runs the shared single-cell parser, merges the generated result with the current override state, and returns the refreshed editor payload without saving changes. Unknown or partially parsed tokens are surfaced explicitly in the returned review data.
-PUT /api/tables/critical/{slug}/cells/{resultId}
{
- "rawCellText": "Corrected imported text",
- "descriptionText": "Rewritten prose after manual review",
- "rawAffixText": "+10H - must parry 2 rnds",
- "parseStatus": "manually_curated",
- "parsedJson": "{\"reviewed\":true}",
- "isCurated": true,
- "isDescriptionOverridden": true,
- "isRawAffixTextOverridden": false,
- "areEffectsOverridden": false,
- "areBranchesOverridden": false,
- "effects": [
- {
- "effectCode": "direct_hits",
- "target": null,
- "valueInteger": 10,
- "valueDecimal": null,
- "valueExpression": null,
- "durationRounds": null,
- "perRound": null,
- "modifier": null,
- "bodyPart": null,
- "isPermanent": false,
- "sourceType": "symbol",
- "sourceText": "+10H",
- "originKey": "base:direct_hits:1",
- "isOverridden": true
- }
- ],
- "branches": []
-}
- The save endpoint replaces the stored base result, branch rows, and effect rows for that cell with the submitted curated payload.
-Engineering-only parser metadata, provenance, and payload inspection live here instead of inside the normal curation modal.
-Loading table list...
- } - else if (!referenceData.CriticalTables.Any()) - { -No critical tables are available yet.
- } - else - { -@detailError
- } - else if (tableDetail is null) - { -The selected table could not be loaded.
- } - else if (!tableDetail.Cells.Any()) - { -The selected table has no filled cells to inspect.
- } - else if (selectedCell is null) - { -Pick another roll band, variant, or severity to inspect a stored result.
-Loading diagnostics...
- } - else if (!string.IsNullOrWhiteSpace(diagnosticsError)) - { -@diagnosticsError
- } - else if (diagnosticsModel is not null) - { -This route moved to @TargetPath. If the redirect does not complete automatically, use the link below.