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,334
Folders
408
Scanned Size
3.84 GB
PHP Files
886
Editable Text Files
12,595
File viewer
guarded to /htdocs
/sitelogs/guardian_lib.php
<?php /************************************************* * guardian_lib.php — SIMON Guardian v2.4 (IONOS-safe) * Shared library + UI shell used by: * summary.php, traffic.php, travel.php, security.php, guardian.php, sitemap_tracker.php * * Goals: * - Multi-log discovery (rotations + .gz) + deterministic IONOS primary path * - Modes: newest | all * - Optional time window (last N days) * - Efficient tail for big files; bounded streaming for .gz * - Robust Apache/IONOS CLF-ish parser * - Aggregation helpers * - Error-log tail helpers (security.php) * - Polished neon-glass UI (page_start/page_end) *************************************************/ declare(strict_types=1); date_default_timezone_set('America/Chicago'); /* ======================= CONFIG — tune here ======================= */ $CFG = [ // Primary fixed file path (IONOS) — keep FIRST for determinism. 'PRIMARY_ACCESS_LOG' => '/kunden/homepages/7/d4299367777/logs/logs/access.log', // Where to look for access logs (directories OR files). // Put broader directories after the primary so "newest" mode is stable. 'LOG_PATHS' => array_values(array_filter([ '/kunden/homepages/7/d4299367777/logs/logs/access.log', '/kunden/homepages/7/d4299367777/logs/logs', // directory (rotations + .gz) dirname(__DIR__) . '/logs', // /htdocs/logs dirname(dirname(__DIR__)) . '/logs', // /htdocs/htdocs/logs dirname(dirname(dirname(__DIR__))) . '/logs', // /logs (common) __DIR__ . '/traffic.log', // safe local copy fallback (e.g. /sitelogs/traffic.log) ], fn($p)=>$p!==false && $p!=='')), // Error logs (security.php) 'ERROR_LOGS' => [ '/kunden/homepages/7/d4299367777/logs/logs/error.log', __DIR__ . '/error.log', ], 'APP_NAME' => 'SIMON Traffic Intelligence Console', 'ADMIN_EMAIL' => 'admin@gaylordsinclair.com', // Read controls 'LOG_READ_MODE' => 'all', // 'newest' | 'all' 'LOG_DAYS' => 14, // 0 disables time cutoff 'LOG_LINES_LIMIT' => 100000, // per file 'TAIL_MAX_BYTES' => 2_000_000, // tail window for plain files 'GZ_MAX_LINES' => 100000, // soft cap for .gz streaming // Optional: ipinfo token (leave empty to disable) 'IPINFO_TOKEN' => '', ]; /* ======================= Small utilities ======================= */ function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function bytes_for_humans(int $b): string { foreach ([['TB',1<<40],['GB',1<<30],['MB',1<<20],['KB',1<<10]] as [$n,$v]) { if ($b >= $v) return number_format($b/$v,2)." $n"; } return number_format($b)." B"; } function device_hint(string $ua): string { $u = strtolower($ua); if ($u === '' || $u === '-') return 'unknown'; if (str_contains($u,'ipad') || str_contains($u,'tablet')) return 'tablet'; if (str_contains($u,'mobile') || str_contains($u,'iphone') || str_contains($u,'android')) return 'mobile'; if (str_contains($u,'windows') || str_contains($u,'macintosh') || str_contains($u,'linux')) return 'desktop'; return 'other'; } function mask_ip(string $ip): string { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $p = explode('.', $ip); $p[3] = 'x'; return implode('.', $p); } if (strpos($ip, ':') !== false) { $seg = explode(':', $ip); $seg[count($seg)-1] = 'xxxx'; return implode(':', $seg); } return $ip; } function send_admin_mail(string $to, string $subject, string $html): bool { $headers = "MIME-Version: 1.0\r\nContent-type:text/html;charset=UTF-8\r\n"; $headers .= "From: SIMON Guardian <no-reply@gaylordsinclair.com>\r\n"; return @mail($to, $subject, $html, $headers); } /* ======================= Log discovery & reading ======================= */ /** Return a list of readable access log files (rotations + .gz), newest first. */ function sg_find_all_logs(array $paths): array { $found = []; $seen = []; // likely patterns (but final filter below controls what we accept) $globs = ['access.log', 'access.log.*', '*.log', '*.log.*', '*.gz', '*.txt']; foreach ($paths as $p) { if (!$p) continue; // direct file if (@is_file($p) && @is_readable($p)) { $rp = realpath($p) ?: $p; if (!isset($seen[$rp])) { $found[] = $rp; $seen[$rp]=true; } continue; } // directory if (@is_dir($p) && @is_readable($p)) { $dir = rtrim($p,'/'); foreach ($globs as $g) { foreach (glob($dir.'/'.$g) ?: [] as $f) { if (!@is_file($f) || !@is_readable($f)) continue; // accept "access-ish" names; allow IONOS variants $bn = basename($f); if (!preg_match('~(^access(\.log)?(\.\d+)?(\.gz)?$)|(\baccess\b.*\.log(\.\d+)?(\.gz)?$)~i', $bn)) continue; $rp = realpath($f) ?: $f; if (!isset($seen[$rp])) { $found[] = $rp; $seen[$rp]=true; } } } } } @usort($found, fn($a,$b)=>(@filemtime($b)?:0) <=> (@filemtime($a)?:0)); return $found; } function find_log(array $paths): ?string { $all = sg_find_all_logs($paths); return $all[0] ?? null; } /** Tail read for plain files only (fast end-scan). */ function tail_lines(string $file, int $maxBytes=2_000_000, int $wantLines=200): array { if (!@is_readable($file) || !@is_file($file)) return []; $fp = @fopen($file, 'rb'); if (!$fp) return []; @fseek($fp, 0, SEEK_END); $len = @ftell($fp) ?: 0; $pos = -1; $line=''; $out=[]; $read=0; while (count($out) < $wantLines && $read < $maxBytes && ($len + $pos) >= 0) { @fseek($fp, $pos, SEEK_END); $c = @fgetc($fp); if ($c === "\n") { if ($line !== '') { array_unshift($out, strrev($line)); $line=''; } } else { $line .= ($c === false) ? '' : $c; } $pos--; $read++; } if ($line !== '') array_unshift($out, strrev($line)); @fclose($fp); return $out; } /** Stream .gz file lines (bounded). */ function gz_read_lines(string $gzPath, int $maxLines=100000): array { $out = []; $gz = @gzopen($gzPath, 'rb'); if (!$gz) return $out; $n = 0; while (!gzeof($gz)) { $ln = gzgets($gz); if ($ln === false) break; $out[] = $ln; if (++$n >= $maxLines) break; } @gzclose($gz); return $out; } /** Parse Apache/IONOS CLF-like line to array or null on mismatch. */ function parse_apache_line(string $line): ?array { if ($line === '') return null; if (strpos($line, "\0") !== false) return null; // Combined (w/ referer + UA) $re = '/^(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"([^"]*)"\s+(\d{3})\s+(\S+)\s+"([^"]*)"\s+"([^"]*)"/'; if (!preg_match($re, $line, $m)) { // Common simpler (no referer/ua) $re2 = '/^(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"([^"]*)"\s+(\d{3})\s+(\S+)/'; if (!preg_match($re2, $line, $m)) return null; $m[6] = '-'; $m[7] = '-'; } $ip = $m[1]; $dt_raw = $m[2]; // 10/Oct/2025:14:03:12 -0500 $req = $m[3]; // GET /path HTTP/1.1 $status = (int)$m[4]; $bytes = ($m[5] === '-' ? 0 : (int)$m[5]); $referer = $m[6]; $ua = $m[7]; $method = $path = $proto = ''; if (preg_match('/^(\S+)\s+(\S+)(?:\s+(\S+))?$/', $req, $rm)) { $method = $rm[1]; $path = $rm[2]; $proto = $rm[3] ?? ''; } $dt = DateTime::createFromFormat('d/M/Y:H:i:s O', $dt_raw) ?: new DateTime('now'); return [ 'ip' => $ip, 'time' => $dt, 'method' => $method, 'path' => $path, 'proto' => $proto, 'status' => $status, 'bytes' => $bytes, 'referer' => $referer, 'ua' => $ua, 'raw' => $line, ]; } /** Aggregate buckets for charts/tables. */ function aggregate(array $rows): array { $tot=0; $ips=[]; $pages=[]; $refs=[]; $codes=[]; $uaD=[]; $hourly=[]; foreach ($rows as $r) { $tot++; $ips[$r['ip']] = true; $pages[$r['path']] = ($pages[$r['path']] ?? 0) + 1; $refs[$r['referer']] = ($refs[$r['referer']] ?? 0) + 1; $codes[$r['status']] = ($codes[$r['status']] ?? 0) + 1; $dh = device_hint((string)($r['ua'] ?? '')); $uaD[$dh] = ($uaD[$dh] ?? 0) + 1; $h = ($r['time'] instanceof DateTimeInterface) ? $r['time']->format('Y-m-d H:00') : 'unknown'; $hourly[$h] = ($hourly[$h] ?? 0) + 1; } arsort($pages); arsort($refs); arsort($codes); arsort($uaD); ksort($hourly); return [ 'total' => $tot, 'unique_ips' => count($ips), 'pages' => $pages, 'refs' => $refs, 'codes' => $codes, 'devices' => $uaD, 'hourly' => $hourly, ]; } function filter_time_range(array $rows, int $hoursBack): array { $cut = (new DateTime())->modify('-'.$hoursBack.' hours'); return array_values(array_filter($rows, fn($r)=>($r['time'] ?? new DateTime('1970-01-01')) >= $cut)); } /** Read rows from logs respecting CFG: mode, days, and per-file tail limits. */ function read_log_rows(?string $ignored=null, int $maxLinesPerFile=0): array { global $CFG; $mode = strtolower((string)($CFG['LOG_READ_MODE'] ?? 'all')); // newest|all $daysLimit = max(0, (int)($CFG['LOG_DAYS'] ?? 0)); $perFile = max(1000, (int)($maxLinesPerFile ?: ($CFG['LOG_LINES_LIMIT'] ?? 100000))); $tailMaxB = max(256_000, (int)($CFG['TAIL_MAX_BYTES'] ?? 2_000_000)); $gzMax = max(10_000, (int)($CFG['GZ_MAX_LINES'] ?? 100000)); $all = sg_find_all_logs($CFG['LOG_PATHS']); if (!$all) return []; $use = ($mode === 'newest') ? [ $all[0] ] : $all; $cutoff = ($daysLimit > 0) ? (new DateTime())->modify('-'.$daysLimit.' days') : null; $rows = []; foreach ($use as $path) { $isGz = str_ends_with(strtolower($path), '.gz'); $lines = $isGz ? gz_read_lines($path, min($perFile, $gzMax)) : tail_lines($path, $tailMaxB, $perFile); foreach ($lines as $ln) { $r = parse_apache_line((string)$ln); if (!$r) continue; if ($cutoff && ($r['time'] instanceof DateTimeInterface) && $r['time'] < $cutoff) continue; $rows[] = $r; } } return $rows; } /* ======================= Error log helpers ======================= */ function sg_find_error_log(): ?string { global $CFG; foreach (($CFG['ERROR_LOGS'] ?? []) as $p) { if (@is_file($p) && @is_readable($p)) return $p; } // also try directory scan adjacent to access logs $dir = dirname((string)($CFG['PRIMARY_ACCESS_LOG'] ?? '')); if ($dir && @is_dir($dir)) { foreach (['error.log','error_log','php_error.log'] as $f) { $cand = rtrim($dir,'/').'/'.$f; if (@is_file($cand) && @is_readable($cand)) return $cand; } } return null; } function read_error_tail(int $lines=250): array { global $CFG; $p = sg_find_error_log(); if (!$p) return []; $tailMaxB = max(256_000, (int)($CFG['TAIL_MAX_BYTES'] ?? 2_000_000)); return tail_lines($p, $tailMaxB, max(50,$lines)); } /* ======================= Health / Diagnostics ======================= */ function sg_health_log_path(): string { return __DIR__ . '/health.log'; } function log_event(array $CFG_in, string $type, string $message, array $meta = []): void { $row = ['t'=>date('c'),'type'=>$type,'msg'=>$message,'meta'=>$meta]; @file_put_contents(sg_health_log_path(), json_encode($row, JSON_UNESCAPED_SLASHES)."\n", FILE_APPEND); } function read_health(array $CFG_in, int $limit = 200): array { $p = sg_health_log_path(); if (!@is_file($p)) return []; $lines = @file($p, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; $out = []; for ($i = count($lines) - 1; $i >= 0 && count($out) < $limit; $i--) { $j = json_decode($lines[$i], true); if (is_array($j)) $out[] = $j; } return $out; } function dir_stats(string $dir, array $explicit = []): array { $scan = $explicit ?: (glob(rtrim($dir,'/').'/*') ?: []); $files = 0; $bytes = 0; $largest = ['name'=>'', 'bytes'=>0]; foreach ($scan as $p) { if (!@is_file($p)) continue; $sz = @filesize($p) ?: 0; $bytes += $sz; $files++; if ($sz > ($largest['bytes'] ?? 0)) $largest = ['name'=>basename($p), 'bytes'=>$sz]; } return ['files'=>$files, 'bytes'=>$bytes, 'largest'=>$largest]; } /* ======================= Optional IP intel ======================= */ function is_public_ip(string $ip): bool { if (!filter_var($ip, FILTER_VALIDATE_IP)) return false; return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); } function rdns_lookup(string $ip): string { $h = @gethostbyaddr($ip); return ($h && $h !== $ip) ? $h : ''; } function ipinfo_fetch(string $ip, string $token): array { if ($token === '' || !is_public_ip($ip)) return []; $ctx = stream_context_create(['http'=>['timeout'=>3,'ignore_errors'=>true]]); $u = 'https://ipinfo.io/'.rawurlencode($ip).'/json?token='.rawurlencode($token); $raw = @file_get_contents($u, false, $ctx); if (!$raw) return []; $j = json_decode($raw, true); return is_array($j) ? $j : []; } function ipinfo_cached(string $ip, string $token): array { if ($token==='' || !is_public_ip($ip)) return []; $cacheDir = __DIR__.'/cache_ipinfo'; if (!@is_dir($cacheDir)) @mkdir($cacheDir, 0755, true); $key = $cacheDir.'/'.preg_replace('/[^A-Za-z0-9:_-]/','_',$ip).'.json'; if (@is_file($key) && time() - (@filemtime($key) ?: 0) < 86400) { $j = json_decode((string)@file_get_contents($key), true); if (is_array($j)) return $j; } $j = ipinfo_fetch($ip, $token); if ($j) @file_put_contents($key, json_encode($j, JSON_UNESCAPED_SLASHES)); return $j ?: []; } /* ======================= UI shell ======================= */ function page_start(string $title): void { global $CFG; $app = h((string)$CFG['APP_NAME']); echo "<!doctype html><html lang='en'><head><meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1,viewport-fit=cover'> <title>".h($title)." — $app</title><meta name='theme-color' content='#070b16'> <style> :root{ --bg:#070b16;--edge:#25345e;--ink:#eaf2ff;--muted:#9db3e6; --c-cyan:#6ae0ff;--c-indigo:#8aa8ff;--c-violet:#7c4dff;--c-green:#48d18a;--c-amber:#ffb86b; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0;color:var(--ink);font:15px/1.55 ui-sans-serif,system-ui,Inter,Segoe UI,Roboto,Arial; background: radial-gradient(140% 110% at 50% -10%, #0b1432 0%, var(--bg) 55%, #050814 100%); overflow-x:hidden; } header{position:sticky;top:0;z-index:40;background:linear-gradient(180deg, rgba(7,11,22,.96), rgba(7,11,22,.66)); border-bottom:1px solid var(--edge);backdrop-filter: blur(10px)} .console{ max-width:1200px; margin:0 auto; padding:12px 14px 16px } .title{ margin:0; font-weight:900; letter-spacing:.6px; line-height:1; font-size: clamp(1.08rem, 2.4vw, 1.5rem); color:transparent; background:conic-gradient(from 90deg,var(--c-cyan),var(--c-indigo),var(--c-violet),var(--c-cyan)); -webkit-background-clip:text; background-clip:text; text-shadow:0 0 18px rgba(124,77,255,.45);} .wrap{max-width:1200px;margin:0 auto;padding:16px} .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px} .card{background:linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.03)); border:1px solid var(--edge); border-radius:16px; padding:12px; box-shadow:0 14px 40px rgba(0,0,0,.35), inset 0 1px 0 rgba(255,255,255,.10)} .k{font-size:.92rem;color:var(--muted)} .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9rem} .btn{padding:6px 10px;border:1px solid #2a3245;border-radius:10px;background:linear-gradient(180deg,rgba(255,255,255,.12),rgba(255,255,255,.04));color:var(--ink);text-decoration:none} input,select,button{background:#141c31;color:var(--ink);border:1px solid #2a3245;border-radius:10px;padding:6px 8px} table{border-collapse:collapse;width:100%} th,td{border-bottom:1px solid #1a2444;padding:6px 8px} th{text-align:left;color:#bcd0ff} footer{padding:14px;color:var(--muted);text-align:center;border-top:1px solid var(--edge);margin-top:24px} nav.tabs{display:flex;gap:10px;margin-top:6px;flex-wrap:wrap} nav.tabs a{padding:6px 10px;border:1px solid var(--edge);border-radius:10px;background:rgba(255,255,255,.06);color:var(--ink);text-decoration:none} </style></head><body> <header><div class='console'> <h1 class='title'>".h((string)$CFG['APP_NAME'])."</h1> <nav class='tabs'> <a href='index.php'>Home</a> <a href='summary.php'>Summary</a> <a href='traffic.php'>Traffic</a> <a href='travel.php'>Travel</a> <a href='security.php'>Security</a> <a href='guardian.php'>Guardian</a> <a href='sitemap_tracker.php'>Sitemap</a> </nav> </div></header> <div class='wrap'>"; } function page_end(): void { echo "</div><footer>© ".date('Y')." Gaylord Sinclair — SIMON Guardian</footer></body></html>"; } /* ======================= Convenience helpers ======================= */ function newest_log_from_cfg(): ?string { global $CFG; return find_log($CFG['LOG_PATHS']); } function abbrev_after_htdocs(string $p): string { $needle = '/htdocs/'; $i = strpos($p, $needle); if ($i !== false) return '…' . substr($p, $i); return (strlen($p) > 80) ? '…' . substr($p, -80) : $p; }
Save file
Quick jump
open a path
Open