Separate UX diagnostics from curation
This commit is contained in:
@@ -667,6 +667,16 @@ Acceptance criteria:
|
||||
|
||||
### Phase 5: Final tooling boundary cleanup
|
||||
|
||||
Status:
|
||||
|
||||
- implemented in the web app on March 15, 2026
|
||||
|
||||
Implemented model:
|
||||
|
||||
- the primary critical-cell editor now keeps only compact correction tools plus advanced review for generated-versus-current comparison
|
||||
- engineering-only parser metadata, source text, and payload inspection moved to a dedicated Diagnostics page in the main navigation
|
||||
- normal curation no longer mixes raw JSON and storage-facing diagnostics into the default correction flow
|
||||
|
||||
Scope:
|
||||
|
||||
- verify that normal curation mode contains only user-facing correction tools
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<span class="bi bi-table" aria-hidden="true"></span> Critical Tables
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="diagnostics">
|
||||
<span class="bi bi-tools" aria-hidden="true"></span> Diagnostics
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
332
src/RolemasterDb.App/Components/Pages/Diagnostics.razor
Normal file
332
src/RolemasterDb.App/Components/Pages/Diagnostics.razor
Normal file
@@ -0,0 +1,332 @@
|
||||
@page "/diagnostics"
|
||||
@rendermode InteractiveServer
|
||||
@using System
|
||||
@using System.Collections.Generic
|
||||
@using System.Linq
|
||||
@inject LookupService LookupService
|
||||
|
||||
<PageTitle>Diagnostics</PageTitle>
|
||||
|
||||
<section class="panel diagnostics-page">
|
||||
<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();
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
@using System
|
||||
@using System.Collections.Generic
|
||||
@using System.Linq
|
||||
@using System.Text.Json
|
||||
@using Microsoft.JSInterop
|
||||
@using RolemasterDb.App.Domain
|
||||
@using RolemasterDb.App.Features
|
||||
@@ -206,11 +205,11 @@
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="critical-editor-section critical-editor-diagnostics-section">
|
||||
<section class="critical-editor-section">
|
||||
<details class="critical-editor-advanced">
|
||||
<summary class="critical-editor-advanced-summary">
|
||||
<span>Advanced Review & Diagnostics</span>
|
||||
<span class="critical-editor-advanced-meta">@GetAdvancedSummary(Model, ComparisonBaseline)</span>
|
||||
<span>Advanced Review</span>
|
||||
<span class="critical-editor-advanced-meta">@GetReviewSummary(Model, ComparisonBaseline)</span>
|
||||
</summary>
|
||||
|
||||
<div class="critical-editor-advanced-body">
|
||||
@@ -257,87 +256,6 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Parser Metadata</strong>
|
||||
<p class="muted critical-editor-inline-copy">Last loaded or re-parsed parser result.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="critical-editor-diagnostic-grid">
|
||||
<div>
|
||||
<dt>OCR Source</dt>
|
||||
<dd>@(string.IsNullOrWhiteSpace(Model.RawCellText) ? "—" : Model.RawCellText)</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Parse Status</dt>
|
||||
<dd>@Model.ParseStatus</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Raw Affix Text</dt>
|
||||
<dd>@(string.IsNullOrWhiteSpace(Model.RawAffixText) ? "—" : Model.RawAffixText)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@if (Model.ValidationMessages.Count > 0)
|
||||
{
|
||||
<div class="critical-editor-validation-list">
|
||||
@foreach (var message in Model.ValidationMessages)
|
||||
{
|
||||
<p class="critical-editor-validation-item">@message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="field-shell">
|
||||
<label>Parsed Effects JSON</label>
|
||||
<pre class="critical-editor-diagnostic-block">@FormatJson(Model.ParsedJson)</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var effectMetadata = GetEffectMetadataRows(Model);
|
||||
}
|
||||
|
||||
@if (effectMetadata.Count > 0)
|
||||
{
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Effect Source Metadata</strong>
|
||||
<p class="muted critical-editor-inline-copy">Stored source markers and labels for the current effect list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="critical-editor-diagnostic-list">
|
||||
@foreach (var entry in effectMetadata)
|
||||
{
|
||||
<div class="critical-editor-diagnostic-item">
|
||||
<div>
|
||||
<strong>@entry.Scope</strong>
|
||||
<p class="muted critical-editor-inline-copy">@entry.EffectLabel</p>
|
||||
</div>
|
||||
<div class="critical-editor-diagnostic-values">
|
||||
<span><strong>Type:</strong> @entry.SourceType</span>
|
||||
<span><strong>Text:</strong> @(string.IsNullOrWhiteSpace(entry.SourceText) ? "—" : entry.SourceText)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Current Save Payload</strong>
|
||||
<p class="muted critical-editor-inline-copy">Request built from the visible editor state, including generated branch internals.</p>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="critical-editor-diagnostic-block">@BuildCurrentSavePayloadJson(Model)</pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
@@ -388,10 +306,6 @@
|
||||
[Parameter, EditorRequired]
|
||||
public EventCallback OnSave { get; set; }
|
||||
|
||||
private static readonly JsonSerializerOptions DiagnosticJsonOptions = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
private static readonly IReadOnlyList<(string Token, IReadOnlyList<CriticalEffectLookupResponse> Effects)> QuickParseLegendEntries =
|
||||
[
|
||||
("+15", [CreateQuickLegendEffect(CriticalEffectCodes.DirectHits, valueInteger: 15)]),
|
||||
@@ -631,7 +545,7 @@
|
||||
effect.IsOverridden = true;
|
||||
}
|
||||
|
||||
private static string GetAdvancedSummary(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||
private static string GetReviewSummary(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||
{
|
||||
var differenceCount = GetComparisonDifferenceCount(model, comparisonBaseline);
|
||||
var noteCount = model.ValidationMessages.Count;
|
||||
@@ -647,13 +561,13 @@
|
||||
segments.Add($"{noteCount} parser note{(noteCount == 1 ? string.Empty : "s")}");
|
||||
}
|
||||
|
||||
return segments.Count == 0 ? "Generated compare and diagnostics" : string.Join(" · ", segments);
|
||||
return segments.Count == 0 ? "Generated compare" : string.Join(" · ", segments);
|
||||
}
|
||||
|
||||
private static string GetParserNoteSummary(int noteCount) =>
|
||||
noteCount == 1
|
||||
? "1 parser note is available under Advanced Review & Diagnostics."
|
||||
: $"{noteCount} parser notes are available under Advanced Review & Diagnostics.";
|
||||
? "1 parser note is available under Advanced Review."
|
||||
: $"{noteCount} parser notes are available under Advanced Review.";
|
||||
|
||||
private static CriticalEffectLookupResponse CreateQuickLegendEffect(
|
||||
string effectCode,
|
||||
@@ -675,27 +589,6 @@
|
||||
"legend",
|
||||
null);
|
||||
|
||||
private static string FormatJson(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return "{}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return JsonSerializer.Serialize(document.RootElement, DiagnosticJsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return json.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCurrentSavePayloadJson(CriticalCellEditorModel model) =>
|
||||
JsonSerializer.Serialize(model.ToRequest(), DiagnosticJsonOptions);
|
||||
|
||||
private static IReadOnlyList<string> GetComparisonSummaryItems(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||
{
|
||||
if (model.GeneratedState is null)
|
||||
@@ -775,37 +668,6 @@
|
||||
.Select(CreatePreviewBranch)
|
||||
.ToList();
|
||||
|
||||
private static List<(string Scope, string EffectLabel, string SourceType, string? SourceText)> GetEffectMetadataRows(CriticalCellEditorModel model)
|
||||
{
|
||||
var rows = new List<(string Scope, string EffectLabel, string SourceType, string? SourceText)>();
|
||||
|
||||
foreach (var effect in model.Effects)
|
||||
{
|
||||
if (ShouldIncludeEffectMetadata(effect))
|
||||
{
|
||||
rows.Add(("Base Result", GetEffectLabel(effect), effect.SourceType, effect.SourceText));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var branch in model.Branches.OrderBy(item => item.SortOrder))
|
||||
{
|
||||
var scope = string.IsNullOrWhiteSpace(branch.ConditionText) ? "Condition" : branch.ConditionText.Trim();
|
||||
foreach (var effect in branch.Effects)
|
||||
{
|
||||
if (ShouldIncludeEffectMetadata(effect))
|
||||
{
|
||||
rows.Add((scope, GetEffectLabel(effect), effect.SourceType, effect.SourceText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static bool ShouldIncludeEffectMetadata(CriticalEffectEditorModel effect) =>
|
||||
!string.IsNullOrWhiteSpace(effect.SourceText) ||
|
||||
!string.IsNullOrWhiteSpace(effect.SourceType);
|
||||
|
||||
private static IReadOnlyList<CriticalEffectLookupResponse> BuildSingleBadgeEffect(CriticalEffectEditorModel effect) =>
|
||||
[CreateBadgeEffect(effect)];
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
@using System
|
||||
@using System.Collections.Generic
|
||||
@using System.Linq
|
||||
@using System.Text.Json
|
||||
@using RolemasterDb.App.Domain
|
||||
@using RolemasterDb.App.Features
|
||||
|
||||
<div class="critical-diagnostics-body">
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Cell Context</strong>
|
||||
<p class="muted critical-editor-inline-copy">Engineering metadata for the selected critical-table result.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="critical-editor-diagnostic-grid">
|
||||
<div>
|
||||
<dt>Result ID</dt>
|
||||
<dd>@Model.ResultId</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Table</dt>
|
||||
<dd>@Model.TableName</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source Document</dt>
|
||||
<dd>@Model.SourceDocument</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Roll Band</dt>
|
||||
<dd>@Model.RollBand</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Severity</dt>
|
||||
<dd>@Model.ColumnLabel</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Variant</dt>
|
||||
<dd>@(string.IsNullOrWhiteSpace(Model.GroupLabel) ? "—" : Model.GroupLabel)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Parser Metadata</strong>
|
||||
<p class="muted critical-editor-inline-copy">Last loaded or re-parsed parser result for this cell.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="critical-editor-diagnostic-grid">
|
||||
<div>
|
||||
<dt>Parse Status</dt>
|
||||
<dd>@Model.ParseStatus</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Raw Affix Text</dt>
|
||||
<dd>@(string.IsNullOrWhiteSpace(Model.RawAffixText) ? "—" : Model.RawAffixText)</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="field-shell">
|
||||
<label>Quick Parse Input</label>
|
||||
<pre class="critical-editor-diagnostic-block">@FormatTextBlock(Model.QuickParseInput)</pre>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label>OCR Source</label>
|
||||
<pre class="critical-editor-diagnostic-block">@FormatTextBlock(Model.RawCellText)</pre>
|
||||
</div>
|
||||
|
||||
@if (Model.ValidationMessages.Count > 0)
|
||||
{
|
||||
<div class="critical-editor-validation-list">
|
||||
@foreach (var message in Model.ValidationMessages)
|
||||
{
|
||||
<p class="critical-editor-validation-item">@message</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="field-shell">
|
||||
<label>Parsed Effects JSON</label>
|
||||
<pre class="critical-editor-diagnostic-block">@FormatJson(Model.ParsedJson)</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@{
|
||||
var effectMetadata = GetEffectMetadataRows(Model);
|
||||
}
|
||||
|
||||
@if (effectMetadata.Count > 0)
|
||||
{
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Effect Source Metadata</strong>
|
||||
<p class="muted critical-editor-inline-copy">Stored source markers and labels for the current effect list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="critical-editor-diagnostic-list">
|
||||
@foreach (var entry in effectMetadata)
|
||||
{
|
||||
<div class="critical-editor-diagnostic-item">
|
||||
<div>
|
||||
<strong>@entry.Scope</strong>
|
||||
<p class="muted critical-editor-inline-copy">@entry.EffectLabel</p>
|
||||
</div>
|
||||
<div class="critical-editor-diagnostic-values">
|
||||
<span><strong>Type:</strong> @entry.SourceType</span>
|
||||
<span><strong>Text:</strong> @(string.IsNullOrWhiteSpace(entry.SourceText) ? "—" : entry.SourceText)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="critical-editor-card nested">
|
||||
<div class="critical-editor-card-header">
|
||||
<div>
|
||||
<strong>Current Save Payload</strong>
|
||||
<p class="muted critical-editor-inline-copy">Request built from the current editor state, including generated branch internals.</p>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="critical-editor-diagnostic-block">@BuildCurrentSavePayloadJson(Model)</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public CriticalCellEditorModel Model { get; set; } = default!;
|
||||
|
||||
private static readonly JsonSerializerOptions DiagnosticJsonOptions = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private static string FormatTextBlock(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? "—" : value.Trim();
|
||||
|
||||
private static string FormatJson(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return "{}";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
return JsonSerializer.Serialize(document.RootElement, DiagnosticJsonOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return json.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCurrentSavePayloadJson(CriticalCellEditorModel model) =>
|
||||
JsonSerializer.Serialize(model.ToRequest(), DiagnosticJsonOptions);
|
||||
|
||||
private static List<(string Scope, string EffectLabel, string SourceType, string? SourceText)> GetEffectMetadataRows(CriticalCellEditorModel model)
|
||||
{
|
||||
var rows = new List<(string Scope, string EffectLabel, string SourceType, string? SourceText)>();
|
||||
|
||||
foreach (var effect in model.Effects)
|
||||
{
|
||||
if (ShouldIncludeEffectMetadata(effect))
|
||||
{
|
||||
rows.Add(("Base Result", GetEffectLabel(effect), effect.SourceType, effect.SourceText));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var branch in model.Branches.OrderBy(item => item.SortOrder))
|
||||
{
|
||||
var scope = string.IsNullOrWhiteSpace(branch.ConditionText) ? "Condition" : branch.ConditionText.Trim();
|
||||
foreach (var effect in branch.Effects)
|
||||
{
|
||||
if (ShouldIncludeEffectMetadata(effect))
|
||||
{
|
||||
rows.Add((scope, GetEffectLabel(effect), effect.SourceType, effect.SourceText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static bool ShouldIncludeEffectMetadata(CriticalEffectEditorModel effect) =>
|
||||
!string.IsNullOrWhiteSpace(effect.SourceText) ||
|
||||
!string.IsNullOrWhiteSpace(effect.SourceType);
|
||||
|
||||
private static string GetEffectLabel(CriticalEffectEditorModel effect)
|
||||
{
|
||||
if (AffixDisplayMap.TryGet(effect.EffectCode, out var info))
|
||||
{
|
||||
return info.Label;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(effect.EffectCode) ? "Custom Effect" : effect.EffectCode;
|
||||
}
|
||||
}
|
||||
@@ -1068,6 +1068,40 @@ textarea {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.diagnostics-page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.diagnostics-page-header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.diagnostics-selector-grid {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.diagnostics-selection-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: 16px;
|
||||
background: rgba(247, 239, 225, 0.65);
|
||||
border: 1px solid rgba(127, 96, 55, 0.12);
|
||||
color: #5d4429;
|
||||
}
|
||||
|
||||
.critical-diagnostics-body {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.critical-editor-inline-button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -1138,4 +1172,10 @@ textarea {
|
||||
justify-items: start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.diagnostics-page-header,
|
||||
.diagnostics-selection-summary {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user