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
+1 -10
View File
@@ -90,7 +90,7 @@ export function AppTopbar({
<header className="topbar">
<div>
<p className="eyebrow">Lokaler Windows-Assistent</p>
<h1>Scanne dein Inventar, triff einfache Artifact-Entscheidungen.</h1>
<h1>Inventar scannen, Artifacts entscheiden.</h1>
</div>
<div className="topbar-actions">
{topbarStatus && <span className="topbar-status">{topbarStatus}</span>}
@@ -107,15 +107,6 @@ export function AppTopbar({
{overlayIcon}
<span>{overlayButtonLabel}</span>
</button>
<button
className="ghost-button"
onClick={handleDemoScan}
disabled={isDemoDisabled}
title={demoButtonTitle}
>
{demoIcon}
{demoButtonLabel}
</button>
</div>
</header>
);
+2 -1
View File
@@ -1,4 +1,4 @@
import { Layers3, Radar, Wand2, Eye } from "lucide-react";
import { Layers3, Radar, Wand2, Eye, Wrench } from "lucide-react";
import type { AppNavigationItem } from "./types";
export const appNavigationItems: AppNavigationItem[] = [
@@ -6,5 +6,6 @@ export const appNavigationItems: AppNavigationItem[] = [
{ id: "triage", label: "Triage", icon: Layers3 },
{ id: "builds", label: "Builds", icon: Wand2 },
{ id: "overlay", label: "Overlay", icon: Eye },
{ id: "diagnose", label: "Diagnose", icon: Wrench },
];
+1 -1
View File
@@ -1,7 +1,7 @@
import { type ComponentType, type ReactNode } from "react";
import type { LucideProps } from "lucide-react";
export type NavigationId = "scan" | "triage" | "builds" | "overlay";
export type NavigationId = "scan" | "triage" | "builds" | "overlay" | "diagnose";
export interface AppNavigationItem {
id: NavigationId;
+22 -2
View File
@@ -1,9 +1,29 @@
import { ScanViewLayout } from "./components/ScanViewLayout";
import { ScanViewLayout } from "./components/ScanViewLayout";
import { DiagnosticsView } from "./components/DiagnosticsView";
import { useScanViewController } from "./hooks/useScanViewController";
import type { ScanViewProps } from "./types";
export function ScanView(props: ScanViewProps) {
interface ScanViewExtraProps {
/** "workspace" shows the clean scan surface; "diagnose" shows all dev info. */
mode?: "workspace" | "diagnose";
onDemoScan?: () => void;
canDemoScan?: boolean;
}
export function ScanView({ mode = "workspace", onDemoScan, canDemoScan, ...props }: ScanViewProps & ScanViewExtraProps) {
const controller = useScanViewController(props);
if (mode === "diagnose") {
return (
<DiagnosticsView
controller={controller}
latestCapture={props.latestCapture}
captureStatus={props.captureStatus}
onDemoScan={onDemoScan}
canDemoScan={canDemoScan}
/>
);
}
return <ScanViewLayout {...props} controller={controller} />;
}
@@ -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>
);
}
@@ -1,4 +1,4 @@
import { AlertTriangle, Camera, Eye } from "lucide-react";
import { AlertTriangle, Camera } from "lucide-react";
import { ArtifactResultCard } from "./ScanResultCards";
import { useScanMainSectionModel } from "./hooks/useScanMainSectionModel";
import type { ScanMainSectionProps } from "./types";
@@ -63,9 +63,7 @@ export function ScanMainSection({
</div>
<div className="capture-stage-meta">
<div><span>Quelle</span><strong>{sourceLabel}</strong></div>
<div><span>Grid</span><strong>{gridLabel}</strong></div>
<div><span>Inventar</span><strong>{inventoryLabel}</strong></div>
<div><span>Modus</span><strong>{captureModeText}</strong></div>
</div>
</div>
@@ -81,17 +79,8 @@ export function ScanMainSection({
<span>{targetLabel}</span>
<span>{dbLabel}</span>
<span>{reviewLabel}</span>
<span>{rulesLabel}</span>
</div>
<div className="scanner-result-actions">
<button
className="ghost-button"
onClick={handleOpenDetails}
disabled={!canOpenDetails}
>
<Eye size={15} />
Details
</button>
<button className="ghost-button" onClick={handleOpenReviewQueue} disabled={autoScanRunning || !canOpenReviewQueue}>
<AlertTriangle size={15} />
Review Queue
@@ -1,46 +1,27 @@
import { ScanDiagnosticsModal } from "./modals/ScanDiagnosticsModal";
import { ScanSettingsModal } from "./modals/ScanSettingsModal";
import { ScanDetailsModal } from "./modals/ScanDetailsModal";
import { ScanReviewQueueModal } from "./modals/ScanReviewQueueModal";
import { ScanSummaryModal } from "./modals/ScanSummaryModal";
import type { ScanModalsSectionProps } from "./types";
// Diagnostics + crop/OCR details moved to the dedicated Diagnose view; only the
// core workspace modals live here.
export function ScanModalsSection({
captureStatus,
latestCapture,
diagnosticsOpen,
settingsOpen,
detailsOpen,
reviewQueueOpen,
controller,
setDiagnosticsOpen,
setSettingsOpen,
setDetailsOpen,
setReviewQueueOpen,
setScanSummary,
}: ScanModalsSectionProps) {
return (
<>
<ScanDiagnosticsModal
open={diagnosticsOpen}
captureStatus={captureStatus}
latestCapture={latestCapture}
controller={controller}
setDetailsOpen={setDetailsOpen}
setDiagnosticsOpen={setDiagnosticsOpen}
/>
<ScanSettingsModal
open={settingsOpen}
latestCapture={latestCapture}
controller={controller}
setSettingsOpen={setSettingsOpen}
/>
<ScanDetailsModal
open={detailsOpen}
latestCapture={latestCapture}
controller={controller}
setDetailsOpen={setDetailsOpen}
/>
<ScanReviewQueueModal
open={reviewQueueOpen}
controller={controller}
@@ -5,7 +5,6 @@ import {
Radar,
RefreshCw,
SlidersHorizontal,
Wrench,
} from "lucide-react";
import type { ScanTopControlsSectionProps } from "./types";
import { useScanTopControlsModel } from "./hooks/useScanTopControlsModel";
@@ -66,7 +65,7 @@ export function ScanTopControlsSection({
<div>
<p className="eyebrow">Scanner</p>
<h2>Artifact capture workspace</h2>
<p className="scanner-subcopy">Grosse Vorschau vorn, klare Aktionen oben, Diagnose und Review nur bei Bedarf.</p>
<p className="scanner-subcopy">Quelle waehlen, Auto-Scan starten, Ergebnis rechts pruefen. Dev-Details im Diagnose-Tab.</p>
</div>
<div className="scanner-header-pills">
<span className={`runtime-pill ${bridgePillClass}`}>{bridgeStatusText}</span>
@@ -118,15 +117,6 @@ export function ScanTopControlsSection({
<SlidersHorizontal size={15} />
Scan-Setup
</button>
<button
className="ghost-button dev-toggle"
onClick={openDiagnostics}
disabled={!bridgeReady}
title={diagnosticsButtonTitle}
>
<Wrench size={15} />
Scanner Diagnose
</button>
</div>
<div className="player-scan-actions">
@@ -1,83 +0,0 @@
import { FieldConfidenceList } from "../ScanResultCards";
import type { ScanDetailsModalProps } from "./types";
import { useScanDetailsModalModel } from "./hooks/useScanDetailsModalModel";
export function ScanDetailsModal({
open,
latestCapture,
controller,
setDetailsOpen,
}: ScanDetailsModalProps) {
const { parsedArtifact } = controller;
const {
closeDetails,
stopPropagation,
parsedNotes,
showParsedNotes,
cropRows,
ocrRows,
debugText,
showCrops,
showOcr,
} = useScanDetailsModalModel({
setDetailsOpen,
parsedArtifact,
latestCapture,
});
if (!open || !latestCapture) return null;
return (
<div className="modal-backdrop" role="presentation" onClick={closeDetails}>
<div className="modal-panel" role="dialog" aria-modal="true" onClick={stopPropagation}>
<div className="modal-header">
<div>
<p className="eyebrow">Dev</p>
<h2>Crops, OCR & Confidence</h2>
</div>
<button className="ghost-button" onClick={closeDetails}>Schliessen</button>
</div>
<div className="modal-body">
{parsedArtifact && (
<>
<FieldConfidenceList parsedArtifact={parsedArtifact} />
{showParsedNotes && (
<div className="parsed-notes compact">
{parsedNotes.map((note) => <span key={note}>{note}</span>)}
</div>
)}
</>
)}
{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>
)}
<p className="capture-debug">{debugText}</p>
</div>
</div>
</div>
);
}
@@ -1,172 +0,0 @@
import { AlertTriangle, Eye, Wrench } from "lucide-react";
import type { ScanDiagnosticsModalProps } from "./types";
import { useScanDiagnosticsModalModel } from "./hooks/useScanDiagnosticsModalModel";
export function ScanDiagnosticsModal({
open,
captureStatus,
latestCapture,
controller,
setDetailsOpen,
setDiagnosticsOpen,
}: ScanDiagnosticsModalProps) {
const {
toggleDevMode,
} = controller;
const {
closeDiagnostics,
openDetails,
handleSaveReviewSample,
stopPropagation,
canOpenDetails,
statusTitle,
rightsClassName,
rightsValue,
genshinClassName,
genshinValue,
shouldShowAdminBanner,
gridSourceClass,
gridMainValue,
gridMetaValue,
learningRulesText,
learningRulesSubtext,
autoScanModeLabel,
autoScanRunning,
fingerprintText,
runtimeRows,
scanLimitText,
scanTipText,
autoScanStatsLines,
playerProgress,
showDevRows,
reviewStatus,
automationLogLines,
canSaveReviewSample,
} = useScanDiagnosticsModalModel({
setDetailsOpen,
setDiagnosticsOpen,
saveReviewSample: controller.saveReviewSample,
canSaveReviewSample: controller.canSaveReviewSample,
latestCapture,
controller,
captureStatus,
});
if (!open) return null;
return (
<div className="modal-backdrop" role="presentation" onClick={closeDiagnostics}>
<div className="modal-panel scanner-diagnostics-modal" role="dialog" aria-modal="true" onClick={stopPropagation}>
<div className="modal-header">
<div>
<p className="eyebrow">Scanner Diagnose</p>
<h2>Input, Grid & Lernstatus</h2>
</div>
<button className="ghost-button" onClick={closeDiagnostics}>Schliessen</button>
</div>
<div className="modal-body">
<div className="diagnostics-actions">
<button className="ghost-button" onClick={toggleDevMode}>
<Wrench size={15} />
{statusTitle}
</button>
</div>
<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 (z.B. Terminal per Rechtsklick "Als Administrator ausfuehren" und darin "npm run dev").
</p>
)}
<div className={`grid-detection-strip ${gridSourceClass}`}>
<span>Tile grid</span>
<strong>{gridMainValue}</strong>
<small>{gridMetaValue}</small>
</div>
<div className="learning-strip">
<span>Learning</span>
<strong>{learningRulesText}</strong>
<small>{learningRulesSubtext}</small>
</div>
{playerProgress.show && (
<div className="auto-scan-strip">
<span>{autoScanModeLabel}</span>
{autoScanStatsLines.map((entry) => (
<span className="auto-scan-stat" key={entry.label}>
<strong>{entry.value}</strong>
<small>{entry.label}</small>
</span>
))}
</div>
)}
<div className="learning-strip">
<span>Fingerprint</span>
<strong>{fingerprintText}</strong>
<small>Active capture fingerprint used for deterministic duplicate guard checks.</small>
</div>
{showDevRows && (
<div className="scanner-status-row diagnostics-status">
{runtimeRows.map((row) => (
<span key={row}>{row}</span>
))}
</div>
)}
{autoScanRunning && playerProgress.show && (
<div className="player-progress">
<div className="player-progress-bar">
<div style={{ width: `${playerProgress.width}%` }} />
</div>
</div>
)}
{reviewStatus && (
<p className="review-status">{reviewStatus}</p>
)}
<div className="automation-log">
<span>Automation</span>
<div className="automation-log-lines">
{automationLogLines.length > 0 ? (
automationLogLines.map((line, index) => (
<span key={`${index}-${line}`}>{line}</span>
))
) : (
<strong>No scan activity yet.</strong>
)}
</div>
</div>
<div className="dev-section-actions">
<button className="ghost-button" onClick={openDetails} disabled={!canOpenDetails}>
<Eye size={15} />
Crops, OCR & Confidence
</button>
<button className="ghost-button" onClick={handleSaveReviewSample} disabled={!canSaveReviewSample}>
<AlertTriangle size={15} />
Review-Sample speichern
</button>
</div>
<p className="scanner-result-caption">{scanLimitText}</p>
<p className="scan-summary-copy">{scanTipText}</p>
</div>
</div>
</div>
);
}
+5 -2
View File
@@ -53,9 +53,12 @@ export function AppPageLayout({ controller }: AppPageLayoutProps) {
overlayIcon={<Eye size={16} />}
demoIcon={<Play size={16} />}
/>
<AppMetrics metricCards={metricCards} />
{activeView === "scan" && (
{activeView !== "diagnose" && <AppMetrics metricCards={metricCards} />}
{(activeView === "scan" || activeView === "diagnose") && (
<ScanView
mode={activeView === "diagnose" ? "diagnose" : "workspace"}
onDemoScan={handleDemoScan}
canDemoScan={!isScanning}
isScanning={isScanning}
snapshot={snapshot}
captureSources={captureSources}
+54
View File
@@ -2182,3 +2182,57 @@ button:disabled {
}
}
/* Diagnose / Dev view — all developer info, separated from the Scan workspace. */
.diagnose-view {
display: grid;
gap: 14px;
width: 100%;
}
.diagnose-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.diagnose-header-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.diagnose-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.diagnose-card {
display: grid;
gap: 10px;
align-content: start;
border: 1px solid var(--line);
border-radius: 8px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.015)),
rgba(18, 12, 35, 0.7);
box-shadow: var(--glass-shadow);
backdrop-filter: blur(18px);
padding: 16px;
}
.diagnose-card-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
@media (max-width: 1200px) {
.diagnose-grid {
grid-template-columns: 1fr;
}
}