feat(ui): separate dev info into a Diagnose view, declutter the scan workspace

Reworks the UI so the Scan tab shows only core actions and all developer /
diagnostic surfaces live in one dedicated view.

- New "Diagnose" nav view. ScanView stays mounted for scan|diagnose (the scan
  controller state persists across the switch) and renders either the clean
  workspace or the new DiagnosticsView by mode.
- DiagnosticsView consolidates runtime/rights, grid detection, learning + data
  staleness, fingerprint, auto-scan counters, the automation log, the dev-output
  toggle, the Demo-Daten action, and the raw crops/OCR/confidence dump. Reuses
  the existing diagnostics/details model hooks.
- Scan workspace decluttered: removed the Scanner Diagnose button and the Details
  button, dropped the rules/grid/mode dev fields from the result brief and capture
  meta, shortened the topbar headline, and moved Demo-Daten out of the topbar.
- Removed the now-dead ScanDiagnosticsModal / ScanDetailsModal components (their
  content moved into the view); metrics grid hidden on the Diagnose view.
- Added dev:web script + .claude/launch.json for browser preview.

Verified in the browser preview (both views render, no console errors); 120
tests + build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-07-06 07:46:31 +02:00
parent b8a38ba547
commit 8d4d24f0bb
14 changed files with 304 additions and 315 deletions
@@ -0,0 +1,203 @@
import { AlertTriangle, Play, Wrench } from "lucide-react";
import type { CaptureResult } from "../../../types/global";
import type { ScanViewControllerResult } from "../types";
import { FieldConfidenceList } from "./ScanResultCards";
import { useScanDiagnosticsModalModel } from "./modals/hooks/useScanDiagnosticsModalModel";
import { useScanDetailsModalModel } from "./modals/hooks/useScanDetailsModalModel";
interface DiagnosticsViewProps {
controller: ScanViewControllerResult;
latestCapture: CaptureResult | null;
captureStatus: string;
onDemoScan?: () => void;
canDemoScan?: boolean;
}
// All developer / diagnostic surfaces live here, separated from the Scan
// workspace: runtime + rights, grid detection, learning + data-package status,
// fingerprint, auto-scan counters, the automation log, and the raw crop/OCR/
// confidence dump. Read-only; it drives no scan action except the demo snapshot.
export function DiagnosticsView({ controller, latestCapture, captureStatus, onDemoScan, canDemoScan }: DiagnosticsViewProps) {
const {
statusTitle,
rightsClassName,
rightsValue,
genshinClassName,
genshinValue,
shouldShowAdminBanner,
gridSourceClass,
gridMainValue,
gridMetaValue,
learningRulesText,
learningRulesSubtext,
autoScanModeLabel,
fingerprintText,
runtimeRows,
scanLimitText,
autoScanStatsLines,
playerProgress,
reviewStatus,
automationLogLines,
canSaveReviewSample,
handleSaveReviewSample,
} = useScanDiagnosticsModalModel({
setDetailsOpen: controller.setDetailsOpen,
setDiagnosticsOpen: controller.setDiagnosticsOpen,
saveReviewSample: controller.saveReviewSample,
canSaveReviewSample: controller.canSaveReviewSample,
latestCapture,
controller,
captureStatus,
});
const { parsedNotes, showParsedNotes, cropRows, ocrRows, debugText, showCrops, showOcr } = useScanDetailsModalModel({
setDetailsOpen: controller.setDetailsOpen,
parsedArtifact: controller.parsedArtifact,
latestCapture,
});
return (
<section className="diagnose-view">
<div className="diagnose-header">
<div>
<p className="eyebrow">Diagnose &amp; Dev</p>
<h2>Laufzeit, Erkennung &amp; Rohdaten</h2>
</div>
<div className="diagnose-header-actions">
<button className="ghost-button" onClick={controller.toggleDevMode}>
<Wrench size={15} />
{statusTitle}
</button>
{onDemoScan && (
<button className="ghost-button" onClick={onDemoScan} disabled={!canDemoScan}>
<Play size={15} />
Demo-Daten laden
</button>
)}
</div>
</div>
<div className="diagnose-grid">
<div className="diagnose-card">
<p className="eyebrow">Status</p>
<div className="scanner-preflight diagnostics-preflight">
<div className={rightsClassName}>
<span>App-Rechte</span>
<strong>{rightsValue}</strong>
</div>
<div className={genshinClassName}>
<span>Genshin</span>
<strong>{genshinValue}</strong>
</div>
</div>
{shouldShowAdminBanner && (
<p className="scanner-subcopy">
App laeuft im Standard-Modus. Auto-Scan braucht Administrator-Rechte: App schliessen und als Administrator neu starten.
</p>
)}
<div className={`grid-detection-strip ${gridSourceClass}`}>
<span>Tile grid</span>
<strong>{gridMainValue}</strong>
<small>{gridMetaValue}</small>
</div>
<div className="learning-strip">
<span>Learning &amp; Daten</span>
<strong>{learningRulesText}</strong>
<small>{learningRulesSubtext}</small>
</div>
<div className="learning-strip">
<span>Fingerprint</span>
<strong>{fingerprintText}</strong>
<small>Aktiver Capture-Fingerprint fuer die Duplikat-Erkennung.</small>
</div>
</div>
<div className="diagnose-card">
<p className="eyebrow">{autoScanModeLabel}</p>
{playerProgress.show ? (
<div className="auto-scan-strip">
{autoScanStatsLines.map((entry) => (
<span className="auto-scan-stat" key={entry.label}>
<strong>{entry.value}</strong>
<small>{entry.label}</small>
</span>
))}
</div>
) : (
<p className="result-empty">Noch keine Scan-Aktivitaet.</p>
)}
<p className="scanner-result-caption">{scanLimitText}</p>
<div className="scanner-status-row diagnostics-status">
{runtimeRows.map((row) => (
<span key={row}>{row}</span>
))}
</div>
{reviewStatus && <p className="review-status">{reviewStatus}</p>}
</div>
</div>
<div className="diagnose-card">
<p className="eyebrow">Automation log</p>
<div className="automation-log-lines">
{automationLogLines.length > 0 ? (
automationLogLines.map((line, index) => <span key={`${index}-${line}`}>{line}</span>)
) : (
<strong>Keine Scan-Aktivitaet.</strong>
)}
</div>
</div>
<div className="diagnose-card">
<div className="diagnose-card-heading">
<p className="eyebrow">Crops, OCR &amp; Confidence</p>
<button className="ghost-button" onClick={handleSaveReviewSample} disabled={!canSaveReviewSample}>
<AlertTriangle size={15} />
Review-Sample speichern
</button>
</div>
{controller.parsedArtifact ? (
<>
<FieldConfidenceList parsedArtifact={controller.parsedArtifact} />
{showParsedNotes && (
<div className="parsed-notes compact">
{parsedNotes.map((note) => (
<span key={note}>{note}</span>
))}
</div>
)}
</>
) : (
<p className="result-empty">Noch kein Artifact gelesen. Lies ein Artifact im Scan-Tab, um Crops und OCR zu sehen.</p>
)}
{showCrops && (
<div className="crop-grid details-grid">
{cropRows.map((crop) => (
<div className="crop-card" key={crop.id}>
<img src={crop.dataUrl} alt={crop.label} />
<div>
<strong>{crop.label}</strong>
<span>{crop.x},{crop.y} - {crop.width}x{crop.height}</span>
</div>
</div>
))}
</div>
)}
{showOcr && (
<div className="ocr-panel">
<strong>OCR candidates</strong>
{ocrRows.map((entry) => (
<div className="ocr-row" key={entry.id}>
<div>
<span>{entry.label}</span>
<small>{entry.confidence}% confidence</small>
</div>
<pre>{entry.text}</pre>
</div>
))}
</div>
)}
{latestCapture && <p className="capture-debug">{debugText}</p>}
</div>
</section>
);
}