971 lines
38 KiB
JavaScript
971 lines
38 KiB
JavaScript
// /public/assets/js/fakecheck/fakecheck.browser.js
|
||
|
||
(function () {
|
||
// Core / Namespace muss geladen sein
|
||
if (!window.usbcheck) return;
|
||
|
||
const {
|
||
cfg,
|
||
log: logLine,
|
||
setStatus,
|
||
setModeLabel,
|
||
setProgress,
|
||
setOverallStatus,
|
||
formatBytes,
|
||
formatMbps,
|
||
formatDuration,
|
||
t,
|
||
tFmt
|
||
} = window.usbcheck;
|
||
|
||
const root = document.getElementById("fc-root");
|
||
if (!root) {
|
||
// Seite ohne Fakecheck-Tool → nichts tun
|
||
return;
|
||
}
|
||
|
||
// --- DOM-Helper ---------------------------------------------------------
|
||
const $ = (sel) => document.querySelector(sel);
|
||
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
||
|
||
const fsapiWarning = $("#fc-fsapi-warning");
|
||
const selectedPathText = $("#fc-selected-path-label");
|
||
const saveHint = $("#fc-save-hint");
|
||
const saveError = $("#fc-save-error");
|
||
|
||
const resMode = $("#fc-res-mode");
|
||
const resDuration = $("#fc-res-duration");
|
||
const resWriteSpeed = $("#fc-res-write-speed");
|
||
const resReadSpeed = $("#fc-res-read-speed");
|
||
const resWritten = $("#fc-res-written-bytes");
|
||
const resVerified = $("#fc-res-verified-bytes");
|
||
|
||
const btnPickDir = $("#fc-btn-pick-directory");
|
||
const btnClearSel = $("#fc-btn-clear-selection");
|
||
const btnStart = $("#fc-btn-start-tests");
|
||
const btnCancel = $("#fc-btn-cancel-tests");
|
||
const modeTiles = $$("#fc-mode-grid .fc-mode-tile");
|
||
|
||
// Login-Indikator
|
||
if (cfg.loggedIn && saveHint) {
|
||
saveHint.style.display = "block";
|
||
}
|
||
|
||
// --- Test-Engine (Browser) ---------------------------------------------
|
||
|
||
class UsbBrowserTester {
|
||
constructor() {
|
||
this.rootHandle = null;
|
||
this.abortController = null;
|
||
this.capacityBytes = null;
|
||
this.capacityProbeFile = "usbcheck_capacity_probe.bin";
|
||
}
|
||
|
||
hasFsApiSupport() {
|
||
return "showDirectoryPicker" in window;
|
||
}
|
||
|
||
async pickDirectory() {
|
||
if (!this.hasFsApiSupport()) {
|
||
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;
|
||
return handle;
|
||
}
|
||
|
||
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 CHUNK_SIZE = 1024 * 1024;
|
||
|
||
const dirHandle = this.rootHandle;
|
||
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"
|
||
);
|
||
|
||
const fileHandle = await dirHandle.getFileHandle(TEST_FILENAME, { create: true });
|
||
const writable = await fileHandle.createWritable();
|
||
|
||
let writtenBytes = 0;
|
||
|
||
const writeStart = performance.now();
|
||
while (writtenBytes < totalBytes) {
|
||
if (abortSignal.aborted) {
|
||
await writable.abort();
|
||
throw new DOMException("Abgebrochen", "AbortError");
|
||
}
|
||
const remaining = totalBytes - writtenBytes;
|
||
const chunkLen = Math.min(CHUNK_SIZE, remaining);
|
||
const chunk = new Uint8Array(chunkLen);
|
||
for (let i = 0; i < chunkLen; i++) {
|
||
chunk[i] = (i + writtenBytes) % 251;
|
||
}
|
||
await writable.write(chunk);
|
||
writtenBytes += chunkLen;
|
||
progressCb((writtenBytes / totalBytes) * 100 * 0.5);
|
||
}
|
||
await writable.close();
|
||
const writeEnd = performance.now();
|
||
|
||
logLine(t("fake_ui.log_quick_verify_start", "Quick-Check: Schreiben abgeschlossen. Verifiziere Daten..."), "info");
|
||
|
||
const file = await fileHandle.getFile();
|
||
const readStart = performance.now();
|
||
|
||
const reader = file.stream().getReader();
|
||
let offset = 0;
|
||
let verifiedBytes = 0;
|
||
while (true) {
|
||
if (abortSignal.aborted) {
|
||
reader.cancel();
|
||
throw new DOMException("Abgebrochen", "AbortError");
|
||
}
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
const chunk = value;
|
||
for (let i = 0; i < chunk.length; i++) {
|
||
const expected = (offset + i) % 251;
|
||
if (chunk[i] !== expected) {
|
||
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;
|
||
verifiedBytes += chunk.length;
|
||
progressCb(50 + (verifiedBytes / totalBytes) * 100 * 0.5);
|
||
}
|
||
|
||
const readEnd = performance.now();
|
||
|
||
const writeSeconds = (writeEnd - writeStart) / 1000;
|
||
const readSeconds = (readEnd - readStart) / 1000;
|
||
|
||
report.quick = {
|
||
mode: "quick",
|
||
test_file: TEST_FILENAME,
|
||
size_bytes: totalBytes,
|
||
write_bytes: writtenBytes,
|
||
read_bytes: verifiedBytes,
|
||
write_duration_s: writeSeconds,
|
||
read_duration_s: readSeconds,
|
||
write_mbit_s: writeSeconds ? (writtenBytes * 8 / (writeSeconds * 1e6)) : null,
|
||
read_mbit_s: readSeconds ? (verifiedBytes * 8 / (readSeconds * 1e6)) : null,
|
||
ok: true
|
||
};
|
||
|
||
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 CHUNK_SIZE = 1024 * 1024;
|
||
|
||
const dirHandle = this.rootHandle;
|
||
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"
|
||
);
|
||
|
||
const fileHandle = await dirHandle.getFileHandle(TEST_FILENAME, { create: true });
|
||
const writable = await fileHandle.createWritable();
|
||
|
||
let writtenBytes = 0;
|
||
|
||
const writeStart = performance.now();
|
||
while (writtenBytes < totalBytes) {
|
||
if (abortSignal.aborted) {
|
||
await writable.abort();
|
||
throw new DOMException("Abgebrochen", "AbortError");
|
||
}
|
||
const remaining = totalBytes - writtenBytes;
|
||
const chunkLen = Math.min(CHUNK_SIZE, remaining);
|
||
const chunk = new Uint8Array(chunkLen);
|
||
for (let i = 0; i < chunkLen; i++) {
|
||
chunk[i] = 0xaa;
|
||
}
|
||
await writable.write(chunk);
|
||
writtenBytes += chunkLen;
|
||
progressCb((writtenBytes / totalBytes) * 100 * 0.4);
|
||
}
|
||
await writable.close();
|
||
const writeEnd = performance.now();
|
||
|
||
logLine(t("fake_ui.log_bench_read_start", "Benchmark: Lesen & Timing..."), "info");
|
||
|
||
const file = await fileHandle.getFile();
|
||
const readStart = performance.now();
|
||
const reader = file.stream().getReader();
|
||
let readBytes = 0;
|
||
while (true) {
|
||
if (abortSignal.aborted) {
|
||
reader.cancel();
|
||
throw new DOMException("Abgebrochen", "AbortError");
|
||
}
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
readBytes += value.length;
|
||
progressCb(40 + (readBytes / totalBytes) * 100 * 0.6);
|
||
}
|
||
const readEnd = performance.now();
|
||
|
||
const writeSeconds = (writeEnd - writeStart) / 1000;
|
||
const readSeconds = (readEnd - readStart) / 1000;
|
||
|
||
report.benchmark = {
|
||
mode: "benchmark",
|
||
test_file: TEST_FILENAME,
|
||
size_bytes: totalBytes,
|
||
write_bytes: writtenBytes,
|
||
read_bytes: readBytes,
|
||
write_duration_s: writeSeconds,
|
||
read_duration_s: readSeconds,
|
||
write_mbit_s: writeSeconds ? (writtenBytes * 8 / (writeSeconds * 1e6)) : null,
|
||
read_mbit_s: readSeconds ? (readBytes * 8 / (readSeconds * 1e6)) : null,
|
||
ok: true
|
||
};
|
||
|
||
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 CHUNK_SIZE = 1024 * 1024;
|
||
|
||
const dirHandle = this.rootHandle;
|
||
if (!dirHandle) throw new Error(t("fake_ui.error_no_directory_selected", "Kein Verzeichnis ausgewählt."));
|
||
|
||
const totalBytesPlanned = this.planSizeBytes({
|
||
defaultMB: 128,
|
||
minMB: 8,
|
||
maxFraction: 0.75
|
||
});
|
||
|
||
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();
|
||
const writeDetails = [];
|
||
const readDetails = [];
|
||
|
||
for (let b = 0; b < BLOCKS; b++) {
|
||
const filename = `${BASE_FILENAME}${String(b + 1).padStart(2, "0")}.bin`;
|
||
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 blockBytesTotal = blockBytes;
|
||
let blockWritten = 0;
|
||
const blockWriteStart = performance.now();
|
||
|
||
while (blockWritten < blockBytesTotal) {
|
||
if (abortSignal.aborted) {
|
||
await writable.abort();
|
||
throw new DOMException("Abgebrochen", "AbortError");
|
||
}
|
||
const remaining = blockBytesTotal - blockWritten;
|
||
const chunkLen = Math.min(CHUNK_SIZE, remaining);
|
||
const chunk = new Uint8Array(chunkLen);
|
||
for (let i = 0; i < chunkLen; i++) {
|
||
chunk[i] = (i + blockWritten + b * 13) % 251;
|
||
}
|
||
await writable.write(chunk);
|
||
blockWritten += chunkLen;
|
||
writtenBytes += chunkLen;
|
||
const progress = (writtenBytes + verifiedBytes) / (totalBytes * 2) * 100;
|
||
progressCb(progress);
|
||
}
|
||
await writable.close();
|
||
const blockWriteEnd = performance.now();
|
||
writeDetails.push({
|
||
block: b + 1,
|
||
bytes: blockBytesTotal,
|
||
duration_s: (blockWriteEnd - blockWriteStart) / 1000
|
||
});
|
||
|
||
const file = await fileHandle.getFile();
|
||
const reader = file.stream().getReader();
|
||
let blockOffset = 0;
|
||
const blockReadStart = performance.now();
|
||
|
||
while (true) {
|
||
if (abortSignal.aborted) {
|
||
reader.cancel();
|
||
throw new DOMException("Abgebrochen", "AbortError");
|
||
}
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
const chunk = value;
|
||
for (let i = 0; i < chunk.length; i++) {
|
||
const expected = (i + blockOffset + b * 13) % 251;
|
||
if (chunk[i] !== expected) {
|
||
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;
|
||
verifiedBytes += chunk.length;
|
||
const progress = (writtenBytes + verifiedBytes) / (totalBytes * 2) * 100;
|
||
progressCb(progress);
|
||
}
|
||
const blockReadEnd = performance.now();
|
||
readDetails.push({
|
||
block: b + 1,
|
||
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();
|
||
const totalDuration = (globalEnd - globalStart) / 1000;
|
||
|
||
const sumWrite = writeDetails.reduce((acc, d) => acc + d.bytes, 0);
|
||
const sumRead = readDetails.reduce((acc, d) => acc + d.bytes, 0);
|
||
const writeSec = writeDetails.reduce((acc, d) => acc + d.duration_s, 0);
|
||
const readSec = readDetails.reduce((acc, d) => acc + d.duration_s, 0);
|
||
|
||
report.writeverify = {
|
||
mode: "writeverify",
|
||
blocks: BLOCKS,
|
||
block_size_bytes: blockBytes,
|
||
total_bytes: totalBytes,
|
||
written_bytes: writtenBytes,
|
||
verified_bytes: verifiedBytes,
|
||
write_duration_s: writeSec,
|
||
read_duration_s: readSec,
|
||
write_mbit_s: writeSec ? (sumWrite * 8 / (writeSec * 1e6)) : null,
|
||
read_mbit_s: readSec ? (sumRead * 8 / (readSec * 1e6)) : null,
|
||
ok: true
|
||
};
|
||
|
||
report.writeverify_total_duration_s = totalDuration;
|
||
|
||
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(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",
|
||
user_agent: navigator.userAgent,
|
||
started_at: new Date().toISOString()
|
||
},
|
||
tool: "usbcheck_browser",
|
||
tool_version: "0.1.0",
|
||
mode_requested: mode,
|
||
quick: null,
|
||
benchmark: null,
|
||
writeverify: null,
|
||
total_duration_s: null
|
||
};
|
||
|
||
const t0 = performance.now();
|
||
|
||
if (mode === "quick") {
|
||
await this.runQuickCheck(report, updateProgressCb, abortSignal);
|
||
} else if (mode === "benchmark") {
|
||
await this.runBenchmark(report, updateProgressCb, abortSignal);
|
||
} else if (mode === "writeverify") {
|
||
await this.runWriteVerify(report, updateProgressCb, abortSignal);
|
||
} else if (mode === "all") {
|
||
const modes = ["quick", "benchmark", "writeverify"];
|
||
for (let i = 0; i < modes.length; i++) {
|
||
const subMode = modes[i];
|
||
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;
|
||
const mapProgress = (p) => {
|
||
const mapped = base + (p / 100) * span;
|
||
updateProgressCb(mapped);
|
||
};
|
||
|
||
if (subMode === "quick") {
|
||
await this.runQuickCheck(report, mapProgress, abortSignal);
|
||
} else if (subMode === "benchmark") {
|
||
await this.runBenchmark(report, mapProgress, abortSignal);
|
||
} else if (subMode === "writeverify") {
|
||
await this.runWriteVerify(report, mapProgress, abortSignal);
|
||
}
|
||
}
|
||
} else {
|
||
throw new Error(tFmt("fake_ui.error_unknown_mode", "Unbekannter Modus: {mode}", { mode }));
|
||
}
|
||
|
||
const t1 = performance.now();
|
||
report.total_duration_s = (t1 - t0) / 1000;
|
||
report.meta.ended_at = new Date().toISOString();
|
||
|
||
return report;
|
||
}
|
||
}
|
||
|
||
const tester = new UsbBrowserTester();
|
||
let currentMode = null;
|
||
let isRunning = false;
|
||
|
||
if (!tester.hasFsApiSupport()) {
|
||
if (fsapiWarning) fsapiWarning.style.display = "block";
|
||
logLine(
|
||
t("fake_ui.log_fsapi_partial", "Dein Browser unterstützt die File System Access API nicht voll. Einige Funktionen sind deaktiviert."),
|
||
"warn"
|
||
);
|
||
}
|
||
|
||
function updateStartButtonState() {
|
||
const hasDir = !!tester.rootHandle;
|
||
const hasMode = !!currentMode;
|
||
if (btnStart) btnStart.disabled = !(hasDir && hasMode && !isRunning);
|
||
if (btnCancel) btnCancel.disabled = !isRunning;
|
||
}
|
||
|
||
// --- Event-Handler ------------------------------------------------------
|
||
|
||
let isPickingDir = false;
|
||
|
||
if (btnPickDir) {
|
||
btnPickDir.addEventListener("click", () => {
|
||
if (isPickingDir) return;
|
||
isPickingDir = true;
|
||
|
||
if (!("showDirectoryPicker" in window)) {
|
||
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;
|
||
}
|
||
|
||
window.showDirectoryPicker()
|
||
.then((handle) => {
|
||
tester.rootHandle = handle;
|
||
tester.capacityBytes = null;
|
||
|
||
if (selectedPathText) {
|
||
selectedPathText.textContent = tFmt(
|
||
"fake_ui.selected_path_label",
|
||
'USB-Ordner ausgewählt (Name: "{name}").',
|
||
{ name: (handle.name || "Unbekannt") }
|
||
);
|
||
}
|
||
if (btnClearSel) btnClearSel.disabled = false;
|
||
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(t("fake_ui.log_pick_abort", "Verzeichnisauswahl abgebrochen."), "warn");
|
||
} else if (err) {
|
||
logLine(
|
||
tFmt(
|
||
"fake_ui.log_pick_error",
|
||
"Fehler bei Verzeichnisauswahl: {msg}",
|
||
{ msg: err.message || "unbekannt" }
|
||
),
|
||
"error"
|
||
);
|
||
} else {
|
||
logLine(t("fake_ui.log_pick_error_unknown", "Unbekannter Fehler bei Verzeichnisauswahl."), "error");
|
||
}
|
||
})
|
||
.finally(() => {
|
||
isPickingDir = false;
|
||
});
|
||
});
|
||
}
|
||
|
||
if (btnClearSel) {
|
||
btnClearSel.addEventListener("click", async () => {
|
||
await tester.clearSelection();
|
||
if (selectedPathText) {
|
||
selectedPathText.textContent = t("fake_ui.selected_path_none", "Noch kein Verzeichnis gewählt.");
|
||
}
|
||
btnClearSel.disabled = true;
|
||
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();
|
||
});
|
||
}
|
||
|
||
modeTiles.forEach((tile) => {
|
||
tile.addEventListener("click", () => {
|
||
if (tile.classList.contains("disabled")) return;
|
||
if (isRunning) return;
|
||
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(
|
||
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();
|
||
});
|
||
});
|
||
|
||
if (btnStart) {
|
||
btnStart.addEventListener("click", async () => {
|
||
if (!currentMode || !tester.rootHandle || isRunning) return;
|
||
isRunning = true;
|
||
updateStartButtonState();
|
||
if (btnCancel) btnCancel.disabled = false;
|
||
if (saveError) saveError.style.display = "none";
|
||
setProgress(0);
|
||
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;
|
||
|
||
const updateProgressCb = (percent) => setProgress(percent);
|
||
let report = null;
|
||
|
||
try {
|
||
report = await tester.run(currentMode, updateProgressCb, abortController.signal);
|
||
|
||
setProgress(100);
|
||
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);
|
||
|
||
await saveReportToBackend(report);
|
||
} catch (err) {
|
||
if (err && err.name === "AbortError") {
|
||
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(
|
||
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(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;
|
||
tester.abortController = null;
|
||
updateStartButtonState();
|
||
if (btnCancel) btnCancel.disabled = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCancel) {
|
||
btnCancel.addEventListener("click", () => {
|
||
if (!isRunning || !tester.abortController) return;
|
||
tester.abortController.abort();
|
||
});
|
||
}
|
||
|
||
// --- Dashboard-Füllung --------------------------------------------------
|
||
|
||
function applyReportToDashboard(report) {
|
||
const modeLabelMap = {
|
||
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) {
|
||
resMode.textContent = modeLabelMap[report.mode_requested] || report.mode_requested || "–";
|
||
}
|
||
if (resDuration) {
|
||
resDuration.textContent = formatDuration(report.total_duration_s);
|
||
}
|
||
|
||
let aggWriteBytes = 0;
|
||
let aggReadBytes = 0;
|
||
let writeSec = 0;
|
||
let readSec = 0;
|
||
|
||
if (report.quick) {
|
||
aggWriteBytes += report.quick.write_bytes || 0;
|
||
aggReadBytes += report.quick.read_bytes || 0;
|
||
writeSec += report.quick.write_duration_s || 0;
|
||
readSec += report.quick.read_duration_s || 0;
|
||
}
|
||
if (report.benchmark) {
|
||
aggWriteBytes += report.benchmark.write_bytes || 0;
|
||
aggReadBytes += report.benchmark.read_bytes || 0;
|
||
writeSec += report.benchmark.write_duration_s || 0;
|
||
readSec += report.benchmark.read_duration_s || 0;
|
||
}
|
||
if (report.writeverify) {
|
||
aggWriteBytes += report.writeverify.written_bytes || 0;
|
||
aggReadBytes += report.writeverify.verified_bytes || 0;
|
||
writeSec += report.writeverify.write_duration_s || 0;
|
||
readSec += report.writeverify.read_duration_s || 0;
|
||
}
|
||
|
||
if (resWritten) {
|
||
resWritten.textContent = aggWriteBytes ? formatBytes(aggWriteBytes) : "–";
|
||
}
|
||
if (resVerified) {
|
||
resVerified.textContent = aggReadBytes ? formatBytes(aggReadBytes) : "–";
|
||
}
|
||
|
||
const avgWriteMbps = (aggWriteBytes && writeSec) ? formatMbps(aggWriteBytes, writeSec) : "–";
|
||
const avgReadMbps = (aggReadBytes && readSec) ? formatMbps(aggReadBytes, readSec) : "–";
|
||
|
||
if (resWriteSpeed) resWriteSpeed.textContent = avgWriteMbps;
|
||
if (resReadSpeed) resReadSpeed.textContent = avgReadMbps;
|
||
}
|
||
|
||
// --- Backend-Speicherung ------------------------------------------------
|
||
|
||
async function saveReportToBackend(report) {
|
||
// apiBase kommt aus fakecheck.core.js (detectApiBase)
|
||
const apiBase = (cfg && cfg.apiBase) ? cfg.apiBase : "";
|
||
const base = apiBase.replace(/\/+$/, "");
|
||
// v1-Endpunkt
|
||
const url = base ? (base + "/v1/browser.quick.test") : "/api/v1/browser.quick.test";
|
||
|
||
try {
|
||
const response = await fetch(url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json"
|
||
},
|
||
body: JSON.stringify(report),
|
||
credentials: "include"
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const msg = "HTTP " + response.status;
|
||
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";
|
||
}
|
||
return;
|
||
}
|
||
|
||
const data = await response.json().catch(() => null);
|
||
|
||
if (!data || data.ok !== true) {
|
||
logLine(
|
||
tFmt(
|
||
"fake_ui.log_backend_save_error_payload",
|
||
"Backend: Testergebnis wurde nicht bestätigt{suffix}.",
|
||
{
|
||
suffix: data && data.error
|
||
? ` (${data.error})`
|
||
: ""
|
||
}
|
||
),
|
||
"warn"
|
||
);
|
||
if (saveError) saveError.style.display = "block";
|
||
return;
|
||
}
|
||
|
||
logLine(
|
||
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(
|
||
tFmt(
|
||
"fake_ui.log_backend_save_error",
|
||
"Fehler beim Speichern im Backend: {msg}",
|
||
{ msg: err ? err.message || "unbekannt" : "unbekannt" }
|
||
),
|
||
"error"
|
||
);
|
||
}
|
||
}
|
||
|
||
// --- Initialzustand -----------------------------------------------------
|
||
|
||
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();
|
||
})();
|