Add tools route compatibility redirects

This commit is contained in:
2026-03-21 13:51:37 +01:00
parent 354c376f1d
commit 8b2984e7bd
4 changed files with 35 additions and 472 deletions

View File

@@ -2,146 +2,4 @@
<PageTitle>API Surface</PageTitle>
<div class="api-grid">
<section class="panel tooling-surface">
<h2 class="panel-title">Reference data</h2>
<p class="panel-copy"><code>GET /api/reference-data</code></p>
<pre class="code-block">{
"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."
}
]
}</pre>
</section>
<section class="panel tooling-surface">
<h2 class="panel-title">Attack lookup</h2>
<p class="panel-copy"><code>POST /api/lookup/attack</code></p>
<pre class="code-block">{
"attackTable": "broadsword",
"armorType": "AT10",
"roll": 111,
"criticalRoll": 72
}</pre>
</section>
<section class="panel tooling-surface">
<h2 class="panel-title">Critical lookup</h2>
<p class="panel-copy"><code>POST /api/lookup/critical</code></p>
<pre class="code-block">{
"criticalType": "mana",
"column": "E",
"roll": 100,
"group": null
}</pre>
<p class="panel-copy">Response now includes table metadata, roll-band bounds, raw imported cell text, parse status, and parsed JSON alongside the gameplay description.</p>
</section>
<section class="panel tooling-surface">
<h2 class="panel-title">Cell editor load</h2>
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}</code></p>
<pre class="code-block">{
"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": []
}</pre>
<p class="panel-copy">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.</p>
</section>
<section class="panel tooling-surface">
<h2 class="panel-title">Cell source image</h2>
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}/source-image</code></p>
<p class="panel-copy">Streams the importer-generated PNG crop for the current critical cell. Returns <code>404</code> when the row has no stored crop or the artifact is missing.</p>
</section>
<section class="panel tooling-surface">
<h2 class="panel-title">Cell re-parse</h2>
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
<pre class="code-block">{
"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": []
}
}</pre>
<p class="panel-copy">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.</p>
</section>
<section class="panel tooling-surface">
<h2 class="panel-title">Cell editor save</h2>
<p class="panel-copy"><code>PUT /api/tables/critical/{slug}/cells/{resultId}</code></p>
<pre class="code-block">{
"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": []
}</pre>
<p class="panel-copy">The save endpoint replaces the stored base result, branch rows, and effect rows for that cell with the submitted curated payload.</p>
</section>
</div>
<CompatibilityRouteRedirect TargetPath="/tools/api" />

View File

