feat(good): wire GOOD import/export UI into the Diagnose view

Completes the GOOD interop point end-to-end (the conversion engine landed earlier
in goodInterop.ts). No new IPC needed - reuses the existing artifacts:load /
artifacts:saveMany / good:export bridge.

- Scan controller gains exportGoodFromStore (loadArtifacts -> storedArtifactsToGood
  -> exportGood) and importGoodArtifacts (saveMany + snapshot refresh), plus a
  canGoodInterop flag.
- DiagnosticsView adds a GOOD Interop card: export the scan store as GOOD, or
  import a GOOD file. The file is read in the renderer via a file input +
  goodDatabaseToStoredArtifacts, so no file-dialog IPC is required.

Verified in the browser preview after a clean restart: the Diagnose view renders
all cards (Status, Last scan, GOOD Interop, Automation log, Crops/OCR), the
Scan<->Diagnose switch works with no console errors (the earlier hook-order
warnings were stale-HMR artifacts from deleting files mid-session). 120 tests +
build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-07-06 07:56:31 +02:00
parent 8d4d24f0bb
commit 39691fd40b
3 changed files with 89 additions and 1 deletions
@@ -1,6 +1,8 @@
import { AlertTriangle, Play, Wrench } from "lucide-react";
import { useRef, useState, type ChangeEvent } from "react";
import { AlertTriangle, Download, Play, Upload, Wrench } from "lucide-react";
import type { CaptureResult } from "../../../types/global";
import type { ScanViewControllerResult } from "../types";
import { goodDatabaseToStoredArtifacts, type GoodImportDatabase } from "../../../lib/goodInterop";
import { FieldConfidenceList } from "./ScanResultCards";
import { useScanDiagnosticsModalModel } from "./modals/hooks/useScanDiagnosticsModalModel";
import { useScanDetailsModalModel } from "./modals/hooks/useScanDetailsModalModel";
@@ -56,6 +58,42 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
latestCapture,
});
const [interopStatus, setInteropStatus] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const handleExportGood = async () => {
setInteropStatus("Exportiere GOOD...");
const result = await controller.exportGoodFromStore();
setInteropStatus(
result.ok
? `GOOD exportiert: ${result.count} Artifacts${result.path ? ` -> ${result.path}` : ""}`
: "GOOD-Export fehlgeschlagen (App im Electron-Fenster oeffnen).",
);
};
const handleImportGood = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = "";
if (!file) return;
try {
const database = JSON.parse(await file.text()) as GoodImportDatabase;
const records = goodDatabaseToStoredArtifacts(database);
if (records.length === 0) {
setInteropStatus("Keine gueltigen Artifacts in der Datei gefunden.");
return;
}
setInteropStatus(`Importiere ${records.length} Artifacts...`);
const result = await controller.importGoodArtifacts(records);
setInteropStatus(
result.ok
? `Importiert: ${result.added} neu, ${result.updated} aktualisiert.`
: "Import fehlgeschlagen (App im Electron-Fenster oeffnen).",
);
} catch {
setInteropStatus("Datei ist kein gueltiges GOOD/JSON.");
}
};
return (
<section className="diagnose-view">
<div className="diagnose-header">
@@ -136,6 +174,27 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
</div>
</div>
<div className="diagnose-card">
<div className="diagnose-card-heading">
<p className="eyebrow">GOOD Interop</p>
<div className="diagnose-header-actions">
<button className="ghost-button" onClick={handleExportGood} disabled={!controller.canGoodInterop}>
<Download size={15} />
GOOD exportieren
</button>
<button className="ghost-button" onClick={() => fileInputRef.current?.click()} disabled={!controller.canGoodInterop}>
<Upload size={15} />
GOOD importieren
</button>
<input ref={fileInputRef} type="file" accept="application/json,.json" hidden onChange={handleImportGood} />
</div>
</div>
<p className="scanner-subcopy">
Exportiert den Scan-Store als GOOD (Genshin Optimizer / Inventory Kamera / Akasha) oder importiert eine GOOD-Datei in den Store.
</p>
{interopStatus && <p className="review-status">{interopStatus}</p>}
</div>
<div className="diagnose-card">
<p className="eyebrow">Automation log</p>
<div className="automation-log-lines">