oh gott was mach ich nur

This commit is contained in:
2025-11-25 03:21:52 +01:00
parent b8471bff3e
commit 888ab3dfa7
21 changed files with 2664 additions and 794 deletions

View File

@@ -13,7 +13,9 @@
setOverallStatus,
formatBytes,
formatMbps,
formatDuration
formatDuration,
t,
tFmt
} = window.usbcheck;
const root = document.getElementById("fc-root");
@@ -53,19 +55,19 @@
class UsbBrowserTester {
constructor() {
this.rootHandle = null;
this.abortController = null;
this.rootHandle = null;
this.abortController = null;
this.capacityBytes = null;
this.capacityProbeFile = "usbcheck_capacity_probe.bin";
}
hasFsApiSupport() {
return "showDirectoryPicker" in window;
}
// Wird aktuell nicht mehr verwendet (Picker ist direkt im Click-Handler),
// kann aber als Fallback/Refactoring-Hook bleiben.
async pickDirectory() {
if (!this.hasFsApiSupport()) {
throw new Error("File System Access API wird von diesem Browser nicht unterstützt.");
throw new Error(t("fake_ui.error_fsapi_not_supported", "File System Access API wird von diesem Browser nicht unterstützt."));
}
const handle = await window.showDirectoryPicker();
this.rootHandle = handle;
@@ -74,21 +76,120 @@
async clearSelection() {
this.rootHandle = null;
this.capacityBytes = null;
}
// Freispeicher-Ermittlung
async ensureCapacity(abortSignal) {
if (this.capacityBytes != null) return this.capacityBytes;
if (!this.rootHandle) {
throw new Error(t("fake_ui.error_no_directory_selected", "Kein Verzeichnis ausgewählt."));
}
const dirHandle = this.rootHandle;
const PROBE_FILENAME = this.capacityProbeFile;
const CHUNK_SIZE = 1024 * 1024; // 1 MiB
const MAX_BYTES = 128 * 1024 * 1024; // max. 128 MiB testen
logLine(t("fake_ui.log_capacity_probe_start", "Ermittle verfügbaren Speicherplatz im gewählten Verzeichnis..."), "info");
let bytesWritten = 0;
let writable = null;
try {
const fileHandle = await dirHandle.getFileHandle(PROBE_FILENAME, { create: true });
writable = await fileHandle.createWritable();
const chunk = new Uint8Array(CHUNK_SIZE);
chunk.fill(0x5a);
while (bytesWritten + CHUNK_SIZE <= MAX_BYTES) {
if (abortSignal && abortSignal.aborted) {
await writable.abort();
throw new DOMException("Abgebrochen", "AbortError");
}
await writable.write(chunk);
bytesWritten += CHUNK_SIZE;
}
await writable.close();
} catch (err) {
if (writable) {
try { await writable.close(); } catch (e) {}
}
if (err && err.name === "AbortError") {
throw err;
}
// sonst: „Out of space“ → okay, wir nehmen bytesWritten als Limit
} finally {
try {
await dirHandle.removeEntry(PROBE_FILENAME);
} catch (e) {
// egal
}
}
if (!bytesWritten) {
throw new Error(t("fake_ui.error_no_space_detected", "Es konnte kein freier Speicher im gewählten Verzeichnis reserviert werden."));
}
this.capacityBytes = bytesWritten;
logLine(
tFmt(
"fake_ui.log_capacity_probe_result",
"Ermittelter für Tests nutzbarer Speicher: {size} (Schreibprobe).",
{ size: formatBytes(bytesWritten) }
),
"info"
);
return this.capacityBytes;
}
planSizeBytes({ defaultMB, minMB, maxFraction }) {
const MiB = 1024 * 1024;
const cap = this.capacityBytes || (64 * MiB);
const minBytes = (minMB || 1) * MiB;
const defaultBytes = (defaultMB || 8) * MiB;
const maxBytesByCap = Math.max(minBytes, cap * (maxFraction || 0.25));
let target = Math.min(defaultBytes, maxBytesByCap);
const CHUNK_SIZE = MiB;
target = Math.floor(target / CHUNK_SIZE) * CHUNK_SIZE;
if (target < CHUNK_SIZE) target = CHUNK_SIZE;
return target;
}
// Quick-Check
async runQuickCheck(report, progressCb, abortSignal) {
const TEST_FILENAME = "usbcheck_quick_test.bin";
const TEST_SIZE_MB = 8;
const CHUNK_SIZE = 1024 * 1024;
const dirHandle = this.rootHandle;
if (!dirHandle) throw new Error("Kein Verzeichnis ausgewählt.");
if (!dirHandle) throw new Error(t("fake_ui.error_no_directory_selected", "Kein Verzeichnis ausgewählt."));
const totalBytes = this.planSizeBytes({
defaultMB: 8,
minMB: 1,
maxFraction: 0.25
});
logLine(
tFmt(
"fake_ui.log_quick_prepare",
"Quick-Check: Vorbereitung... (Testgröße: {size})",
{ size: formatBytes(totalBytes) }
),
"info"
);
logLine("Quick-Check: Vorbereitung...", "info");
const fileHandle = await dirHandle.getFileHandle(TEST_FILENAME, { create: true });
const writable = await fileHandle.createWritable();
const totalBytes = TEST_SIZE_MB * 1024 * 1024;
let writtenBytes = 0;
const writeStart = performance.now();
@@ -110,7 +211,7 @@
await writable.close();
const writeEnd = performance.now();
logLine("Quick-Check: Schreiben abgeschlossen. Verifiziere Daten...", "info");
logLine(t("fake_ui.log_quick_verify_start", "Quick-Check: Schreiben abgeschlossen. Verifiziere Daten..."), "info");
const file = await fileHandle.getFile();
const readStart = performance.now();
@@ -129,8 +230,21 @@
for (let i = 0; i < chunk.length; i++) {
const expected = (offset + i) % 251;
if (chunk[i] !== expected) {
logLine(`Quick-Check: Datenfehler bei Byte ${offset + i}`, "error");
throw new Error(`Datenfehler im Quick-Check bei Byte ${offset + i}`);
logLine(
tFmt(
"fake_ui.log_quick_data_error",
"Quick-Check: Datenfehler bei Byte {byte}",
{ byte: (offset + i).toString() }
),
"error"
);
throw new Error(
tFmt(
"fake_ui.error_quick_data_error",
"Datenfehler im Quick-Check bei Byte {byte}",
{ byte: (offset + i).toString() }
)
);
}
}
offset += chunk.length;
@@ -156,22 +270,41 @@
ok: true
};
logLine("Quick-Check: Erfolgreich abgeschlossen.", "info");
try {
await dirHandle.removeEntry(TEST_FILENAME);
} catch (e) {
logLine(t("fake_ui.log_quick_delete_warn", "Quick-Check: Konnte Testdatei nicht löschen (nicht kritisch)."), "warn");
}
logLine(t("fake_ui.log_quick_success", "Quick-Check: Erfolgreich abgeschlossen."), "info");
}
// Benchmark
async runBenchmark(report, progressCb, abortSignal) {
const TEST_FILENAME = "usbcheck_benchmark.bin";
const TEST_SIZE_MB = 32;
const CHUNK_SIZE = 1024 * 1024;
const dirHandle = this.rootHandle;
if (!dirHandle) throw new Error("Kein Verzeichnis ausgewählt.");
if (!dirHandle) throw new Error(t("fake_ui.error_no_directory_selected", "Kein Verzeichnis ausgewählt."));
const totalBytes = this.planSizeBytes({
defaultMB: 32,
minMB: 4,
maxFraction: 0.5
});
logLine(
tFmt(
"fake_ui.log_bench_start",
"Benchmark: Start schreibe Testdatei ({size})...",
{ size: formatBytes(totalBytes) }
),
"info"
);
logLine("Benchmark: Start schreibe Testdatei...", "info");
const fileHandle = await dirHandle.getFileHandle(TEST_FILENAME, { create: true });
const writable = await fileHandle.createWritable();
const totalBytes = TEST_SIZE_MB * 1024 * 1024;
let writtenBytes = 0;
const writeStart = performance.now();
@@ -193,7 +326,7 @@
await writable.close();
const writeEnd = performance.now();
logLine("Benchmark: Lesen & Timing...", "info");
logLine(t("fake_ui.log_bench_read_start", "Benchmark: Lesen & Timing..."), "info");
const file = await fileHandle.getFile();
const readStart = performance.now();
@@ -227,22 +360,51 @@
ok: true
};
logLine("Benchmark: Erfolgreich abgeschlossen.", "info");
try {
await dirHandle.removeEntry(TEST_FILENAME);
} catch (e) {
logLine(t("fake_ui.log_bench_delete_warn", "Benchmark: Konnte Testdatei nicht löschen (nicht kritisch)."), "warn");
}
logLine(t("fake_ui.log_bench_success", "Benchmark: Erfolgreich abgeschlossen."), "info");
}
// Write/Verify
async runWriteVerify(report, progressCb, abortSignal) {
const BASE_FILENAME = "usbcheck_block_";
const BLOCKS = 4;
const BLOCK_SIZE_MB = 32;
const CHUNK_SIZE = 1024 * 1024;
const dirHandle = this.rootHandle;
if (!dirHandle) throw new Error("Kein Verzeichnis ausgewählt.");
if (!dirHandle) throw new Error(t("fake_ui.error_no_directory_selected", "Kein Verzeichnis ausgewählt."));
logLine("Write/Verify: Start mehrere Blöcke werden getestet...", "info");
const totalBytesPlanned = this.planSizeBytes({
defaultMB: 128,
minMB: 8,
maxFraction: 0.75
});
const totalBytes = BLOCKS * BLOCK_SIZE_MB * 1024 * 1024;
let writtenBytes = 0;
const blockBytes = Math.max(
CHUNK_SIZE,
Math.floor(totalBytesPlanned / BLOCKS / CHUNK_SIZE) * CHUNK_SIZE
);
const totalBytes = blockBytes * BLOCKS;
logLine(
tFmt(
"fake_ui.log_wv_start",
"Write/Verify: Start {blocks} Blöcke à {size} (gesamt {total})...",
{
blocks: BLOCKS.toString(),
size: formatBytes(blockBytes),
total: formatBytes(totalBytes)
}
),
"info"
);
let writtenBytes = 0;
let verifiedBytes = 0;
const globalStart = performance.now();
@@ -251,21 +413,32 @@
for (let b = 0; b < BLOCKS; b++) {
const filename = `${BASE_FILENAME}${String(b + 1).padStart(2, "0")}.bin`;
logLine(`Write/Verify: Block ${b + 1}/${BLOCKS} ${filename}`, "info");
logLine(
tFmt(
"fake_ui.log_wv_block_start",
"Write/Verify: Block {num}/{blocks} {file}",
{
num: (b + 1).toString(),
blocks: BLOCKS.toString(),
file: filename
}
),
"info"
);
const fileHandle = await dirHandle.getFileHandle(filename, { create: true });
const writable = await fileHandle.createWritable();
const blockBytes = BLOCK_SIZE_MB * 1024 * 1024;
const blockBytesTotal = blockBytes;
let blockWritten = 0;
const blockWriteStart = performance.now();
while (blockWritten < blockBytes) {
while (blockWritten < blockBytesTotal) {
if (abortSignal.aborted) {
await writable.abort();
throw new DOMException("Abgebrochen", "AbortError");
}
const remaining = blockBytes - blockWritten;
const remaining = blockBytesTotal - blockWritten;
const chunkLen = Math.min(CHUNK_SIZE, remaining);
const chunk = new Uint8Array(chunkLen);
for (let i = 0; i < chunkLen; i++) {
@@ -281,7 +454,7 @@
const blockWriteEnd = performance.now();
writeDetails.push({
block: b + 1,
bytes: blockBytes,
bytes: blockBytesTotal,
duration_s: (blockWriteEnd - blockWriteStart) / 1000
});
@@ -301,8 +474,27 @@
for (let i = 0; i < chunk.length; i++) {
const expected = (i + blockOffset + b * 13) % 251;
if (chunk[i] !== expected) {
logLine(`Write/Verify: Datenfehler in Block ${b + 1} bei Byte ${blockOffset + i}`, "error");
throw new Error(`Datenfehler in Block ${b + 1} bei Byte ${blockOffset + i}`);
logLine(
tFmt(
"fake_ui.log_wv_data_error",
"Write/Verify: Datenfehler in Block {num} bei Byte {byte}",
{
num: (b + 1).toString(),
byte: (blockOffset + i).toString()
}
),
"error"
);
throw new Error(
tFmt(
"fake_ui.error_wv_data_error",
"Datenfehler in Block {num} bei Byte {byte}",
{
num: (b + 1).toString(),
byte: (blockOffset + i).toString()
}
)
);
}
}
blockOffset += chunk.length;
@@ -313,9 +505,22 @@
const blockReadEnd = performance.now();
readDetails.push({
block: b + 1,
bytes: blockBytes,
bytes: blockBytesTotal,
duration_s: (blockReadEnd - blockReadStart) / 1000
});
try {
await dirHandle.removeEntry(filename);
} catch (e) {
logLine(
tFmt(
"fake_ui.log_wv_delete_warn",
"Write/Verify: Konnte Blockdatei {file} nicht löschen (nicht kritisch).",
{ file: filename }
),
"warn"
);
}
}
const globalEnd = performance.now();
@@ -329,7 +534,7 @@
report.writeverify = {
mode: "writeverify",
blocks: BLOCKS,
block_size_mb: BLOCK_SIZE_MB,
block_size_bytes: blockBytes,
total_bytes: totalBytes,
written_bytes: writtenBytes,
verified_bytes: verifiedBytes,
@@ -342,18 +547,20 @@
report.writeverify_total_duration_s = totalDuration;
logLine("Write/Verify: Alle Blöcke erfolgreich verifiziert.", "info");
logLine(t("fake_ui.log_wv_success", "Write/Verify: Alle Blöcke erfolgreich verifiziert."), "info");
}
async run(mode, updateProgressCb, abortSignal) {
if (!this.rootHandle) {
throw new Error("Kein USB-Verzeichnis ausgewählt.");
throw new Error(t("fake_ui.error_no_directory_selected", "Kein Verzeichnis ausgewählt."));
}
await this.ensureCapacity(abortSignal);
const report = {
meta: {
base_url: cfg.baseUrl || "",
locale: cfg.locale || "en",
locale: cfg.locale || "en",
user_agent: navigator.userAgent,
started_at: new Date().toISOString()
},
@@ -375,11 +582,21 @@
} else if (mode === "writeverify") {
await this.runWriteVerify(report, updateProgressCb, abortSignal);
} else if (mode === "all") {
// Alle drei Tests nacheinander im selben Report
const modes = ["quick", "benchmark", "writeverify"];
for (let i = 0; i < modes.length; i++) {
const subMode = modes[i];
logLine(`All-Inclusive: Starte Teiltest "${subMode}" (${i + 1}/${modes.length})...`, "info");
logLine(
tFmt(
"fake_ui.log_all_subtest_start",
"All-Inclusive: Starte Teiltest \"{mode}\" ({num}/{total})...",
{
mode: subMode,
num: (i + 1).toString(),
total: modes.length.toString()
}
),
"info"
);
const base = (i / modes.length) * 100;
const span = (1 / modes.length) * 100;
@@ -397,7 +614,7 @@
}
}
} else {
throw new Error("Unbekannter Modus: " + mode);
throw new Error(tFmt("fake_ui.error_unknown_mode", "Unbekannter Modus: {mode}", { mode }));
}
const t1 = performance.now();
@@ -415,7 +632,7 @@
if (!tester.hasFsApiSupport()) {
if (fsapiWarning) fsapiWarning.style.display = "block";
logLine(
"Dein Browser unterstützt die File System Access API nicht voll. Einige Funktionen sind deaktiviert.",
t("fake_ui.log_fsapi_partial", "Dein Browser unterstützt die File System Access API nicht voll. Einige Funktionen sind deaktiviert."),
"warn"
);
}
@@ -429,8 +646,6 @@
// --- Event-Handler ------------------------------------------------------
// Wichtig: showDirectoryPicker direkt im Click-Handler per Promise,
// kein async/await direkt im Listener → vermeidet "User activation is required".
let isPickingDir = false;
if (btnPickDir) {
@@ -439,7 +654,7 @@
isPickingDir = true;
if (!("showDirectoryPicker" in window)) {
logLine("File System Access API wird von diesem Browser nicht unterstützt.", "error");
logLine(t("fake_ui.error_fsapi_not_supported", "File System Access API wird von diesem Browser nicht unterstützt."), "error");
if (fsapiWarning) fsapiWarning.style.display = "block";
isPickingDir = false;
return;
@@ -447,23 +662,42 @@
window.showDirectoryPicker()
.then((handle) => {
tester.rootHandle = handle;
tester.rootHandle = handle;
tester.capacityBytes = null;
if (selectedPathText) {
selectedPathText.textContent =
'USB-Ordner ausgewählt (Name: "' + (handle.name || "Unbekannt") + '").';
selectedPathText.textContent = tFmt(
"fake_ui.selected_path_label",
'USB-Ordner ausgewählt (Name: "{name}").',
{ name: (handle.name || "Unbekannt") }
);
}
if (btnClearSel) btnClearSel.disabled = false;
setStatus("USB-Verzeichnis ausgewählt. Wähle jetzt einen Testmodus.");
logLine("Verzeichnis ausgewählt: " + (handle.name || "[ohne Namen]"));
setStatus(t("fake_ui.status_dir_selected", "USB-Verzeichnis ausgewählt. Wähle jetzt einen Testmodus."));
logLine(
tFmt(
"fake_ui.log_dir_selected",
"Verzeichnis ausgewählt: {name}",
{ name: (handle.name || "[ohne Namen]") }
),
"info"
);
updateStartButtonState();
})
.catch((err) => {
if (err && err.name === "AbortError") {
logLine("Verzeichnisauswahl abgebrochen.", "warn");
logLine(t("fake_ui.log_pick_abort", "Verzeichnisauswahl abgebrochen."), "warn");
} else if (err) {
logLine("Fehler bei Verzeichnisauswahl: " + err.message, "error");
logLine(
tFmt(
"fake_ui.log_pick_error",
"Fehler bei Verzeichnisauswahl: {msg}",
{ msg: err.message || "unbekannt" }
),
"error"
);
} else {
logLine("Unbekannter Fehler bei Verzeichnisauswahl.", "error");
logLine(t("fake_ui.log_pick_error_unknown", "Unbekannter Fehler bei Verzeichnisauswahl."), "error");
}
})
.finally(() => {
@@ -476,11 +710,11 @@
btnClearSel.addEventListener("click", async () => {
await tester.clearSelection();
if (selectedPathText) {
selectedPathText.textContent = "Noch kein Verzeichnis gewählt.";
selectedPathText.textContent = t("fake_ui.selected_path_none", "Noch kein Verzeichnis gewählt.");
}
btnClearSel.disabled = true;
setStatus("Bereit. Wähle zuerst deinen USB-Stick aus.");
logLine("Verzeichnisauswahl zurückgesetzt.", "info");
setStatus(t("fake_ui.status_ready", "Bereit. Wähle zuerst deinen USB-Stick aus."));
logLine(t("fake_ui.log_dir_reset", "Verzeichnisauswahl zurückgesetzt."), "info");
updateStartButtonState();
});
}
@@ -489,14 +723,27 @@
tile.addEventListener("click", () => {
if (tile.classList.contains("disabled")) return;
if (isRunning) return;
modeTiles.forEach((t) => t.classList.remove("selected"));
modeTiles.forEach((tEl) => tEl.classList.remove("selected"));
tile.classList.add("selected");
currentMode = tile.getAttribute("data-mode");
const titleEl = tile.querySelector("h4");
const label = titleEl ? titleEl.textContent : currentMode;
setModeLabel(label || "");
setStatus(`Modus "${label}" ausgewählt. Du kannst den Test jetzt starten.`);
logLine("Modus gewählt: " + currentMode, "info");
setStatus(
tFmt(
"fake_ui.status_mode_selected",
'Modus "{mode}" ausgewählt. Du kannst den Test jetzt starten.',
{ mode: label || currentMode || "" }
)
);
logLine(
tFmt(
"fake_ui.log_mode_selected",
"Modus gewählt: {mode}",
{ mode: currentMode || "" }
),
"info"
);
updateStartButtonState();
});
});
@@ -509,9 +756,16 @@
if (btnCancel) btnCancel.disabled = false;
if (saveError) saveError.style.display = "none";
setProgress(0);
logLine("Starte Tests im Modus: " + currentMode.toUpperCase(), "info");
setStatus("Test läuft... bitte USB-Stick nicht entfernen.");
setOverallStatus("warn", "Test läuft...");
logLine(
tFmt(
"fake_ui.log_test_start",
"Starte Tests im Modus: {mode}",
{ mode: (currentMode || "").toUpperCase() }
),
"info"
);
setStatus(t("fake_ui.status_running", "Test läuft... bitte USB-Stick nicht entfernen."));
setOverallStatus("warn", t("fake_ui.overall_running", "Test läuft..."));
const abortController = new AbortController();
tester.abortController = abortController;
@@ -523,8 +777,8 @@
report = await tester.run(currentMode, updateProgressCb, abortController.signal);
setProgress(100);
setStatus("Test abgeschlossen.");
setOverallStatus("ok", "Browser-Test erfolgreich abgeschlossen.");
setStatus(t("fake_ui.status_done", "Test abgeschlossen."));
setOverallStatus("ok", t("fake_ui.overall_done", "Browser-Test erfolgreich abgeschlossen."));
applyReportToDashboard(report);
console.log("USB Browser Test Report (fakecheck):", report);
@@ -532,17 +786,30 @@
await saveReportToBackend(report);
} catch (err) {
if (err && err.name === "AbortError") {
setStatus("Test wurde abgebrochen.");
setOverallStatus("warn", "Test abgebrochen.");
logLine("Test wurde vom Benutzer abgebrochen.", "warn");
setStatus(t("fake_ui.status_aborted", "Test wurde abgebrochen."));
setOverallStatus("warn", t("fake_ui.overall_aborted", "Test abgebrochen."));
logLine(t("fake_ui.log_test_aborted", "Test wurde vom Benutzer abgebrochen."), "warn");
} else if (err) {
setStatus("Fehler: " + err.message);
setOverallStatus("bad", "Fehler im Browser-Test.");
logLine("Fehler im Test: " + err.message, "error");
setStatus(
tFmt(
"fake_ui.status_error",
"Fehler: {msg}",
{ msg: err.message || "unbekannt" }
)
);
setOverallStatus("bad", t("fake_ui.overall_error", "Fehler im Browser-Test."));
logLine(
tFmt(
"fake_ui.log_test_error",
"Fehler im Test: {msg}",
{ msg: err.message || "unbekannt" }
),
"error"
);
} else {
setStatus("Unbekannter Fehler im Test.");
setOverallStatus("bad", "Fehler im Browser-Test.");
logLine("Unbekannter Fehler im Test.", "error");
setStatus(t("fake_ui.status_error_unknown", "Unbekannter Fehler im Test."));
setOverallStatus("bad", t("fake_ui.overall_error", "Fehler im Browser-Test."));
logLine(t("fake_ui.log_test_error_unknown", "Unbekannter Fehler im Test."), "error");
}
} finally {
isRunning = false;
@@ -564,10 +831,10 @@
function applyReportToDashboard(report) {
const modeLabelMap = {
quick: "Quick-Check",
benchmark: "Benchmark",
writeverify: "Write & Verify",
all: "All-Inclusive (alle Browser-Tests)"
quick: t("fake_ui.mode_quick", "Quick-Check"),
benchmark: t("fake_ui.mode_benchmark", "Benchmark"),
writeverify: t("fake_ui.mode_writeverify", "Write & Verify"),
all: t("fake_ui.mode_all", "All-Inclusive (alle Browser-Tests)")
};
if (resMode) {
@@ -631,7 +898,14 @@
if (!response.ok) {
const msg = "HTTP " + response.status;
logLine("Backend: Konnte Ergebnis nicht speichern (" + msg + ").", "warn");
logLine(
tFmt(
"fake_ui.log_backend_save_error_status",
"Backend: Konnte Ergebnis nicht speichern ({status}).",
{ status: msg }
),
"warn"
);
if (response.status >= 500 && saveError) {
saveError.style.display = "block";
}
@@ -640,21 +914,34 @@
const data = await response.json().catch(() => null);
logLine(
"Backend: Testergebnis gespeichert" +
(data && data.id ? ` (ID: ${data.id})` : ""),
tFmt(
"fake_ui.log_backend_save_ok",
"Backend: Testergebnis gespeichert{suffix}",
{ suffix: (data && data.id ? ` (ID: ${data.id})` : "") }
),
"info"
);
} catch (err) {
if (saveError) saveError.style.display = "block";
logLine("Fehler beim Speichern im Backend: " + (err ? err.message : "unbekannt"), "error");
logLine(
tFmt(
"fake_ui.log_backend_save_error",
"Fehler beim Speichern im Backend: {msg}",
{ msg: err ? err.message || "unbekannt" : "unbekannt" }
),
"error"
);
}
}
// --- Initialzustand -----------------------------------------------------
setStatus("Bereit. Wähle zuerst deinen USB-Stick aus.");
setModeLabel("Kein Modus selektiert");
setOverallStatus("ok", "Noch kein Test durchgeführt.");
logLine("USB-Browser-Test (fakecheck) geladen. Warte auf Verzeichnisauswahl und Modus.");
setStatus(t("fake_ui.status_ready", "Bereit. Wähle zuerst deinen USB-Stick aus."));
setModeLabel(t("fake_ui.status_mode_none", "Kein Modus selektiert"));
setOverallStatus("ok", t("fake_ui.overall_initial", "Noch kein Test durchgeführt."));
logLine(
t("fake_ui.log_loaded", "USB-Browser-Test (fakecheck) geladen. Warte auf Verzeichnisauswahl und Modus."),
"info"
);
updateStartButtonState();
})();

View File

@@ -4,6 +4,7 @@
// Namespace
window.usbcheck = window.usbcheck || {};
const usbcheck = window.usbcheck;
const cfg = window.usbConfig || {};
@@ -16,14 +17,81 @@
return "https://api.usbcheck.it";
}
// Locale aus Config:
// - bevorzugt fakecheck.locale (aus PHP fakecheck-Block)
// - sonst cfg.lang
const locale =
(cfg.fakecheck && cfg.fakecheck.locale)
|| cfg.lang
|| "en";
// Config bereitstellen
usbcheck.cfg = {
baseUrl: cfg.baseUrl || "",
locale: (cfg.lang || "en").toLowerCase(),
apiBase: cfg.apiBaseUrl || detectApiBaseUrl(),
// wenn fakecheck.baseUrl vorhanden ist → bevorzugen
baseUrl: (cfg.fakecheck && cfg.fakecheck.baseUrl) || cfg.baseUrl || "",
locale: (locale || "en").toLowerCase(),
apiBase: (cfg.fakecheck && cfg.fakecheck.apiBaseUrl) || cfg.apiBaseUrl || detectApiBaseUrl(),
loggedIn: !!cfg.isLoggedIn
};
// -----------------------------------
// i18n JSON laden + Helfer
// -----------------------------------
usbcheck.locale = usbcheck.cfg.locale || "en";
usbcheck.i18n = {};
function loadI18n() {
// Pfad wie bei deinen JSONs: /public/assets/i18n/de.json etc.
const assetsBase = cfg.assetsBase || "/assets";
const url = assetsBase.replace(/\/+$/, "") + "/i18n/" + usbcheck.locale + ".json";
fetch(url)
.then(res => {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(data => {
usbcheck.i18n = data || {};
// console.debug("usbcheck i18n loaded:", usbcheck.locale, usbcheck.i18n);
})
.catch(err => {
console.warn("usbcheck i18n konnte nicht geladen werden:", err);
usbcheck.i18n = {};
});
}
// Key-Lookup: "fake_ui.status_ready"
usbcheck.t = function t(key, fallback) {
if (!key) return (fallback != null ? fallback : "");
const parts = String(key).split(".");
let cur = usbcheck.i18n;
for (let i = 0; i < parts.length; i++) {
const p = parts[i];
if (!cur || typeof cur !== "object" || !(p in cur)) {
return (fallback != null ? fallback : key);
}
cur = cur[p];
}
if (cur == null) return (fallback != null ? fallback : key);
return cur;
};
// Format-Variante mit Platzhaltern {name}, {mode}, {size}, ...
usbcheck.tFmt = function tFmt(key, fallback, vars) {
let str = usbcheck.t(key, fallback);
if (!vars) return str;
for (const k in vars) {
if (!Object.prototype.hasOwnProperty.call(vars, k)) continue;
const re = new RegExp("\\{" + k.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&") + "\\}", "g");
str = str.replace(re, vars[k]);
}
return str;
};
// direkt beim Laden JSON holen
loadI18n();
// ---------- Hilfsfunktionen ----------
usbcheck.log = function logLine(message, level = "info") {

View File

@@ -1,25 +1,178 @@
// /public/assets/js/fakecheck.js
// /public/assets/js/fakecheck/fakecheck.serial.js
(function() {
const base = "/assets/js/fakecheck/";
(function () {
if (!window.usbcheck) return;
const scripts = [
base + "core.js?v=1",
base + "fakecheck.browser.js?v=1",
base + "fakecheck.serial.js?v=1"
];
const {
cfg,
t,
tFmt,
log: logLine
} = window.usbcheck;
function loadScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = src;
s.async = false; // Reihenfolge sicherstellen
s.onload = resolve;
s.onerror = () => reject(new Error("Konnte " + src + " nicht laden"));
document.head.appendChild(s);
});
const rootSc = document.getElementById("serialcheck-root");
if (!rootSc) return; // Partial nicht eingebunden → nichts tun
// Elemente
const form = rootSc.querySelector("#serialcheck-form");
const errorBox = rootSc.querySelector("#serialcheck-error");
const resultBox = rootSc.querySelector("#serialcheck-result");
const manufacturerInput = rootSc.querySelector("#sc-manufacturer");
const vidInput = rootSc.querySelector("#sc-vid");
const pidInput = rootSc.querySelector("#sc-pid");
const serialInput = rootSc.querySelector("#sc-serial");
const apiUrl = cfg.apiBase.replace(/\/+$/, "") + "/quickcheck";
// -------------------------------------------------------
// Fehleranzeige
// -------------------------------------------------------
function showScError(msgKey, fallback, vars = {}) {
if (!errorBox) return;
const msg = tFmt(msgKey, fallback, vars);
errorBox.textContent = msg;
errorBox.classList.remove("hidden");
if (resultBox) resultBox.classList.add("hidden");
}
scripts.reduce((p, src) => p.then(() => loadScript(src)), Promise.resolve())
.catch(err => console.error("Fakecheck Loader Fehler:", err));
function clearScError() {
if (!errorBox) return;
errorBox.classList.add("hidden");
errorBox.textContent = "";
}
// -------------------------------------------------------
// Ergebnis rendern
// -------------------------------------------------------
function renderScResult(data) {
if (!resultBox) return;
clearScError();
resultBox.classList.remove("hidden");
const rating = data.rating || "unknown";
const input = data.input || {};
const vendorInfo = data.vendor_detected || {};
const serialInfo = data.serial_analysis || {};
const consistency = data.consistency || {};
// Rating → Label + Beschreibung
const ratingLabel = t(`serial.rating.${rating}.label`, rating);
const ratingDesc = t(`serial.rating.${rating}.desc`, "");
// Auffälligkeiten
const issues = serialInfo.issues || [];
const issuesHtml = issues.length
? '<ul class="list-disc list-inside mt-1">' +
issues.map(i => `<li>${i}</li>`).join("") +
'</ul>'
: `<span class="text-emerald-600 text-[11px]">${t("serial.issues_none", "Keine besonderen Auffälligkeiten.")}</span>`;
// Vendor
const vendorLine = vendorInfo.found
? tFmt("serial.vendor_detected", "{vendor} (VID {vid})", {
vendor: vendorInfo.vendor,
vid: vendorInfo.vid
})
: vendorInfo.vid
? tFmt("serial.vendor_unknown", "Unbekannter Hersteller für VID {vid}", { vid: vendorInfo.vid })
: t("serial.vendor_none", "Keine Vendor-ID angegeben");
// Konsistenz-Notizen
const notes = consistency.notes || [];
const notesHtml = notes.length
? '<ul class="list-disc list-inside mt-1 text-[11px]">' +
notes.map(n => `<li>${n}</li>`).join("") +
'</ul>'
: "";
// HTML einsetzen
resultBox.innerHTML = `
<div class="mb-3">
<span class="inline-flex items-center rounded-full px-3 py-1 text-[11px] font-semibold
${rating === "ok" ? "bg-emerald-100 text-emerald-800" : ""}
${rating === "needs_review" ? "bg-amber-100 text-amber-800" : ""}
${rating === "suspicious" ? "bg-red-100 text-red-800" : ""}
${rating === "invalid" ? "bg-slate-100 text-slate-700" : ""}
">
${t("serial.rating_label", "Bewertung")}: ${ratingLabel}
</span>
<p class="mt-1 text-xs text-slate-600 dark:text-slate-300">${ratingDesc}</p>
</div>
<div class="border border-slate-200 dark:border-slate-700 rounded-xl p-3 mb-3">
<h3 class="text-xs font-semibold mb-1">${t("serial.heading_input", "Eingabedaten")}</h3>
<dl class="text-[11px] space-y-1 text-slate-600 dark:text-slate-300">
<div><dt class="font-medium">${t("serial.input.manufacturer", "Hersteller (Angabe):")}</dt><dd>${input.manufacturer || '<span class="text-slate-400">' + t("serial.none", "keine Angabe") + '</span>'}</dd></div>
<div><dt class="font-medium">${t("serial.input.vidpid", "VID/PID:")}</dt><dd>${(input.vid || "") + " / " + (input.pid || "")}</dd></div>
<div><dt class="font-medium">${t("serial.input.vendor_detected", "Vendor aus VID:")}</dt><dd>${vendorLine}</dd></div>
</dl>
</div>
<div class="border border-slate-200 dark:border-slate-700 rounded-xl p-3 mb-3">
<h3 class="text-xs font-semibold mb-1">${t("serial.heading_analysis", "Seriennummer-Analyse")}</h3>
<dl class="text-[11px] space-y-1 text-slate-600 dark:text-slate-300">
<div><dt class="font-medium">${t("serial.analysis.serial", "Seriennummer:")}</dt><dd><code class="text-[10px] bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded">${serialInfo.serial || ""}</code></dd></div>
<div><dt class="font-medium">${t("serial.analysis.length", "Länge:")}</dt><dd>${serialInfo.length || 0} ${t("serial.chars", "Zeichen")}</dd></div>
<div><dt class="font-medium">${t("serial.analysis.category", "Kategorie:")}</dt><dd>${serialInfo.category || "-"}</dd></div>
<div><dt class="font-medium">${t("serial.analysis.score", "Score:")}</dt><dd>${typeof serialInfo.score === "number" ? serialInfo.score : "-"} / 100</dd></div>
<div><dt class="font-medium">${t("serial.analysis.issues", "Auffälligkeiten:")}</dt><dd>${issuesHtml}</dd></div>
</dl>
</div>
<div class="border border-slate-200 dark:border-slate-700 rounded-xl p-3">
<h3 class="text-xs font-semibold mb-1">${t("serial.heading_consistency", "Hersteller-Konsistenz")}</h3>
${notesHtml}
<p class="mt-2 text-[10px] text-slate-500">
${t("serial.disclaimer", "Diese Einschätzung basiert auf Heuristiken und kann keine Echtheit garantieren.")}
</p>
</div>
`;
}
// -------------------------------------------------------
// Submit-Handler
// -------------------------------------------------------
if (!form) return;
form.addEventListener("submit", (e) => {
e.preventDefault();
clearScError();
if (resultBox) resultBox.classList.add("hidden");
const payload = {
manufacturer: manufacturerInput ? manufacturerInput.value.trim() : "",
vid: vidInput ? vidInput.value.trim() : "",
pid: pidInput ? pidInput.value.trim() : "",
serial: serialInput ? serialInput.value.trim() : ""
};
if (!payload.serial) {
showScError("serial.error.no_serial", "Bitte gib eine Seriennummer ein.");
return;
}
fetch(apiUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
})
.then(res => {
if (!res.ok) {
throw new Error("HTTP " + res.status);
}
return res.json();
})
.then(data => {
if (!data || !data.success) {
throw new Error((data && data.error) || t("serial.error.unknown", "Unerwartete Antwort vom Server."));
}
renderScResult(data);
})
.catch(err => {
showScError("serial.error.api", "Fehler bei der Prüfung: {msg}", { msg: err.message });
});
});
})();