Files
usbcheck.it/public/assets/js/fakecheck/fakecheck.browser.js
2025-11-25 01:14:23 +01:00

661 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// /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();
})();