diff options
| author | Agando <roduccomm@gmail.com> | 2026-07-29 20:46:12 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-07-29 20:46:12 +0200 |
| commit | 19b8da0faccfc4438ed19fbc99d992a6a07af003 (patch) | |
| tree | 8bcc4112a20b57a8b03a360097b20101b75e7b4a | |
| parent | 2538c91876455fe96febf441fc8dde20f2606023 (diff) | |
auto: 2026-07-29_20:50 changes — gunes sponsorluk page, bunq payment modal, AI platforms, news W30
| -rw-r--r-- | api/debug/clusters.php | 98 | ||||
| -rw-r--r-- | api/debug/config.php | 59 | ||||
| -rw-r--r-- | api/debug/dashboard.php | 200 | ||||
| -rw-r--r-- | api/debug/error.php | 36 | ||||
| -rw-r--r-- | api/debug/errors.php | 43 | ||||
| -rw-r--r-- | api/debug/health.php | 20 | ||||
| -rw-r--r-- | api/debug/ingest.php | 117 | ||||
| -rw-r--r-- | api/debug/stats.php | 55 | ||||
| -rw-r--r-- | main.html | 2 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 153 | ||||
| -rw-r--r-- | modules/core/module-debug-supervisor.js | 153 | ||||
| -rw-r--r-- | review-newos4.html | 20 |
12 files changed, 925 insertions, 31 deletions
diff --git a/api/debug/clusters.php b/api/debug/clusters.php new file mode 100644 index 0000000..5174317 --- /dev/null +++ b/api/debug/clusters.php @@ -0,0 +1,98 @@ +<?php +require_once __DIR__ . '/config.php'; + +$errors = load_json(ERRORS_FILE); +$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters'; + +// Check if cached clusters exist and are fresh +if (file_exists(CLUSTERS_FILE) && !file_exists($rebuildFlag)) { + $clusters = load_json(CLUSTERS_FILE); + json_response([ + 'success' => true, + 'cached' => true, + 'clusters' => $clusters, + 'total_errors' => count($errors), + ]); +} + +// Rebuild clusters +$normalized = []; +foreach ($errors as $e) { + $msg = strtolower($e['message'] ?? ''); + $msg = preg_replace('/\b0x[a-f0-9]+\b/', '0xHEX', $msg); + $msg = preg_replace('/\b\d+\b/', 'N', $msg); + $msg = preg_replace('/["\'].*?["\']/', '"STR"', $msg); + $msg = preg_replace('/[^\w\s]/', ' ', $msg); + $msg = preg_replace('/\s+/', ' ', $msg); + $normalized[] = trim($msg); +} + +$clusters = []; +$assigned = []; +$threshold = CLUSTER_SIMILARITY_THRESHOLD; + +for ($i = 0; $i < count($errors); $i++) { + if (isset($assigned[$i])) continue; + + $cluster = [ + 'errors' => [$i], + 'messages' => [$errors[$i]['message']], + 'count' => $errors[$i]['count'] ?? 1, + 'types' => [$errors[$i]['type'] ?? 'unknown'], + 'categories' => [$errors[$i]['category'] ?? 'runtime'], + 'label' => $errors[$i]['message'] ?? '', + ]; + $assigned[$i] = true; + + for ($j = $i + 1; $j < count($errors); $j++) { + if (isset($assigned[$j])) continue; + + // Jaccard similarity + $tokensA = array_unique(array_filter(explode(' ', $normalized[$i]))); + $tokensB = array_unique(array_filter(explode(' ', $normalized[$j]))); + if (empty($tokensA) && empty($tokensB)) { + $similarity = 1; + } else { + $intersection = count(array_intersect($tokensA, $tokensB)); + $union = count($tokensA) + count($tokensB) - $intersection; + $similarity = $union > 0 ? $intersection / $union : 0; + } + + if ($similarity >= $threshold) { + $cluster['errors'][] = $j; + $cluster['messages'][] = $errors[$j]['message']; + $cluster['count'] += $errors[$j]['count'] ?? 1; + if (!in_array($errors[$j]['type'] ?? 'unknown', $cluster['types'])) { + $cluster['types'][] = $errors[$j]['type'] ?? 'unknown'; + } + if (!in_array($errors[$j]['category'] ?? 'runtime', $cluster['categories'])) { + $cluster['categories'][] = $errors[$j]['category'] ?? 'runtime'; + } + $assigned[$j] = true; + } + } + + // Most frequent message as label + $freq = array_count_values($cluster['messages']); + arsort($freq); + $cluster['label'] = substr(key($freq) ?: $cluster['label'], 0, 200); + $cluster['error_count'] = count($cluster['errors']); + unset($cluster['errors']); + unset($cluster['messages']); + + $clusters[] = $cluster; +} + +usort($clusters, fn($a, $b) => $b['count'] - $a['count']); + +// Cache +save_json(CLUSTERS_FILE, $clusters); +if (file_exists($rebuildFlag)) unlink($rebuildFlag); + +json_response([ + 'success' => true, + 'cached' => false, + 'clusters' => array_slice($clusters, 0, 50), + 'total_errors' => count($errors), + 'total_clusters' => count($clusters), +]); diff --git a/api/debug/config.php b/api/debug/config.php new file mode 100644 index 0000000..4a2bd05 --- /dev/null +++ b/api/debug/config.php @@ -0,0 +1,59 @@ +<?php +// api/debug/config.php — Shared config for debug backend +header('Content-Type: application/json; charset=utf-8'); +header('Access-Control-Allow-Origin: https://webx11.duckdns.org'); +header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); +header('Access-Control-Allow-Headers: Content-Type'); +header('Access-Control-Max-Age: 86400'); + +// Handle preflight +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(204); + exit; +} + +date_default_timezone_set('UTC'); + +define('DEBUG_DATA_DIR', __DIR__ . '/../../data/debug'); +define('MAX_ERRORS_PER_BATCH', 100); +define('MAX_BATCH_SIZE_KB', 64); +define('CLUSTER_SIMILARITY_THRESHOLD', 0.35); +define('ERRORS_FILE', DEBUG_DATA_DIR . '/errors.json'); +define('SESSIONS_FILE', DEBUG_DATA_DIR . '/sessions.json'); +define('CLUSTERS_FILE', DEBUG_DATA_DIR . '/clusters.json'); + +if (!is_dir(DEBUG_DATA_DIR)) { + mkdir(DEBUG_DATA_DIR, 0755, true); +} + +function load_json($path) { + if (!file_exists($path)) return []; + $data = json_decode(file_get_contents($path), true); + return is_array($data) ? $data : []; +} + +function save_json($path, $data) { + $encoded = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $fp = fopen($path, 'c'); + if ($fp && flock($fp, LOCK_EX)) { + ftruncate($fp, 0); + fwrite($fp, $encoded); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + } else { + // Fallback: write without lock + file_put_contents($path, $encoded); + } + @chmod($path, 0664); +} + +function json_response($data, $code = 200) { + http_response_code($code); + echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +function error_response($msg, $code = 400) { + json_response(['success' => false, 'error' => $msg], $code); +} diff --git a/api/debug/dashboard.php b/api/debug/dashboard.php new file mode 100644 index 0000000..7a46661 --- /dev/null +++ b/api/debug/dashboard.php @@ -0,0 +1,200 @@ +<?php +require_once __DIR__ . '/config.php'; +header('Content-Type: text/html; charset=utf-8'); + +$errors = load_json(ERRORS_FILE); +$sessions = load_json(SESSIONS_FILE); + +$totalErrors = count($errors); +$totalSessions = count($sessions); +$byType = []; +$bySeverity = []; +$byCategory = []; +$byModule = []; +$byDay = []; +$topErrors = []; + +foreach ($errors as $e) { + $t = $e['type'] ?? 'unknown'; + $byType[$t] = ($byType[$t] ?? 0) + ($e['count'] ?? 1); + $s = $e['severity'] ?? 'error'; + $bySeverity[$s] = ($bySeverity[$s] ?? 0) + ($e['count'] ?? 1); + $m = $e['module'] ?? 'unknown'; + $byModule[$m] = ($byModule[$m] ?? 0) + ($e['count'] ?? 1); + $c = $e['category'] ?? 'runtime'; + $byCategory[$c] = ($byCategory[$c] ?? 0) + ($e['count'] ?? 1); + $day = substr($e['ingested_at'] ?? $e['last_seen'], 0, 10); + $byDay[$day] = ($byDay[$day] ?? 0) + ($e['count'] ?? 1); + + $hash = $e['hash'] ?? ''; + if ($hash) { + if (!isset($topErrors[$hash])) { + $topErrors[$hash] = ['hash' => $hash, 'message' => $e['message'] ?? '', 'count' => 0, 'type' => $t]; + } + $topErrors[$hash]['count'] += $e['count'] ?? 1; + } +} +usort($topErrors, fn($a, $b) => $b['count'] - $a['count']); +$topErrors = array_slice($topErrors, 0, 10); +ksort($byDay); + +$recentSessions = array_slice($sessions, -10, 10, true); +?> +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>newOS4 Debug Dashboard</title> +<style> + :root { --bg: #0d0d12; --surface: #16161e; --border: #1e1e2a; --text: #e8e8f0; --muted: #888; --accent: #00e5c8; --error: #ff5555; --warn: #ffaa00; --info: #00aaff; } + * { box-sizing: border-box; margin: 0; padding: 0; } + body { background: var(--bg); color: var(--text); font-family: 'Courier New', monospace; padding: 20px; } + h1 { color: var(--accent); font-size: 1.4em; margin-bottom: 4px; } + .subtitle { color: var(--muted); font-size: 0.85em; margin-bottom: 20px; } + .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-bottom: 20px; } + .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px; } + .card h3 { font-size: 0.8em; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; } + .stat-value { font-size: 2em; font-weight: bold; } + .stat-value.error { color: var(--error); } + .stat-value.warn { color: var(--warn); } + .stat-value.ok { color: var(--accent); } + table { width: 100%; border-collapse: collapse; font-size: 0.85em; } + th { text-align: left; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--border); font-size: 0.8em; text-transform: uppercase; } + td { padding: 6px 8px; border-bottom: 1px solid var(--border); } + .bar { display: inline-block; height: 6px; border-radius: 3px; background: var(--accent); margin-right: 6px; vertical-align: middle; } + .bar.error { background: var(--error); } + .bar.warn { background: var(--warn); } + .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.75em; background: var(--border); } + .tag.error { background: #3a1010; color: var(--error); } + .tag.warn { background: #3a2a00; color: var(--warn); } + .tag.ok { background: #002a20; color: var(--accent); } + .refresh { color: var(--muted); font-size: 0.8em; margin-top: 20px; text-align: center; } + a { color: var(--accent); text-decoration: none; } + a:hover { text-decoration: underline; } + .max-w-300 { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +</style> +</head> +<body> +<h1>🔍 newOS4 Debug Dashboard</h1> +<div class="subtitle">Server-side error aggregation · <?= date('Y-m-d H:i:s') ?> · <a href="stats.php">JSON Stats</a> · <a href="clusters.php">Clusters</a> · <a href="errors.php">Raw Errors</a> · <a href="health.php">Health</a></div> + +<?php +// Find last sync time +$lastSync = null; +if (!empty($errors)) { + $lastSync = end($errors)['ingested_at'] ?? null; +} +if ($lastSync): ?> +<div style="color:var(--muted);font-size:0.8em;margin-bottom:16px;">Last sync: <?= htmlspecialchars(substr($lastSync, 0, 19)) ?> UTC</div> +<?php endif; ?> + +<div class="grid"> + <div class="card"> + <h3>Total Errors</h3> + <div class="stat-value <?= $totalErrors > 0 ? 'error' : 'ok' ?>"><?= $totalErrors ?></div> + </div> + <div class="card"> + <h3>Sessions</h3> + <div class="stat-value"><?= $totalSessions ?></div> + </div> + <div class="card"> + <h3>Error Types</h3> + <div class="stat-value"><?= count($byType) ?></div> + </div> + <div class="card"> + <h3>Modules Affected</h3> + <div class="stat-value"><?= count($byModule) ?></div> + </div> +</div> + +<div class="grid"> + <div class="card"> + <h3>By Severity</h3> + <table> + <?php foreach (['fatal' => 'error', 'error' => 'error', 'warning' => 'warn'] as $sev => $cls): ?> + <tr><td><span class="tag <?= $cls ?>"><?= $sev ?></span></td><td><?= $bySeverity[$sev] ?? 0 ?></td></tr> + <?php endforeach; ?> + </table> + </div> + <div class="card"> + <h3>By Category</h3> + <table> + <?php foreach (['network', 'module', 'runtime', 'vfs', 'security', 'tool'] as $cat): ?> + <?php if (($byCategory[$cat] ?? 0) > 0): ?> + <tr><td><?= $cat ?></td><td><?= $byCategory[$cat] ?? 0 ?></td></tr> + <?php endif; ?> + <?php endforeach; ?> + </table> + </div> + <div class="card"> + <h3>By Module</h3> + <table> + <?php + arsort($byModule); + $i = 0; + foreach (array_slice($byModule, 0, 8) as $mod => $count): + if ($i++ > 5) break; + ?> + <tr><td class="max-w-300"><?= htmlspecialchars($mod) ?></td><td><?= $count ?></td></tr> + <?php endforeach; ?> + </table> + </div> +</div> + +<div class="card" style="margin-bottom:20px;"> + <h3>Top Errors</h3> + <table> + <tr><th>#</th><th>Message</th><th>Type</th><th>Count</th></tr> + <?php $i = 1; foreach ($topErrors as $e): ?> + <tr> + <td><?= $i++ ?></td> + <td class="max-w-300"><a href="error.php?hash=<?= urlencode($e['hash'] ?? '') ?>" style="color:var(--text);text-decoration:none;" title="View details"><?= htmlspecialchars(substr($e['message'] ?? '', 0, 80)) ?></a></td> + <td><span class="tag"><?= htmlspecialchars($e['type'] ?? '?') ?></span></td> + <td><?= $e['count'] ?></td> + </tr> + <?php endforeach; ?> + <?php if (empty($topErrors)): ?> + <tr><td colspan="4" style="color:var(--muted);text-align:center;">No errors recorded</td></tr> + <?php endif; ?> + </table> +</div> + +<div class="card" style="margin-bottom:20px;"> + <h3>Daily Trend (last 14 days)</h3> + <table> + <tr><th>Day</th><th>Errors</th><th></th></tr> + <?php + $maxDay = max($byDay) ?: 1; + $days = array_slice($byDay, -14, 14, true); + foreach ($days as $day => $count): + $pct = round($count / $maxDay * 100, 0); + ?> + <tr> + <td><?= $day ?></td> + <td><?= $count ?></td> + <td><div class="bar" style="width:<?= $pct ?>%;"></div></td> + </tr> + <?php endforeach; ?> + </table> +</div> + +<div class="card"> + <h3>Recent Sessions</h3> + <table> + <tr><th>Session</th><th>Version</th><th>Errors</th><th>Last Seen</th></tr> + <?php foreach ($recentSessions as $sid => $s): ?> + <tr> + <td class="max-w-300"><a href="errors.php?session=<?= urlencode($sid) ?>" style="color:var(--text);text-decoration:none;" title="View this session's errors"><?= htmlspecialchars($sid) ?></a></td> + <td><?= htmlspecialchars($s['version'] ?? '?') ?></td> + <td><?= $s['error_count'] ?? 0 ?></td> + <td><?= htmlspecialchars(substr($s['last_seen'] ?? '', 0, 16)) ?></td> + </tr> + <?php endforeach; ?> + </table> +</div> + +<div class="refresh">Auto-refresh every 30s</div> +<script>setTimeout(() => location.reload(), 30000);</script> +</body> +</html> diff --git a/api/debug/error.php b/api/debug/error.php new file mode 100644 index 0000000..242dfca --- /dev/null +++ b/api/debug/error.php @@ -0,0 +1,36 @@ +<?php +require_once __DIR__ . '/config.php'; + +$hash = $_GET['hash'] ?? ''; +if (!$hash) error_response('Missing hash parameter'); + +$errors = load_json(ERRORS_FILE); +$sessions = load_json(SESSIONS_FILE); + +$matches = array_filter($errors, fn($e) => ($e['hash'] ?? '') === $hash); +$matches = array_values($matches); + +if (empty($matches)) { + error_response('Error not found', 404); +} + +$totalCount = array_sum(array_column($matches, 'count')); +$uniqueSessions = array_unique(array_column($matches, 'session_id')); +$firstSeen = min(array_column($matches, 'first_seen')); +$lastSeen = max(array_column($matches, 'last_seen')); + +json_response([ + 'success' => true, + 'hash' => $hash, + 'total_occurrences' => count($matches), + 'total_count' => $totalCount, + 'unique_sessions' => count($uniqueSessions), + 'first_seen' => $firstSeen, + 'last_seen' => $lastSeen, + 'type' => $matches[0]['type'] ?? 'unknown', + 'severity' => $matches[0]['severity'] ?? 'error', + 'category' => $matches[0]['category'] ?? 'runtime', + 'sample_message' => $matches[0]['message'] ?? '', + 'sample_stack' => $matches[0]['stack'] ?? null, + 'occurrences' => array_slice($matches, 0, 20), +]); diff --git a/api/debug/errors.php b/api/debug/errors.php new file mode 100644 index 0000000..1ed9a2f --- /dev/null +++ b/api/debug/errors.php @@ -0,0 +1,43 @@ +<?php +require_once __DIR__ . '/config.php'; + +$errors = load_json(ERRORS_FILE); + +// Filters +$type = $_GET['type'] ?? null; +$severity = $_GET['severity'] ?? null; +$module = $_GET['module'] ?? null; +$category = $_GET['category'] ?? null; +$search = $_GET['search'] ?? null; +$from = $_GET['from'] ?? null; +$to = $_GET['to'] ?? null; +$session = $_GET['session'] ?? null; +$page = max(1, (int)($_GET['page'] ?? 1)); +$perPage = min(100, max(1, (int)($_GET['per_page'] ?? 50))); + +$filtered = array_filter($errors, function($e) use ($type, $severity, $module, $category, $search, $from, $to, $session) { + if ($type && ($e['type'] ?? '') !== $type) return false; + if ($severity && ($e['severity'] ?? '') !== $severity) return false; + if ($module && ($e['module'] ?? '') !== $module) return false; + if ($category && ($e['category'] ?? '') !== $category) return false; + if ($session && ($e['session_id'] ?? '') !== $session) return false; + if ($search && stripos($e['message'] ?? '', $search) === false) return false; + if ($from && ($e['ingested_at'] ?? '') < $from) return false; + if ($to && ($e['ingested_at'] ?? '') > $to) return false; + return true; +}); + +$filtered = array_values($filtered); +$total = count($filtered); +$totalPages = max(1, ceil($total / $perPage)); +$offset = ($page - 1) * $perPage; +$pageErrors = array_slice($filtered, $offset, $perPage); + +json_response([ + 'success' => true, + 'total' => $total, + 'page' => $page, + 'per_page' => $perPage, + 'total_pages' => $totalPages, + 'errors' => $pageErrors, +]); diff --git a/api/debug/health.php b/api/debug/health.php new file mode 100644 index 0000000..5a0048f --- /dev/null +++ b/api/debug/health.php @@ -0,0 +1,20 @@ +<?php +require_once __DIR__ . '/config.php'; + +$errors = load_json(ERRORS_FILE); +$sessions = load_json(SESSIONS_FILE); + +$lastError = !empty($errors) ? end($errors) : null; + +json_response([ + 'success' => true, + 'status' => 'ok', + 'uptime' => filemtime(__FILE__) ? (time() - filemtime(__FILE__)) : 0, + 'total_errors' => count($errors), + 'total_sessions' => count($sessions), + 'errors_file_size_kb' => file_exists(ERRORS_FILE) ? round(filesize(ERRORS_FILE) / 1024, 1) : 0, + 'sessions_file_size_kb' => file_exists(SESSIONS_FILE) ? round(filesize(SESSIONS_FILE) / 1024, 1) : 0, + 'last_ingest' => $lastError ? ($lastError['ingested_at'] ?? null) : null, + 'last_ingest_message' => $lastError ? substr($lastError['message'] ?? '', 0, 100) : null, + 'generated_at' => date('c'), +]); diff --git a/api/debug/ingest.php b/api/debug/ingest.php new file mode 100644 index 0000000..363dd09 --- /dev/null +++ b/api/debug/ingest.php @@ -0,0 +1,117 @@ +<?php +require_once __DIR__ . '/config.php'; + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + error_response('Method not allowed', 405); +} + +$raw = file_get_contents('php://input'); +if (!$raw) error_response('Empty request body'); + +$payload = json_decode($raw, true); +if (!$payload || !isset($payload['errors']) || !is_array($payload['errors'])) { + error_response('Invalid payload: expected { errors: [...], session: {...} }'); +} + +$errors = $payload['errors']; +$session = $payload['session'] ?? []; +$clientVersion = $payload['version'] ?? 'unknown'; + +// Validate batch size +if (count($errors) > MAX_ERRORS_PER_BATCH) { + $errors = array_slice($errors, 0, MAX_ERRORS_PER_BATCH); +} +if (strlen($raw) > MAX_BATCH_SIZE_KB * 1024) { + error_response('Payload too large (max ' . MAX_BATCH_SIZE_KB . 'KB)', 413); +} + +// Load existing data +$allErrors = load_json(ERRORS_FILE); +$sessions = load_json(SESSIONS_FILE); + +$sessionId = $session['id'] ?? ('anon_' . bin2hex(random_bytes(4))); +$ingested = 0; +$now = date('c'); + +// Update session metadata +if (!isset($sessions[$sessionId])) { + $sessions[$sessionId] = [ + 'id' => $sessionId, + 'first_seen' => $now, + 'version' => $clientVersion, + 'modules' => $session['modules'] ?? [], + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', + 'error_count' => 0, + ]; +} +$sessions[$sessionId]['last_seen'] = $now; +$sessions[$sessionId]['error_count'] += count($errors); +$sessions[$sessionId]['version'] = $clientVersion; + +// Build a lookup of existing hashes for dedup +$existingHashes = []; +foreach ($allErrors as $idx => $existing) { + $h = $existing['hash'] ?? ''; + if ($h) { + $existingHashes[$h] = $idx; + } +} + +// Ingest each error (dedup by hash) +foreach ($errors as $e) { + $hash = $e['hash'] ?? md5(($e['message'] ?? '') . '|' . ($e['source'] ?? '') . '|' . ($e['type'] ?? '')); + + if (isset($existingHashes[$hash])) { + // Update existing entry + $idx = $existingHashes[$hash]; + $allErrors[$idx]['count'] += $e['count'] ?? 1; + $allErrors[$idx]['last_seen'] = $e['lastSeen'] ?? $now; + $allErrors[$idx]['ingested_at'] = $now; + if (!empty($e['stack'])) { + $allErrors[$idx]['stack'] = substr($e['stack'], 0, 2000); + } + $ingested++; + continue; + } + + $entry = [ + 'hash' => $hash, + 'message' => substr($e['message'] ?? '', 0, 500), + 'type' => $e['type'] ?? 'unknown', + 'severity' => $e['severity'] ?? 'error', + 'category' => $e['category'] ?? 'runtime', + 'source' => $e['source'] ?? '', + 'module' => $e['module'] ?? null, + 'stack' => isset($e['stack']) ? substr($e['stack'], 0, 2000) : null, + 'session_id' => $sessionId, + 'count' => $e['count'] ?? 1, + 'first_seen' => $e['firstSeen'] ?? $now, + 'last_seen' => $e['lastSeen'] ?? $now, + 'ingested_at' => $now, + ]; + + $allErrors[] = $entry; + $existingHashes[$hash] = count($allErrors) - 1; + $ingested++; +} + +// Trim to last 10,000 errors (ring buffer) +if (count($allErrors) > 10000) { + $allErrors = array_slice($allErrors, -10000); +} + +save_json(ERRORS_FILE, $allErrors); +save_json(SESSIONS_FILE, $sessions); + +// Rebuild clusters asynchronously (flag file) +$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters'; +file_put_contents($rebuildFlag, time()); + +json_response([ + 'success' => true, + 'ingested' => $ingested, + 'session_id' => $sessionId, + 'total_errors' => count($allErrors), + 'total_sessions' => count($sessions), +]); diff --git a/api/debug/stats.php b/api/debug/stats.php new file mode 100644 index 0000000..dd0b0db --- /dev/null +++ b/api/debug/stats.php @@ -0,0 +1,55 @@ +<?php +require_once __DIR__ . '/config.php'; + +$errors = load_json(ERRORS_FILE); +$sessions = load_json(SESSIONS_FILE); + +$total = count($errors); +$byType = []; +$bySeverity = []; +$byCategory = []; +$byModule = []; +$byDay = []; +$topErrors = []; + +foreach ($errors as $e) { + $t = $e['type'] ?? 'unknown'; + $byType[$t] = ($byType[$t] ?? 0) + ($e['count'] ?? 1); + + $s = $e['severity'] ?? 'error'; + $bySeverity[$s] = ($bySeverity[$s] ?? 0) + ($e['count'] ?? 1); + + $c = $e['category'] ?? 'runtime'; + $byCategory[$c] = ($byCategory[$c] ?? 0) + ($e['count'] ?? 1); + + $m = $e['module'] ?? 'unknown'; + $byModule[$m] = ($byModule[$m] ?? 0) + ($e['count'] ?? 1); + + $day = substr($e['ingested_at'] ?? $e['last_seen'], 0, 10); + $byDay[$day] = ($byDay[$day] ?? 0) + ($e['count'] ?? 1); + + $hash = $e['hash'] ?? ''; + if ($hash) { + if (!isset($topErrors[$hash])) { + $topErrors[$hash] = ['hash' => $hash, 'message' => $e['message'] ?? '', 'count' => 0, 'type' => $t]; + } + $topErrors[$hash]['count'] += $e['count'] ?? 1; + } +} + +usort($topErrors, fn($a, $b) => $b['count'] - $a['count']); +$topErrors = array_slice($topErrors, 0, 20); +ksort($byDay); + +json_response([ + 'success' => true, + 'total_errors' => $total, + 'total_sessions' => count($sessions), + 'by_type' => $byType, + 'by_severity' => $bySeverity, + 'by_category' => $byCategory, + 'by_module' => $byModule, + 'by_day' => $byDay, + 'top_errors' => $topErrors, + 'generated_at' => date('c'), +]); @@ -1342,7 +1342,7 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.10"></script> +<script src="modules/core/core-bundle.js?v=v4.6.11"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> <script src="modules/core/module-loader.js?v=v4.6.10"></script> diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index aab0206..2bb076a 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -19164,6 +19164,8 @@ class DebugSupervisorModule { 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)', 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error', 'debug-health': 'debug-health — Check supervisor module health status', + 'debug-sync': 'debug-sync [url] — Sync errors to backend API (or set sync URL)', + 'debug-sync-url': 'debug-sync-url [url] — View or set the backend sync endpoint', }; } @@ -19174,6 +19176,8 @@ class DebugSupervisorModule { this.commands['debug-trends'] = (args) => this._cmdTrends(args); this.commands['debug-fix'] = (args) => this._cmdFix(args); this.commands['debug-health'] = () => this._cmdHealth(); + this.commands['debug-sync'] = (args) => this._cmdSync(args); + this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args); } seed() { @@ -19183,6 +19187,36 @@ class DebugSupervisorModule { init() { this._loadDB(); this._installInterceptors(); + // Start periodic sync (every 5 minutes) + this._syncInterval = setInterval(() => this._syncToBackend(), 300000); + // Also sync on first load after a short delay + setTimeout(() => this._syncToBackend(), 5000); + } + + destroy() { + // Clear sync interval + if (this._syncInterval) { + clearInterval(this._syncInterval); + this._syncInterval = null; + } + // Restore original console.error + if (this._originalConsole.error) { + console.error = this._originalConsole.error; + } + // Restore original window.onerror + if (this._originalOnError) { + window.onerror = this._originalOnError; + } + // Restore original fetch + if (this._originalFetch) { + window.fetch = this._originalFetch; + } + // Remove unhandledrejection listener (stored for cleanup) + if (this._rejectionHandler) { + window.removeEventListener('unhandledrejection', this._rejectionHandler); + } + this._interceptorInstalled = false; + this._healthy = false; } // ── Error Database ── @@ -19252,11 +19286,7 @@ class DebugSupervisorModule { _checkQuota() { try { - const total = 0; - for (const key in localStorage) { - // Estimate: key + value length - } - // Simple check: try writing a test key + // Test localStorage is writable const testKey = '__quota_test__'; localStorage.setItem(testKey, '1'); localStorage.removeItem(testKey); @@ -19290,7 +19320,7 @@ class DebugSupervisorModule { _detectSeverity(type, message) { if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal'; - if (type === 'HTTP' && message.includes('5')) return 'error'; + if (type === 'HTTP' && /\b5\d{2}\b/.test(message)) return 'error'; if (type === 'timeout' || type === 'security') return 'error'; if (type === 'tool') return 'warning'; return 'error'; @@ -19347,10 +19377,11 @@ class DebugSupervisorModule { // Unhandled rejections this._originalOnRejection = window.onunhandledrejection; - window.addEventListener('unhandledrejection', function(e) { + this._rejectionHandler = function(e) { const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection'; self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null); - }); + }; + window.addEventListener('unhandledrejection', this._rejectionHandler); // Fetch failures this._originalFetch = window.fetch; @@ -19761,9 +19792,11 @@ class DebugSupervisorModule { if (!fix.fixes || fix.fixes.length === 0) return; const firstFix = fix.fixes[0]; try { + // Sanitize params: only allow alphanumeric, hyphens, underscores, dots + const safeParams = (firstFix.params || '').replace(/[^a-zA-Z0-9_\-\.]/g, ''); if (firstFix.action === 'loadmod' && this.terminal) { - this.terminal.executeCommand('loadmod ' + (firstFix.params || '')); - this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success'); + this.terminal.executeCommand('loadmod ' + safeParams); + this.terminal.print(' ✅ Applied: loadmod ' + safeParams, 'success'); } else if (firstFix.action === 'test-net' && this.terminal) { this.terminal.executeCommand('test-net'); this.terminal.print(' ✅ Applied: test-net', 'success'); @@ -19788,6 +19821,106 @@ class DebugSupervisorModule { this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error'); } + // ── Sync to Backend ── + + _getSyncUrl() { + try { + return localStorage.getItem('webx11_debug_sync_url') || 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php'; + } catch (e) { + return 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php'; + } + } + + async _syncToBackend() { + if (!this._healthy) return; + const db = this._errorDB.errors; + const keys = Object.keys(db); + if (keys.length === 0) return; + + const payload = { + version: window.NEWOS4_VERSION || 'v4.x', + session: { + id: this._sessionId, + modules: this.terminal?.moduleManager ? Object.keys(this.terminal.moduleManager.modules) : [], + }, + errors: keys.map(k => ({ + hash: k, + message: db[k].message, + type: db[k].type, + severity: db[k].severity, + category: db[k].category, + source: db[k].source, + module: db[k].module, + stack: db[k].stack, + count: db[k].count, + firstSeen: new Date(db[k].firstSeen).toISOString(), + lastSeen: new Date(db[k].lastSeen).toISOString(), + })), + }; + + try { + const resp = await fetch(this._getSyncUrl(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(10000), + }); + if (resp.ok) { + const result = await resp.json(); + if (result.success) { + const sent = keys.length; + const accepted = result.ingested || 0; + if (accepted < sent) { + console.warn('[DebugSupervisor] Sync truncated: sent ' + sent + ' errors, server accepted ' + accepted + ' (max ' + (result.total_errors || '?') + ' total)'); + } else { + console.log('[DebugSupervisor] Synced ' + accepted + ' errors to backend'); + } + this._lastSyncOk = Date.now(); + return true; + } + } + console.warn('[DebugSupervisor] Sync returned HTTP ' + resp.status); + return false; + } catch (e) { + console.warn('[DebugSupervisor] Sync failed: ' + e.message); + return false; + } + } + + _cmdSync(args) { + const url = (args && args[0]) ? args[0] : null; + if (url) { + try { + localStorage.setItem('webx11_debug_sync_url', url); + this.terminal.print('✅ Sync URL set to: ' + url, 'success'); + } catch (e) { + this.terminal.print('❌ Failed to save sync URL', 'error'); + } + return; + } + this.terminal.print('🔄 Syncing errors to backend...', 'info'); + this._syncToBackend().then(ok => { + if (ok) { + this.terminal.print('✅ Sync complete', 'success'); + } else { + this.terminal.print('❌ Sync failed', 'error'); + } + }); + } + + _cmdSyncUrl(args) { + if (args && args[0]) { + try { + localStorage.setItem('webx11_debug_sync_url', args[0]); + this.terminal.print('✅ Sync URL set to: ' + args[0], 'success'); + } catch (e) { + this.terminal.print('❌ Failed to save sync URL', 'error'); + } + } else { + this.terminal.print('Current sync URL: ' + this._getSyncUrl(), 'info'); + } + } + // ── Dashboard Data ── getDashboardData() { diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js index 15fe5d4..fc6a919 100644 --- a/modules/core/module-debug-supervisor.js +++ b/modules/core/module-debug-supervisor.js @@ -39,6 +39,8 @@ class DebugSupervisorModule { 'debug-trends': 'debug-trends [days] — Error frequency over time (default: 7)', 'debug-fix': 'debug-fix <hash> — Suggest and optionally apply fix for an error', 'debug-health': 'debug-health — Check supervisor module health status', + 'debug-sync': 'debug-sync [url] — Sync errors to backend API (or set sync URL)', + 'debug-sync-url': 'debug-sync-url [url] — View or set the backend sync endpoint', }; } @@ -49,6 +51,8 @@ class DebugSupervisorModule { this.commands['debug-trends'] = (args) => this._cmdTrends(args); this.commands['debug-fix'] = (args) => this._cmdFix(args); this.commands['debug-health'] = () => this._cmdHealth(); + this.commands['debug-sync'] = (args) => this._cmdSync(args); + this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args); } seed() { @@ -58,6 +62,36 @@ class DebugSupervisorModule { init() { this._loadDB(); this._installInterceptors(); + // Start periodic sync (every 5 minutes) + this._syncInterval = setInterval(() => this._syncToBackend(), 300000); + // Also sync on first load after a short delay + setTimeout(() => this._syncToBackend(), 5000); + } + + destroy() { + // Clear sync interval + if (this._syncInterval) { + clearInterval(this._syncInterval); + this._syncInterval = null; + } + // Restore original console.error + if (this._originalConsole.error) { + console.error = this._originalConsole.error; + } + // Restore original window.onerror + if (this._originalOnError) { + window.onerror = this._originalOnError; + } + // Restore original fetch + if (this._originalFetch) { + window.fetch = this._originalFetch; + } + // Remove unhandledrejection listener (stored for cleanup) + if (this._rejectionHandler) { + window.removeEventListener('unhandledrejection', this._rejectionHandler); + } + this._interceptorInstalled = false; + this._healthy = false; } // ── Error Database ── @@ -127,11 +161,7 @@ class DebugSupervisorModule { _checkQuota() { try { - const total = 0; - for (const key in localStorage) { - // Estimate: key + value length - } - // Simple check: try writing a test key + // Test localStorage is writable const testKey = '__quota_test__'; localStorage.setItem(testKey, '1'); localStorage.removeItem(testKey); @@ -165,7 +195,7 @@ class DebugSupervisorModule { _detectSeverity(type, message) { if (type === 'SyntaxError' || type === 'ReferenceError' || type === 'quota') return 'fatal'; - if (type === 'HTTP' && message.includes('5')) return 'error'; + if (type === 'HTTP' && /\b5\d{2}\b/.test(message)) return 'error'; if (type === 'timeout' || type === 'security') return 'error'; if (type === 'tool') return 'warning'; return 'error'; @@ -222,10 +252,11 @@ class DebugSupervisorModule { // Unhandled rejections this._originalOnRejection = window.onunhandledrejection; - window.addEventListener('unhandledrejection', function(e) { + this._rejectionHandler = function(e) { const msg = e.reason ? (e.reason.message || String(e.reason)) : 'Unhandled Promise rejection'; self._recordError(msg, 'unhandledrejection', e.reason ? e.reason.stack : null); - }); + }; + window.addEventListener('unhandledrejection', this._rejectionHandler); // Fetch failures this._originalFetch = window.fetch; @@ -636,9 +667,11 @@ class DebugSupervisorModule { if (!fix.fixes || fix.fixes.length === 0) return; const firstFix = fix.fixes[0]; try { + // Sanitize params: only allow alphanumeric, hyphens, underscores, dots + const safeParams = (firstFix.params || '').replace(/[^a-zA-Z0-9_\-\.]/g, ''); if (firstFix.action === 'loadmod' && this.terminal) { - this.terminal.executeCommand('loadmod ' + (firstFix.params || '')); - this.terminal.print(' ✅ Applied: loadmod ' + (firstFix.params || ''), 'success'); + this.terminal.executeCommand('loadmod ' + safeParams); + this.terminal.print(' ✅ Applied: loadmod ' + safeParams, 'success'); } else if (firstFix.action === 'test-net' && this.terminal) { this.terminal.executeCommand('test-net'); this.terminal.print(' ✅ Applied: test-net', 'success'); @@ -663,6 +696,106 @@ class DebugSupervisorModule { this.terminal.print(' Quota: ' + (this._checkQuota() ? '✅ available' : '🔴 low'), this._checkQuota() ? 'success' : 'error'); } + // ── Sync to Backend ── + + _getSyncUrl() { + try { + return localStorage.getItem('webx11_debug_sync_url') || 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php'; + } catch (e) { + return 'https://webx11.duckdns.org/spaces/newos4/api/debug/ingest.php'; + } + } + + async _syncToBackend() { + if (!this._healthy) return; + const db = this._errorDB.errors; + const keys = Object.keys(db); + if (keys.length === 0) return; + + const payload = { + version: window.NEWOS4_VERSION || 'v4.x', + session: { + id: this._sessionId, + modules: this.terminal?.moduleManager ? Object.keys(this.terminal.moduleManager.modules) : [], + }, + errors: keys.map(k => ({ + hash: k, + message: db[k].message, + type: db[k].type, + severity: db[k].severity, + category: db[k].category, + source: db[k].source, + module: db[k].module, + stack: db[k].stack, + count: db[k].count, + firstSeen: new Date(db[k].firstSeen).toISOString(), + lastSeen: new Date(db[k].lastSeen).toISOString(), + })), + }; + + try { + const resp = await fetch(this._getSyncUrl(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(10000), + }); + if (resp.ok) { + const result = await resp.json(); + if (result.success) { + const sent = keys.length; + const accepted = result.ingested || 0; + if (accepted < sent) { + console.warn('[DebugSupervisor] Sync truncated: sent ' + sent + ' errors, server accepted ' + accepted + ' (max ' + (result.total_errors || '?') + ' total)'); + } else { + console.log('[DebugSupervisor] Synced ' + accepted + ' errors to backend'); + } + this._lastSyncOk = Date.now(); + return true; + } + } + console.warn('[DebugSupervisor] Sync returned HTTP ' + resp.status); + return false; + } catch (e) { + console.warn('[DebugSupervisor] Sync failed: ' + e.message); + return false; + } + } + + _cmdSync(args) { + const url = (args && args[0]) ? args[0] : null; + if (url) { + try { + localStorage.setItem('webx11_debug_sync_url', url); + this.terminal.print('✅ Sync URL set to: ' + url, 'success'); + } catch (e) { + this.terminal.print('❌ Failed to save sync URL', 'error'); + } + return; + } + this.terminal.print('🔄 Syncing errors to backend...', 'info'); + this._syncToBackend().then(ok => { + if (ok) { + this.terminal.print('✅ Sync complete', 'success'); + } else { + this.terminal.print('❌ Sync failed', 'error'); + } + }); + } + + _cmdSyncUrl(args) { + if (args && args[0]) { + try { + localStorage.setItem('webx11_debug_sync_url', args[0]); + this.terminal.print('✅ Sync URL set to: ' + args[0], 'success'); + } catch (e) { + this.terminal.print('❌ Failed to save sync URL', 'error'); + } + } else { + this.terminal.print('Current sync URL: ' + this._getSyncUrl(), 'info'); + } + } + // ── Dashboard Data ── getDashboardData() { diff --git a/review-newos4.html b/review-newos4.html index 975a0e8..deff556 100644 --- a/review-newos4.html +++ b/review-newos4.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <meta name="version" content="4.5.82-newos4"> + <meta name="version" content="4.6.10-newos4"> <title>ChatStudio newOS4 - Review</title> <style> :root { @@ -165,31 +165,31 @@ <div class="version-header"> <span class="version-icon">🧩</span> - <span class="version-title">newOS4 v4.5.82 — Terminal OS</span> + <span class="version-title">newOS4 v4.6.10 — Terminal OS</span> <span class="version-badge" style="background:#a855f7;">Most Advanced</span> </div> <div class="meta-info"> - <span>v4.5.82</span> - <span>1,375 lines (main.html skeleton, 68KB)</span> + <span>v4.6.10</span> + <span>1,477 lines (main.html skeleton, 72KB)</span> <span>3 bundle scripts + 22 standalone ext modules</span> <span>12 AI-managed skills (17 in index)</span> - <span>~1,020KB total bundle size</span> + <span>~1,200KB total bundle size</span> </div> <div class="section"> <h2>Architecture</h2> <p style="color: var(--text-muted); margin-bottom: 1rem;"> - A full in-browser terminal OS with bundle-based loading. The main.html (68KB) is a lean skeleton — all functionality is bundled into 2 optimized scripts (core-bundle.js & critical-ext-bundle.js) plus module-loader.js, with 22 additional standalone ext modules loaded on demand. + A full in-browser terminal OS with bundle-based loading. The main.html (72KB) is a lean skeleton — all functionality is bundled into 2 optimized scripts (core-bundle.js & critical-ext-bundle.js) plus module-loader.js, with 22 additional standalone ext modules loaded on demand. </p> <div class="arch-grid"> <div class="arch-item"> <h4>🧠 Core Bundle</h4> - <p>core-bundle.js (904KB, 17,202 lines) — 17 core classes: VirtualFileSystem, ModuleManager, OllamaManager, SecretVault, WebX11Terminal, WebX11SecurityManager, plus 11 module classes (system, fs, ollama, aiassist, searxng, config, smartctx, servermon, simplegui, duckduckgo, fetcher) and supporting classes (ollama-manager, virtual-fs, terminal, secret-vault, module-manager).</p> + <p>core-bundle.js (1,048KB, 19,828 lines) — 17 core classes: VirtualFileSystem, ModuleManager, OllamaManager, SecretVault, WebX11Terminal, WebX11SecurityManager, plus 11 module classes (system, fs, ollama, aiassist, searxng, config, smartctx, servermon, simplegui, duckduckgo, fetcher) and supporting classes (ollama-manager, virtual-fs, terminal, secret-vault, module-manager).</p> </div> <div class="arch-item"> <h4>🔧 Critical Ext Bundle</h4> - <p>critical-ext-bundle.js (112KB, 2,541 lines) — 12 bundled ext modules: alias, busybox, calc, console, cron, cryptogzip, diff, editor, filemgr, finder, shell, tutorial.</p> + <p>critical-ext-bundle.js (116KB, 2,624 lines) — 12 bundled ext modules: alias, busybox, calc, console, cron, cryptogzip, diff, editor, filemgr, finder, shell, tutorial.</p> </div> <div class="arch-item"> <h4>🧩 Standalone Ext Modules</h4> @@ -289,7 +289,7 @@ <h3>Cons</h3> <ul> <li>Ollama dependency — requires Ollama backend for AI features</li> - <li>Large bundle size (~1,020KB across all scripts)</li> + <li>Large bundle size (~1,200KB across all scripts)</li> <li>No streaming AI responses (request/response cycle only)</li> <li>No PDF support</li> <li>Single-language (English only)</li> @@ -304,7 +304,7 @@ <div class="section"> <h2>Verdict</h2> <p style="color: var(--text-muted); margin-bottom: 0.5rem;"> - <strong>newOS4 v4.5.82</strong> is the most advanced ChatStudio variant — an in-browser terminal OS with agentic AI, a virtual file system, and self-improving skills. It's designed for power users, developers, and AI enthusiasts who want maximum control and capability. The bundle-based architecture (core-bundle + critical-ext-bundle + 22 standalone modules) keeps script loading efficient while maintaining flexibility. + <strong>newOS4 v4.6.10</strong> is the most advanced ChatStudio variant — an in-browser terminal OS with agentic AI, a virtual file system, and self-improving skills. It's designed for power users, developers, and AI enthusiasts who want maximum control and capability. The bundle-based architecture (core-bundle + critical-ext-bundle + 22 standalone modules) keeps script loading efficient while maintaining flexibility. </p> <div class="rating">🧩🧩🧩🧩 — <strong>4/5</strong> — Most advanced, consistently improved</div> </div> |