update
This commit is contained in:
54
api/result/browser-quick-test.php
Normal file
54
api/result/browser-quick-test.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
// public/api/results/save-browser-test.php
|
||||||
|
// Achtung: Passe Pfad und Bootstrapping an dein Projekt an!
|
||||||
|
|
||||||
|
require __DIR__ . '/../../../config/fileload.php';
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Nur eingeloggte User
|
||||||
|
if (empty($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'not_authenticated']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userId = (int)$_SESSION['user_id'];
|
||||||
|
|
||||||
|
// JSON einlesen
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$data = json_decode($raw, true);
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'invalid_json']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimale Validierung
|
||||||
|
$mode = $data['mode_requested'] ?? null;
|
||||||
|
$totalDuration = $data['total_duration_s'] ?? null;
|
||||||
|
|
||||||
|
// DB: Beispiel (mysqli/PDO – hier PDO angenommen)
|
||||||
|
$db = get_db_connection(); // implementiere in deiner config
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO usb_results_browser
|
||||||
|
(user_id, mode, total_duration_s, report_json, created_at)
|
||||||
|
VALUES
|
||||||
|
(:user_id, :mode, :total_duration_s, :report_json, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':user_id' => $userId,
|
||||||
|
':mode' => $mode,
|
||||||
|
':total_duration_s'=> $totalDuration,
|
||||||
|
':report_json' => json_encode($data, JSON_UNESCAPED_UNICODE),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$id = (int)$db->lastInsertId();
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'ok',
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
@@ -1,36 +1,174 @@
|
|||||||
<section id="webcheck" class="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
|
<?php
|
||||||
<div class="grid gap-8 lg:grid-cols-[minmax(0,2fr)_minmax(0,1.4fr)] items-start">
|
// fakecheck.php – Template für das Browser-Test-Tool im Bereich /fakecheck
|
||||||
<!-- Links: Test-Steuerung (Platzhalter) -->
|
?>
|
||||||
<div class="space-y-6">
|
<section class="fc-container" id="fakecheck-tool">
|
||||||
<div class="rounded-2xl border border-brand-border bg-brand-surface/80 p-6 sm:p-7 shadow-soft">
|
<div class="fc-header">
|
||||||
<h2 class="font-heading text-xl sm:text-2xl font-semibold mb-2 text-white" data-i18n="fake_app_title">
|
<div class="fc-header-text">
|
||||||
Browser-Testoberfläche (Preview)
|
<h2>Browser-basierter USB-Test</h2>
|
||||||
</h2>
|
<p>
|
||||||
<p class="text-sm text-brand-muted mb-4" data-i18n="fake_app_intro">
|
Führe einen Schnelltest direkt im Browser durch – ohne Installation.
|
||||||
Hier entsteht die eigentliche FakeCheck-Web-App: Auswahl des Testordners, Konfiguration der Testmenge, Fortschrittsanzeige und Ergebnisübersicht. Aktuell zeigt der Button oben nur eine Demo-Ausgabe.
|
Für tiefere Analysen gibt es später den Pro-Modus mit lokalem Helper.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
<ul class="space-y-2 text-xs text-brand-muted">
|
<div class="fc-header-badge">
|
||||||
<li data-i18n="fake_app_point1">• Quick-Test mit kleiner Datenmenge.</li>
|
<span class="dot"></span>
|
||||||
<li data-i18n="fake_app_point2">• Light-Benchmark: Schreib-/Lesegeschwindigkeit über begrenzte Zeit.</li>
|
<span id="fc-env-label">
|
||||||
<li data-i18n="fake_app_point3">• Write/Verify: Testdateien schreiben und direkt wieder verifizieren.</li>
|
Browser-Modus aktiv – keine Installation nötig
|
||||||
</ul>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Rechts: Ergebnis / JSON-Report-Preview -->
|
<div class="fc-layout" id="fc-root">
|
||||||
<div class="space-y-4">
|
<!-- Linke Seite: Steuerung & Fortschritt -->
|
||||||
<div id="resultContainer" class="hidden rounded-2xl border border-brand-border bg-brand-surface/80 p-5 sm:p-6 shadow-soft">
|
<section class="fc-card fc-main">
|
||||||
<h3 class="font-heading text-lg font-semibold mb-2 text-white" data-i18n="fake_result_title">
|
<h3 class="fc-card-title">Schritt 1: USB-Stick wählen</h3>
|
||||||
Demo-Ausgabe des Browser-Tests
|
<p class="fc-card-subtitle">
|
||||||
</h3>
|
Wähle das Wurzelverzeichnis deines USB-Sticks. Der Browser erhält nur Zugriff auf diesen Bereich.
|
||||||
<p class="text-xs text-brand-muted mb-3" data-i18n="fake_result_hint">
|
|
||||||
Diese Ausgabe dient nur als Vorschau. Später wird hier der echte JSON-Report aus dem Browser-Test angezeigt.
|
|
||||||
</p>
|
</p>
|
||||||
<pre id="resultOutput"
|
|
||||||
class="text-[11px] whitespace-pre-wrap bg-black/40 text-brand-muted rounded-xl p-3 font-mono overflow-x-auto min-h-[140px]">
|
<div class="fc-actions">
|
||||||
</pre>
|
<button type="button" class="fc-btn" id="fc-btn-pick-directory">
|
||||||
|
USB-Stick auswählen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="fc-btn fc-btn-secondary" id="fc-btn-clear-selection" disabled>
|
||||||
|
Auswahl zurücksetzen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="fc-help" id="fc-selected-path-label">
|
||||||
|
Noch kein Verzeichnis gewählt.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="fc-alert fc-alert-warn" id="fc-fsapi-warning" style="display:none;">
|
||||||
|
Dein Browser unterstützt die File System Access API nicht oder nur eingeschränkt.
|
||||||
|
Du kannst dieses Browser-Test-Tool nur mit aktuellen Chrome-/Edge-Versionen voll nutzen.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class="fc-divider">
|
||||||
|
|
||||||
|
<h3 class="fc-card-title">Schritt 2: Testmodus</h3>
|
||||||
|
<p class="fc-card-subtitle">
|
||||||
|
Wähle, welche Tests du durchführen möchtest. Alle Tests arbeiten nur mit Testdateien
|
||||||
|
im gewählten Ordner.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="fc-mode-grid" id="fc-mode-grid">
|
||||||
|
<article class="fc-mode-tile" data-mode="quick">
|
||||||
|
<div class="pill">Quick-Check</div>
|
||||||
|
<h4>Basis-Check</h4>
|
||||||
|
<p>Schreibt und liest eine kleine Testdatei, um grundlegende Schreib-/Lesefehler zu erkennen.</p>
|
||||||
|
<small>Empfohlen für einen ersten Eindruck in wenigen Sekunden.</small>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="fc-mode-tile" data-mode="benchmark">
|
||||||
|
<div class="pill">Benchmark</div>
|
||||||
|
<h4>Speed-Test</h4>
|
||||||
|
<p>Misst sequentielle Schreib- und Leseraten auf deinem Stick im Browser.</p>
|
||||||
|
<small>Ideal für „Ist der Stick so langsam, wie er sich anfühlt?“</small>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="fc-mode-tile" data-mode="writeverify">
|
||||||
|
<div class="pill">Write & Verify</div>
|
||||||
|
<h4>Intensiver Dateitest</h4>
|
||||||
|
<p>Schreibt mehrere Testblöcke und prüft sie direkt wieder – nur in Test-Dateien.</p>
|
||||||
|
<small>Dauert länger, liefert aber eine bessere Aussage zur Stabilität.</small>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="fc-mode-tile" data-mode="all">
|
||||||
|
<div class="pill">All-Inclusive</div>
|
||||||
|
<h4>Alle Browser-Tests</h4>
|
||||||
|
<p>Führt Quick-Check, Benchmark und Write/Verify nacheinander aus.</p>
|
||||||
|
<small>Maximale Aussagekraft im Browsermodus (ohne Pro-Helper).</small>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="fc-help">
|
||||||
|
Hinweis: Diese Tests erzeugen nur eigene Testdateien in deinem gewählten Ordner
|
||||||
|
und überschreiben keine bestehenden Dateien. Für vollflächige, destruktive Tests
|
||||||
|
ist später der Pro-Helper zuständig.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr class="fc-divider">
|
||||||
|
|
||||||
|
<div class="fc-status-line">
|
||||||
|
<span id="fc-status-text">Bereit. Wähle zuerst deinen USB-Stick aus.</span>
|
||||||
|
<span id="fc-status-mode" class="fc-tag">Kein Modus selektiert</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fc-progress" aria-hidden="true">
|
||||||
|
<div class="fc-progress-inner" id="fc-progress-inner"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fc-actions fc-actions-bottom">
|
||||||
|
<button type="button" class="fc-btn" id="fc-btn-start-tests" disabled>
|
||||||
|
Tests starten
|
||||||
|
</button>
|
||||||
|
<button type="button" class="fc-btn fc-btn-secondary" id="fc-btn-cancel-tests" disabled>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fc-log" id="fc-log"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Rechte Seite: Ergebnis-Dashboard -->
|
||||||
|
<aside class="fc-card fc-sidebar">
|
||||||
|
<h3 class="fc-card-title">Ergebnis-Dashboard</h3>
|
||||||
|
<p class="fc-card-subtitle">
|
||||||
|
Zusammenfassung der Browser-Tests. Für tiefergehende Analysen kannst du später den Pro-Modus nutzen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="fc-overall-status-wrap">
|
||||||
|
<span id="fc-overall-status-pill" class="fc-pill-status fc-pill-ok">
|
||||||
|
<span class="fc-pill-dot"></span>
|
||||||
|
<span id="fc-overall-status-text">Noch kein Test durchgeführt</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="fc-kv-list">
|
||||||
|
<div class="fc-kv-item">
|
||||||
|
<dt>Modus</dt>
|
||||||
|
<dd id="fc-res-mode">–</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fc-kv-item">
|
||||||
|
<dt>Gesamtlaufzeit</dt>
|
||||||
|
<dd id="fc-res-duration">–</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fc-kv-item">
|
||||||
|
<dt>Write-Speed (Ø)</dt>
|
||||||
|
<dd id="fc-res-write-speed">–</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fc-kv-item">
|
||||||
|
<dt>Read-Speed (Ø)</dt>
|
||||||
|
<dd id="fc-res-read-speed">–</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fc-kv-item">
|
||||||
|
<dt>Geschriebene Testdaten</dt>
|
||||||
|
<dd id="fc-res-written-bytes">–</dd>
|
||||||
|
</div>
|
||||||
|
<div class="fc-kv-item">
|
||||||
|
<dt>Verifizierte Testdaten</dt>
|
||||||
|
<dd id="fc-res-verified-bytes">–</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="fc-alert fc-alert-info" id="fc-save-hint" style="display:none;">
|
||||||
|
Testergebnisse werden – sofern du eingeloggt bist – automatisch als Resultat gespeichert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fc-alert fc-alert-error" id="fc-save-error" style="display:none;">
|
||||||
|
Ergebnis konnte nicht automatisch gespeichert werden. Du kannst es später erneut versuchen.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="fc-help">
|
||||||
|
Technischer Hinweis: Diese Seite arbeitet ausschließlich mit Testdateien im gewählten Ordner
|
||||||
|
und übermittelt keine Inhalte deiner privaten Dateien.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="fc-help">
|
||||||
|
Der vollständige JSON-Report ist nach dem Test in der Browser-Konsole einsehbar
|
||||||
|
(F12 → Console).
|
||||||
|
</p>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ if ($isLoggedIn) {
|
|||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
|
|
||||||
<!-- Hauptnavigation -->
|
<!-- Hauptnavigation -->
|
||||||
<nav class="hidden md:flex items-center gap-6 text-xs font-medium text-brand-muted uppercase tracking-[0.18em]">
|
<nav class="flex flex-wrap items-center gap-4 sm:gap-6 text-xs font-medium text-brand-muted uppercase tracking-[0.18em]">
|
||||||
<?php foreach ($navAnchors as $item): ?>
|
<?php foreach ($navAnchors as $item): ?>
|
||||||
<a href="<?= htmlspecialchars($item['href']) ?>"
|
<a href="<?= htmlspecialchars($item['href']) ?>"
|
||||||
class="hover:text-brand-primary transition-colors"
|
class="hover:text-brand-primary transition-colors"
|
||||||
|
|||||||
399
public/assets/css/fakecheck.css
Normal file
399
public/assets/css/fakecheck.css
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
/* /public/assets/css/fakecheck.css */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--fc-brand-primary: #2563eb;
|
||||||
|
--fc-brand-secondary: #0f172a;
|
||||||
|
--fc-brand-bg: #020617;
|
||||||
|
--fc-brand-border: #1f2937;
|
||||||
|
--fc-success: #16a34a;
|
||||||
|
--fc-warning: #f59e0b;
|
||||||
|
--fc-danger: #dc2626;
|
||||||
|
--fc-text: #e5e7eb;
|
||||||
|
--fc-text-muted: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-container {
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1.25rem 3rem;
|
||||||
|
color: var(--fc-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-header-text h2 {
|
||||||
|
margin: 0 0 0.3rem;
|
||||||
|
font-family: "Montserrat", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-header-text p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-header-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(37, 99, 235, 0.15);
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-header-badge .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 2fr) minmax(0, 1.35fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.fc-layout {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karten */
|
||||||
|
|
||||||
|
.fc-card {
|
||||||
|
background: rgba(15, 23, 42, 0.96);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid var(--fc-brand-border);
|
||||||
|
padding: 1.5rem 1.5rem 1.75rem;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-card-title {
|
||||||
|
margin: 0 0 0.45rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-family: "Montserrat", system-ui, sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-card-subtitle {
|
||||||
|
margin: 0 0 0.8rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
|
||||||
|
.fc-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-actions-bottom {
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.55rem 1.1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
background: linear-gradient(to right, #1d4ed8, #3b82f6);
|
||||||
|
color: #f9fafb;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.15s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-btn[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-btn:hover:not([disabled]) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
border-color: rgba(148, 163, 184, 0.4);
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-btn-secondary:hover:not([disabled]) {
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modus-Kacheln */
|
||||||
|
|
||||||
|
.fc-mode-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.fc-mode-grid {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile {
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
padding: 0.9rem 0.95rem;
|
||||||
|
border: 1px solid rgba(31, 41, 55, 0.9);
|
||||||
|
background: radial-gradient(circle at top left, rgba(37, 99, 235, 0.14), rgba(15, 23, 42, 0.96));
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile h4 {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile .pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.4);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile.selected {
|
||||||
|
border-color: var(--fc-brand-primary);
|
||||||
|
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.58), 0 12px 30px rgba(15, 23, 42, 0.9);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-mode-tile.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Divider, Status, Progress */
|
||||||
|
|
||||||
|
.fc-divider {
|
||||||
|
margin: 1.25rem 0 1rem;
|
||||||
|
height: 1px;
|
||||||
|
border: none;
|
||||||
|
background: radial-gradient(circle, rgba(148, 163, 184, 0.35), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-status-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.15rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(75, 85, 99, 0.8);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-progress {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(15, 23, 42, 0.9);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(31, 41, 55, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-progress-inner {
|
||||||
|
height: 100%;
|
||||||
|
width: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(to right, #22c55e, #16a34a);
|
||||||
|
transition: width 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log */
|
||||||
|
|
||||||
|
.fc-log {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.6rem 0.7rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: rgba(15, 23, 42, 0.98);
|
||||||
|
border: 1px dashed rgba(55, 65, 81, 0.9);
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-log-line {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-log-line strong {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help-Text */
|
||||||
|
|
||||||
|
.fc-help {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
|
||||||
|
.fc-alert {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-alert-info {
|
||||||
|
background: rgba(37, 99, 235, 0.08);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.45);
|
||||||
|
color: #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-alert-warn {
|
||||||
|
background: rgba(245, 158, 11, 0.08);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.45);
|
||||||
|
color: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-alert-error {
|
||||||
|
background: rgba(220, 38, 38, 0.1);
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.6);
|
||||||
|
color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard / Key-Value-Liste */
|
||||||
|
|
||||||
|
.fc-kv-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem 1.2rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.fc-kv-list {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-kv-item dt {
|
||||||
|
margin: 0 0 0.15rem;
|
||||||
|
color: var(--fc-text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-kv-item dd {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overall-Status */
|
||||||
|
|
||||||
|
.fc-overall-status-wrap {
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-ok {
|
||||||
|
background: rgba(22, 163, 74, 0.12);
|
||||||
|
color: #bbf7d0;
|
||||||
|
border-color: rgba(34, 197, 94, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-ok .fc-pill-dot {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-warn {
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
color: #fef3c7;
|
||||||
|
border-color: rgba(245, 158, 11, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-warn .fc-pill-dot {
|
||||||
|
background: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-bad {
|
||||||
|
background: rgba(220, 38, 38, 0.12);
|
||||||
|
color: #fee2e2;
|
||||||
|
border-color: rgba(220, 38, 38, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-pill-bad .fc-pill-dot {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
@@ -1,37 +1,692 @@
|
|||||||
// /public/assets/js/fakecheck.js
|
// /public/assets/js/fakecheck.js
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// Werte kommen aus globalen Variablen, die wir in PHP setzen:
|
|
||||||
const baseUrl = window.fakecheckBaseUrl || "";
|
const baseUrl = window.fakecheckBaseUrl || "";
|
||||||
const locale = window.fakecheckLocale || "en";
|
const locale = window.fakecheckLocale || "en";
|
||||||
|
|
||||||
const startButton = document.getElementById("startButton");
|
const root = document.getElementById("fc-root");
|
||||||
const resultContainer = document.getElementById("resultContainer");
|
if (!root) {
|
||||||
const resultOutput = document.getElementById("resultOutput");
|
// Auf anderen Seiten eingebunden? Dann einfach nichts tun.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!startButton || !resultContainer || !resultOutput) return;
|
// --- DOM-Helper ---------------------------------------------------------
|
||||||
|
const $ = (sel) => document.querySelector(sel);
|
||||||
|
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
||||||
|
|
||||||
startButton.addEventListener("click", () => {
|
const logEl = $("#fc-log");
|
||||||
const now = new Date().toISOString();
|
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 demoReport = {
|
const resMode = $("#fc-res-mode");
|
||||||
meta: {
|
const resDuration = $("#fc-res-duration");
|
||||||
tool: baseUrl,
|
const resWriteSpeed = $("#fc-res-write-speed");
|
||||||
mode: "browser-demo",
|
const resReadSpeed = $("#fc-res-read-speed");
|
||||||
timestamp: now,
|
const resWritten = $("#fc-res-written-bytes");
|
||||||
locale: locale
|
const resVerified = $("#fc-res-verified-bytes");
|
||||||
},
|
|
||||||
tests: [
|
const btnPickDir = $("#fc-btn-pick-directory");
|
||||||
{ id: "quick_test", label: "Quick-Test (Demo)", status: "pending" },
|
const btnClearSel = $("#fc-btn-clear-selection");
|
||||||
{ id: "light_benchmark", label: "Light-Benchmark (Demo)", status: "pending" },
|
const btnStart = $("#fc-btn-start-tests");
|
||||||
{ id: "write_verify", label: "Write/Verify (Demo)", status: "pending" }
|
const btnCancel = $("#fc-btn-cancel-tests");
|
||||||
],
|
const modeTiles = $$("#fc-mode-grid .fc-mode-tile");
|
||||||
note: "Dies ist nur eine Platzhalter-Ausgabe. Die echte Web-Testlogik (File System Access, Fortschritt, realer JSON-Report) implementieren wir im nächsten Schritt."
|
|
||||||
|
// Falls du einen Login-Indikator hast, kannst du ihn global setzen,
|
||||||
|
// z. B. window.fakecheckIsLoggedIn = true/false
|
||||||
|
if (window.fakecheckIsLoggedIn) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
resultOutput.textContent = JSON.stringify(demoReport, null, 2);
|
logLine("Quick-Check: Erfolgreich abgeschlossen.", "info");
|
||||||
|
}
|
||||||
|
|
||||||
resultContainer.classList.remove("hidden");
|
async runBenchmark(report, progressCb, abortSignal) {
|
||||||
resultContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
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 ------------------------------------------------------
|
||||||
|
|
||||||
|
if (btnPickDir) {
|
||||||
|
btnPickDir.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const handle = await tester.pickDirectory();
|
||||||
|
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.name === "AbortError") {
|
||||||
|
logLine("Verzeichnisauswahl abgebrochen.", "warn");
|
||||||
|
} else {
|
||||||
|
logLine("Fehler bei Verzeichnisauswahl: " + err.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.name === "AbortError") {
|
||||||
|
setStatus("Test wurde abgebrochen.");
|
||||||
|
setOverallStatus("warn", "Test abgebrochen.");
|
||||||
|
logLine("Test wurde vom Benutzer abgebrochen.", "warn");
|
||||||
|
} else {
|
||||||
|
setStatus("Fehler: " + err.message);
|
||||||
|
setOverallStatus("bad", "Fehler im Browser-Test.");
|
||||||
|
logLine("Fehler im Test: " + err.message, "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) {
|
||||||
|
// Absoluter Pfad – dein Script liegt oberhalb des public-Roots:
|
||||||
|
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) {
|
||||||
|
// 401 => nicht eingeloggt; 500 => Fehler etc.
|
||||||
|
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.message, "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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
// public/assets/js/header.js
|
// public/assets/js/header.js
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const supportedLangs = ['de', 'en', 'it', 'fr'];
|
||||||
|
|
||||||
|
function resolveCurrentLang() {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const urlLang = (url.searchParams.get('lang') || '').toLowerCase();
|
||||||
|
const docLang = (document.documentElement.getAttribute('lang') || '').toLowerCase();
|
||||||
|
const globalLang = (window.currentLang || '').toLowerCase();
|
||||||
|
|
||||||
|
if (supportedLangs.includes(urlLang)) return urlLang;
|
||||||
|
if (supportedLangs.includes(globalLang)) return globalLang;
|
||||||
|
if (supportedLangs.includes(docLang)) return docLang;
|
||||||
|
return 'de';
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// LOGIN-BUTTON → /login mit redirect + lang
|
// LOGIN-BUTTON → /login mit redirect + lang
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
@@ -13,12 +27,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const currentQuery = window.location.search || '';
|
const currentQuery = window.location.search || '';
|
||||||
const redirect = currentPath + currentQuery;
|
const redirect = currentPath + currentQuery;
|
||||||
|
|
||||||
// Sprache aus dem Label oben ziehen, falls vorhanden
|
// Sprache aus URL / globalem State ableiten
|
||||||
const langLabelEl = document.getElementById('langCurrentLabel');
|
const lang = resolveCurrentLang();
|
||||||
let lang = 'de';
|
|
||||||
if (langLabelEl && langLabelEl.textContent) {
|
|
||||||
lang = langLabelEl.textContent.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn wir bereits auf /login sind → nur zum #auth scrollen
|
// Wenn wir bereits auf /login sind → nur zum #auth scrollen
|
||||||
if (currentPath === '/login' || currentPath === '/login/') {
|
if (currentPath === '/login' || currentPath === '/login/') {
|
||||||
|
|||||||
Reference in New Issue
Block a user