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,310
Folders
408
Scanned Size
3.84 GB
PHP Files
891
Editable Text Files
12,571
File viewer
guarded to /htdocs
/tools/site_map.php
<?php /** * /tools/site_map.php — SIMON Site Map + Audit Intelligence (FULL + LOG BLOCK AUDIT) * * Adds: * - Plan-based classification: public | private | blocked | unknown * - Issue flags: * - not_in_plan * - stale_year * - duplicate_index * - exposes_log * - log_file * - missing_log_htaccess * - Folder rollups (dirAgg) and top-level rollups (topAgg) * - Findings section + JSON export: ?json=1 * * Security: * - Protect /tools/ with Basic Auth or IP allowlist. */ header('Content-Type: text/html; charset=utf-8'); function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); } $root = rtrim((string)($_SERVER['DOCUMENT_ROOT'] ?? ''), '/'); if ($root === '' || !is_dir($root)) { http_response_code(500); echo 'DOCUMENT_ROOT not found.'; exit; } $host = (string)($_SERVER['HTTP_HOST'] ?? ''); $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $baseUrl = ($host !== '') ? ($scheme . '://' . $host) : ''; /* ----------------------------- SIMON NDJSON logger ------------------------------ */ function simon_log_dir(string $root): string { $d = $root . '/_logs'; if (is_dir($d) || @mkdir($d, 0755, true)) { return $d; } return sys_get_temp_dir(); } function simon_log_event(string $root, array $evt): void { $dir = simon_log_dir($root); $file = rtrim($dir, '/') . '/simon_site_map.ndjson'; $evt['ts'] = $evt['ts'] ?? gmdate('c'); $evt['ip'] = $evt['ip'] ?? (string)($_SERVER['REMOTE_ADDR'] ?? ''); $evt['ua'] = $evt['ua'] ?? (string)($_SERVER['HTTP_USER_AGENT'] ?? ''); $evt['rid'] = $evt['rid'] ?? bin2hex(random_bytes(8)); $line = json_encode($evt, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; @file_put_contents($file, $line, FILE_APPEND | LOCK_EX); } /* ----------------------------- Plan loader ------------------------------ */ function normalizePath(string $p): string { $p = str_replace('\\', '/', $p); $p = preg_replace('#/+#', '/', $p); return $p ?: '/'; } function load_plan(string $root): array { $path = $root . '/tools/site_plan.json'; $plan = [ 'expected_year' => (int)gmdate('Y'), 'public_roots' => ['/'], 'public_pages' => ['/index.php', '/sitemap.xml', '/robots.txt'], 'private_roots' => ['/tools/', '/_inc/', '/_secrets/', '/_logs/', '/orb/_inc/', '/sitelogs/'], 'blocked_roots' => ['/_secrets/', '/_logs/', '/sitelogs/'], 'ignore_patterns' => [], 'duplicate_index_patterns' => [], ]; if (is_file($path)) { $txt = @file_get_contents($path); $j = is_string($txt) ? json_decode($txt, true) : null; if (is_array($j)) { $plan = array_replace_recursive($plan, $j); } } foreach (['public_roots', 'public_pages', 'private_roots', 'blocked_roots'] as $k) { $plan[$k] = array_values(array_unique(array_map('normalizePath', (array)($plan[$k] ?? [])))); } $plan['expected_year'] = (int)($plan['expected_year'] ?? (int)gmdate('Y')); return $plan; } $PLAN = load_plan($root); function relPath(string $abs, string $root): string { $abs = normalizePath($abs); $root = normalizePath($root); if (strpos($abs, $root) === 0) { $r = substr($abs, strlen($root)); return normalizePath('/' . ltrim($r, '/')); } return $abs; } function extGroup(string $ext): string { $ext = strtolower($ext); if (in_array($ext, ['php'], true)) { return 'php'; } if (in_array($ext, ['html', 'htm'], true)) { return 'html'; } if (in_array($ext, ['js', 'mjs'], true)) { return 'js'; } if (in_array($ext, ['css'], true)) { return 'css'; } if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg', 'ico'], true)) { return 'img'; } if (in_array($ext, ['mp4', 'webm', 'mov', 'mp3', 'wav', 'm4a', 'ogg'], true)) { return 'media'; } if (in_array($ext, ['json', 'xml', 'txt', 'md', 'yml', 'yaml'], true)) { return 'data'; } return 'other'; } function fmtBytes(int $b): string { $u = ['B', 'KB', 'MB', 'GB', 'TB']; $i = 0; $v = (float)$b; while ($v >= 1024 && $i < count($u) - 1) { $v /= 1024; $i++; } return sprintf(($i === 0 ? '%d %s' : '%.2f %s'), $v, $u[$i]); } /* ----------------------------- Classification helpers ------------------------------ */ function starts_with_dir(string $path, string $dir): bool { $path = normalizePath($path); $dir = normalizePath($dir); if (!str_ends_with($dir, '/')) { $dir .= '/'; } return $path === rtrim($dir, '/') || str_starts_with($path, $dir); } function in_roots(string $path, array $roots): bool { foreach ($roots as $r) { if (starts_with_dir($path, $r)) { return true; } } return false; } function matches_any_pattern(string $path, array $patterns): bool { foreach ($patterns as $pat) { if (!is_string($pat) || $pat === '') { continue; } if (@preg_match($pat, $path)) { return true; } } return false; } function classify_path(string $path, array $PLAN): string { if (in_roots($path, (array)$PLAN['blocked_roots'])) { return 'blocked'; } if (in_roots($path, (array)$PLAN['private_roots'])) { return 'private'; } foreach ((array)$PLAN['public_pages'] as $p) { if (normalizePath($p) === normalizePath($path)) { return 'public'; } } if (in_roots($path, (array)$PLAN['public_roots'])) { return 'public'; } return 'unknown'; } /* ----------------------------- Log detection helpers Goal: BLOCK logs but not other files ------------------------------ */ function is_log_path(string $path): bool { $p = normalizePath($path); if (preg_match('#^/(?:_logs|sitelogs|web360/log)(?:/|$)#i', $p)) { return true; } if (preg_match('#^/(?:assets|assetts)/_logs(?:/|$)#i', $p)) { return true; } if (preg_match('#^/simon_ai/_logs(?:/|$)#i', $p)) { return true; } if (preg_match('#\.(?:ndjson|log)$#i', $p)) { return true; } return false; } function dir_has_htaccess(string $root, string $dirRel): bool { $dirRel = normalizePath($dirRel); if (!str_ends_with($dirRel, '/')) { $dirRel .= '/'; } $abs = rtrim($root, '/') . $dirRel; return is_dir($abs) && is_file($abs . '/.htaccess'); } function log_block_status(string $root, string $path): string { $p = normalizePath($path); $dir = ($p === '/' ? '/' : normalizePath(dirname($p))); if ($dir === '.' || $dir === '') { $dir = '/'; } if (dir_has_htaccess($root, $dir)) { return 'blocked_htaccess'; } return 'should_block'; } /* ----------------------------- Config ------------------------------ */ $MAX_FILES = 20000; $MAX_DEPTH = 25; $INCLUDE_HIDDEN = false; $FOLLOW_SYMLINKS = false; $excludeDirs = [ '/.git', '/.svn', '/.idea', '/vendor', '/node_modules', '/tmp', '/cache', ]; $excludeFiles = [ '/.htaccess', '/.htpasswd', '/wp-config.php', '/config.php', ]; $sort = (string)($_GET['sort'] ?? 'path'); // path|size|mtime|type $dirFilter = (string)($_GET['dir'] ?? ''); $q = trim((string)($_GET['q'] ?? '')); $only = (string)($_GET['only'] ?? ''); // php|html|js|css|img|media|data|other|all $jsonOut = isset($_GET['json']); $wantAudit = isset($_GET['audit']) || $jsonOut; /* ----------------------------- Scan target ------------------------------ */ $dirFilter = normalizePath('/' . ltrim($dirFilter, '/')); $scanBaseAbs = $root . ($dirFilter === '/' ? '' : $dirFilter); if (!is_dir($scanBaseAbs)) { $scanBaseAbs = $root; $dirFilter = '/'; } /* ----------------------------- Ignore via plan patterns ------------------------------ */ function isExcluded(string $rel, array $excludeDirs, array $excludeFiles, array $PLAN): bool { foreach ($excludeFiles as $f) { if ($rel === normalizePath($f)) { return true; } } foreach ($excludeDirs as $d) { $d = normalizePath($d); if ($rel === $d || str_starts_with($rel, $d . '/')) { return true; } } if (matches_any_pattern($rel, (array)($PLAN['ignore_patterns'] ?? []))) { return true; } return false; } /* ----------------------------- Locked detection + error capture ------------------------------ */ $lockedDirs = []; $lockedFiles = []; $scanErrors = []; set_error_handler(function ($severity, $message, $file, $line) use (&$scanErrors) { $scanErrors[] = [ 'op' => 'php_warning', 'path' => (string)$file, 'reason' => (string)$message, 'line' => (int)$line, 'severity' => (int)$severity, ]; return true; }); /* ----------------------------- Inventory scan ------------------------------ */ $files = []; $count = 0; $stack = [['abs' => $scanBaseAbs, 'depth' => 0]]; while ($stack) { $node = array_pop($stack); $absDir = (string)$node['abs']; $depth = (int)$node['depth']; if ($depth > $MAX_DEPTH) { continue; } $relDir = relPath($absDir, $root); if (isExcluded($relDir, $excludeDirs, $excludeFiles, $PLAN)) { continue; } if (!is_readable($absDir) || !is_executable($absDir)) { $reason = []; if (!is_readable($absDir)) { $reason[] = 'not_readable'; } if (!is_executable($absDir)) { $reason[] = 'not_executable'; } $lockedDirs[] = ['path' => normalizePath($relDir) . '/', 'reason' => implode('+', $reason) ?: 'permission']; $files[] = [ 'path' => normalizePath($relDir) . '/', 'type' => 'dir', 'size' => 0, 'mtime' => @filemtime($absDir) ?: 0, 'ext' => '', 'group' => 'dir', 'url' => '', 'locked' => true, 'lock_reason' => implode('+', $reason) ?: 'permission', ]; continue; } $items = @scandir($absDir); if (!is_array($items)) { $scanErrors[] = ['op' => 'scandir', 'path' => normalizePath($relDir) . '/', 'reason' => 'scandir_failed']; $lockedDirs[] = ['path' => normalizePath($relDir) . '/', 'reason' => 'scandir_failed']; $files[] = [ 'path' => normalizePath($relDir) . '/', 'type' => 'dir', 'size' => 0, 'mtime' => @filemtime($absDir) ?: 0, 'ext' => '', 'group' => 'dir', 'url' => '', 'locked' => true, 'lock_reason' => 'scandir_failed', ]; continue; } $files[] = [ 'path' => normalizePath($relDir) . '/', 'type' => 'dir', 'size' => 0, 'mtime' => @filemtime($absDir) ?: 0, 'ext' => '', 'group' => 'dir', 'url' => '', ]; foreach ($items as $name) { if ($name === '.' || $name === '..') { continue; } if (!$INCLUDE_HIDDEN && str_starts_with($name, '.')) { continue; } $abs = normalizePath($absDir . '/' . $name); $rel = relPath($abs, $root); if (isExcluded($rel, $excludeDirs, $excludeFiles, $PLAN)) { continue; } if (is_link($abs) && !$FOLLOW_SYMLINKS) { $files[] = [ 'path' => $rel, 'type' => 'symlink', 'size' => 0, 'mtime' => @filemtime($abs) ?: 0, 'ext' => '', 'group' => 'other', 'url' => '', ]; continue; } if (is_dir($abs)) { $stack[] = ['abs' => $abs, 'depth' => $depth + 1]; continue; } if (is_file($abs)) { $count++; if ($count > $MAX_FILES) { break 2; } $ext = pathinfo($abs, PATHINFO_EXTENSION) ?: ''; $group = extGroup($ext); if ($only !== '' && $only !== 'all') { if ($only === 'img') { if ($group !== 'img') { continue; } } else { if ($group !== $only) { continue; } } } if ($q !== '' && stripos($rel, $q) === false) { continue; } if (!is_readable($abs)) { $lockedFiles[] = ['path' => $rel, 'reason' => 'not_readable']; $files[] = [ 'path' => $rel, 'type' => 'file', 'size' => 0, 'mtime' => @filemtime($abs) ?: 0, 'ext' => strtolower((string)$ext), 'group' => $group, 'url' => '', 'locked' => true, 'lock_reason' => 'not_readable', ]; continue; } $size = @filesize($abs) ?: 0; $mtime = @filemtime($abs) ?: 0; $url = $baseUrl ? ($baseUrl . $rel) : $rel; $files[] = [ 'path' => $rel, 'type' => 'file', 'size' => $size, 'mtime' => $mtime, 'ext' => strtolower((string)$ext), 'group' => $group, 'url' => $url, ]; } } } restore_error_handler(); /* ----------------------------- Sort ------------------------------ */ usort($files, function ($a, $b) use ($sort) { $aSize = (int)($a['size'] ?? 0); $bSize = (int)($b['size'] ?? 0); $aM = (int)($a['mtime'] ?? 0); $bM = (int)($b['mtime'] ?? 0); $aP = (string)($a['path'] ?? ''); $bP = (string)($b['path'] ?? ''); $aG = (string)($a['group'] ?? ''); $bG = (string)($b['group'] ?? ''); if ($sort === 'size') { return ($bSize <=> $aSize) ?: strcmp($aP, $bP); } if ($sort === 'mtime') { return ($bM <=> $aM) ?: strcmp($aP, $bP); } if ($sort === 'type') { return strcmp($aG, $bG) ?: strcmp($aP, $bP); } return strcmp($aP, $bP); }); /* ----------------------------- Audit intelligence ------------------------------ */ $expectedYear = (int)($PLAN['expected_year'] ?? (int)gmdate('Y')); $byGroup = []; $totalSize = 0; $latest = []; $largest = []; $dirAgg = []; $topAgg = []; $issuesCount = [ 'not_in_plan' => 0, 'stale_year' => 0, 'duplicate_index' => 0, 'exposes_log' => 0, 'log_file' => 0, 'missing_log_htaccess' => 0, ]; $issuesTop = [ 'not_in_plan' => [], 'stale_year' => [], 'duplicate_index' => [], 'exposes_log' => [], 'log_file' => [], 'missing_log_htaccess' => [], ]; function top_level(string $path): string { $p = normalizePath($path); if ($p === '/' || $p === '') { return '/'; } $p = ltrim($p, '/'); $first = explode('/', $p, 2)[0] ?? ''; return $first === '' ? '/' : '/' . $first . '/'; } foreach ($files as &$f) { $g = $f['group'] ?? 'other'; $byGroup[$g] = ($byGroup[$g] ?? 0) + 1; $totalSize += (int)($f['size'] ?? 0); $path = (string)($f['path'] ?? ''); $type = (string)($f['type'] ?? ''); $isFile = ($type === 'file' && empty($f['locked'])); $class = classify_path($path, $PLAN); $f['class'] = $class; $dir = rtrim(dirname($path), '/'); if ($dir === '.' || $dir === '') { $dir = '/'; } $dirAgg[$dir]['count'] = ($dirAgg[$dir]['count'] ?? 0) + 1; $dirAgg[$dir]['size'] = ($dirAgg[$dir]['size'] ?? 0) + (int)($f['size'] ?? 0); $top = top_level($path); $topAgg[$top]['count'] = ($topAgg[$top]['count'] ?? 0) + 1; $topAgg[$top]['size'] = ($topAgg[$top]['size'] ?? 0) + (int)($f['size'] ?? 0); $topAgg[$top]['class_' . $class] = ($topAgg[$top]['class_' . $class] ?? 0) + 1; $f['issues'] = []; $f['plan_ok'] = true; if ($isFile && is_log_path($path)) { $f['class'] = 'blocked'; $f['issues'][] = 'log_file'; $f['plan_ok'] = false; $status = log_block_status($root, $path); $f['log_block_status'] = $status; if ($status !== 'blocked_htaccess') { $f['issues'][] = 'missing_log_htaccess'; } $topAgg[$top]['logs'] = ($topAgg[$top]['logs'] ?? 0) + 1; } if ($f['class'] === 'unknown' && $isFile) { $f['issues'][] = 'not_in_plan'; $f['plan_ok'] = false; } if ( $isFile && ( preg_match('#/(_logs|assetts/_logs|assets/_logs)/#i', $path) || preg_match('#\.ndjson$#i', $path) || preg_match('#\.log$#i', $path) ) ) { $f['issues'][] = 'exposes_log'; $f['plan_ok'] = false; } if ($isFile && matches_any_pattern($path, (array)($PLAN['duplicate_index_patterns'] ?? []))) { $f['issues'][] = 'duplicate_index'; $f['plan_ok'] = false; } if ( $wantAudit && $isFile && in_array((string)($f['group'] ?? ''), ['php', 'html'], true) && (int)($f['size'] ?? 0) > 0 && (int)($f['size'] ?? 0) <= 250000 ) { $abs = $root . $path; $txt = @file_get_contents($abs); if (is_string($txt) && $txt !== '') { if (preg_match_all('/\b(20\d{2})\b/', $txt, $m)) { $years = array_map('intval', $m[1]); $maxY = $years ? max($years) : 0; if ($maxY > 0 && $maxY < $expectedYear) { $f['issues'][] = 'stale_year'; $f['plan_ok'] = false; $f['year_max'] = $maxY; } } } } if ($isFile) { $latest[] = $f; $largest[] = $f; } foreach ($f['issues'] as $iss) { $issuesCount[$iss] = ($issuesCount[$iss] ?? 0) + 1; if (count($issuesTop[$iss]) < 50 && $isFile) { $issuesTop[$iss][] = $path; } } } unset($f); usort($latest, fn($a, $b) => ((int)$b['mtime'] <=> (int)$a['mtime']) ?: strcmp($a['path'], $b['path'])); usort($largest, fn($a, $b) => ((int)$b['size'] <=> (int)$a['size']) ?: strcmp($a['path'], $b['path'])); $hotDirs = []; foreach ($dirAgg as $d => $info) { $hotDirs[] = [ 'dir' => $d, 'count' => (int)$info['count'], 'size' => (int)$info['size'], ]; } usort($hotDirs, fn($a, $b) => ($b['size'] <=> $a['size']) ?: ($b['count'] <=> $a['count']) ?: strcmp($a['dir'], $b['dir'])); $topDirs = []; foreach ($topAgg as $d => $info) { $topDirs[] = [ 'top' => $d, 'count' => (int)($info['count'] ?? 0), 'size' => (int)($info['size'] ?? 0), 'public' => (int)($info['class_public'] ?? 0), 'private' => (int)($info['class_private'] ?? 0), 'blocked' => (int)($info['class_blocked'] ?? 0), 'unknown' => (int)($info['class_unknown'] ?? 0), 'logs' => (int)($info['logs'] ?? 0), ]; } usort($topDirs, fn($a, $b) => ($b['size'] <=> $a['size']) ?: ($b['count'] <=> $a['count']) ?: strcmp($a['top'], $b['top'])); /* ----------------------------- Findings ------------------------------ */ $missingLogHtDirs = []; foreach ($files as $f2) { if (($f2['type'] ?? '') !== 'file') { continue; } if (!empty($f2['locked'])) { continue; } $p2 = (string)($f2['path'] ?? ''); if (!is_log_path($p2)) { continue; } $status = (string)($f2['log_block_status'] ?? ''); if ($status === 'blocked_htaccess') { continue; } $d2 = normalizePath(dirname($p2)); if ($d2 === '.' || $d2 === '') { $d2 = '/'; } $missingLogHtDirs[$d2] = true; } $missingLogHtDirs = array_values(array_keys($missingLogHtDirs)); sort($missingLogHtDirs, SORT_STRING); /* ----------------------------- Log Intelligence events ------------------------------ */ simon_log_event($root, [ 'event' => 'site_map_view', 'dir' => $dirFilter, 'q' => $q, 'only' => $only ?: 'all', 'count' => count($files), ]); simon_log_event($root, [ 'event' => 'site_map_audit_findings', 'expected_year' => $expectedYear, 'counts' => [ 'items' => count($files), 'locked_dirs' => count($lockedDirs), 'locked_files' => count($lockedFiles), 'scan_errors' => count($scanErrors), 'issues' => $issuesCount, 'missing_log_htaccess_dirs' => count($missingLogHtDirs), ], 'top' => [ 'not_in_plan' => array_slice($issuesTop['not_in_plan'], 0, 25), 'stale_year' => array_slice($issuesTop['stale_year'], 0, 25), 'duplicate_index' => array_slice($issuesTop['duplicate_index'], 0, 25), 'exposes_log' => array_slice($issuesTop['exposes_log'], 0, 25), 'log_file' => array_slice($issuesTop['log_file'], 0, 25), 'missing_log_htaccess' => array_slice($issuesTop['missing_log_htaccess'], 0, 25), ], 'logs' => [ 'folders_missing_htaccess' => array_slice($missingLogHtDirs, 0, 50), ], ]); /* ----------------------------- JSON export ------------------------------ */ if ($jsonOut) { header('Content-Type: application/json; charset=utf-8'); echo json_encode([ 'ok' => true, 'root' => $root, 'base_url' => $baseUrl, 'dir' => $dirFilter, 'count' => count($files), 'plan' => [ 'expected_year' => $expectedYear, 'public_roots' => $PLAN['public_roots'], 'public_pages' => $PLAN['public_pages'], 'private_roots' => $PLAN['private_roots'], 'blocked_roots' => $PLAN['blocked_roots'], ], 'summary' => [ 'total_size' => $totalSize, 'by_group' => $byGroup, 'locked_dirs' => count($lockedDirs), 'locked_files' => count($lockedFiles), 'scan_errors' => count($scanErrors), 'issues' => $issuesCount, 'missing_log_htaccess_dirs' => count($missingLogHtDirs), ], 'findings' => [ 'missing_log_htaccess_dirs' => $missingLogHtDirs, ], 'insights' => [ 'largest_files' => array_slice($largest, 0, 25), 'latest_changes' => array_slice($latest, 0, 25), 'hot_directories' => array_slice($hotDirs, 0, 20), 'top_level_directories' => array_slice($topDirs, 0, 200), ], 'locked' => [ 'dirs' => $lockedDirs, 'files' => $lockedFiles, 'errors' => $scanErrors, ], 'files' => $files, ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit; } ?> <!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 Intelligence — Site Map (Full)</title> <meta name="theme-color" content="#0b1022"/> <style> :root{--bg:#070b16;--edge:rgba(255,255,255,.12);--ink:#eaf2ff;--muted:rgba(234,242,255,.70);--r:18px} *{box-sizing:border-box} body{margin:0;background:radial-gradient(1200px 700px at 50% 0%, rgba(107,77,255,.16), transparent 60%),radial-gradient(900px 600px at 10% 40%, rgba(106,224,255,.10), transparent 60%),var(--bg);color:var(--ink);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial} .wrap{max-width:1280px;margin:22px auto;padding:0 14px} .card{border:1px solid var(--edge);background:linear-gradient(180deg, rgba(255,255,255,.07), rgba(255,255,255,.03));border-radius:var(--r);padding:14px;box-shadow:0 20px 70px rgba(0,0,0,.55)} .row{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px} .pill{padding:8px 10px;border-radius:999px;border:1px solid var(--edge);background:rgba(0,0,0,.22);font-size:12px;color:var(--muted)} h2{margin:0 0 10px;font-size:13px;letter-spacing:.10em;text-transform:uppercase;color:rgba(234,242,255,.82)} .meta{color:var(--muted);font-size:13px;line-height:1.5} input,select{background:rgba(0,0,0,.25);border:1px solid var(--edge);color:var(--ink);border-radius:12px;padding:10px 12px;font-size:13px} button,.btn{background:rgba(107,77,255,.20);border:1px solid rgba(107,77,255,.45);color:var(--ink);border-radius:12px;padding:10px 12px;font-size:13px;cursor:pointer;text-decoration:none;display:inline-block} table{width:100%;border-collapse:collapse;margin-top:12px;overflow:hidden;border-radius:14px} th,td{padding:10px;border-bottom:1px solid rgba(255,255,255,.10);font-size:13px;vertical-align:top} th{text-align:left;color:rgba(234,242,255,.82);letter-spacing:.08em;text-transform:uppercase;font-size:11px} .path{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;word-break:break-all} .tag{padding:3px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(0,0,0,.18);color:rgba(234,242,255,.78);font-size:11px} .tag.lock{border-color:rgba(255,120,120,.35);background:rgba(255,120,120,.10)} .tag.public{border-color:rgba(120,255,180,.30)} .tag.private{border-color:rgba(255,220,120,.30)} .tag.blocked{border-color:rgba(255,120,120,.45)} .tag.unknown{border-color:rgba(170,170,255,.30)} .issues{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px} .issue{padding:2px 8px;border-radius:999px;border:1px solid rgba(255,80,160,.30);background:rgba(255,80,160,.10);font-size:11px;color:rgba(234,242,255,.85)} .issue.log_file{border-color:rgba(255,80,80,.45)} .issue.missing_log_htaccess{border-color:rgba(255,180,80,.45)} .controls{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px} code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace} </style> </head> <body> <div class="wrap"> <div class="card"> <h2>SIMON Site Map (Full) + Audit + Log Block Audit</h2> <div class="meta"> Root: <span class="path"><?= h($root) ?></span><br/> Base URL: <span class="path"><?= h($baseUrl ?: '(unknown)') ?></span><br/> Scanning: <span class="path"><?= h($dirFilter) ?></span><br/> Expected Year: <span class="path"><?= h((string)$expectedYear) ?></span><br/> NDJSON: <span class="path">/_logs/simon_site_map.ndjson</span> </div> <div class="row"> <div class="pill">Items: <b><?= h((string)count($files)) ?></b></div> <div class="pill">Total size: <b><?= h(fmtBytes($totalSize)) ?></b></div> <div class="pill">Locked dirs: <b><?= h((string)count($lockedDirs)) ?></b></div> <div class="pill">Locked files: <b><?= h((string)count($lockedFiles)) ?></b></div> <div class="pill">Scan errors: <b><?= h((string)count($scanErrors)) ?></b></div> <div class="pill">Issues: <b><?= h((string)array_sum($issuesCount)) ?></b></div> </div> <div class="row"> <div class="pill">not_in_plan: <b><?= h((string)$issuesCount['not_in_plan']) ?></b></div> <div class="pill">stale_year: <b><?= h((string)$issuesCount['stale_year']) ?></b></div> <div class="pill">duplicate_index: <b><?= h((string)$issuesCount['duplicate_index']) ?></b></div> <div class="pill">log_file: <b><?= h((string)$issuesCount['log_file']) ?></b></div> <div class="pill">missing_log_htaccess: <b><?= h((string)$issuesCount['missing_log_htaccess']) ?></b></div> <a class="btn" href="/tools/site_map_summary.php">Open Folder Summary</a> </div> <form class="controls" method="get" action=""> <input name="dir" placeholder="dir filter (e.g. simon_ai)" value="<?= h($dirFilter === '/' ? '' : ltrim($dirFilter, '/')) ?>" /> <input name="q" placeholder="search path contains..." value="<?= h($q) ?>" /> <select name="only"> <?php $opts = ['all'=>'all','php'=>'php','html'=>'html','js'=>'js','css'=>'css','img'=>'img','media'=>'media','data'=>'data','other'=>'other']; foreach ($opts as $k=>$v) { $sel = ($only === $k) ? 'selected' : ''; echo '<option value="' . h($k) . '" ' . $sel . '>' . h($v) . '</option>'; } ?> </select> <select name="sort"> <?php $sopts = ['path'=>'path','size'=>'size','mtime'=>'mtime','type'=>'type']; foreach ($sopts as $k=>$v) { $sel = ($sort === $k) ? 'selected' : ''; echo '<option value="' . h($k) . '" ' . $sel . '>' . h($v) . '</option>'; } ?> </select> <button type="submit">Refresh</button> <a class="btn" href="?<?= h(http_build_query(array_filter([ 'dir' => $dirFilter === '/' ? '' : ltrim($dirFilter, '/'), 'q' => $q ?: null, 'only' => $only ?: null, 'sort' => $sort ?: null, 'audit' => 1, 'json' => 1, ], fn($v) => $v !== null && $v !== ''))) ?>">Export Audit JSON</a> </form> <h2 style="margin-top:14px;">Findings</h2> <div class="meta"> Log folders missing <code>.htaccess</code> (should be blocked): <?php if (!$missingLogHtDirs): ?> <b>none</b> <?php else: ?> <ul> <?php foreach (array_slice($missingLogHtDirs, 0, 30) as $d): ?> <li class="path"><?= h($d) ?>/</li> <?php endforeach; ?> </ul> <div class="meta">Fix: add a <code>.htaccess</code> inside each folder with “Require all denied”.</div> <?php endif; ?> </div> <h2 style="margin-top:14px;">Top-level directory rollup</h2> <table> <thead> <tr> <th>Top Folder</th><th>Items</th><th>Size</th><th>Public</th><th>Private</th><th>Blocked</th><th>Unknown</th><th>Logs</th><th>Drill</th> </tr> </thead> <tbody> <?php foreach (array_slice($topDirs, 0, 60) as $t): ?> <tr> <td class="path"><?= h($t['top']) ?></td> <td><?= h((string)$t['count']) ?></td> <td><?= h(fmtBytes((int)$t['size'])) ?></td> <td><?= h((string)$t['public']) ?></td> <td><?= h((string)$t['private']) ?></td> <td><?= h((string)$t['blocked']) ?></td> <td><?= h((string)$t['unknown']) ?></td> <td><?= h((string)$t['logs']) ?></td> <td><a class="btn" href="/tools/site_map.php?dir=<?= h(urlencode(trim($t['top'], '/'))) ?>">open</a></td> </tr> <?php endforeach; ?> </tbody> </table> <h2 style="margin-top:14px;">Full inventory</h2> <table> <thead> <tr> <th>Path</th><th>Class</th><th>Status</th><th>Size</th><th>Modified</th><th>URL</th> </tr> </thead> <tbody> <?php foreach ($files as $f): ?> <tr> <td class="path"><?= h((string)$f['path']) ?></td> <td> <?php $cls = (string)($f['class'] ?? 'unknown'); echo '<span class="tag ' . h($cls) . '">' . h(strtoupper($cls)) . '</span>'; ?> <?php if (!empty($f['issues'])): ?> <div class="issues"> <?php foreach ((array)$f['issues'] as $iss): ?> <span class="issue <?= h((string)$iss) ?>"><?= h((string)$iss) ?></span> <?php endforeach; ?> </div> <?php endif; ?> <?php if (!empty($f['log_block_status'])): ?> <div class="meta">log_block_status: <span class="path"><?= h((string)$f['log_block_status']) ?></span></div> <?php endif; ?> <?php if (!empty($f['year_max'])): ?> <div class="meta">max_year_found: <span class="path"><?= h((string)$f['year_max']) ?></span></div> <?php endif; ?> </td> <td> <?php if (!empty($f['locked'])): ?> <span class="tag lock">LOCKED</span> <small><?= h((string)($f['lock_reason'] ?? '')) ?></small> <?php else: ?> <span class="tag"><?= h(strtoupper((string)($f['group'] ?? 'OTHER'))) ?></span> <small><?= h((string)($f['type'] ?? '')) ?></small> <?php endif; ?> </td> <td><?= (($f['type'] ?? '') === 'file' && empty($f['locked'])) ? h(fmtBytes((int)($f['size'] ?? 0))) : '—' ?></td> <td><?= !empty($f['mtime']) ? h(date('Y-m-d H:i:s', (int)$f['mtime'])) : '—' ?></td> <td> <?php if (!empty($f['url'])): ?> <a href="<?= h((string)$f['url']) ?>" target="_blank" rel="noopener">open</a> <?php else: ?>—<?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <div class="meta" style="margin-top:12px;"> Classification uses <span class="path">/tools/site_plan.json</span>. Unknown files are “not in master plan” candidates.<br/> Logs are forced to <b>BLOCKED</b> by detection (folders like <code>/_logs</code>, <code>/assetts/_logs</code>, <code>/sitelogs</code>, <code>/web360/log</code>, and extensions <code>.log</code>/<code>.ndjson</code>). </div> </div> </div> </body> </html>
Save file
Quick jump
open a path
Open