aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-29 20:46:12 +0200
committerAgando <roduccomm@gmail.com>2026-07-29 20:46:12 +0200
commit0003ad95fa091353827198d0a45cd63a3784479f (patch)
tree4ea79302c13f34b2f1b84a56ed33cc149cd99a21
parentcf7461ffeb4f53f3f50c9c692a2e4fdcecf17e82 (diff)
auto: 2026-07-29_20:50 changes — gunes sponsorluk page, bunq payment modal, AI platforms, news W30
-rw-r--r--api/debug/clusters.php98
-rw-r--r--api/debug/config.php59
-rw-r--r--api/debug/dashboard.php200
-rw-r--r--api/debug/error.php36
-rw-r--r--api/debug/errors.php43
-rw-r--r--api/debug/health.php20
-rw-r--r--api/debug/ingest.php117
-rw-r--r--api/debug/stats.php55
-rw-r--r--main.html2
-rw-r--r--modules/core/core-bundle.js153
-rw-r--r--modules/core/module-debug-supervisor.js153
-rw-r--r--review-newos4.html20
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 &middot; <?= date('Y-m-d H:i:s') ?> &middot; <a href="stats.php">JSON Stats</a> &middot; <a href="clusters.php">Clusters</a> &middot; <a href="errors.php">Raw Errors</a> &middot; <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'),
+]);
diff --git a/main.html b/main.html
index 1663fc0..feb80f8 100644
--- a/main.html
+++ b/main.html
@@ -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 &mdash; Terminal OS</span>
+ <span class="version-title">newOS4 v4.6.10 &mdash; 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 &amp; 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 &amp; 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>