This commit is contained in:
2025-11-25 01:14:23 +01:00
parent 78cbccfaea
commit 258de0b0d6
5 changed files with 826 additions and 897 deletions

View 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();
})();