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
/sitelogs/summary.php
<?php declare(strict_types=1); require_once __DIR__ . '/_bootstrap.php'; require __DIR__ . '/guardian_lib.php'; $title = 'Summary — What happened on the site? (ELI5)'; page_start($title); // --- Controls (simple, “for dummies”) --- $days = max(0, (int)($_GET['days'] ?? ($CFG['LOG_DAYS'] ?? 14))); // 0 = all seen $limit = max(1000, (int)($_GET['lines'] ?? ($CFG['LOG_LINES_LIMIT'] ?? 100000))); $statusF = trim((string)($_GET['status'] ?? '')); // "", "404", "4xx", "5xx" $pathRx = trim((string)($_GET['path'] ?? '')); // regex $showTips = ($_GET['tips'] ?? '1') === '1'; // Pull rows via Guardian engine $rows = read_log_rows(null, $limit); // Normalize order (multi-file reads can interleave) usort($rows, function($a,$b){ $ta = $a['time'] instanceof DateTimeInterface ? $a['time']->getTimestamp() : 0; $tb = $b['time'] instanceof DateTimeInterface ? $b['time']->getTimestamp() : 0; return $ta <=> $tb; }); // Apply "days" range (0 = all) if ($days > 0) { $cut = (new DateTimeImmutable('now'))->modify("-{$days} days")->getTimestamp(); $rows = array_values(array_filter($rows, function($r) use ($cut){ $t = ($r['time'] instanceof DateTimeInterface) ? $r['time']->getTimestamp() : 0; return $t >= $cut; })); } // Apply “for dummies” filters safely $rows = array_values(array_filter($rows, function($r) use($statusF,$pathRx){ if ($statusF !== '') { $s = (int)($r['status'] ?? 0); if ($statusF === '4xx' && !($s>=400 && $s<500)) return false; if ($statusF === '5xx' && !($s>=500 && $s<600)) return false; if (ctype_digit($statusF) && $s !== (int)$statusF) return false; } if ($pathRx !== '' && @preg_match($pathRx, '') !== false) { if (!preg_match($pathRx, (string)($r['path'] ?? ''))) return false; } return true; })); $agg = aggregate($rows); // Build easy lists $top = function(array $map, int $n=10) { arsort($map); $out=[]; $i=0; foreach ($map as $k=>$v) { $out[] = ['item'=>$k,'count'=>$v]; if(++$i>=$n) break; } return $out; }; $topPages = $top($agg['pages'] ?? [], 10); $topRef = $top($agg['refs'] ?? [], 10); $codes = $agg['codes'] ?? []; $devices = $agg['devices'] ?? []; $hourly = $agg['hourly'] ?? []; // Basic “daily” from hourly labels $daily = []; foreach ($hourly as $h=>$c) { $d = substr((string)$h, 0, 10); $daily[$d] = ($daily[$d] ?? 0) + (int)$c; } ksort($daily); // Bytes & humans/bots quick tallies (from same filtered rows) $totalBytes = 0; $humDaily=[]; $botDaily=[]; foreach ($rows as $r) { $totalBytes += (int)($r['bytes'] ?? 0); if (!($r['time'] instanceof DateTimeInterface)) continue; $d = $r['time']->format('Y-m-d'); $ua = (string)($r['ua'] ?? ''); $isBot = (bool)preg_match('/bot|spider|crawler|preview|monitor|curl|wget|httpclient|facebookexternalhit|BingPreview|Ahrefs|Semrush|Slackbot|Discordbot|WhatsApp/i', $ua); if ($isBot) $botDaily[$d] = ($botDaily[$d] ?? 0) + 1; else $humDaily[$d] = ($humDaily[$d] ?? 0) + 1; } ksort($humDaily); ksort($botDaily); // Align Humans vs Bots day axis $allDays = array_unique(array_merge(array_keys($humDaily), array_keys($botDaily))); sort($allDays); $humSeries = []; $botSeries = []; foreach ($allDays as $d) { $humSeries[] = (int)($humDaily[$d] ?? 0); $botSeries[] = (int)($botDaily[$d] ?? 0); } ?> <!-- Chart.js (one line, safe CDN) --> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <div class="card"> <h3>Log Source</h3> <p class="k"> Read mode: <b><?=h((string)($CFG['LOG_READ_MODE'] ?? 'unknown'))?></b> · Lines limit: <b><?=number_format((int)$limit)?></b> · Days: <b><?=h((string)$days)?></b> </p> </div> <div class="card"> <h3>At a Glance</h3> <p class="k"> This page answers: <b>How busy?</b> <b>What pages?</b> <b>From where?</b><br> “ELI5” means we explain each widget in plain English. </p> <form method="get" style="display:flex;flex-wrap:wrap;gap:10px;align-items:center"> <label>Days <input type="number" name="days" min="0" value="<?=h((string)$days)?>" /> </label> <label>Max lines/file <input type="number" name="lines" min="1000" step="1000" value="<?=h((string)$limit)?>" /> </label> <label>Status (404 / 4xx / 5xx) <input name="status" value="<?=h($statusF)?>" size="6" /> </label> <label>Path regex <input name="path" value="<?=h($pathRx)?>" size="20" /> </label> <label style="display:flex;gap:6px;align-items:center"> <input type="checkbox" name="tips" value="1" <?= $showTips?'checked':''?>/> Show tips </label> <button class="btn" type="submit">Apply</button> </form> <div class="cards" style="margin-top:10px"> <div class="card"> <h3>Total hits</h3> <div style="font-size:1.6rem;font-weight:800"><?=number_format((int)($agg['total'] ?? 0))?></div> <div class="k">Every line in the access log is one “hit.”</div> </div> <div class="card"> <h3>Unique IPs</h3> <div style="font-size:1.6rem;font-weight:800"><?=number_format((int)($agg['unique_ips'] ?? 0))?></div> <div class="k">Roughly “unique visitors” (not perfect).</div> </div> <div class="card"> <h3>Bandwidth</h3> <div style="font-size:1.6rem;font-weight:800"><?=h(bytes_for_humans($totalBytes))?></div> <div class="k">Sum of the “bytes sent” column.</div> </div> <div class="card"> <h3>Error rate</h3> <?php $err = 0; foreach ($codes as $c=>$n) { if ((int)$c >= 400) $err += (int)$n; } $total = (int)($agg['total'] ?? 0); $rate = ($total > 0) ? round($err / $total * 100, 2) : 0; ?> <div style="font-size:1.6rem;font-weight:800"><?=number_format($rate,2)?>%</div> <div class="k">4xx and 5xx divided by all hits.</div> </div> </div> </div> <div class="cards"> <div class="card"> <h3>Status mix</h3> <canvas id="st" height="140"></canvas> <?php if ($showTips): ?><p class="k">2xx = OK, 3xx = redirect, 4xx = client errors (e.g., 404 not found), 5xx = server errors.</p><?php endif; ?> </div> <div class="card"> <h3>Devices</h3> <canvas id="dev" height="140"></canvas> <?php if ($showTips): ?><p class="k">We guess device type from the User-Agent string. It’s not perfect, but useful.</p><?php endif; ?> </div> </div> <div class="card"> <h3>Activity over time (daily)</h3> <canvas id="daily" height="160"></canvas> <?php if ($showTips): ?><p class="k">Each dot is a day. Trend lines help you spot spikes or dips.</p><?php endif; ?> </div> <div class="cards"> <div class="card"> <h3>Top pages</h3> <table><tr><th>Path</th><th>Hits</th></tr> <?php foreach ($topPages as $r): ?> <tr><td><?=h((string)$r['item'])?></td><td><?=number_format((int)$r['count'])?></td></tr> <?php endforeach; ?> </table> <?php if ($showTips): ?><p class="k">These are the most requested URLs. “/” is the homepage.</p><?php endif; ?> </div> <div class="card"> <h3>Top referrers</h3> <table><tr><th>Referrer</th><th>Hits</th></tr> <?php foreach ($topRef as $r): ?> <tr><td><?=h((string)$r['item'])?></td><td><?=number_format((int)$r['count'])?></td></tr> <?php endforeach; ?> </table> <?php if ($showTips): ?><p class="k">Where people clicked from. “-” means they typed it or the referrer was hidden.</p><?php endif; ?> </div> </div> <script> (() => { const st = document.getElementById('st').getContext('2d'); new Chart(st, { type:'doughnut', data:{ labels: <?=json_encode(array_keys($codes))?>, datasets:[{ data: <?=json_encode(array_values($codes))?> }] }, options:{ plugins:{ legend:{ position:'bottom' }}}); const dv = document.getElementById('dev').getContext('2d'); new Chart(dv, { type:'doughnut', data:{ labels: <?=json_encode(array_keys($devices))?>, datasets:[{ data: <?=json_encode(array_values($devices))?> }] }, options:{ plugins:{ legend:{ position:'bottom' }}}); const d = document.getElementById('daily').getContext('2d'); const labels = <?=json_encode(array_keys($daily))?>; const values = <?=json_encode(array_values($daily))?>; // 7-day moving average const ma = []; for (let i=0;i<values.length;i++){ const lo=Math.max(0,i-6); const slice=values.slice(lo,i+1); ma.push(slice.reduce((a,b)=>a+b,0)/slice.length); } new Chart(d, { type:'line', data:{ labels, datasets:[ { label:'Daily hits', data:values, borderWidth:2, pointRadius:0, tension:.25 }, { label:'7-day avg', data:ma, borderWidth:2, borderDash:[6,6], pointRadius:0, tension:.25 } ]}, options:{ scales:{ y:{ beginAtZero:true }}, plugins:{ legend:{ position:'bottom' }}} }); })(); </script> <div class="card"> <h3>Humans vs Bots (daily)</h3> <canvas id="hb" height="140"></canvas> <?php if ($showTips): ?><p class="k">Very rough split based on common bot/crawler indicators in the User-Agent.</p><?php endif; ?> </div> <script> (() => { const ctx = document.getElementById('hb').getContext('2d'); new Chart(ctx,{ type:'line', data:{ labels: <?=json_encode($allDays)?>, datasets:[ { label:'Humans', data: <?=json_encode($humSeries)?>, borderWidth:2, pointRadius:0 }, { label:'Bots', data: <?=json_encode($botSeries)?>, borderWidth:2, pointRadius:0 } ] }, options:{ scales:{ y:{ beginAtZero:true }}, plugins:{ legend:{ position:'bottom' }}} }); })(); </script> <div class="card"> <h3>Notes & Path Privacy</h3> <p class="k"> We abbreviate file paths after <code>/htdocs/</code> in some panels to avoid exposing full server paths. You can find the exact paths in <a class="btn" href="console.php?tab=files">Console → Files</a>. </p> </div> <?php page_end(); ?>
Save file
Quick jump
open a path
Open