Dev #1

Merged
NiekRabelink merged 2 commits from dev into main 2026-09-07 11:18:54 +00:00
5 changed files with 612 additions and 78 deletions
+73 -8
View File
@@ -26,6 +26,43 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
$host = $_SERVER['HTTP_HOST'] ?? 'www.webenpcstudio.nl'; $host = $_SERVER['HTTP_HOST'] ?? 'www.webenpcstudio.nl';
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'Onbekend'; $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'Onbekend';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS' : 'HTTP'; $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS' : 'HTTP';
// Git repository inspectie (met veilige fallback)
$activeBranch = 'dev';
$commits = [];
if (function_exists('shell_exec')) {
$branchOutput = @shell_exec('git rev-parse --abbrev-ref HEAD 2>&1');
if ($branchOutput && strpos($branchOutput, 'fatal:') === false) {
$activeBranch = trim($branchOutput);
}
$logOutput = @shell_exec('git log -n 5 --pretty=format:"%h|%an|%ad|%s" --date=format:"%d-%m-%Y %H:%M" 2>&1');
if ($logOutput && strpos($logOutput, 'fatal:') === false) {
$lines = explode("\n", trim($logOutput));
foreach ($lines as $line) {
$parts = explode('|', $line, 4);
if (count($parts) === 4) {
$commits[] = [
'hash' => htmlspecialchars($parts[0]),
'author' => htmlspecialchars($parts[1]),
'date' => htmlspecialchars($parts[2]),
'msg' => htmlspecialchars($parts[3])
];
}
}
}
}
// Fallback commits bij afgeschermde shell_exec
if (empty($commits)) {
$commits = [
['hash' => '4400bc2', 'author' => 'Niek Rabelink', 'date' => '07-09-2026 13:04', 'msg' => 'Add a new line at the end of style.css for consistency'],
['hash' => 'cc029ca', 'author' => 'Niek Rabelink', 'date' => '07-09-2026 13:04', 'msg' => 'Add sleek dark theme styles and implement Dev Tools functionality'],
['hash' => 'caa2c2f', 'author' => 'Niek Rabelink', 'date' => '07-09-2026 11:49', 'msg' => 'Test automatic deployment via webhook'],
['hash' => 'cee3e27', 'author' => 'Niek Rabelink', 'date' => '07-09-2026 10:24', 'msg' => 'Eerste test']
];
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="nl"> <html lang="nl">
@@ -34,6 +71,7 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web &amp; PC Studio - Systeem Dashboard</title> <title>Web &amp; PC Studio - Systeem Dashboard</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="theme.js"></script>
</head> </head>
<body> <body>
<div class="main-wrapper"> <div class="main-wrapper">
@@ -52,6 +90,7 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
<a href="tools.php" class="nav-item">🛠️ Dev Tools</a> <a href="tools.php" class="nav-item">🛠️ Dev Tools</a>
</div> </div>
<div class="nav-status"> <div class="nav-status">
<button class="btn btn-secondary btn-sm theme-btn" onclick="toggleTheme()">🌙 Thema</button>
<span class="nav-badge"> <span class="nav-badge">
<span class="pulse-dot"></span> <span class="pulse-dot"></span>
Git Live Git Live
@@ -111,7 +150,7 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
<div class="card-title"> <div class="card-title">
<span>🌿</span> Git CI/CD Sync <span>🌿</span> Git CI/CD Sync
</div> </div>
<span class="badge-pill">Verbonden</span> <span class="badge-pill">Branch: <?= htmlspecialchars($activeBranch); ?></span>
</div> </div>
<p class="card-desc"> <p class="card-desc">
Status van de gekoppelde Git repository en automatische webhook updates. Status van de gekoppelde Git repository en automatische webhook updates.
@@ -128,7 +167,7 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
</tr> </tr>
<tr> <tr>
<td class="label">Actieve Branch:</td> <td class="label">Actieve Branch:</td>
<td class="val">main</td> <td class="val" style="color: #38bdf8; font-weight: 700;"><?= htmlspecialchars($activeBranch); ?></td>
</tr> </tr>
<tr> <tr>
<td class="label">Deploy Type:</td> <td class="label">Deploy Type:</td>
@@ -142,11 +181,37 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
</div> </div>
</div> </div>
<!-- Kaart 3: Live Tijd en Server Ping Diagnose --> <!-- Kaart 3: Laatste Git Commits & Deployment Log -->
<div class="card" style="margin-bottom: 24px;"> <div class="card" style="margin-bottom: 24px;">
<div class="card-header"> <div class="card-header">
<div class="card-title"> <div class="card-title">
<span>⏱️</span> Realtime Verbinding &amp; Diagnose <span>📜</span> Laatste Git Commits (Deployment Log)
</div>
<span class="badge-pill">Live Repository History</span>
</div>
<p class="card-desc">
Overzicht van de meest recente commits die naar deze webserver zijn gepusht via Git.
</p>
<div class="commit-timeline">
<?php foreach ($commits as $commit): ?>
<div class="commit-item">
<div class="commit-top">
<span class="commit-hash"><?= $commit['hash']; ?></span>
<span class="commit-date"><?= $commit['date']; ?></span>
</div>
<div class="commit-msg"><?= $commit['msg']; ?></div>
<div class="commit-author">Auteur: <?= $commit['author']; ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Kaart 4: Live Tijd en Server Ping Diagnose -->
<div class="card" style="margin-bottom: 24px;">
<div class="card-header">
<div class="card-title">
<span>⏱️</span> Realtime Verbinding &amp; Latency
</div> </div>
<button class="btn btn-primary btn-sm" id="btn-ping" onclick="testServerPing()"> <button class="btn btn-primary btn-sm" id="btn-ping" onclick="testServerPing()">
⚡ Test Server Ping (AJAX) ⚡ Test Server Ping (AJAX)
@@ -156,7 +221,7 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
<div class="metric-grid"> <div class="metric-grid">
<div class="metric-box"> <div class="metric-box">
<div class="metric-label">Server Tijd (PHP)</div> <div class="metric-label">Server Tijd (PHP)</div>
<div class="metric-val" id="server-time" style="font-size: 0.95rem; color: #f1f5f9;"><?= htmlspecialchars($serverTime); ?></div> <div class="metric-val" id="server-time" style="font-size: 0.95rem; color: var(--text-main);"><?= htmlspecialchars($serverTime); ?></div>
</div> </div>
<div class="metric-box"> <div class="metric-box">
<div class="metric-label">Browser Tijd (JS)</div> <div class="metric-label">Browser Tijd (JS)</div>
@@ -180,15 +245,15 @@ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS'
<a href="lab.php" class="card" style="text-decoration: none; display: flex; align-items: center; gap: 16px;"> <a href="lab.php" class="card" style="text-decoration: none; display: flex; align-items: center; gap: 16px;">
<div style="font-size: 2rem; background: rgba(14, 165, 233, 0.12); padding: 12px; border-radius: 12px;">🚀</div> <div style="font-size: 2rem; background: rgba(14, 165, 233, 0.12); padding: 12px; border-radius: 12px;">🚀</div>
<div> <div>
<h3 style="font-size: 1.05rem; color: #fff; margin-bottom: 4px;">Webserver Benchmark Suite &rarr;</h3> <h3 style="font-size: 1.05rem; color: var(--text-main); margin-bottom: 4px;">Webserver Benchmark Suite &rarr;</h3>
<p style="font-size: 0.82rem; color: var(--text-muted);">Meet CPU rekenkracht, NVMe Disk I/O en gelijktijdige requests.</p> <p style="font-size: 0.82rem; color: var(--text-muted);">Meet CPU rekenkracht, NVMe Disk I/O en gelijktijdige requests.</p>
</div> </div>
</a> </a>
<a href="tools.php" class="card" style="text-decoration: none; display: flex; align-items: center; gap: 16px;"> <a href="tools.php" class="card" style="text-decoration: none; display: flex; align-items: center; gap: 16px;">
<div style="font-size: 2rem; background: rgba(139, 92, 246, 0.12); padding: 12px; border-radius: 12px;">🛠️</div> <div style="font-size: 2rem; background: rgba(139, 92, 246, 0.12); padding: 12px; border-radius: 12px;">🛠️</div>
<div> <div>
<h3 style="font-size: 1.05rem; color: #fff; margin-bottom: 4px;">Developer &amp; Systeem Tools &rarr;</h3> <h3 style="font-size: 1.05rem; color: var(--text-main); margin-bottom: 4px;">Developer &amp; Systeem Tools &rarr;</h3>
<p style="font-size: 0.82rem; color: var(--text-muted);">Inspecteer HTTP headers, cURL verbindingen en cryptografie.</p> <p style="font-size: 0.82rem; color: var(--text-muted);">DNS lookup, PHP extensie matrix en HTTP header inspectie.</p>
</div> </div>
</a> </a>
</div> </div>
+168 -10
View File
@@ -68,11 +68,10 @@ function benchmarkDiskIO() {
return ['status' => 'error', 'message' => 'Kan tijdelijk bestand niet aanmaken']; return ['status' => 'error', 'message' => 'Kan tijdelijk bestand niet aanmaken'];
} }
$chunkSize = 64 * 1024; // 64 KB $chunkSize = 64 * 1024;
$chunks = 48; // ~3 MB totaal $chunks = 48; // ~3 MB
$dummyData = str_repeat('WPCStudio2026_TEST_STRING_ABCD!', 2048); $dummyData = str_repeat('WPCStudio2026_TEST_STRING_ABCD!', 2048);
// Write test
$writeStart = microtime(true); $writeStart = microtime(true);
$fp = fopen($tempFile, 'wb'); $fp = fopen($tempFile, 'wb');
if (!$fp) { if (!$fp) {
@@ -86,7 +85,6 @@ function benchmarkDiskIO() {
fclose($fp); fclose($fp);
$writeTime = max(0.0005, microtime(true) - $writeStart); $writeTime = max(0.0005, microtime(true) - $writeStart);
// Read test
$readStart = microtime(true); $readStart = microtime(true);
$fp = fopen($tempFile, 'rb'); $fp = fopen($tempFile, 'rb');
if ($fp) { if ($fp) {
@@ -284,6 +282,7 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web &amp; PC Studio - Webserver Benchmark</title> <title>Web &amp; PC Studio - Webserver Benchmark</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="theme.js"></script>
<style> <style>
.bench-master { .bench-master {
background: linear-gradient(135deg, rgba(15, 23, 42, 0.9), rgba(11, 15, 25, 0.95)); background: linear-gradient(135deg, rgba(15, 23, 42, 0.9), rgba(11, 15, 25, 0.95));
@@ -299,6 +298,11 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
flex-wrap: wrap; flex-wrap: wrap;
} }
[data-theme="light"] .bench-master {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(241, 245, 249, 0.95));
box-shadow: 0 15px 35px -10px rgba(0, 0, 0, 0.08);
}
.score-box { .score-box {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -309,6 +313,10 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
} }
[data-theme="light"] .score-box {
background: rgba(241, 245, 249, 0.85);
}
.score-circle { .score-circle {
width: 76px; width: 76px;
height: 76px; height: 76px;
@@ -350,6 +358,12 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
.conc-pill.success { border-color: #10b981; color: #34d399; } .conc-pill.success { border-color: #10b981; color: #34d399; }
.conc-pill.slow { border-color: #f59e0b; color: #fbbf24; } .conc-pill.slow { border-color: #f59e0b; color: #fbbf24; }
.conc-pill.error { border-color: #ef4444; color: #f87171; } .conc-pill.error { border-color: #ef4444; color: #f87171; }
@media print {
.navbar, .btn, .nav-status, .sound-toggle, footer { display: none !important; }
body { background: #fff !important; color: #000 !important; }
.card, .bench-master { box-shadow: none !important; border: 1px solid #ccc !important; }
}
</style> </style>
</head> </head>
<body> <body>
@@ -369,6 +383,7 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
<a href="tools.php" class="nav-item">🛠️ Dev Tools</a> <a href="tools.php" class="nav-item">🛠️ Dev Tools</a>
</div> </div>
<div class="nav-status"> <div class="nav-status">
<button class="btn btn-secondary btn-sm theme-btn" onclick="toggleTheme()">🌙 Thema</button>
<button class="btn btn-secondary btn-sm" id="sound-btn" onclick="toggleAudio()" title="Schakel geluid in/uit"> <button class="btn btn-secondary btn-sm" id="sound-btn" onclick="toggleAudio()" title="Schakel geluid in/uit">
<span id="sound-icon">🔈</span> Geluid: <span id="sound-status">Uit</span> <span id="sound-icon">🔈</span> Geluid: <span id="sound-status">Uit</span>
</button> </button>
@@ -386,16 +401,22 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
<!-- Master Benchmark Banner --> <!-- Master Benchmark Banner -->
<div class="bench-master"> <div class="bench-master">
<div style="flex: 1; min-width: 280px;"> <div style="flex: 1; min-width: 280px;">
<h2 style="font-size: 1.3rem; color: #fff; margin-bottom: 6px;">⚡ Volledige Server Benchmark</h2> <h2 style="font-size: 1.3rem; color: var(--text-main); margin-bottom: 6px;">⚡ Volledige Server Benchmark</h2>
<p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 18px;"> <p style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 18px;">
Voert in één geautomatiseerde cyclus alle 5 hardware- &amp; softwaretests uit op www.webenpcstudio.nl. Voert in één geautomatiseerde cyclus alle 5 hardware- &amp; softwaretests uit op www.webenpcstudio.nl.
</p> </p>
<div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;"> <div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
<button class="btn btn-primary" id="btn-run-all" onclick="runFullBenchmark()"> <button class="btn btn-primary" id="btn-run-all" onclick="runFullBenchmark()">
▶ Start Volledige Benchmark ▶ Start Volledige Benchmark
</button> </button>
<button class="btn btn-secondary" onclick="copyReport()"> <button class="btn btn-secondary btn-sm" onclick="copyReport()">
📋 Kopieer Rapport 📋 Kopieer
</button>
<button class="btn btn-secondary btn-sm" onclick="downloadJSONReport()">
📥 Download JSON
</button>
<button class="btn btn-secondary btn-sm" onclick="printReport()">
🖨️ PDF / Print
</button> </button>
</div> </div>
<div class="progress-bar-wrap" id="p-bar-wrap"> <div class="progress-bar-wrap" id="p-bar-wrap">
@@ -406,7 +427,7 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
<!-- Score Display --> <!-- Score Display -->
<div class="score-box"> <div class="score-box">
<div class="score-circle" id="score-circle"> <div class="score-circle" id="score-circle">
<span style="font-size: 1.45rem; color: #fff; line-height: 1;" id="score-num">--</span> <span style="font-size: 1.45rem; color: var(--text-main); line-height: 1;" id="score-num">--</span>
<span style="font-size: 0.65rem; color: var(--text-muted);">/ 100</span> <span style="font-size: 0.65rem; color: var(--text-muted);">/ 100</span>
</div> </div>
<div> <div>
@@ -416,6 +437,25 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
</div> </div>
</div> </div>
<!-- Historische Vergelijking & Trend Grafiek -->
<div class="card" style="margin-bottom: 24px;">
<div class="card-header">
<div>
<div class="card-title"><span>📈</span> Historische Benchmark Trend (Opeenvolgende Runs)</div>
<p class="card-desc" style="margin-bottom: 0;">Vergelijk eerdere benchmark scores over tijd om server-stabiliteit te monitoren.</p>
</div>
<button class="btn btn-secondary btn-sm" onclick="clearHistory()">🗑️ Wis Historie</button>
</div>
<div class="chart-wrap">
<canvas id="history-canvas" style="width: 100%; height: 100%; display: block;"></canvas>
</div>
<div id="history-legend" style="font-size: 0.78rem; color: var(--text-muted); display: flex; justify-content: space-between;">
<span>Eerste meting</span>
<span id="history-count">0 eerdere benchmarks opgeslagen</span>
<span>Laatste meting</span>
</div>
</div>
<!-- 6 Test Cards --> <!-- 6 Test Cards -->
<div class="grid-3"> <div class="grid-3">
<!-- CPU --> <!-- CPU -->
@@ -554,7 +594,7 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
</div> </div>
<div style="display: flex; gap: 8px; align-items: center;"> <div style="display: flex; gap: 8px; align-items: center;">
<button class="btn btn-secondary btn-sm" onclick="setConc(5, this)">5 Calls</button> <button class="btn btn-secondary btn-sm" onclick="setConc(5, this)">5 Calls</button>
<button class="btn btn-secondary btn-sm" style="border-color: var(--primary); color: #fff;" onclick="setConc(12, this)">12 Calls</button> <button class="btn btn-secondary btn-sm" style="border-color: var(--primary);" onclick="setConc(12, this)">12 Calls</button>
<button class="btn btn-secondary btn-sm" onclick="setConc(24, this)">24 Calls</button> <button class="btn btn-secondary btn-sm" onclick="setConc(24, this)">24 Calls</button>
<button class="btn btn-primary btn-sm" id="btn-conc" onclick="runConcurrencyTest()">⚡ Start Burst</button> <button class="btn btn-primary btn-sm" id="btn-conc" onclick="runConcurrencyTest()">⚡ Start Burst</button>
</div> </div>
@@ -626,6 +666,94 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
let lastReport = null; let lastReport = null;
// Historie beheer
function getHistory() {
try {
return JSON.parse(localStorage.getItem('wpc_bench_history') || '[]');
} catch(e) { return []; }
}
function saveHistoryItem(item) {
let history = getHistory();
history.push(item);
if (history.length > 20) history.shift();
localStorage.setItem('wpc_bench_history', JSON.stringify(history));
drawHistoryChart();
}
function clearHistory() {
localStorage.removeItem('wpc_bench_history');
drawHistoryChart();
}
function drawHistoryChart() {
const history = getHistory();
const canvas = document.getElementById('history-canvas');
const ctx = canvas.getContext('2d');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
ctx.clearRect(0, 0, canvas.width, canvas.height);
document.getElementById('history-count').textContent = `${history.length} benchmark runs opgeslagen`;
if (history.length === 0) {
ctx.fillStyle = '#94a3b8';
ctx.font = '12px var(--font-sans)';
ctx.textAlign = 'center';
ctx.fillText('Nog geen eerdere benchmarks opgeslagen. Voer een test uit!', canvas.width / 2, canvas.height / 2);
return;
}
const stepX = canvas.width / Math.max(1, history.length - 1);
// Gradient fill
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
grad.addColorStop(0, 'rgba(14, 165, 233, 0.4)');
grad.addColorStop(1, 'rgba(14, 165, 233, 0.0)');
ctx.beginPath();
history.forEach((h, i) => {
const x = history.length === 1 ? canvas.width / 2 : i * stepX;
const y = canvas.height - ((h.score / 100) * (canvas.height - 30)) - 15;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
if (history.length > 1) {
ctx.lineTo((history.length - 1) * stepX, canvas.height);
ctx.lineTo(0, canvas.height);
ctx.fillStyle = grad;
ctx.fill();
}
// Stroke
ctx.beginPath();
history.forEach((h, i) => {
const x = history.length === 1 ? canvas.width / 2 : i * stepX;
const y = canvas.height - ((h.score / 100) * (canvas.height - 30)) - 15;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.strokeStyle = '#0ea5e9';
ctx.lineWidth = 2.5;
ctx.stroke();
// Dots & labels
history.forEach((h, i) => {
const x = history.length === 1 ? canvas.width / 2 : i * stepX;
const y = canvas.height - ((h.score / 100) * (canvas.height - 30)) - 15;
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.fillStyle = '#38bdf8';
ctx.font = '10px var(--font-mono)';
ctx.textAlign = 'center';
ctx.fillText(h.score, x, y - 8);
});
}
async function runFullBenchmark() { async function runFullBenchmark() {
const btn = document.getElementById('btn-run-all'); const btn = document.getElementById('btn-run-all');
const pWrap = document.getElementById('p-bar-wrap'); const pWrap = document.getElementById('p-bar-wrap');
@@ -667,6 +795,13 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
document.getElementById('db-time').textContent = data.db.duration_ms + ' ms'; document.getElementById('db-time').textContent = data.db.duration_ms + ' ms';
} }
// Opslaan in historie
saveHistoryItem({
score: data.score,
grade: data.grade,
time: data.timestamp
});
playTone(880, 'triangle', 0.2); playTone(880, 'triangle', 0.2);
} catch (err) { } catch (err) {
alert('Benchmark fout: ' + err.message); alert('Benchmark fout: ' + err.message);
@@ -785,6 +920,29 @@ $serverSoftware = $_SERVER['SERVER_SOFTWARE'] ?? 'Apache / Nginx';
playTone(660, 'sine', 0.1); playTone(660, 'sine', 0.1);
}); });
} }
function downloadJSONReport() {
if (!lastReport) {
alert('Voer eerst een benchmark uit om de JSON data te downloaden.');
return;
}
const blob = new Blob([JSON.stringify(lastReport, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `benchmark_webenpcstudio_${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function printReport() {
window.print();
}
window.addEventListener('resize', drawHistoryChart);
document.addEventListener('DOMContentLoaded', drawHistoryChart);
</script> </script>
</body> </body>
</html> </html>
+145
View File
@@ -38,6 +38,54 @@
--radius-xl: 22px; --radius-xl: 22px;
} }
[data-theme="light"] {
--bg-base: #f8fafc;
--bg-surface: rgba(255, 255, 255, 0.9);
--bg-surface-elevated: rgba(241, 245, 249, 0.95);
--bg-subtle: rgba(0, 0, 0, 0.03);
--border-subtle: rgba(0, 0, 0, 0.08);
--border-hover: rgba(0, 0, 0, 0.16);
--border-accent: rgba(14, 165, 233, 0.5);
--text-main: #0f172a;
--text-muted: #475569;
--text-subtle: #64748b;
}
[data-theme="light"] body {
background-image:
radial-gradient(at 0% 0%, rgba(14, 165, 233, 0.08) 0px, transparent 45%),
radial-gradient(at 100% 100%, rgba(139, 92, 246, 0.06) 0px, transparent 45%),
radial-gradient(at 50% 50%, rgba(16, 185, 129, 0.04) 0px, transparent 60%);
}
[data-theme="light"] .navbar {
background: rgba(255, 255, 255, 0.85);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
}
[data-theme="light"] .nav-title {
color: #0f172a;
}
[data-theme="light"] .nav-menu {
background: rgba(0, 0, 0, 0.04);
}
[data-theme="light"] .card {
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.06);
}
[data-theme="light"] .card-title {
color: #0f172a;
}
[data-theme="light"] .metric-box {
background: rgba(241, 245, 249, 0.85);
border-color: rgba(0, 0, 0, 0.06);
}
* { * {
box-sizing: border-box; box-sizing: border-box;
margin: 0; margin: 0;
@@ -451,6 +499,102 @@ body {
overflow-y: auto; overflow-y: auto;
} }
/* ==========================================================================
Commit Timeline & DNS Badges
========================================================================== */
.commit-timeline {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.commit-item {
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 12px 14px;
transition: all 0.2s ease;
}
[data-theme="light"] .commit-item {
background: rgba(241, 245, 249, 0.7);
}
.commit-item:hover {
border-color: var(--border-hover);
background: var(--bg-subtle);
}
.commit-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 0.8rem;
}
.commit-hash {
font-family: var(--font-mono);
color: var(--primary);
font-weight: 700;
background: rgba(14, 165, 233, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.commit-date {
color: var(--text-subtle);
font-size: 0.76rem;
}
.commit-msg {
font-size: 0.86rem;
color: var(--text-main);
font-weight: 500;
line-height: 1.4;
}
.commit-author {
font-size: 0.76rem;
color: var(--text-muted);
margin-top: 4px;
}
/* DNS Record Pill */
.dns-pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 4px 10px;
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--text-main);
margin: 3px;
}
[data-theme="light"] .dns-pill {
background: rgba(241, 245, 249, 0.85);
}
/* Historical Chart Canvas */
.chart-wrap {
width: 100%;
height: 140px;
background: rgba(0, 0, 0, 0.35);
border-radius: var(--radius-md);
border: 1px solid var(--border-subtle);
margin: 14px 0;
position: relative;
}
[data-theme="light"] .chart-wrap {
background: rgba(241, 245, 249, 0.85);
}
/* ========================================================================== /* ==========================================================================
Footer Footer
========================================================================== */ ========================================================================== */
@@ -472,3 +616,4 @@ body {
text-decoration: underline; text-decoration: underline;
} }
+36
View File
@@ -0,0 +1,36 @@
/**
* Web & PC Studio - Theme Manager (Dark / Light Mode)
* Persisteert gebruikersvoorkeur in localStorage over alle pagina's.
*/
(function() {
const savedTheme = localStorage.getItem('wpc_theme') || 'dark';
document.documentElement.setAttribute('data-theme', savedTheme);
})();
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('wpc_theme', next);
updateThemeButton(next);
}
function updateThemeButton(theme) {
const btns = document.querySelectorAll('.theme-btn');
btns.forEach(btn => {
if (theme === 'light') {
btn.innerHTML = '☀️ Thema: Licht';
btn.title = 'Schakel over naar Donker thema';
} else {
btn.innerHTML = '🌙 Thema: Donker';
btn.title = 'Schakel over naar Licht thema';
}
});
}
document.addEventListener('DOMContentLoaded', () => {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
updateThemeButton(current);
});
+191 -61
View File
@@ -1,14 +1,59 @@
<?php <?php
/** /**
* Web & PC Studio - Dev Tools & System Inspector * Web & PC Studio - Dev Tools & System Inspector
* Technische inspectie en utilities voor serverbeheer en debugging. * Technische inspectie en utilities voor serverbeheer, DNS en debugging.
*/ */
// PHP Backend AJAX handler // PHP Backend AJAX handler
if (isset($_GET['action'])) { if (isset($_GET['action'])) {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
// Outbound Connectiviteitstest // 1. DNS Record Lookup
if ($_GET['action'] === 'dns_lookup') {
$domain = trim($_GET['domain'] ?? 'webenpcstudio.nl');
$domain = preg_replace('#^https?://#', '', $domain);
$domain = rtrim(explode('/', $domain)[0], '.');
if (empty($domain)) {
echo json_encode(['status' => 'error', 'message' => 'Geen geldig domein opgegeven.']);
exit;
}
$records = [];
if (function_exists('dns_get_record')) {
$raw = @dns_get_record($domain, DNS_A + DNS_AAAA + DNS_MX + DNS_NS + DNS_TXT);
if ($raw) {
foreach ($raw as $r) {
$type = $r['type'] ?? 'UNKNOWN';
$val = $r['ip'] ?? ($r['ipv6'] ?? ($r['target'] ?? ($r['txt'] ?? '')));
$records[] = [
'host' => $r['host'] ?? $domain,
'type' => $type,
'value' => $val,
'ttl' => $r['ttl'] ?? 3600
];
}
}
}
// Fallback als dns_get_record leeg is
if (empty($records)) {
$ip = @gethostbyname($domain);
if ($ip && $ip !== $domain) {
$records[] = ['host' => $domain, 'type' => 'A', 'value' => $ip, 'ttl' => 3600];
}
}
echo json_encode([
'status' => 'success',
'domain' => $domain,
'count' => count($records),
'records' => $records
]);
exit;
}
// 2. Outbound Connectiviteitstest (cURL)
if ($_GET['action'] === 'outbound_ping') { if ($_GET['action'] === 'outbound_ping') {
$target = $_GET['target'] ?? 'https://git.webenpcstudio.nl'; $target = $_GET['target'] ?? 'https://git.webenpcstudio.nl';
$allowed = ['https://git.webenpcstudio.nl', 'https://www.google.com', 'https://api.github.com']; $allowed = ['https://git.webenpcstudio.nl', 'https://www.google.com', 'https://api.github.com'];
@@ -43,7 +88,7 @@ if (isset($_GET['action'])) {
exit; exit;
} }
// Hash & Encoding API // 3. Hash & Encoding API
if ($_GET['action'] === 'transform' && isset($_POST['input'])) { if ($_GET['action'] === 'transform' && isset($_POST['input'])) {
$text = (string)$_POST['input']; $text = (string)$_POST['input'];
echo json_encode([ echo json_encode([
@@ -69,7 +114,18 @@ $phpVersion = PHP_VERSION;
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'Onbekend'; $clientIp = $_SERVER['REMOTE_ADDR'] ?? 'Onbekend';
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Onbekend'; $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Onbekend';
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS' : 'HTTP'; $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'HTTPS' : 'HTTP';
$sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd');
// Extensies check
$extensions = [
'Database' => ['pdo', 'pdo_mysql', 'pdo_sqlite', 'mysqli'],
'Beveiliging & Crypto' => ['openssl', 'sodium', 'hash'],
'Netwerk & Web' => ['curl', 'sockets', 'fileinfo'],
'Compressie & Media' => ['zip', 'zlib', 'gd', 'mbstring']
];
// Server load
$load = function_exists('sys_getloadavg') ? @sys_getloadavg() : false;
$loadDisplay = $load ? implode(' / ', array_map(function($v) { return round($v, 2); }, $load)) : 'Niet beschikbaar op OS';
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="nl"> <html lang="nl">
@@ -78,6 +134,7 @@ $sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd'
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web &amp; PC Studio - Dev Tools &amp; Inspector</title> <title>Web &amp; PC Studio - Dev Tools &amp; Inspector</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="theme.js"></script>
</head> </head>
<body> <body>
<div class="main-wrapper"> <div class="main-wrapper">
@@ -96,6 +153,7 @@ $sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd'
<a href="tools.php" class="nav-item active">🛠️ Dev Tools</a> <a href="tools.php" class="nav-item active">🛠️ Dev Tools</a>
</div> </div>
<div class="nav-status"> <div class="nav-status">
<button class="btn btn-secondary btn-sm theme-btn" onclick="toggleTheme()">🌙 Thema</button>
<span class="nav-badge"> <span class="nav-badge">
<span class="pulse-dot"></span> <span class="pulse-dot"></span>
Git Live Git Live
@@ -107,79 +165,57 @@ $sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd'
<header class="hero"> <header class="hero">
<h1>Systeem &amp; Developer Tools</h1> <h1>Systeem &amp; Developer Tools</h1>
<p> <p>
Inspecteer binnenkomende HTTP headers, valideer server-connectiviteit en gebruik realtime cryptografische encoding tools. Realtime DNS lookup utility, HTTP header inspectie, PHP extensie matrix en outbound netwerk diagnostics.
</p> </p>
</header> </header>
<!-- Grid: 2 Kolommen --> <!-- Grid: 2 Kolommen -->
<div class="grid-2"> <div class="grid-2">
<!-- Paneel 1: HTTP Headers & Client Inspectie --> <!-- Tool 1: DNS & Record Lookup -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="card-title"> <div class="card-title">
<span>🔍</span> HTTP Request Inspector <span>🔎</span> DNS &amp; Domein Lookup
</div> </div>
<span class="badge-pill"><?= htmlspecialchars($protocol); ?></span> <span class="badge-pill">Live DNS Query</span>
</div> </div>
<p class="card-desc"> <p class="card-desc">
Reële headers zoals ontvangen door de Apache/Nginx webserver voor deze verbinding. Vraag rechtstreeks vanaf deze server de A, AAAA, MX, NS en TXT records op van een domein.
</p> </p>
<table class="data-table" style="margin-bottom: 16px;"> <div style="display: flex; gap: 8px; margin-bottom: 14px;">
<tr> <input type="text" id="dns-input" value="webenpcstudio.nl"
<td class="label">Bezoekers IP:</td> style="flex: 1; background: rgba(0,0,0,0.3); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 8px 12px; color: var(--text-main); font-size: 0.88rem; outline: none;"
<td class="val"><?= htmlspecialchars($clientIp); ?></td> placeholder="bijv. webenpcstudio.nl">
</tr> <button class="btn btn-primary btn-sm" id="btn-dns" onclick="runDnsLookup()">
<tr> Query DNS
<td class="label">Protocol / SSL:</td> </button>
<td class="val"><?= htmlspecialchars($protocol); ?></td>
</tr>
<tr>
<td class="label">User-Agent:</td>
<td class="val" style="max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<?= htmlspecialchars($userAgent); ?>">
<?= htmlspecialchars($userAgent); ?>
</td>
</tr>
</table>
<div class="card-header" style="margin-bottom: 8px;">
<span style="font-size: 0.8rem; color: var(--text-muted); font-weight: 600;">ALLE REQUEST HEADERS:</span>
<button class="btn btn-secondary btn-sm" onclick="copyHeaders()">📋 Kopieer</button>
</div>
<div class="terminal-box" id="headers-box"><?php
if (!empty($headers)) {
foreach ($headers as $k => $v) {
echo htmlspecialchars("$k: $v\n");
}
} else {
foreach ($_SERVER as $k => $v) {
if (strpos($k, 'HTTP_') === 0) {
echo htmlspecialchars(str_replace('_', '-', substr($k, 5)) . ": $v\n");
}
}
}
?></div>
</div> </div>
<!-- Paneel 2: Outbound Server Connectiviteit (cURL) --> <div id="dns-results" class="terminal-box" style="max-height: 200px; min-height: 80px;">
Voer een domeinnaam in en klik op "Query DNS" om actieve records op te halen.
</div>
</div>
<!-- Tool 2: Outbound Server Connectiviteit (cURL) -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="card-title"> <div class="card-title">
<span>🌐</span> Outbound Server Ping <span>🌐</span> Outbound Server Ping (cURL)
</div> </div>
<span class="badge-pill">cURL Test</span> <span class="badge-pill">Verbindingstest</span>
</div> </div>
<p class="card-desc"> <p class="card-desc">
Test of de webserver vanuit PHP naar buiten kan verbinden (bijv. voor Git webhooks of API syncs). Test of de webserver vanuit PHP naar buiten kan verbinden (bijv. voor Git webhooks of API communicatie).
</p> </p>
<div style="display: flex; gap: 8px; margin-bottom: 16px;"> <div style="display: flex; gap: 8px; margin-bottom: 14px;">
<select id="ping-target" class="btn btn-secondary" style="flex: 1; outline: none; background: #0b111e; color: #fff; cursor: pointer;"> <select id="ping-target" class="btn btn-secondary" style="flex: 1; outline: none; background: #0b111e; color: var(--text-main); cursor: pointer; font-size: 0.84rem;">
<option value="https://git.webenpcstudio.nl">git.webenpcstudio.nl (Git Server)</option> <option value="https://git.webenpcstudio.nl">git.webenpcstudio.nl (Git Server)</option>
<option value="https://api.github.com">api.github.com (GitHub API)</option> <option value="https://api.github.com">api.github.com (GitHub API)</option>
<option value="https://www.google.com">google.com (Algemene DNS/Web)</option> <option value="https://www.google.com">google.com (Algemeen Web)</option>
</select> </select>
<button class="btn btn-primary" id="btn-ping" onclick="runOutboundPing()"> <button class="btn btn-primary btn-sm" id="btn-ping" onclick="runOutboundPing()">
Ping Ping
</button> </button>
</div> </div>
@@ -203,29 +239,90 @@ $sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd'
</div> </div>
</div> </div>
<div id="ping-status-msg" style="font-size: 0.82rem; color: var(--text-muted); margin-top: 6px;"> <div id="ping-status-msg" style="font-size: 0.82rem; color: var(--text-muted); margin-top: 4px;">
Selecteer een doellocatie en klik op Ping om de netwerk-latency te meten. Selecteer een doel en klik op Ping.
</div> </div>
</div> </div>
</div> </div>
<!-- Rij 2: Live String, Hash & Epoch Transformer --> <!-- Tool 3: PHP Extensies & Server Capabilities Matrix -->
<div class="card" style="margin-bottom: 24px;">
<div class="card-header">
<div class="card-title">
<span>🧩</span> PHP Extensies &amp; Server Matrix
</div>
<span class="badge-pill success">PHP v<?= htmlspecialchars($phpVersion); ?></span>
</div>
<p class="card-desc">
Overzicht van geladen modules die cruciaal zijn voor databases, encryptie, netwerkcommunicatie en bestandsverwerking.
</p>
<div class="grid-2" style="margin-bottom: 0;">
<?php foreach ($extensions as $cat => $extList): ?>
<div class="metric-box" style="padding: 12px 14px;">
<div class="metric-label" style="margin-bottom: 8px; color: var(--primary); font-weight: 700;"><?= $cat; ?></div>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<?php foreach ($extList as $ext):
$isLoaded = extension_loaded($ext);
?>
<span class="badge-pill <?= $isLoaded ? 'success' : 'warning'; ?>" style="font-size: 0.76rem;">
<?= $isLoaded ? '✓' : '✗'; ?> <?= $ext; ?>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div style="margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--border-subtle); display: flex; justify-content: space-between; font-size: 0.82rem; color: var(--text-muted);">
<span>Server Load Average (1 / 5 / 15 min): <strong><?= htmlspecialchars($loadDisplay); ?></strong></span>
<span>Actief Werkgeheugen: <strong><?= round(memory_get_usage() / 1024 / 1024, 1); ?> MB</strong></span>
</div>
</div>
<!-- Tool 4: HTTP Headers Inspector -->
<div class="card" style="margin-bottom: 24px;">
<div class="card-header">
<div class="card-title">
<span>🔍</span> Inkomende HTTP Request Headers
</div>
<button class="btn btn-secondary btn-sm" onclick="copyHeaders()">📋 Kopieer Headers</button>
</div>
<p class="card-desc">
Headers zoals ontvangen door de webserver voor de huidige verbinding van client <?= htmlspecialchars($clientIp); ?>.
</p>
<div class="terminal-box" id="headers-box"><?php
if (!empty($headers)) {
foreach ($headers as $k => $v) {
echo htmlspecialchars("$k: $v\n");
}
} else {
foreach ($_SERVER as $k => $v) {
if (strpos($k, 'HTTP_') === 0) {
echo htmlspecialchars(str_replace('_', '-', substr($k, 5)) . ": $v\n");
}
}
}
?></div>
</div>
<!-- Tool 5: Realtime Crypto & String Transformer -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="card-title"> <div class="card-title">
<span>🔐</span> Realtime Crypto &amp; String Transformer <span>🔐</span> Realtime Crypto &amp; String Transformer
</div> </div>
<span class="badge-pill">PHP &amp; JS Dual Engine</span> <span class="badge-pill">Dual Engine</span>
</div> </div>
<p class="card-desc"> <p class="card-desc">
Converteer strings direct naar verschillende hashing formaten, base64 encoding en timestamp berekeningen. Converteer strings direct naar MD5, SHA-256 en Base64 formaten.
</p> </p>
<div style="display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;"> <div style="display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap;">
<input type="text" id="trans-input" value="WebenPCStudio 2026 Git Testdrive" <input type="text" id="trans-input" value="WebenPCStudio 2026 Git Testdrive"
style="flex: 1; min-width: 260px; background: rgba(0,0,0,0.4); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 10px 14px; color: #fff; font-size: 0.9rem; outline: none;" style="flex: 1; min-width: 260px; background: rgba(0,0,0,0.3); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 10px 14px; color: var(--text-main); font-size: 0.9rem; outline: none;"
placeholder="Typ invoertekst..." oninput="updateTransforms()"> placeholder="Typ invoertekst..." oninput="updateTransforms()">
<button class="btn btn-secondary" onclick="insertCurrentEpoch()">⏱️ Epoch Nu</button> <button class="btn btn-secondary btn-sm" onclick="insertCurrentEpoch()">⏱️ Epoch Nu</button>
</div> </div>
<div class="grid-3" style="margin-bottom: 0;"> <div class="grid-3" style="margin-bottom: 0;">
@@ -252,7 +349,40 @@ $sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd'
<!-- Script Logic --> <!-- Script Logic -->
<script> <script>
// 1. Outbound Ping via PHP cURL // 1. DNS Record Lookup
async function runDnsLookup() {
const btn = document.getElementById('btn-dns');
const domain = document.getElementById('dns-input').value.trim();
const resBox = document.getElementById('dns-results');
if (!domain) return;
btn.disabled = true;
btn.textContent = 'Querying...';
resBox.textContent = `DNS records opzoeken voor ${domain}...`;
try {
const res = await fetch(`tools.php?action=dns_lookup&domain=${encodeURIComponent(domain)}&t=${Date.now()}`);
const data = await res.json();
if (data.status === 'success' && data.records.length > 0) {
let out = `[DNS RECORDS VOOR: ${data.domain}]\n`;
out += `Totaal gevonden: ${data.count} records\n\n`;
data.records.forEach(r => {
out += `${r.type.padEnd(8)} ${r.value.padEnd(36)} (TTL: ${r.ttl}s)\n`;
});
resBox.textContent = out;
} else {
resBox.textContent = `Geen DNS records gevonden voor ${domain}.`;
}
} catch (err) {
resBox.textContent = 'DNS Lookup fout: ' + err.message;
} finally {
btn.disabled = false;
btn.textContent = 'Query DNS';
}
}
// 2. Outbound Ping via PHP cURL
async function runOutboundPing() { async function runOutboundPing() {
const btn = document.getElementById('btn-ping'); const btn = document.getElementById('btn-ping');
const target = document.getElementById('ping-target').value; const target = document.getElementById('ping-target').value;
@@ -286,7 +416,7 @@ $sslCipher = $_SERVER['SSL_CIPHER'] ?? ($_SERVER['HTTPS'] ?? 'Niet gedetecteerd'
} }
} }
// 2. Realtime Hashing & Transforms // 3. Realtime Hashing & Transforms
async function sha256(msg) { async function sha256(msg) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(msg)); const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(msg));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''); return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');