diff --git a/partials/structure/app_config.php b/partials/structure/app_config.php
index 94905b9..b6db9de 100644
--- a/partials/structure/app_config.php
+++ b/partials/structure/app_config.php
@@ -6,9 +6,11 @@ $host = $_SERVER['HTTP_HOST'] ?? app_primary_domain();
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$usbConfig = [
- 'lang' => $lang ?? 'en',
- 'assetsBase'=> '/assets',
- 'env' => $GLOBALS['ENV'] ?? 'prod',
+ 'lang' => $lang ?? 'en',
+ 'assetsBase' => '/assets',
+ // NEU: Versionierung für JS/CSS aus PHP-Config
+ 'assetVersion'=> defined('ASSET_VERSION') ? ASSET_VERSION : null,
+ 'env' => $GLOBALS['ENV'] ?? 'prod',
'domains' => [
'primaryDomain' => app_primary_domain(),
@@ -18,9 +20,9 @@ $usbConfig = [
],
'fakecheck' => [
- 'baseUrl' => $GLOBALS['usb_base_url'] ?? '',
- 'apiBaseUrl'=> $GLOBALS['usb_api_base'] ?? 'https://api.usbcheck.it',
- 'locale' => $lang ?? 'en',
+ 'baseUrl' => $GLOBALS['usb_base_url'] ?? '',
+ 'apiBaseUrl' => $GLOBALS['usb_api_base'] ?? 'https://api.usbcheck.it',
+ 'locale' => $lang ?? 'en',
],
];
?>
diff --git a/public/assets/js/fakecheck.js b/public/assets/js/fakecheck.js
index 9c9f8e5..11ba591 100644
--- a/public/assets/js/fakecheck.js
+++ b/public/assets/js/fakecheck.js
@@ -1,895 +1,37 @@
// /public/assets/js/fakecheck.js
-
-document.addEventListener("DOMContentLoaded", () => {
- // Zentrale Config aus PHP (app_config.php o.ä.)
- const cfg = window.usbConfig || {};
- const baseUrl = cfg.baseUrl || "";
- const locale = (cfg.lang || "en").toLowerCase();
-
- // Neu: API-Basis-URL (prod/staging) – über Config oder Fallback anhand Host
- function detectApiBaseUrl() {
- // Fallback, falls in usbConfig nichts hinterlegt ist
- const host = window.location.hostname || "";
- if (host === "staging.usbcheck.it" || host.endsWith(".staging.usbcheck.it")) {
- return "https://api.staging.usbcheck.it";
- }
- return "https://api.usbcheck.it";
- }
-
- const apiBaseUrl = cfg.apiBaseUrl || detectApiBaseUrl();
-
- const root = document.getElementById("fc-root");
- if (!root) {
- // Auf anderen Seiten eingebunden? Dann einfach nichts tun.
- return;
- }
-
- // --- DOM-Helper ---------------------------------------------------------
- const $ = (sel) => document.querySelector(sel);
- const $$ = (sel) => Array.from(document.querySelectorAll(sel));
-
- const logEl = $("#fc-log");
- const statusTextEl = $("#fc-status-text");
- const statusModeEl = $("#fc-status-mode");
- const progressInner = $("#fc-progress-inner");
- const overallPill = $("#fc-overall-status-pill");
- const overallStatus = $("#fc-overall-status-text");
- 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 aus usbConfig
- if (cfg.isLoggedIn && saveHint) {
- saveHint.style.display = "block";
- }
-
- function logLine(message, level = "info") {
- if (!logEl) return;
- const line = document.createElement("div");
- line.className = "fc-log-line";
- const prefix =
- level === "error" ? "[ERROR] " :
- level === "warn" ? "[WARN] " :
- "[INFO] ";
- line.innerHTML = `${prefix}${message}`;
- logEl.appendChild(line);
- logEl.scrollTop = logEl.scrollHeight;
- }
-
- function setStatus(text) {
- if (statusTextEl) statusTextEl.textContent = text;
- }
-
- function setModeLabel(text) {
- if (statusModeEl) statusModeEl.textContent = text;
- }
-
- function setProgress(percent) {
- if (!progressInner) return;
- const v = Math.min(100, Math.max(0, percent));
- progressInner.style.width = v + "%";
- }
-
- function setOverallStatus(state, text) {
- if (!overallPill || !overallStatus) return;
- overallPill.classList.remove("fc-pill-ok", "fc-pill-warn", "fc-pill-bad");
- if (state === "ok") overallPill.classList.add("fc-pill-ok");
- else if (state === "warn") overallPill.classList.add("fc-pill-warn");
- else overallPill.classList.add("fc-pill-bad");
- overallStatus.textContent = text;
- }
-
- function formatBytes(bytes) {
- if (bytes == null) return "–";
- const units = ["B", "KB", "MB", "GB", "TB"];
- let u = 0;
- let v = bytes;
- while (v >= 1024 && u < units.length - 1) {
- v /= 1024;
- u++;
- }
- return v.toFixed(1) + " " + units[u];
- }
-
- function formatMbps(bytes, seconds) {
- if (!seconds || seconds <= 0) return "–";
- const bits = bytes * 8;
- const mbits = bits / (1000 * 1000);
- return mbits.toFixed(1) + " Mbit/s";
- }
-
- function formatDuration(seconds) {
- if (!seconds) return "–";
- const s = Math.round(seconds);
- if (s < 60) return s + " s";
- const m = Math.floor(s / 60);
- const r = s % 60;
- if (m < 60) return `${m} min ${r}s`;
- const h = Math.floor(m / 60);
- const rm = m % 60;
- return `${h} h ${rm} min`;
- }
-
- // --- Test-Engine (Browser) ---------------------------------------------
-
- class UsbBrowserTester {
- constructor() {
- this.rootHandle = null;
- this.abortController = null;
- }
-
- hasFsApiSupport() {
- return "showDirectoryPicker" in window;
- }
-
- // Kann theoretisch bleiben, wird aktuell aber NICHT benutzt,
- // damit showDirectoryPicker direkt im Click-Handler liegt.
- async pickDirectory() {
- if (!this.hasFsApiSupport()) {
- throw new Error("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;
- }
-
- 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.");
-
- 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();
- 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("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(`Quick-Check: Datenfehler bei Byte ${offset + i}`, "error");
- throw new Error(`Datenfehler im Quick-Check bei Byte ${offset + i}`);
- }
- }
- 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
- };
-
- logLine("Quick-Check: Erfolgreich abgeschlossen.", "info");
- }
-
- 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.");
-
- 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();
- 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("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
- };
-
- logLine("Benchmark: Erfolgreich abgeschlossen.", "info");
- }
-
- 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.");
-
- logLine("Write/Verify: Start – mehrere Blöcke werden getestet...", "info");
-
- const totalBytes = BLOCKS * BLOCK_SIZE_MB * 1024 * 1024;
- 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(`Write/Verify: Block ${b + 1}/${BLOCKS} – ${filename}`, "info");
-
- const fileHandle = await dirHandle.getFileHandle(filename, { create: true });
- const writable = await fileHandle.createWritable();
-
- const blockBytes = BLOCK_SIZE_MB * 1024 * 1024;
- let blockWritten = 0;
- const blockWriteStart = performance.now();
-
- while (blockWritten < blockBytes) {
- if (abortSignal.aborted) {
- await writable.abort();
- throw new DOMException("Abgebrochen", "AbortError");
- }
- const remaining = blockBytes - 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: blockBytes,
- 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(`Write/Verify: Datenfehler in Block ${b + 1} bei Byte ${blockOffset + i}`, "error");
- throw new Error(`Datenfehler in Block ${b + 1} bei Byte ${blockOffset + i}`);
- }
- }
- 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: blockBytes,
- duration_s: (blockReadEnd - blockReadStart) / 1000
- });
- }
-
- 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_mb: BLOCK_SIZE_MB,
- 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("Write/Verify: Alle Blöcke erfolgreich verifiziert.", "info");
- }
-
- async run(mode, updateProgressCb, abortSignal) {
- if (!this.rootHandle) {
- throw new Error("Kein USB-Verzeichnis ausgewählt.");
- }
-
- const report = {
- meta: {
- base_url: baseUrl,
- locale: locale,
- 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(`All-Inclusive: Starte Teiltest "${subMode}" (${i + 1}/${modes.length})...`, "info");
- await this.run(
- subMode,
- (p) => {
- const base = (i / modes.length) * 100;
- const span = (1 / modes.length) * 100;
- updateProgressCb(base + (p / 100) * span);
- },
- abortSignal
- );
- }
- } else {
- throw new Error("Unbekannter Modus: " + 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(
- "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 ------------------------------------------------------
-
- // WICHTIG: showDirectoryPicker direkt im Click-Handler (ohne async/await),
- // damit "User activation is required" nicht mehr triggert.
- let isPickingDir = false;
-
- if (btnPickDir) {
- btnPickDir.addEventListener("click", () => {
- if (isPickingDir) return; // Doppelklick-Schutz
- isPickingDir = true;
-
- if (!("showDirectoryPicker" in window)) {
- logLine("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;
- if (selectedPathText) {
- selectedPathText.textContent =
- 'USB-Ordner ausgewählt (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]"));
- updateStartButtonState();
- })
- .catch((err) => {
- if (err && err.name === "AbortError") {
- logLine("Verzeichnisauswahl abgebrochen.", "warn");
- } else if (err) {
- logLine("Fehler bei Verzeichnisauswahl: " + err.message, "error");
- } else {
- logLine("Unbekannter Fehler bei Verzeichnisauswahl.", "error");
- }
- })
- .finally(() => {
- isPickingDir = false;
- });
+// Loader für alle Fakecheck-Module
+
+(function () {
+ const cfg = window.usbConfig || {};
+ const assetsBase = cfg.assetsBase || "/assets";
+
+ // Version aus PHP-Config (ASSET_VERSION) → app_config.php
+ const assetVersion = cfg.assetVersion;
+ const versionQuery = assetVersion
+ ? ("?v=" + encodeURIComponent(assetVersion))
+ : "";
+
+ // Basis-Pfad für die Teil-Skripte
+ const base = assetsBase.replace(/\/+$/, "") + "/js/fakecheck/";
+
+ const scripts = [
+ base + "fakecheck.core.js" + versionQuery,
+ base + "fakecheck.browser.js" + versionQuery,
+ base + "fakecheck.serial.js" + versionQuery
+ ];
+
+ 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);
});
}
- if (btnClearSel) {
- btnClearSel.addEventListener("click", async () => {
- await tester.clearSelection();
- if (selectedPathText) {
- selectedPathText.textContent = "Noch kein Verzeichnis gewählt.";
- }
- btnClearSel.disabled = true;
- setStatus("Bereit. Wähle zuerst deinen USB-Stick aus.");
- logLine("Verzeichnisauswahl zurückgesetzt.", "info");
- updateStartButtonState();
- });
- }
-
- modeTiles.forEach((tile) => {
- tile.addEventListener("click", () => {
- if (tile.classList.contains("disabled")) return;
- if (isRunning) return;
- modeTiles.forEach((t) => t.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");
- 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("Starte Tests im Modus: " + currentMode.toUpperCase(), "info");
- setStatus("Test läuft... bitte USB-Stick nicht entfernen.");
- setOverallStatus("warn", "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("Test abgeschlossen.");
- setOverallStatus("ok", "Browser-Test erfolgreich abgeschlossen.");
- applyReportToDashboard(report);
-
- // Debug / Support:
- console.log("USB Browser Test Report (fakecheck):", report);
-
- // Ergebnis speichern: Backend entscheidet, ob der User eingeloggt ist
- 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");
- } else if (err) {
- setStatus("Fehler: " + err.message);
- setOverallStatus("bad", "Fehler im Browser-Test.");
- logLine("Fehler im Test: " + err.message, "error");
- } else {
- setStatus("Unbekannter Fehler im Test.");
- setOverallStatus("bad", "Fehler im Browser-Test.");
- logLine("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: "Quick-Check",
- benchmark: "Benchmark",
- writeverify: "Write & Verify",
- 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) {
- const url = "/api/result/browser-quick-test.php";
- 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("Backend: Konnte Ergebnis nicht speichern (" + msg + ").", "warn");
- if (response.status >= 500 && saveError) {
- saveError.style.display = "block";
- }
- return;
- }
-
- const data = await response.json().catch(() => null);
- logLine(
- "Backend: Testergebnis gespeichert" +
- (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");
- }
- }
-
- // --- Seriennummer-Quickcheck (API-gestützt) -----------------------------
-
- function initSerialCheckWidget() {
- const rootSc = document.getElementById("serialcheck-root");
- if (!rootSc) return; // Serialcheck-Partial nicht eingebunden → nichts tun
-
- 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");
-
- function showScError(msg) {
- if (!errorBox) return;
- errorBox.textContent = msg || "Es ist ein Fehler aufgetreten.";
- errorBox.classList.remove("hidden");
- if (resultBox) resultBox.classList.add("hidden");
- }
-
- function clearScError() {
- if (!errorBox) return;
- errorBox.classList.add("hidden");
- errorBox.textContent = "";
- }
-
- function renderScResult(data) {
- if (!resultBox) return;
- clearScError();
- resultBox.classList.remove("hidden");
-
- const rating = data.rating || "unknown";
- let ratingLabel = "";
- let ratingDesc = "";
-
- if (rating === "ok") {
- ratingLabel = "Plausibel";
- ratingDesc = "Keine deutlichen Auffälligkeiten erkannt.";
- } else if (rating === "needs_review") {
- ratingLabel = "Überprüfen empfohlen";
- ratingDesc = "Leichte Auffälligkeiten. In Kombination mit einem technischen Test ergibt sich ein klareres Bild.";
- } else if (rating === "suspicious") {
- ratingLabel = "Auffällig / Verdächtig";
- ratingDesc = "Deutliche Auffälligkeiten erkannt. Ein Kapazitäts-/Geschwindigkeitstest ist dringend empfohlen.";
- } else if (rating === "invalid") {
- ratingLabel = "Ungültig";
- ratingDesc = "Die Seriennummer konnte nicht sinnvoll bewertet werden.";
- } else {
- ratingLabel = "Unklar";
- ratingDesc = "Bewertung nicht eindeutig möglich.";
- }
-
- const input = data.input || {};
- const vendorInfo = data.vendor_detected || {};
- const serialInfo = data.serial_analysis || {};
- const consistency = data.consistency || {};
- const issues = serialInfo.issues || [];
- const notes = consistency.notes || [];
-
- const issuesList = issues.length
- ? '
' +
- issues.map(i => '- ' + i + '
').join("") +
- "
"
- : 'Keine besonderen Auffälligkeiten.';
-
- const vendorLine = vendorInfo.found
- ? (vendorInfo.vendor + " (VID " + vendorInfo.vid + ")")
- : (vendorInfo.vid ? ("Unbekannter Hersteller für VID " + vendorInfo.vid) : "Keine Vendor-ID angegeben");
-
- const notesList = notes.length
- ? '' +
- notes.map(n => "- " + n + "
").join("") +
- "
"
- : "";
-
- resultBox.innerHTML = `
-
-
- Bewertung: ${ratingLabel}
-
-
${ratingDesc}
-
-
-
-
Eingabedaten
-
- - Hersteller (Angabe):
- ${input.manufacturer || 'keine Angabe'}
- - VID / PID:
- ${(input.vid || "–") + " / " + (input.pid || "–")}
- - Vendor aus VID:
- ${vendorLine}
-
-
-
-
-
Seriennummer-Analyse
-
- - Seriennummer:
${serialInfo.serial || ""}
- - Länge:
- ${serialInfo.length || 0} Zeichen
- - Kategorie:
- ${serialInfo.category || "-"}
- - Score:
- ${typeof serialInfo.score === "number" ? serialInfo.score : "-"} / 100
- - Auffälligkeiten:
- ${issuesList}
-
-
-
-
-
Hersteller-Konsistenz
- ${notesList}
-
- Diese Einschätzung basiert auf Heuristiken und kann keine Echtheit garantieren.
-
-
- `;
- }
-
- 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("Bitte gib eine Seriennummer ein.");
- return;
- }
-
- fetch(apiBaseUrl.replace(/\/+$/, "") + "/quickcheck", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload)
- })
- .then(res => {
- if (!res.ok) {
- throw new Error("Server returned status " + res.status);
- }
- return res.json();
- })
- .then(data => {
- if (!data || !data.success) {
- throw new Error((data && data.error) || "Unerwartete Antwort vom Server.");
- }
- renderScResult(data);
- })
- .catch(err => {
- showScError("Fehler bei der Prüfung: " + err.message);
- });
- });
- }
-
- // --- 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.");
- updateStartButtonState();
-
- // Serialcheck nur initialisieren, wenn das Partial vorhanden ist
- initSerialCheckWidget();
-});
+ scripts
+ .reduce((p, src) => p.then(() => loadScript(src)), Promise.resolve())
+ .catch(err => console.error("Fakecheck Loader Fehler:", err));
+})();
diff --git a/public/assets/js/fakecheck/fakecheck.browser.js b/public/assets/js/fakecheck/fakecheck.browser.js
new file mode 100644
index 0000000..fde29a5
--- /dev/null
+++ b/public/assets/js/fakecheck/fakecheck.browser.js
@@ -0,0 +1,660 @@
+// /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
+ } = 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;
+ }
+
+ 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.");
+ }
+ const handle = await window.showDirectoryPicker();
+ this.rootHandle = handle;
+ return handle;
+ }
+
+ async clearSelection() {
+ this.rootHandle = null;
+ }
+
+ 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.");
+
+ 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();
+ 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("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(`Quick-Check: Datenfehler bei Byte ${offset + i}`, "error");
+ throw new Error(`Datenfehler im Quick-Check bei Byte ${offset + i}`);
+ }
+ }
+ 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
+ };
+
+ logLine("Quick-Check: Erfolgreich abgeschlossen.", "info");
+ }
+
+ 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.");
+
+ 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();
+ 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("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
+ };
+
+ logLine("Benchmark: Erfolgreich abgeschlossen.", "info");
+ }
+
+ 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.");
+
+ logLine("Write/Verify: Start – mehrere Blöcke werden getestet...", "info");
+
+ const totalBytes = BLOCKS * BLOCK_SIZE_MB * 1024 * 1024;
+ 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(`Write/Verify: Block ${b + 1}/${BLOCKS} – ${filename}`, "info");
+
+ const fileHandle = await dirHandle.getFileHandle(filename, { create: true });
+ const writable = await fileHandle.createWritable();
+
+ const blockBytes = BLOCK_SIZE_MB * 1024 * 1024;
+ let blockWritten = 0;
+ const blockWriteStart = performance.now();
+
+ while (blockWritten < blockBytes) {
+ if (abortSignal.aborted) {
+ await writable.abort();
+ throw new DOMException("Abgebrochen", "AbortError");
+ }
+ const remaining = blockBytes - 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: blockBytes,
+ 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(`Write/Verify: Datenfehler in Block ${b + 1} bei Byte ${blockOffset + i}`, "error");
+ throw new Error(`Datenfehler in Block ${b + 1} bei Byte ${blockOffset + i}`);
+ }
+ }
+ 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: blockBytes,
+ duration_s: (blockReadEnd - blockReadStart) / 1000
+ });
+ }
+
+ 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_mb: BLOCK_SIZE_MB,
+ 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("Write/Verify: Alle Blöcke erfolgreich verifiziert.", "info");
+ }
+
+ async run(mode, updateProgressCb, abortSignal) {
+ if (!this.rootHandle) {
+ throw new Error("Kein USB-Verzeichnis ausgewählt.");
+ }
+
+ 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") {
+ // 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");
+
+ 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("Unbekannter Modus: " + 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(
+ "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 ------------------------------------------------------
+
+ // Wichtig: showDirectoryPicker direkt im Click-Handler per Promise,
+ // kein async/await direkt im Listener → vermeidet "User activation is required".
+ let isPickingDir = false;
+
+ if (btnPickDir) {
+ btnPickDir.addEventListener("click", () => {
+ if (isPickingDir) return;
+ isPickingDir = true;
+
+ if (!("showDirectoryPicker" in window)) {
+ logLine("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;
+ if (selectedPathText) {
+ selectedPathText.textContent =
+ 'USB-Ordner ausgewählt (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]"));
+ updateStartButtonState();
+ })
+ .catch((err) => {
+ if (err && err.name === "AbortError") {
+ logLine("Verzeichnisauswahl abgebrochen.", "warn");
+ } else if (err) {
+ logLine("Fehler bei Verzeichnisauswahl: " + err.message, "error");
+ } else {
+ logLine("Unbekannter Fehler bei Verzeichnisauswahl.", "error");
+ }
+ })
+ .finally(() => {
+ isPickingDir = false;
+ });
+ });
+ }
+
+ if (btnClearSel) {
+ btnClearSel.addEventListener("click", async () => {
+ await tester.clearSelection();
+ if (selectedPathText) {
+ selectedPathText.textContent = "Noch kein Verzeichnis gewählt.";
+ }
+ btnClearSel.disabled = true;
+ setStatus("Bereit. Wähle zuerst deinen USB-Stick aus.");
+ logLine("Verzeichnisauswahl zurückgesetzt.", "info");
+ updateStartButtonState();
+ });
+ }
+
+ modeTiles.forEach((tile) => {
+ tile.addEventListener("click", () => {
+ if (tile.classList.contains("disabled")) return;
+ if (isRunning) return;
+ modeTiles.forEach((t) => t.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");
+ 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("Starte Tests im Modus: " + currentMode.toUpperCase(), "info");
+ setStatus("Test läuft... bitte USB-Stick nicht entfernen.");
+ setOverallStatus("warn", "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("Test abgeschlossen.");
+ setOverallStatus("ok", "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("Test wurde abgebrochen.");
+ setOverallStatus("warn", "Test abgebrochen.");
+ logLine("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");
+ } else {
+ setStatus("Unbekannter Fehler im Test.");
+ setOverallStatus("bad", "Fehler im Browser-Test.");
+ logLine("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: "Quick-Check",
+ benchmark: "Benchmark",
+ writeverify: "Write & Verify",
+ 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) {
+ const url = "/api/result/browser-quick-test.php";
+ 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("Backend: Konnte Ergebnis nicht speichern (" + msg + ").", "warn");
+ if (response.status >= 500 && saveError) {
+ saveError.style.display = "block";
+ }
+ return;
+ }
+
+ const data = await response.json().catch(() => null);
+ logLine(
+ "Backend: Testergebnis gespeichert" +
+ (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");
+ }
+ }
+
+ // --- 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.");
+ updateStartButtonState();
+})();
diff --git a/public/assets/js/fakecheck/fakecheck.core.js b/public/assets/js/fakecheck/fakecheck.core.js
new file mode 100644
index 0000000..1088e2e
--- /dev/null
+++ b/public/assets/js/fakecheck/fakecheck.core.js
@@ -0,0 +1,100 @@
+// /public/assets/js/fakecheck/fakecheck.core.js
+
+(function() {
+
+ // Namespace
+ window.usbcheck = window.usbcheck || {};
+
+ const cfg = window.usbConfig || {};
+
+ // URL-Detection
+ function detectApiBaseUrl() {
+ const host = window.location.hostname || "";
+ if (host === "staging.usbcheck.it" || host.endsWith(".staging.usbcheck.it")) {
+ return "https://api.staging.usbcheck.it";
+ }
+ return "https://api.usbcheck.it";
+ }
+
+ // Config bereitstellen
+ usbcheck.cfg = {
+ baseUrl: cfg.baseUrl || "",
+ locale: (cfg.lang || "en").toLowerCase(),
+ apiBase: cfg.apiBaseUrl || detectApiBaseUrl(),
+ loggedIn: !!cfg.isLoggedIn
+ };
+
+
+ // ---------- Hilfsfunktionen ----------
+ usbcheck.log = function logLine(message, level = "info") {
+ const logEl = document.querySelector("#fc-log");
+ if (!logEl) return;
+ const line = document.createElement("div");
+ line.className = "fc-log-line";
+ const prefix =
+ level === "error" ? "[ERROR] " :
+ level === "warn" ? "[WARN] " :
+ "[INFO] ";
+ line.innerHTML = `${prefix}${message}`;
+ logEl.appendChild(line);
+ logEl.scrollTop = logEl.scrollHeight;
+ };
+
+ usbcheck.setStatus = function(msg) {
+ const el = document.querySelector("#fc-status-text");
+ if (el) el.textContent = msg;
+ };
+
+ usbcheck.setModeLabel = function(msg) {
+ const el = document.querySelector("#fc-status-mode");
+ if (el) el.textContent = msg;
+ };
+
+ usbcheck.setProgress = function(percent) {
+ const el = document.querySelector("#fc-progress-inner");
+ if (!el) return;
+ const v = Math.min(100, Math.max(0, percent));
+ el.style.width = v + "%";
+ };
+
+ usbcheck.setOverallStatus = function(state, text) {
+ const pill = document.querySelector("#fc-overall-status-pill");
+ const label = document.querySelector("#fc-overall-status-text");
+ if (!pill || !label) return;
+
+ pill.classList.remove("fc-pill-ok","fc-pill-warn","fc-pill-bad");
+
+ if (state === "ok") pill.classList.add("fc-pill-ok");
+ else if (state === "warn") pill.classList.add("fc-pill-warn");
+ else pill.classList.add("fc-pill-bad");
+
+ label.textContent = text;
+ };
+
+ usbcheck.formatBytes = function(bytes) {
+ if (bytes == null) return "–";
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let u = 0, v = bytes;
+ while (v >= 1024 && u < units.length - 1) { v /= 1024; u++; }
+ return v.toFixed(1) + " " + units[u];
+ };
+
+ usbcheck.formatMbps = function(bytes, seconds) {
+ if (!seconds || seconds <= 0) return "–";
+ const mbits = (bytes * 8) / 1e6;
+ return mbits.toFixed(1) + " Mbit/s";
+ };
+
+ usbcheck.formatDuration = function(seconds) {
+ if (!seconds) return "–";
+ const s = Math.round(seconds);
+ if (s < 60) return s + " s";
+ const m = Math.floor(s / 60);
+ const r = s % 60;
+ if (m < 60) return `${m} min ${r}s`;
+ const h = Math.floor(m / 60);
+ const rm = m % 60;
+ return `${h} h ${rm} min`;
+ };
+
+})();
diff --git a/public/assets/js/fakecheck/fakecheck.serial.js b/public/assets/js/fakecheck/fakecheck.serial.js
new file mode 100644
index 0000000..850427a
--- /dev/null
+++ b/public/assets/js/fakecheck/fakecheck.serial.js
@@ -0,0 +1,25 @@
+// /public/assets/js/fakecheck.js
+
+(function() {
+ const base = "/assets/js/fakecheck/";
+
+ const scripts = [
+ base + "core.js?v=1",
+ base + "fakecheck.browser.js?v=1",
+ base + "fakecheck.serial.js?v=1"
+ ];
+
+ 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);
+ });
+ }
+
+ scripts.reduce((p, src) => p.then(() => loadScript(src)), Promise.resolve())
+ .catch(err => console.error("Fakecheck Loader Fehler:", err));
+})();