SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,312
Folders
410
Scanned Size
3.85 GB
PHP Files
891
Editable Text Files
12,573
File viewer
guarded to /htdocs
/simon/old/file2.php
<?php declare(strict_types=1); /** * SIMON Master Console Rebuild * Drop into: /htdocs/simon/master_console_rebuild.php * * Purpose: * - Command center dashboard * - Interactive file tree * - File viewer * - File editor (guarded to root scope) * - System map + dynamic graphics * - Wire-ready companion to /simon/console.php */ error_reporting(E_ALL); ini_set('display_errors', '1'); date_default_timezone_set('America/Chicago'); $ROOT = realpath(dirname(__DIR__)); // /htdocs $SIMON_DIR = realpath(__DIR__); // /htdocs/simon $SELF_PATH = __FILE__; $SELF_NAME = basename(__FILE__); $defaultDataDir = $SIMON_DIR . DIRECTORY_SEPARATOR . 'data'; if (!is_dir($defaultDataDir)) { @mkdir($defaultDataDir, 0775, true); } $notesFile = $defaultDataDir . DIRECTORY_SEPARATOR . 'master_console_notes.json'; function h(string $value): string { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); } function format_bytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $size = max(0, $bytes); $i = 0; while ($size >= 1024 && $i < count($units) - 1) { $size /= 1024; $i++; } return number_format($size, $i === 0 ? 0 : 2) . ' ' . $units[$i]; } function rel_path(string $root, string $path): string { $root = rtrim(str_replace('\\', '/', $root), '/'); $path = str_replace('\\', '/', $path); if (strpos($path, $root) === 0) { $rel = substr($path, strlen($root)); return $rel === '' ? '/' : $rel; } return $path; } function safe_realpath_from_rel(string $root, string $rel): ?string { $rel = trim($rel); $candidate = $rel === '' || $rel === '/' ? $root : $root . DIRECTORY_SEPARATOR . ltrim(str_replace(['../', '..\\'], '', $rel), '/\\'); $real = realpath($candidate); if ($real === false) { $parent = realpath(dirname($candidate)); if ($parent === false) { return null; } $real = $parent . DIRECTORY_SEPARATOR . basename($candidate); } $rootNorm = str_replace('\\', '/', $root); $realNorm = str_replace('\\', '/', $real); if (strpos($realNorm, $rootNorm) !== 0) { return null; } return $real; } function is_text_file(string $path): bool { if (!is_file($path)) return false; $ext = strtolower((string)pathinfo($path, PATHINFO_EXTENSION)); $textExt = [ 'php','phtml','html','htm','css','js','json','txt','md','xml','yml','yaml','ini','env','log','sql','csv','svg','sh' ]; if (in_array($ext, $textExt, true)) return true; $sample = @file_get_contents($path, false, null, 0, 512); if ($sample === false) return false; return !preg_match('/[\x00-\x08\x0E-\x1F]/', $sample); } function read_json_safe(string $file, array $fallback = []): array { if (!is_file($file)) return $fallback; $raw = @file_get_contents($file); if ($raw === false || trim($raw) === '') return $fallback; $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : $fallback; } function write_json_safe(string $file, array $data): bool { return @file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) !== false; } function classify_file(string $relative): array { $p = strtolower($relative); $badges = []; if (str_contains($p, '/simon/')) $badges[] = ['SIMON', 'cyan']; if (str_contains($p, '/web360/')) $badges[] = ['WEB360', 'violet']; if (str_contains($p, '/social/')) $badges[] = ['SOCIAL', 'green']; if (str_contains($p, '/api/')) $badges[] = ['API', 'gold']; if (str_contains($p, '/data/') || preg_match('/\.(json|log)$/', $p)) $badges[] = ['DATA', 'blue']; if (preg_match('/(^|\/)\.htaccess$/', $p) || str_contains($p, 'secrets') || str_contains($p, '.env')) $badges[] = ['SENSITIVE', 'red']; if (preg_match('/\.(php|phtml)$/', $p)) $badges[] = ['PHP', 'slate']; return $badges; } function scan_branch(string $root, string $path, int $depth, int $maxDepth, bool $showHidden = false): array { $nodes = []; if ($depth > $maxDepth || !is_dir($path)) return $nodes; $items = @scandir($path); if ($items === false) return $nodes; natcasesort($items); foreach ($items as $item) { if ($item === '.' || $item === '..') continue; if (!$showHidden && str_starts_with($item, '.')) continue; $full = $path . DIRECTORY_SEPARATOR . $item; $isDir = is_dir($full); $relative = rel_path($root, $full); $node = [ 'name' => $item, 'path' => $relative, 'type' => $isDir ? 'dir' : 'file', 'size' => $isDir ? 0 : (int)@filesize($full), 'mtime' => (int)@filemtime($full), 'writable' => is_writable($full), 'children' => $isDir ? scan_branch($root, $full, $depth + 1, $maxDepth, $showHidden) : [], 'badges' => classify_file($relative), ]; $nodes[] = $node; } return $nodes; } function flatten_nodes(array $nodes, array &$rows): void { foreach ($nodes as $node) { $rows[] = $node; if (!empty($node['children'])) flatten_nodes($node['children'], $rows); } } $maxDepth = isset($_GET['depth']) ? max(2, min(8, (int)$_GET['depth'])) : 5; $showHidden = isset($_GET['hidden']) && $_GET['hidden'] === '1'; $viewRel = isset($_GET['view']) ? trim((string)$_GET['view']) : '/simon'; $tab = isset($_GET['tab']) ? trim((string)$_GET['tab']) : 'dashboard'; $action = $_POST['action'] ?? ''; $message = ''; $error = ''; if ($action === 'save_note') { $notes = read_json_safe($notesFile, ['notes' => []]); $notes['notes'][] = [ 'time' => date('Y-m-d H:i:s'), 'title' => trim((string)($_POST['title'] ?? 'Quick Note')), 'body' => trim((string)($_POST['body'] ?? '')), ]; if (write_json_safe($notesFile, $notes)) { $message = 'Note saved.'; } else { $error = 'Could not save note.'; } } if ($action === 'save_file') { $fileRel = trim((string)($_POST['file_rel'] ?? '')); $target = safe_realpath_from_rel($ROOT, $fileRel); if ($target === null || !is_file($target)) { $error = 'Target file not found or outside allowed root.'; } elseif (!is_text_file($target)) { $error = 'Only text-based files can be edited here.'; } elseif (!is_writable($target)) { $error = 'That file is not writable.'; } else { $content = (string)($_POST['content'] ?? ''); if (@file_put_contents($target, $content) !== false) { $message = 'File saved: ' . rel_path($ROOT, $target); $viewRel = rel_path($ROOT, $target); $tab = 'viewer'; } else { $error = 'Save failed.'; } } } $treeRoot = safe_realpath_from_rel($ROOT, '/'); $nodes = scan_branch($ROOT, $treeRoot ?: $ROOT, 0, $maxDepth, $showHidden); $rows = []; flatten_nodes($nodes, $rows); $totalFiles = 0; $totalDirs = 0; $totalBytes = 0; $phpFiles = 0; $editableFiles = 0; $modBuckets = ['simon' => 0, 'web360' => 0, 'social' => 0, 'api' => 0, 'content' => 0]; foreach ($rows as $row) { if ($row['type'] === 'dir') { $totalDirs++; continue; } $totalFiles++; $totalBytes += (int)$row['size']; if (strtolower((string)pathinfo($row['name'], PATHINFO_EXTENSION)) === 'php') $phpFiles++; if (is_text_file($ROOT . $row['path'])) $editableFiles++; $lp = strtolower($row['path']); if (str_contains($lp, '/simon/')) $modBuckets['simon']++; elseif (str_contains($lp, '/web360/')) $modBuckets['web360']++; elseif (str_contains($lp, '/social/')) $modBuckets['social']++; elseif (str_contains($lp, '/api/')) $modBuckets['api']++; else $modBuckets['content']++; } $viewPath = safe_realpath_from_rel($ROOT, $viewRel); $viewExists = $viewPath !== null && file_exists($viewPath); $viewIsFile = $viewPath !== null && is_file($viewPath); $viewContent = ''; $viewRelativeResolved = $viewExists ? rel_path($ROOT, $viewPath) : $viewRel; if ($viewIsFile && is_text_file($viewPath)) { $viewContent = (string)@file_get_contents($viewPath); } $notes = read_json_safe($notesFile, ['notes' => []]); $recentNotes = array_slice(array_reverse($notes['notes'] ?? []), 0, 6); function render_tree(array $nodes): string { $html = '<ul class="tree-list">'; foreach ($nodes as $node) { $isDir = $node['type'] === 'dir'; $icon = $isDir ? '📁' : '📄'; $target = '?tab=viewer&view=' . rawurlencode($node['path']); $badges = ''; foreach ($node['badges'] as $badge) { $badges .= '<span class="badge ' . h($badge[1]) . '">' . h($badge[0]) . '</span>'; } $meta = $isDir ? 'folder' : format_bytes((int)$node['size']); $html .= '<li>'; $html .= '<div class="tree-row">'; $html .= '<a class="tree-link" href="' . h($target) . '">' . $icon . ' ' . h($node['name']) . '</a>'; $html .= '<div class="tree-meta"><span>' . h($meta) . '</span>' . $badges . '</div>'; $html .= '</div>'; if (!empty($node['children'])) { $html .= render_tree($node['children']); } $html .= '</li>'; } $html .= '</ul>'; return $html; } ?><!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <title>SIMON Master Console Rebuild</title> <meta name="theme-color" content="#07111f"> <style> :root{--bg:#06101b;--bg2:#09182b;--panel:rgba(10,20,38,.82);--panel2:rgba(14,26,49,.9);--line:rgba(120,185,255,.18);--text:#edf6ff;--muted:#96abc7;--cyan:#6de9ff;--violet:#a88dff;--blue:#72abff;--green:#67f0b0;--gold:#ffd46f;--red:#ff7f98;--slate:#d7e7ff;--shadow:0 18px 60px rgba(0,0,0,.38);--radius:20px} *{box-sizing:border-box}html,body{margin:0;padding:0}body{font:14px/1.5 Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:var(--text);background:radial-gradient(circle at top, rgba(109,233,255,.12), transparent 30%),radial-gradient(circle at 85% 18%, rgba(168,141,255,.12), transparent 28%),linear-gradient(180deg,var(--bg),var(--bg2));min-height:100vh} a{color:inherit;text-decoration:none}.wrap{max-width:1540px;margin:0 auto;padding:20px}.hero,.card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}.hero{padding:24px;position:relative;overflow:hidden}.hero:before{content:"";position:absolute;right:-90px;top:-90px;width:320px;height:320px;border-radius:50%;background:radial-gradient(circle, rgba(109,233,255,.18), transparent 68%);filter:blur(14px)}.hero-grid{display:grid;grid-template-columns:1.2fr .8fr;gap:20px;align-items:center}.orb{width:180px;height:180px;border-radius:50%;margin-left:auto;background:radial-gradient(circle at 35% 30%, rgba(255,255,255,.96), rgba(109,233,255,.56) 15%, rgba(114,171,255,.26) 34%, rgba(168,141,255,.14) 52%, transparent 70%);box-shadow:0 0 42px rgba(109,233,255,.28), inset 0 0 30px rgba(255,255,255,.12);position:relative}.orb:before,.orb:after{content:"";position:absolute;inset:-16px;border-radius:50%;border:1px solid rgba(109,233,255,.18);animation:spin 12s linear infinite}.orb:after{inset:-30px;border-color:rgba(168,141,255,.18);animation-direction:reverse;animation-duration:18s}@keyframes spin{to{transform:rotate(360deg)}}.kicker{display:inline-flex;padding:7px 11px;border:1px solid var(--line);border-radius:999px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);font-size:12px}h1{margin:12px 0 8px;font-size:clamp(28px,4vw,48px)}.sub{color:#d8e7fb;max-width:880px}.tabs{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}.tab{padding:10px 14px;border:1px solid var(--line);border-radius:999px;background:rgba(255,255,255,.04)}.tab.active{background:rgba(109,233,255,.1);border-color:rgba(109,233,255,.35)}.grid{display:grid;gap:18px;margin-top:18px}.stats{grid-template-columns:repeat(5,1fr)}.main{grid-template-columns:1.1fr .9fr}.card{padding:18px}.head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px}.head h2{margin:0;font-size:20px}.mini{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.1em}.metric{padding:14px;border:1px solid rgba(255,255,255,.06);border-radius:16px;background:rgba(255,255,255,.03)}.metric .label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.08em}.metric .value{font-size:28px;font-weight:800;margin-top:6px}.tree-wrap{max-height:760px;overflow:auto;padding-right:6px}.tree-list{list-style:none;margin:0;padding-left:14px}.tree-list .tree-list{border-left:1px dashed rgba(255,255,255,.08);margin-left:10px}.tree-row{display:flex;justify-content:space-between;gap:12px;align-items:flex-start;padding:9px 10px;border-radius:12px;background:rgba(255,255,255,.02);margin:8px 0}.tree-link{font-weight:700}.tree-meta{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end;align-items:center;color:var(--muted);font-size:12px}.badge{display:inline-flex;padding:4px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.08);font-size:11px;font-weight:700}.badge.cyan{color:var(--cyan)}.badge.violet{color:var(--violet)}.badge.green{color:var(--green)}.badge.gold{color:var(--gold)}.badge.red{color:var(--red)}.badge.blue{color:var(--blue)}.badge.slate{color:var(--slate)}.viewer{display:grid;gap:14px}.path{font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#cfe7ff;padding:10px 12px;border-radius:12px;background:#06101e;border:1px solid var(--line)}textarea,input[type=text]{width:100%;background:#081221;color:var(--text);border:1px solid rgba(255,255,255,.12);border-radius:14px;padding:12px 14px;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}textarea{min-height:460px;resize:vertical}.btn{display:inline-flex;align-items:center;justify-content:center;padding:11px 14px;border-radius:14px;border:1px solid var(--line);background:linear-gradient(135deg, rgba(109,233,255,.18), rgba(168,141,255,.16));color:#fff;cursor:pointer;font-weight:700}.btn-row{display:flex;gap:10px;flex-wrap:wrap}.note-list{display:grid;gap:12px}.note{padding:14px;border-radius:16px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06)}.note .time{color:var(--muted);font-size:12px}.status.ok,.status.err{padding:12px 14px;border-radius:14px;margin-top:14px}.status.ok{background:rgba(103,240,176,.08);border:1px solid rgba(103,240,176,.22);color:#dcffec}.status.err{background:rgba(255,127,152,.08);border:1px solid rgba(255,127,152,.22);color:#ffd9e3}.map{height:420px;border-radius:18px;border:1px solid var(--line);background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));position:relative;overflow:hidden}.node{position:absolute;padding:12px 14px;border-radius:16px;background:rgba(9,18,34,.9);border:1px solid var(--line);min-width:160px;text-align:center;box-shadow:var(--shadow)}.node strong{display:block}.node small{color:var(--muted)}.n1{left:40%;top:40%}.n2{left:8%;top:12%}.n3{right:8%;top:12%}.n4{left:8%;bottom:12%}.n5{right:8%;bottom:12%}.n6{left:40%;bottom:8%}.canvas-wrap{height:260px;border-radius:18px;border:1px solid var(--line);overflow:hidden;background:#050d19}.list-table{width:100%;border-collapse:collapse}.list-table th,.list-table td{padding:11px 10px;border-bottom:1px solid rgba(255,255,255,.08);text-align:left;vertical-align:top}.list-table th{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}.muted{color:var(--muted)}@media (max-width:1200px){.hero-grid,.main,.stats{grid-template-columns:1fr}.orb{margin:10px auto 0}} </style> </head> <body> <div class="wrap"> <section class="hero"> <div class="kicker">SIMON Master Console Rebuild</div> <div class="hero-grid"> <div> <h1>True command center for SIMON + WEB360</h1> <div class="sub">This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing <code>console.php</code> and become the stronger replacement path.</div> <?php if ($message !== ''): ?><div class="status ok"><?= h($message) ?></div><?php endif; ?> <?php if ($error !== ''): ?><div class="status err"><?= h($error) ?></div><?php endif; ?> </div> <div class="orb" aria-hidden="true"></div> </div> <div class="tabs"> <?php foreach (['dashboard'=>'Dashboard','tree'=>'Tree','viewer'=>'Viewer','map'=>'System Map','files'=>'File Registry','notes'=>'Notes'] as $key => $label): ?> <a class="tab <?= $tab === $key ? 'active' : '' ?>" href="?tab=<?= h($key) ?>&view=<?= rawurlencode($viewRelativeResolved) ?><?= $showHidden ? '&hidden=1' : '' ?>"> <?= h($label) ?> </a> <?php endforeach; ?> </div> </section> <section class="grid stats"> <div class="metric"><div class="label">Files</div><div class="value"><?= number_format($totalFiles) ?></div></div> <div class="metric"><div class="label">Folders</div><div class="value"><?= number_format($totalDirs) ?></div></div> <div class="metric"><div class="label">Scanned Size</div><div class="value"><?= h(format_bytes($totalBytes)) ?></div></div> <div class="metric"><div class="label">PHP Files</div><div class="value"><?= number_format($phpFiles) ?></div></div> <div class="metric"><div class="label">Editable Text Files</div><div class="value"><?= number_format($editableFiles) ?></div></div> </section> <?php if ($tab === 'dashboard'): ?> <section class="grid main"> <div class="card"> <div class="head"><h2>Dynamic graphics</h2><span class="mini">live motion canvas</span></div> <div class="canvas-wrap"><canvas id="viz"></canvas></div> </div> <div class="card"> <div class="head"><h2>Module distribution</h2><span class="mini">current file buckets</span></div> <table class="list-table"> <tr><th>Module</th><th>Files</th></tr> <?php foreach ($modBuckets as $name => $count): ?> <tr><td><?= h(strtoupper($name)) ?></td><td><?= number_format($count) ?></td></tr> <?php endforeach; ?> </table> </div> </section> <?php elseif ($tab === 'tree'): ?> <section class="card"> <div class="head"><h2>Interactive file tree</h2><span class="mini">click a file to inspect</span></div> <div class="tree-wrap"><?= render_tree($nodes) ?></div> </section> <?php elseif ($tab === 'viewer'): ?> <section class="grid main"> <div class="card viewer"> <div class="head"><h2>File viewer</h2><span class="mini">guarded to /htdocs</span></div> <div class="path"><?= h($viewRelativeResolved) ?></div> <?php if (!$viewExists): ?> <div class="muted">That path does not exist inside the allowed root.</div> <?php elseif (!$viewIsFile): ?> <div class="muted">This is a folder. Use the tree to select a file.</div> <?php elseif (!is_text_file($viewPath)): ?> <div class="muted">Binary/non-text file preview is disabled here.</div> <?php else: ?> <form method="post"> <input type="hidden" name="action" value="save_file"> <input type="hidden" name="file_rel" value="<?= h($viewRelativeResolved) ?>"> <textarea name="content"><?= h($viewContent) ?></textarea> <div class="btn-row" style="margin-top:12px"><button class="btn" type="submit">Save file</button></div> </form> <?php endif; ?> </div> <div class="card"> <div class="head"><h2>Quick jump</h2><span class="mini">open a path</span></div> <form method="get"> <input type="hidden" name="tab" value="viewer"> <input type="text" name="view" value="<?= h($viewRelativeResolved) ?>"> <div class="btn-row" style="margin-top:12px"><button class="btn" type="submit">Open</button></div> </form> </div> </section> <?php elseif ($tab === 'map'): ?> <section class="grid main"> <div class="card"> <div class="head"><h2>System map</h2><span class="mini">current production + future flow</span></div> <div class="map"> <div class="node n1"><strong>SIMON</strong><small>brain · routing · memory</small></div> <div class="node n2"><strong>WEB360</strong><small>builder · preview · export</small></div> <div class="node n3"><strong>API</strong><small>providers · execution · status</small></div> <div class="node n4"><strong>Social</strong><small>growth · campaigns · queue</small></div> <div class="node n5"><strong>Data</strong><small>logs · memory · analytics</small></div> <div class="node n6"><strong>Investor / Ops</strong><small>value · phases · reporting</small></div> <svg viewBox="0 0 1000 600" style="position:absolute;inset:0;width:100%;height:100%"><g fill="none" stroke="rgba(109,233,255,.45)" stroke-width="2"><path d="M500 280 C 340 220, 260 170, 180 120"/><path d="M500 280 C 660 220, 740 170, 820 120"/><path d="M500 280 C 320 360, 230 430, 180 500"/><path d="M500 280 C 680 360, 770 430, 820 500"/><path d="M500 280 C 500 360, 500 420, 500 520"/></g></svg> </div> </div> <div class="card"> <div class="head"><h2>Flow</h2><span class="mini">operator sequence</span></div> <table class="list-table"> <tr><td>1</td><td>User prompt / task / click enters system</td></tr> <tr><td>2</td><td>SIMON classifies and routes</td></tr> <tr><td>3</td><td>WEB360 builds or edits output</td></tr> <tr><td>4</td><td>API layer executes provider/service calls</td></tr> <tr><td>5</td><td>Data/logs record the event</td></tr> <tr><td>6</td><td>Social / investor / dashboard layers update</td></tr> </table> </div> </section> <?php elseif ($tab === 'files'): ?> <section class="card"> <div class="head"><h2>File registry</h2><span class="mini">flattened scan</span></div> <div style="overflow:auto;max-height:760px"> <table class="list-table"> <thead><tr><th>Path</th><th>Type</th><th>Size</th><th>Modified</th><th>Open</th></tr></thead> <tbody> <?php foreach ($rows as $row): ?> <tr> <td><?= h($row['path']) ?></td> <td><?= h($row['type']) ?></td> <td><?= $row['type'] === 'file' ? h(format_bytes((int)$row['size'])) : '—' ?></td> <td><?= !empty($row['mtime']) ? h(date('Y-m-d H:i', (int)$row['mtime'])) : '—' ?></td> <td><a class="btn" style="padding:6px 10px;border-radius:10px" href="?tab=viewer&view=<?= rawurlencode($row['path']) ?>">Open</a></td> </tr> <?php endforeach; ?> </tbody> </table> </div> </section> <?php elseif ($tab === 'notes'): ?> <section class="grid main"> <div class="card"> <div class="head"><h2>Operator notes</h2><span class="mini">stored in /simon/data</span></div> <form method="post"> <input type="hidden" name="action" value="save_note"> <input type="text" name="title" placeholder="Title"> <textarea name="body" placeholder="Note / next action / improvement idea"></textarea> <div class="btn-row" style="margin-top:12px"><button class="btn" type="submit">Save note</button></div> </form> </div> <div class="card"> <div class="head"><h2>Recent notes</h2><span class="mini">latest six</span></div> <div class="note-list"> <?php if (!$recentNotes): ?><div class="muted">No notes yet.</div><?php endif; ?> <?php foreach ($recentNotes as $note): ?> <div class="note"> <strong><?= h((string)($note['title'] ?? 'Note')) ?></strong> <div class="time"><?= h((string)($note['time'] ?? '')) ?></div> <div style="margin-top:8px"><?= nl2br(h((string)($note['body'] ?? ''))) ?></div> </div> <?php endforeach; ?> </div> </div> </section> <?php endif; ?> </div> <script> (() => { const c = document.getElementById('viz'); if (!c) return; const ctx = c.getContext('2d'); const dpr = Math.max(1, window.devicePixelRatio || 1); const particles = []; function size(){ const r = c.parentElement.getBoundingClientRect(); c.width = Math.floor(r.width * dpr); c.height = Math.floor(r.height * dpr); c.style.width = r.width + 'px'; c.style.height = r.height + 'px'; ctx.setTransform(dpr,0,0,dpr,0,0); } function reset(){ particles.length = 0; const count = Math.max(30, Math.floor(c.clientWidth / 18)); for(let i=0;i<count;i++){ particles.push({ x: Math.random()*c.clientWidth, y: Math.random()*c.clientHeight, vx:(Math.random()-.5)*.45, vy:(Math.random()-.5)*.45, r:1+Math.random()*2.6 }); } } function draw(){ ctx.clearRect(0,0,c.clientWidth,c.clientHeight); const g = ctx.createLinearGradient(0,0,c.clientWidth,c.clientHeight); g.addColorStop(0,'rgba(109,233,255,.08)'); g.addColorStop(1,'rgba(168,141,255,.05)'); ctx.fillStyle = g; ctx.fillRect(0,0,c.clientWidth,c.clientHeight); for(let i=0;i<particles.length;i++){ const p = particles[i]; p.x += p.vx; p.y += p.vy; if(p.x<0||p.x>c.clientWidth) p.vx*=-1; if(p.y<0||p.y>c.clientHeight) p.vy*=-1; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fillStyle='rgba(109,233,255,.8)'; ctx.fill(); for(let j=i+1;j<particles.length;j++){ const q=particles[j]; const dx=p.x-q.x, dy=p.y-q.y; const dist=Math.sqrt(dx*dx+dy*dy); if(dist<120){ ctx.strokeStyle='rgba(114,171,255,' + (0.18-(dist/120)*0.16) + ')'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(p.x,p.y); ctx.lineTo(q.x,q.y); ctx.stroke(); } } } requestAnimationFrame(draw); } window.addEventListener('resize', ()=>{size(); reset();}); size(); reset(); draw(); })(); </script> </body> </html>
Save file
Quick jump
open a path
Open