aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-22 01:23:47 +0200
committerAgando <roduccomm@gmail.com>2026-10-22 01:23:47 +0200
commit8dd236e7a03b3cfe3cfb4a0d77b62ce112e4d418 (patch)
tree807a8736b0c711b8d7d9a3b667df67d78165ca5d
parent70b9a2f8cc2b249362ca4893fdbb6703c0ea7b8a (diff)
fix(newos4): dashboard shows the real world model and stops dying after 10 s (v4.6.51)
Two defects, both verified live before/after: 1. The World Model panel always said "Not synthesized yet". The page had a renderer for data.worldModel and _collectDashboardData() never set it -- the model was right there in WorldModelModule._model. Now collected and mapped to the renderer's exact keys (model has modules.loaded/skills.count/memory.nodes, the page reads modules/skills/memoryNodes), plus changesSinceLastSnapshot and the recent-error sample, with an honest "not synthesized" fallback. 2. The page discarded any payload older than 10 s and its own 5 s auto-refresh then showed "Cannot connect to newOS4". Stale payloads are now rendered with their age ("Data as of 1:20:14 AM - 3m ago"), labelled amber past 5 min. Also: the Refresh button wrote a localStorage key nobody read -> the terminal now listens for it and re-collects (page says "No terminal tab answered" when no terminal tab is open); storage events repaint instantly; the dead /api/dashboard fetch (404 on all three paths, 5 s stall) is gone; `dashboard --watch` / `--stop` re-collects every 5 s; header says the data is this browser's instance. Verified v4.6.51: panel rows "Synthesized 34s ago / v4.6.51 / 46-22 / 136-3 / 1 nodes 0 edges / OK / none recorded"; 130 s-old payload still renders with "- 3m ago" and no banner (previously "Cannot connect"); storage event moves the footer 13s -> 0s; watch timer advances the payload, --stop clears it; mobile 360/390 px all OK; 0 console errors.
-rw-r--r--downloads/dashboard.html126
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js73
-rw-r--r--modules/core/module-aiassist.js31
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/terminal.js42
6 files changed, 192 insertions, 88 deletions
diff --git a/downloads/dashboard.html b/downloads/dashboard.html
index d4275f2..799bdbb 100644
--- a/downloads/dashboard.html
+++ b/downloads/dashboard.html
@@ -252,6 +252,9 @@
<header>
<div>
<h1>⬡ newOS4 Dashboard</h1>
+ <div class="sub" style="color:var(--text-dim);font-size:0.85rem">
+ Live view of this browser's newOS4 instance · world model, agents, tokens, latency
+ </div>
<div class="meta">Live system telemetry · <span id="version-display">v?</span></div>
</div>
<div class="header-controls">
@@ -353,7 +356,8 @@
</div>
<div class="last-updated">
- Last updated: <span id="last-updated-time">--</span> · Auto-refresh every 5s
+ Data as of <span id="last-updated-time">--</span> · updates when the terminal re-collects
+ (<code>dashboard</code> or <code>dashboard --watch</code>)
</div>
</div>
@@ -395,18 +399,17 @@
}
}
+ // 2026-10-22: a 10 s cutoff threw away perfectly good data and the page's own auto-refresh
+ // then showed "Cannot connect". Keep whatever exists and let the UI label its age.
function fetchDashboardData() {
- // Try to get data from localStorage (set by the AI tool or terminal command)
try {
const raw = localStorage.getItem(DASHBOARD_KEY);
- if (raw) {
- const data = JSON.parse(raw);
- if (data && data.timestamp && (Date.now() - data.timestamp < 10000)) {
- return data; // fresh enough
- }
- }
- } catch (e) {}
- return null;
+ if (!raw) return null;
+ const data = JSON.parse(raw);
+ if (!data || !data.timestamp) return null;
+ data._ageMs = Math.max(0, Date.now() - data.timestamp);
+ return data;
+ } catch (e) { return null; }
}
function renderDashboard(data) {
@@ -418,6 +421,10 @@
hideError();
updateConnectionStatus(true);
+
+ const ageSec = Math.round((data._ageMs || 0) / 1000);
+ const ageEl = document.getElementById('last-updated-time');
+ if (ageEl) ageEl.textContent = new Date(data.timestamp).toLocaleTimeString() + ' · ' + (ageSec < 90 ? ageSec + 's ago' : Math.round(ageSec / 60) + 'm ago');
lastData = data;
// Version
@@ -536,26 +543,34 @@
// ── World Model ──
const wmDiv = document.getElementById('world-model-metrics');
const wmData = data.worldModel;
- if (wmData) {
- const synTime = wmData.synthesized ? new Date(wmData.synthesized).toLocaleTimeString() : '?';
+ if (wmData && !wmData.missing) {
+ const ageMs = wmData.synthesized ? (Date.now() - new Date(wmData.synthesized).getTime()) : null;
+ const ageStr = ageMs === null ? '?' : (ageMs < 90000 ? Math.round(ageMs / 1000) + 's ago' : Math.round(ageMs / 60000) + 'm ago');
+ const stale = ageMs !== null && ageMs > 5 * 60 * 1000;
let wmHtml = '';
+ wmHtml += '<div class="metric-row"><span>Synthesized</span><span' + (stale ? ' style="color:var(--yellow,#e0b341)"' : '') + '>' + ageStr + (stale ? ' (stale)' : '') + '</span></div>';
wmHtml += '<div class="metric-row"><span>Version</span><span>' + (wmData.version || '?') + '</span></div>';
- wmHtml += '<div class="metric-row"><span>Last synthesized</span><span>' + synTime + '</span></div>';
- wmHtml += '<div class="metric-row"><span>Modules</span><span>' + (wmData.modules || '?') + '</span></div>';
- wmHtml += '<div class="metric-row"><span>Skills</span><span>' + (wmData.skills || 0) + '</span></div>';
- wmHtml += '<div class="metric-row"><span>Files</span><span>' + (wmData.files || 0) + '</span></div>';
- wmHtml += '<div class="metric-row"><span>Agents</span><span>' + (wmData.agents || 0) + '</span></div>';
- wmHtml += '<div class="metric-row"><span>Memory graph</span><span>' + (wmData.memoryNodes || 0) + ' nodes, ' + (wmData.memoryEdges || 0) + ' edges</span></div>';
+ wmHtml += '<div class="metric-row"><span>Modules / skills</span><span>' + (wmData.modules ?? '?') + ' / ' + (wmData.skills ?? '?') + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Files / agents</span><span>' + (wmData.files ?? '?') + ' / ' + (wmData.agents ?? '?') + '</span></div>';
+ wmHtml += '<div class="metric-row"><span>Memory graph</span><span>' + (wmData.memoryNodes ?? 0) + ' nodes · ' + (wmData.memoryEdges ?? 0) + ' edges</span></div>';
wmHtml += '<div class="metric-row"><span>Health</span><span>' + (wmData.health || '?') + '</span></div>';
- if (wmData.activeProjects && wmData.activeProjects.length > 0) {
- wmHtml += '<div class="metric-row"><span>Active projects</span><span>' + wmData.activeProjects.join(', ') + '</span></div>';
+ if (wmData.changes && wmData.changes.length) {
+ wmHtml += '<div class="metric-row"><span>Since last snapshot</span><span>' + wmData.changes.join('; ') + '</span></div>';
}
- if (wmData.recent && wmData.recent.length > 0) {
- wmHtml += '<div class="metric-row"><span>Recent changes</span><span>' + wmData.recent.join('; ') + '</span></div>';
+ if (wmData.errors && wmData.errors.length) {
+ wmHtml += '<div class="metric-row"><span>Recent errors</span><span>' + wmData.errors.join(' | ') + '</span></div>';
+ } else {
+ wmHtml += '<div class="metric-row"><span>Recent errors</span><span>✅ none recorded</span></div>';
}
+ if (wmData.recent && wmData.recent.length) {
+ wmHtml += '<div class="metric-row"><span>Current</span><span>' + wmData.recent.join('; ') + '</span></div>';
+ }
+ wmHtml += '<div class="metric-row" style="color:var(--text-dim)"><span>Ask it anything</span><span>world-model-ask &lt;question&gt;</span></div>';
wmDiv.innerHTML = wmHtml;
+ } else if (wmData && wmData.error) {
+ wmDiv.innerHTML = '<div class="metric-row" style="color:var(--red,#e5534b)">World model read failed: ' + wmData.error + '</div>';
} else {
- wmDiv.innerHTML = '<div class="metric-row" style="color:var(--text-dim)">World model not synthesized — run world-model-refresh in terminal</div>';
+ wmDiv.innerHTML = '<div class="metric-row" style="color:var(--text-dim)">World model not synthesized — run <code>world-model-refresh</code> in the terminal</div>';
}
// ── System Health ──
@@ -570,51 +585,36 @@
healthDiv.innerHTML = healthHtml;
// Timestamp
- document.getElementById('last-updated-time').textContent = new Date().toLocaleTimeString();
+ // Footer age is set at the top of renderDashboard from data.timestamp (payload age);
+ // the old line here wrote the wall clock and silently clobbered it.
}
function refreshNow() {
- // Signal the terminal to update dashboard data
- try {
- localStorage.setItem(DASHBOARD_KEY + '_request', Date.now().toString());
- } catch (e) {}
- // Try reading fresh data
+ try { localStorage.setItem(DASHBOARD_KEY + '_request', Date.now().toString()); } catch (e) {}
+ window._dashRefreshAskedAt = Date.now();
const data = fetchDashboardData();
- if (data) {
- renderDashboard(data);
- } else {
- // If no data, try to fetch via the newOS4 API
- fetchDashboardFromAPI();
- }
+ if (data) renderDashboard(data);
+ setTimeout(function () {
+ const d = fetchDashboardData();
+ const fresh = d && d.timestamp > (window._dashRefreshAskedAt || 0);
+ if (!fresh) showError('No terminal tab answered — run dashboard (or dashboard --watch) in newOS4, then press ⟳ again');
+ else hideError();
+ }, 1500);
}
- async function fetchDashboardFromAPI() {
- showLoading();
+ // 2026-10-22: the terminal writes this key on every collect; repaint immediately instead of
+ // waiting for the 5 s poll (and never go stale while the terminal tab is open).
+ window.addEventListener('storage', function (ev) {
+ if (ev.key !== DASHBOARD_KEY || !ev.newValue) return;
try {
- // Try the proxy API endpoint
- const resp = await fetch('/api/dashboard', { signal: AbortSignal.timeout(5000) });
- if (resp.ok) {
- const data = await resp.json();
- localStorage.setItem(DASHBOARD_KEY, JSON.stringify({...data, timestamp: Date.now()}));
- renderDashboard(data);
- showContent();
- return;
- }
- } catch (e) {
- // API not available — try localStorage fallback
- }
-
- // Fallback: check localStorage
- const data = fetchDashboardData();
- if (data) {
- renderDashboard(data);
+ const d = JSON.parse(ev.newValue);
+ d._ageMs = 0;
+ renderDashboard(d);
showContent();
- } else {
- showError('Cannot connect to newOS4. Open the terminal and run: dashboard');
- updateConnectionStatus(false);
- showContent();
- }
- }
+ hideError();
+ updateConnectionStatus(true);
+ } catch (e) {}
+ });
// Auto-refresh loop
function startAutoRefresh() {
@@ -625,13 +625,15 @@
}, REFRESH_INTERVAL);
}
- // Init
+ // Init: render whatever the terminal left behind, otherwise say how to produce data.
const initialData = fetchDashboardData();
if (initialData) {
renderDashboard(initialData);
showContent();
} else {
- fetchDashboardFromAPI();
+ showError('No dashboard data yet — run dashboard in the newOS4 terminal');
+ updateConnectionStatus(false);
+ showContent();
}
startAutoRefresh();
diff --git a/main.html b/main.html
index c89d183..f41b12d 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.50"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.51"></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.50"></script>
+<script src="modules/core/module-loader.js?v=v4.6.51"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.50"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.51"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 2264ef0..245f2bd 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -2918,6 +2918,16 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
this.hideAutocomplete();
}
});
+ // 2026-10-22: dashboard.html wrote this key on ⟳ Refresh and nothing listened.
+ window.addEventListener('storage', function (ev) {
+ if (ev.key !== 'webx11_dashboard_data_request') return;
+ const aiMod = this.moduleManager && this.moduleManager.modules
+ ? this.moduleManager.modules.WebX11AIAssistantModule : null;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData();
+ }
+ }.bind(this));
+
window.addEventListener('online', () => this.updateNetworkStatus());
window.addEventListener('offline', () => this.updateNetworkStatus());
document.getElementById('file-upload-input').addEventListener('change', (e) => {
@@ -4014,18 +4024,28 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
if (cmd === 'dashboard') {
- // Collect dashboard data and open the dashboard page
- const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
- if (aiMod && typeof aiMod._collectDashboardData === 'function') {
- aiMod._collectDashboardData().then(function() {
- this.print('📊 Dashboard data collected — opening dashboard...', 'success');
- window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
- }.bind(this));
- } else {
- // Fallback: just open the page (it will try localStorage)
- this.print('📊 Opening dashboard...', 'info');
- window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ const aiMod = this.moduleManager && this.moduleManager.modules
+ ? this.moduleManager.modules.WebX11AIAssistantModule : null;
+ const collect = function () {
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') return aiMod._collectDashboardData();
+ return Promise.resolve(null);
+ };
+ if (args.indexOf('--stop') !== -1) {
+ if (this._dashTimer) { clearInterval(this._dashTimer); this._dashTimer = null; this.print('📊 Dashboard watch stopped', 'info'); }
+ else this.print('📊 Dashboard watch is not running', 'warning');
+ return;
+ }
+ if (args.indexOf('--watch') !== -1) {
+ if (this._dashTimer) clearInterval(this._dashTimer);
+ collect();
+ this._dashTimer = setInterval(collect, 5000);
+ this.print('📊 Dashboard watch on (5s) — open the dashboard tab; `dashboard --stop` to stop', 'success');
+ return;
}
+ collect().then(function () {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
return;
}
if (cmd === 'config') {
@@ -12794,6 +12814,37 @@ buildSystemPrompt() {
predictorAccuracy: predictorAcc
};
+ // 2026-10-22: the panel had a renderer and no collector, so it always said
+ // "Not synthesized yet" while WorldModelModule._model was right there.
+ // Map the model shape (modules.loaded, skills.count, files.total, memory.nodes…)
+ // onto the keys the page reads (modules, skills, files, memoryNodes…).
+ try {
+ var wm = mm ? mm.modules['WorldModelModule'] : null;
+ var wmModel = wm && wm._model;
+ if (wmModel) {
+ data.worldModel = {
+ synthesized: wmModel.synthesized || null,
+ version: wmModel.version || '?',
+ modules: wmModel.modules ? wmModel.modules.loaded : null,
+ skills: wmModel.skills ? wmModel.skills.count : null,
+ files: wmModel.files ? wmModel.files.total : null,
+ agents: wmModel.agents ? wmModel.agents.agents : null,
+ memoryNodes: wmModel.memory ? wmModel.memory.nodes : null,
+ memoryEdges: wmModel.memory ? wmModel.memory.edges : null,
+ health: wmModel.system ? wmModel.system.health : null,
+ activeProjects: ((wmModel.projects && wmModel.projects.activeProjects) || [])
+ .map(function (p) { return p.name + ' (' + p.accesses + 'x)'; }),
+ recent: (wmModel.recent || []).slice(0, 5),
+ changes: (typeof wm._diffSincePrev === 'function') ? wm._diffSincePrev() : [],
+ errors: (typeof wm._errorSample === 'function') ? wm._errorSample() : []
+ };
+ } else {
+ data.worldModel = { missing: true };
+ }
+ } catch (e) {
+ data.worldModel = { missing: true, error: e.message };
+ }
+
data.timestamp = Date.now();
// Write to localStorage for the dashboard HTML page to read
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index a8c0ae1..abbe9ce 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -4632,6 +4632,37 @@ buildSystemPrompt() {
predictorAccuracy: predictorAcc
};
+ // 2026-10-22: the panel had a renderer and no collector, so it always said
+ // "Not synthesized yet" while WorldModelModule._model was right there.
+ // Map the model shape (modules.loaded, skills.count, files.total, memory.nodes…)
+ // onto the keys the page reads (modules, skills, files, memoryNodes…).
+ try {
+ var wm = mm ? mm.modules['WorldModelModule'] : null;
+ var wmModel = wm && wm._model;
+ if (wmModel) {
+ data.worldModel = {
+ synthesized: wmModel.synthesized || null,
+ version: wmModel.version || '?',
+ modules: wmModel.modules ? wmModel.modules.loaded : null,
+ skills: wmModel.skills ? wmModel.skills.count : null,
+ files: wmModel.files ? wmModel.files.total : null,
+ agents: wmModel.agents ? wmModel.agents.agents : null,
+ memoryNodes: wmModel.memory ? wmModel.memory.nodes : null,
+ memoryEdges: wmModel.memory ? wmModel.memory.edges : null,
+ health: wmModel.system ? wmModel.system.health : null,
+ activeProjects: ((wmModel.projects && wmModel.projects.activeProjects) || [])
+ .map(function (p) { return p.name + ' (' + p.accesses + 'x)'; }),
+ recent: (wmModel.recent || []).slice(0, 5),
+ changes: (typeof wm._diffSincePrev === 'function') ? wm._diffSincePrev() : [],
+ errors: (typeof wm._errorSample === 'function') ? wm._errorSample() : []
+ };
+ } else {
+ data.worldModel = { missing: true };
+ }
+ } catch (e) {
+ data.worldModel = { missing: true, error: e.message };
+ }
+
data.timestamp = Date.now();
// Write to localStorage for the dashboard HTML page to read
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 64ece7c..46aad17 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.50';
+ const LATEST_VER = 'v4.6.51';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 425c5d6..c586538 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -479,6 +479,16 @@
this.hideAutocomplete();
}
});
+ // 2026-10-22: dashboard.html wrote this key on ⟳ Refresh and nothing listened.
+ window.addEventListener('storage', function (ev) {
+ if (ev.key !== 'webx11_dashboard_data_request') return;
+ const aiMod = this.moduleManager && this.moduleManager.modules
+ ? this.moduleManager.modules.WebX11AIAssistantModule : null;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData();
+ }
+ }.bind(this));
+
window.addEventListener('online', () => this.updateNetworkStatus());
window.addEventListener('offline', () => this.updateNetworkStatus());
document.getElementById('file-upload-input').addEventListener('change', (e) => {
@@ -1575,18 +1585,28 @@
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
if (cmd === 'dashboard') {
- // Collect dashboard data and open the dashboard page
- const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
- if (aiMod && typeof aiMod._collectDashboardData === 'function') {
- aiMod._collectDashboardData().then(function() {
- this.print('📊 Dashboard data collected — opening dashboard...', 'success');
- window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
- }.bind(this));
- } else {
- // Fallback: just open the page (it will try localStorage)
- this.print('📊 Opening dashboard...', 'info');
- window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ const aiMod = this.moduleManager && this.moduleManager.modules
+ ? this.moduleManager.modules.WebX11AIAssistantModule : null;
+ const collect = function () {
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') return aiMod._collectDashboardData();
+ return Promise.resolve(null);
+ };
+ if (args.indexOf('--stop') !== -1) {
+ if (this._dashTimer) { clearInterval(this._dashTimer); this._dashTimer = null; this.print('📊 Dashboard watch stopped', 'info'); }
+ else this.print('📊 Dashboard watch is not running', 'warning');
+ return;
}
+ if (args.indexOf('--watch') !== -1) {
+ if (this._dashTimer) clearInterval(this._dashTimer);
+ collect();
+ this._dashTimer = setInterval(collect, 5000);
+ this.print('📊 Dashboard watch on (5s) — open the dashboard tab; `dashboard --stop` to stop', 'success');
+ return;
+ }
+ collect().then(function () {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
return;
}
if (cmd === 'config') {