@@ -1,332 +1,5 @@
@page "/diagnostics"
@rendermode InteractiveServer
@using System
@using System.Collections.Generic
@using System.Linq
@inject LookupService LookupService
<PageTitle>Diagnostics</PageTitle>
<section class="panel diagnostics-page tooling-surface">
<header class="diagnostics-page-header">
<div>
<h2 class="panel-title">Critical Cell Diagnostics</h2>
<p class="panel-copy">Engineering-only parser metadata, provenance, and payload inspection live here instead of inside the normal curation modal.</p>
</div>
</header>
@if (referenceData is null)
{
<p class="muted">Loading table list...</p>
}
else if (!referenceData.CriticalTables.Any())
{
<p class="muted">No critical tables are available yet.</p>
}
else
{
<div class="diagnostics-selector-grid">
<div class="field-shell">
<label for="diagnostics-table-select">Table</label>
<select
id="diagnostics-table-select"
class="input-shell"
value="@selectedTableSlug"
@onchange="HandleTableChanged"
disabled="@isBusy">
@foreach (var table in referenceData.CriticalTables)
{
<option value="@table.Key">@table.Label</option>
}
</select>
</div>
<div class="field-shell">
<label for="diagnostics-roll-band-select">Roll Band</label>
<select
id="diagnostics-roll-band-select"
class="input-shell"
value="@selectedRollBand"
@onchange="HandleRollBandChanged"
disabled="@(isBusy || tableDetail is null || !tableDetail.RollBands.Any())">
@if (tableDetail is not null)
{
@foreach (var rollBand in tableDetail.RollBands)
{
<option value="@rollBand.Label">@rollBand.Label</option>
}
}
</select>
</div>
@if (tableDetail is { Groups.Count: > 0 })
{
<div class="field-shell">
<label for="diagnostics-group-select">Variant</label>
<select
id="diagnostics-group-select"
class="input-shell"
value="@selectedGroupKey"
@onchange="HandleGroupChanged"
disabled="@isBusy">
@foreach (var group in tableDetail.Groups)
{
<option value="@group.Key">@group.Label</option>
}
</select>
</div>
}
<div class="field-shell">
<label for="diagnostics-column-select">Severity</label>
<select
id="diagnostics-column-select"
class="input-shell"
value="@selectedColumnKey"
@onchange="HandleColumnChanged"
disabled="@(isBusy || tableDetail is null || !tableDetail.Columns.Any())">
@if (tableDetail is not null)
{
@foreach (var column in tableDetail.Columns)
{
<option value="@column.Key">@column.Label</option>
}
}
</select>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(detailError))
{
<p class="error-text">@detailError</p>
}
else if (tableDetail is null)
{
<p class="muted">The selected table could not be loaded.</p>
}
else if (!tableDetail.Cells.Any())
{
<p class="muted">The selected table has no filled cells to inspect.</p>
}
else if (selectedCell is null)
{
<div class="critical-editor-card nested">
<div class="critical-editor-card-header">
<div>
<strong>No Filled Cell At This Position</strong>
<p class="muted critical-editor-inline-copy">Pick another roll band, variant, or severity to inspect a stored result.</p>
</div>
</div>
</div>
}
else
{
<div class="diagnostics-selection-summary">
<strong>Inspecting</strong>
<span>@tableDetail.DisplayName</span>
<span>· Roll band <strong>@selectedCell.RollBand</strong></span>
<span>· Severity <strong>@selectedCell.ColumnLabel</strong></span>
@if (!string.IsNullOrWhiteSpace(selectedCell.GroupLabel))
{
<span>· Variant <strong>@selectedCell.GroupLabel</strong></span>
}
<span>· Result ID <strong>@selectedCell.ResultId</strong></span>
</div>
@if (isDiagnosticsLoading)
{
<p class="muted">Loading diagnostics...</p>
}
else if (!string.IsNullOrWhiteSpace(diagnosticsError))
{
<p class="error-text">@diagnosticsError</p>
}
else if (diagnosticsModel is not null)
{
<CriticalCellEngineeringDiagnostics Model="diagnosticsModel" />
}
}
}
</section>
@code {
private LookupReferenceData? referenceData;
private CriticalTableDetail? tableDetail;
private CriticalTableCellDetail? selectedCell;
private CriticalCellEditorModel? diagnosticsModel;
private string selectedTableSlug = string.Empty;
private string selectedRollBand = string.Empty;
private string selectedColumnKey = string.Empty;
private string? selectedGroupKey;
private bool isDetailLoading;
private bool isDiagnosticsLoading;
private string? detailError;
private string? diagnosticsError;
private bool isBusy => isDetailLoading || isDiagnosticsLoading;
protected override async Task OnInitializedAsync()
{
referenceData = await LookupService.GetReferenceDataAsync();
selectedTableSlug = referenceData.CriticalTables.FirstOrDefault()?.Key ?? string.Empty;
await LoadTableDetailAsync();
}
private async Task HandleTableChanged(ChangeEventArgs args)
{
selectedTableSlug = args.Value?.ToString() ?? string.Empty;
await LoadTableDetailAsync();
}
private async Task HandleRollBandChanged(ChangeEventArgs args)
{
selectedRollBand = args.Value?.ToString() ?? string.Empty;
ResolveSelectedCell();
await LoadSelectedCellDiagnosticsAsync();
}
private async Task HandleGroupChanged(ChangeEventArgs args)
{
selectedGroupKey = NormalizeOptionalText(args.Value?.ToString());
ResolveSelectedCell();
await LoadSelectedCellDiagnosticsAsync();
}
private async Task HandleColumnChanged(ChangeEventArgs args)
{
selectedColumnKey = args.Value?.ToString() ?? string.Empty;
ResolveSelectedCell();
await LoadSelectedCellDiagnosticsAsync();
}
private async Task LoadTableDetailAsync()
{
if (string.IsNullOrWhiteSpace(selectedTableSlug))
{
tableDetail = null;
selectedCell = null;
diagnosticsModel = null;
return;
}
isDetailLoading = true;
detailError = null;
diagnosticsError = null;
diagnosticsModel = null;
selectedCell = null;
try
{
tableDetail = await LookupService.GetCriticalTableAsync(selectedTableSlug);
if (tableDetail is null)
{
detailError = "The selected table could not be loaded.";
return;
}
SetDefaultSelection(tableDetail);
ResolveSelectedCell();
await LoadSelectedCellDiagnosticsAsync();
}
catch (Exception exception)
{
detailError = exception.Message;
tableDetail = null;
selectedCell = null;
diagnosticsModel = null;
}
finally
{
isDetailLoading = false;
}
}
private void SetDefaultSelection(CriticalTableDetail detail)
{
if (!detail.Cells.Any())
{
selectedRollBand = detail.RollBands.FirstOrDefault()?.Label ?? string.Empty;
selectedColumnKey = detail.Columns.FirstOrDefault()?.Key ?? string.Empty;
selectedGroupKey = detail.Groups.FirstOrDefault()?.Key;
return;
}
var rollOrder = detail.RollBands
.Select((rollBand, index) => new { rollBand.Label, index })
.ToDictionary(item => item.Label, item => item.index, StringComparer.Ordinal);
var columnOrder = detail.Columns
.Select((column, index) => new { column.Key, index })
.ToDictionary(item => item.Key, item => item.index, StringComparer.Ordinal);
var groupOrder = detail.Groups
.Select((group, index) => new { group.Key, index })
.ToDictionary(item => item.Key, item => item.index, StringComparer.Ordinal);
var firstCell = detail.Cells
.OrderBy(cell => rollOrder.GetValueOrDefault(cell.RollBand, int.MaxValue))
.ThenBy(cell =>
{
if (cell.GroupKey is null)
{
return -1;
}
return groupOrder.GetValueOrDefault(cell.GroupKey, int.MaxValue);
})
.ThenBy(cell => columnOrder.GetValueOrDefault(cell.ColumnKey, int.MaxValue))
.First();
selectedRollBand = firstCell.RollBand;
selectedColumnKey = firstCell.ColumnKey;
selectedGroupKey = firstCell.GroupKey;
}
private void ResolveSelectedCell()
{
if (tableDetail is null)
{
selectedCell = null;
return;
}
selectedCell = tableDetail.Cells.FirstOrDefault(cell =>
string.Equals(cell.RollBand, selectedRollBand, StringComparison.Ordinal) &&
string.Equals(cell.ColumnKey, selectedColumnKey, StringComparison.Ordinal) &&
string.Equals(cell.GroupKey ?? string.Empty, selectedGroupKey ?? string.Empty, StringComparison.Ordinal));
}
private async Task LoadSelectedCellDiagnosticsAsync()
{
diagnosticsError = null;
diagnosticsModel = null;
if (selectedCell is null || string.IsNullOrWhiteSpace(selectedTableSlug))
{
return;
}
isDiagnosticsLoading = true;
try
{
var response = await LookupService.GetCriticalCellEditorAsync(selectedTableSlug, selectedCell.ResultId);
if (response is null)
{
diagnosticsError = "The selected cell could not be loaded.";
return;
}
diagnosticsModel = CriticalCellEditorModel.FromResponse(response);
}
catch (Exception exception)
{
diagnosticsError = exception.Message;
}
finally
{
isDiagnosticsLoading = false;
}
}
private static string? NormalizeOptionalText(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
<CompatibilityRouteRedirect TargetPath="/tools/diagnostics" />

View File

@@ -0,0 +1,30 @@
@inject NavigationManager NavigationManager
<section class="panel tooling-surface">
<h1 class="panel-title">Redirecting…</h1>
<p class="panel-copy">This route moved to <code>@TargetPath</code>. If the redirect does not complete automatically, use the link below.</p>
<div class="action-row">
<a class="btn-link" href="@BuildTargetUri()">Continue</a>
</div>
</section>
@code {
[Parameter, EditorRequired]
public string TargetPath { get; set; } = string.Empty;
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender)
{
return;
}
NavigationManager.NavigateTo(BuildTargetUri(), replace: true);
}
private string BuildTargetUri()
{
var currentUri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
return string.Concat(TargetPath, currentUri.Query, currentUri.Fragment);
}
}