sda
This commit is contained in:
@@ -7,7 +7,9 @@ $requestUri = $_SERVER['REQUEST_URI'] ?? '/';
|
|||||||
|
|
||||||
$usbConfig = [
|
$usbConfig = [
|
||||||
'lang' => $lang ?? 'en',
|
'lang' => $lang ?? 'en',
|
||||||
'assetsBase'=> '/assets',
|
'assetsBase' => '/assets',
|
||||||
|
// NEU: Versionierung für JS/CSS aus PHP-Config
|
||||||
|
'assetVersion'=> defined('ASSET_VERSION') ? ASSET_VERSION : null,
|
||||||
'env' => $GLOBALS['ENV'] ?? 'prod',
|
'env' => $GLOBALS['ENV'] ?? 'prod',
|
||||||
|
|
||||||
'domains' => [
|
'domains' => [
|
||||||
@@ -19,7 +21,7 @@ $usbConfig = [
|
|||||||
|
|
||||||
'fakecheck' => [
|
'fakecheck' => [
|
||||||
'baseUrl' => $GLOBALS['usb_base_url'] ?? '',
|
'baseUrl' => $GLOBALS['usb_base_url'] ?? '',
|
||||||
'apiBaseUrl'=> $GLOBALS['usb_api_base'] ?? 'https://api.usbcheck.it',
|
'apiBaseUrl' => $GLOBALS['usb_api_base'] ?? 'https://api.usbcheck.it',
|
||||||
'locale' => $lang ?? 'en',
|
'locale' => $lang ?? 'en',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,895 +1,37 @@
|
|||||||
// /public/assets/js/fakecheck.js
|
// /public/assets/js/fakecheck.js
|
||||||
|
// Loader für alle Fakecheck-Module
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
(function () {
|
||||||
// Zentrale Config aus PHP (app_config.php o.ä.)
|
|
||||||
const cfg = window.usbConfig || {};
|
const cfg = window.usbConfig || {};
|
||||||
const baseUrl = cfg.baseUrl || "";
|
const assetsBase = cfg.assetsBase || "/assets";
|
||||||
const locale = (cfg.lang || "en").toLowerCase();
|
|
||||||
|
|
||||||
// Neu: API-Basis-URL (prod/staging) – über Config oder Fallback anhand Host
|
// Version aus PHP-Config (ASSET_VERSION) → app_config.php
|
||||||
function detectApiBaseUrl() {
|
const assetVersion = cfg.assetVersion;
|
||||||
// Fallback, falls in usbConfig nichts hinterlegt ist
|
const versionQuery = assetVersion
|
||||||
const host = window.location.hostname || "";
|
? ("?v=" + encodeURIComponent(assetVersion))
|
||||||
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 = `<strong>${prefix}</strong>${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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
? '<ul class="list-disc list-inside mt-1">' +
|
|
||||||
issues.map(i => '<li>' + i + '</li>').join("") +
|
|
||||||
"</ul>"
|
|
||||||
: '<span class="text-emerald-600 text-[11px]">Keine besonderen Auffälligkeiten.</span>';
|
|
||||||
|
|
||||||
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
|
|
||||||
? '<ul class="list-disc list-inside mt-1 text-[11px]">' +
|
|
||||||
notes.map(n => "<li>" + n + "</li>").join("") +
|
|
||||||
"</ul>"
|
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
resultBox.innerHTML = `
|
// Basis-Pfad für die Teil-Skripte
|
||||||
<div class="mb-3">
|
const base = assetsBase.replace(/\/+$/, "") + "/js/fakecheck/";
|
||||||
<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" : ""}
|
|
||||||
">
|
|
||||||
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">
|
const scripts = [
|
||||||
<h3 class="text-xs font-semibold mb-1 text-slate-800 dark:text-slate-100">Eingabedaten</h3>
|
base + "fakecheck.core.js" + versionQuery,
|
||||||
<dl class="text-[11px] space-y-1 text-slate-600 dark:text-slate-300">
|
base + "fakecheck.browser.js" + versionQuery,
|
||||||
<div><dt class="font-medium">Hersteller (Angabe):</dt><dd>${input.manufacturer || '<span class="text-slate-400">keine Angabe</span>'}</dd></div>
|
base + "fakecheck.serial.js" + versionQuery
|
||||||
<div><dt class="font-medium">VID / PID:</dt><dd>${(input.vid || "–") + " / " + (input.pid || "–")}</dd></div>
|
];
|
||||||
<div><dt class="font-medium">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">
|
function loadScript(src) {
|
||||||
<h3 class="text-xs font-semibold mb-1 text-slate-800 dark:text-slate-100">Seriennummer-Analyse</h3>
|
return new Promise((resolve, reject) => {
|
||||||
<dl class="text-[11px] space-y-1 text-slate-600 dark:text-slate-300">
|
const s = document.createElement("script");
|
||||||
<div><dt class="font-medium">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>
|
s.src = src;
|
||||||
<div><dt class="font-medium">Länge:</dt><dd>${serialInfo.length || 0} Zeichen</dd></div>
|
s.async = false; // Reihenfolge sicherstellen
|
||||||
<div><dt class="font-medium">Kategorie:</dt><dd>${serialInfo.category || "-"}</dd></div>
|
s.onload = resolve;
|
||||||
<div><dt class="font-medium">Score:</dt><dd>${typeof serialInfo.score === "number" ? serialInfo.score : "-"} / 100</dd></div>
|
s.onerror = () => reject(new Error("Konnte " + src + " nicht laden"));
|
||||||
<div><dt class="font-medium">Auffälligkeiten:</dt><dd>${issuesList}</dd></div>
|
document.head.appendChild(s);
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border border-slate-200 dark:border-slate-700 rounded-xl p-3">
|
|
||||||
<h3 class="text-xs font-semibold mb-1 text-slate-800 dark:text-slate-100">Hersteller-Konsistenz</h3>
|
|
||||||
${notesList}
|
|
||||||
<p class="mt-2 text-[10px] text-slate-500">
|
|
||||||
Diese Einschätzung basiert auf Heuristiken und kann keine Echtheit garantieren.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -----------------------------------------------------
|
scripts
|
||||||
|
.reduce((p, src) => p.then(() => loadScript(src)), Promise.resolve())
|
||||||
setStatus("Bereit. Wähle zuerst deinen USB-Stick aus.");
|
.catch(err => console.error("Fakecheck Loader Fehler:", err));
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|||||||
660
public/assets/js/fakecheck/fakecheck.browser.js
Normal file
660
public/assets/js/fakecheck/fakecheck.browser.js
Normal file
@@ -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();
|
||||||
|
})();
|
||||||
100
public/assets/js/fakecheck/fakecheck.core.js
Normal file
100
public/assets/js/fakecheck/fakecheck.core.js
Normal file
@@ -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 = `<strong>${prefix}</strong>${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`;
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
25
public/assets/js/fakecheck/fakecheck.serial.js
Normal file
25
public/assets/js/fakecheck/fakecheck.serial.js
Normal file
@@ -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));
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user