diff options
Diffstat (limited to 'modules/core/core-bundle.js')
| -rw-r--r-- | modules/core/core-bundle.js | 336 |
1 files changed, 284 insertions, 52 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index b365fa0..ddcac45 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -557,6 +557,19 @@ class VirtualFileSystem { } return { dir, name: segs[segs.length - 1] }; } + // newOS4 patch: _resolvePath() validates intermediate segments only, so it returns a + // result for a leaf that does not exist. Mutating callers then acted on a phantom + // path and reported success (deleteCheckpoint printed 'Deleted checkpoint' and + // deleted nothing). _requireLeaf() refuses any resolved path whose leaf is absent. + _requireLeaf(resolved) { + if (!resolved) return null; + if (resolved.name) { + const node = (resolved.dir && resolved.dir.content) ? resolved.dir.content[resolved.name] : null; + if (!node) return null; + } + return resolved; + } + _resolveDir(path) { if (this._isVirtualPath(path)) { const r = this._resolveVirtualPath(path); @@ -626,7 +639,7 @@ class VirtualFileSystem { } deleteFile(path) { if (this._isVirtualPath(path)) return false; - const resolved = this._resolvePath(path); + const resolved = this._requireLeaf(this._resolvePath(path)); if (!resolved || !resolved.name) return false; const node = resolved.dir.content[resolved.name]; if (node && node.type === 'file') { @@ -1013,6 +1026,10 @@ class VirtualFileSystem { delete rootClone.content['_sessions']; delete rootClone.content['_snapshots']; delete rootClone.content['.trash']; + // newOS4 patch: module originals and the semantic index dominated every + // snapshot (one checkpoint reached 4.13MB) and grew on each new checkpoint. + delete rootClone.content['_mods']; + delete rootClone.content['.semantic_index']; } const data = { version: 1, @@ -1047,7 +1064,7 @@ class VirtualFileSystem { // clone verbatim deletes every checkpoint, every agent session and the trash, so keep the // live copies and re-attach them after the root swap. const keep = {}; - for (const dir of ['_snapshots', '_sessions', '.trash']) { + for (const dir of ['_snapshots', '_sessions', '.trash', '_mods', '.semantic_index']) { const node = this.root && this.root.content ? this.root.content[dir] : null; if (node) keep[dir] = node; } @@ -1067,7 +1084,7 @@ class VirtualFileSystem { } deleteCheckpoint(name) { const path = '/_snapshots/checkpoint-' + name + '.json'; - const resolved = this._resolvePath(path); + const resolved = this._requireLeaf(this._resolvePath(path)); if (resolved && resolved.name) { delete resolved.dir.content[resolved.name]; this.saveToStorage(); @@ -3253,9 +3270,14 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } } updateNetworkStatus() { + // navigator.onLine is a hint, not a fact: an Android WebView reports false while + // fully connected (the same lie `status` used to print). Never turn that hint into + // "❌ Offline" — claim Online only when the browser says so, otherwise say unknown + // and point at `doctor`, which probes for real. const isOnline = navigator.onLine; - this.networkStatus.textContent = isOnline ? '📶 Online' : '❌ Offline'; - this.networkStatus.style.color = isOnline ? 'var(--text-color)' : 'var(--error-color)'; + this.networkStatus.textContent = isOnline ? '📶 Online' : '📶 Network?'; + this.networkStatus.title = isOnline ? '' : 'Browser reports offline — run doctor for a live probe'; + this.networkStatus.style.color = isOnline ? 'var(--text-color)' : 'var(--text-muted)'; } updatePrompt() { const path = '/' + this.fs.currentPath.join('/'); @@ -5886,6 +5908,84 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm 'checkpoint-del': 'checkpoint-del <name> — Delete a VFS checkpoint', }; } + + // ── doctor measurement helpers ────────────────────────────────────── + // UTF-8 byte length of a string. Blob is the reference implementation + // (Node 17+/every target browser), TextEncoder the browser fallback, and a + // code-point walk the last resort so a stripped-down WebView still gets a + // number instead of a thrown command. + _utf8Bytes(str) { + if (typeof str !== 'string' || str.length === 0) return 0; + try { + if (typeof Blob === 'function') return new Blob([str]).size; + } catch (e) {} + try { + if (typeof TextEncoder === 'function') return new TextEncoder().encode(str).length; + } catch (e) {} + let n = 0; + for (let i = 0; i < str.length; i++) { + const cp = str.codePointAt(i); + if (cp > 0xffff) i++; // surrogate pair consumed + n += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4; + } + return n; + } + + // Sum of UTF-8 bytes of every file's content under `root`. This is the unit + // `vfs-size` reports, without JSON.stringify's braces/quoting overhead and + // without the old `chars * 2` fudge. Depth- and node-capped so a malformed + // or cyclic tree cannot hang the command. + _vfsContentBytes(root) { + let bytes = 0, nodes = 0; + const walk = (node, depth) => { + if (!node || typeof node !== 'object' || depth > 64) return; + if (node.type === 'file') { + if (typeof node.content === 'string') bytes += this._utf8Bytes(node.content); + return; + } + const kids = node.content; + if (!kids || typeof kids !== 'object') return; + for (const name of Object.keys(kids)) { + if (++nodes > 20000) return; + walk(kids[name], depth + 1); + } + }; + walk(root, 0); + return bytes; + } + + // Live reachability probe. The legacy `onLine` boolean is not consulted anywhere: + // in Android WebView it reports false while the page is fully connected, which is + // exactly how `status` used to tell working users they were offline. + async _probeNetwork() { + try { + if (typeof fetch !== 'function') return { state: 'unprobed', detail: 'fetch unavailable' }; + } catch (e) { + return { state: 'unprobed', detail: 'fetch unavailable' }; + } + let ctl = null, timer = null; + try { if (typeof AbortController === 'function') ctl = new AbortController(); } catch (e) {} + try { + if (ctl) timer = setTimeout(() => { try { ctl.abort(); } catch (e) {} }, 4000); + const resp = await fetch('./', { + method: 'HEAD', + cache: 'no-store', + signal: ctl ? ctl.signal : undefined, + }); + if (!resp) return { state: 'unreachable', detail: 'empty response' }; + if (resp.ok) return { state: 'reachable', detail: 'HTTP ' + resp.status }; + return { state: 'http-' + resp.status, detail: 'HTTP ' + resp.status }; + } catch (e) { + const aborted = e && (e.name === 'AbortError' || e.name === 'TimeoutError'); + return { + state: 'unreachable', + detail: aborted ? 'timed out after 4s' : ((e && e.message) || 'request failed'), + }; + } finally { + if (timer) clearTimeout(timer); + } + } + registerCommands() { this.commands['help'] = (args) => this.handleHelp(args); this.commands['echo'] = (args) => this.terminal.print(args.join(' ')); @@ -5934,7 +6034,10 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm }, 500); this.commands['status'] = () => { this.terminal.print(`System: WebX11 ${window.NEWOS4_VERSION || 'v4.0'}`); - this.terminal.print(`Network: ${navigator.onLine ? 'Online' : 'Offline'}`); + // The legacy `onLine` boolean is NOT evidence of reachability: Android + // WebView reports false on a fully connected page, and true on a captive + // portal. Say so, and point at the command that actually probes. + this.terminal.print('Network: unknown — run doctor for a live probe'); this.terminal.print(`Files: ${this.terminal.fs.ls().length} items`); this.terminal.print(`Modules: ${Object.keys(this.terminal.moduleManager.modules).length} loaded`); this.terminal.print(`Ollama: ${this.terminal.ollamaManager.connected ? 'Connected' : 'Disconnected'}`); @@ -5961,7 +6064,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this.terminal.print(` Build: ${buildDate}`); this.terminal.print(` Cache key: ${cacheKey}`); this.terminal.print(` Browser: ${ua}`); - this.terminal.print(` Online: ${navigator.onLine ? 'yes' : 'no'}`); + this.terminal.print(` Network: unknown — run doctor for a live probe`); }; // v4.5.43: uptime — time since page load + last save time @@ -6017,40 +6120,102 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm }; // v4.5.43: doctor — run all health checks, rank issues, show top 3 - this.commands['doctor'] = () => { + // v4.6.x: the legacy `onLine` boolean is GONE from this file. It reports false + // on a fully connected Android WebView, which is how `status`/`doctor` used to + // announce an outage on a working page. Reachability now comes from a real HEAD + // probe, and every size is measured in the unit the store actually charges for. + this.commands['doctor'] = async () => { const issues = []; const fs = this.terminal.fs; + const MB = 1024 * 1024; - // Check 1: Network - if (!navigator.onLine) issues.push({ severity: 'high', msg: 'Network: offline' }); + // Check 1: Network — live probe, 4s budget + const net = await this._probeNetwork(); + if (net.state === 'unreachable') { + issues.push({ severity: 'high', msg: `Network: unreachable (HEAD ./ — ${net.detail})` }); + } else if (net.state.indexOf('http-') === 0) { + // A non-ok HEAD is not proof the page is broken (some servers answer + // 405 to HEAD); report the status and let the user judge. + issues.push({ severity: 'medium', msg: `Network: HTTP probe to ./ returned ${net.state} (page may still work)` }); + } // Check 2: Ollama if (!this.terminal.ollamaManager.connected) { issues.push({ severity: 'medium', msg: 'Ollama: not connected (AI features limited)' }); } - // Check 3: VFS size + // Origin quota — context only. It is the whole-origin quota (Cache Storage + // included, typically hundreds of MB to GBs). localStorage has its own + // ~5 MB per-origin cap, so this number must NEVER set a severity: a 4.5 MB + // localStorage is 96% of its cap and 0.2% of a 2 GB origin quota. + let originUsage = 0, originQuota = 0; try { - const vfsBytes = JSON.stringify(fs.root).length; - if (vfsBytes > 524288) issues.push({ severity: 'high', msg: `VFS: ${(vfsBytes/1024).toFixed(0)}KB > 512KB limit` }); - else if (vfsBytes > 262144) issues.push({ severity: 'medium', msg: `VFS: ${(vfsBytes/1024).toFixed(0)}KB > 256KB (consider reset-vfs)` }); + if (typeof navigator !== 'undefined' && navigator.storage && + typeof navigator.storage.estimate === 'function') { + const est = await navigator.storage.estimate(); + if (est && typeof est.quota === 'number' && est.quota > 0) { + originQuota = est.quota; + originUsage = typeof est.usage === 'number' ? est.usage : 0; + } + } + } catch (e) {} + + // Check 3: VFS — UTF-8 bytes of file content (the unit vfs-size reports), + // never JSON.stringify(root).length and never "chars × 2". + let vfsBytes = null; + try { vfsBytes = this._vfsContentBytes(fs.root); } catch (e) { vfsBytes = null; } + + // Check 4: localStorage — JS strings are UTF-16, so each key and value is + // charged 2 bytes per code unit. Σ (key.length + value.length) × 2 is the + // real storage cost, not a fudge factor. + let lsBytes = 0, lsKeys = 0; + try { + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (!k) continue; + lsKeys++; + lsBytes += (k.length + (localStorage.getItem(k) || '').length) * 2; + } } catch (e) {} - // Check 4: Heap + // VFS thresholds: a VFS only threatens the browser budget when it is BOTH + // large on its own and the bulk of what is actually stored. + if (vfsBytes !== null) { + const vfsMB = vfsBytes / MB; + const share = lsBytes > 0 ? vfsBytes / lsBytes : 0; + const budget = lsBytes > 0 + ? `, ${(share * 100).toFixed(0)}% of the ${(lsBytes / MB).toFixed(1)} MB localStorage budget` + : ', no localStorage copy measured'; + if (vfsBytes > 3 * MB && (lsBytes === 0 || share > 0.8)) { + issues.push({ severity: 'high', msg: `VFS: ${vfsMB.toFixed(1)} MB of file content${budget} — close to the ~5 MB per-origin cap` }); + } else if (vfsBytes > 2 * MB) { + issues.push({ severity: 'medium', msg: `VFS: ${vfsMB.toFixed(1)} MB of file content${budget} (consider deleting stale checkpoints)` }); + } + } + + // localStorage thresholds: the cap is ~5 MB per origin, whatever + // navigator.storage.estimate() says the whole origin may use. + if (lsBytes > 4 * MB) { + issues.push({ severity: 'high', msg: `localStorage: ${(lsBytes / MB).toFixed(1)} MB of UTF-16 data in ${lsKeys} keys — approaching the ~5 MB per-origin cap` }); + } else if (lsBytes > 3 * MB) { + issues.push({ severity: 'medium', msg: `localStorage: ${(lsBytes / MB).toFixed(1)} MB of UTF-16 data in ${lsKeys} keys — past half of the ~5 MB per-origin cap` }); + } + + // Check 5: Heap if (performance.memory) { const heapMB = performance.memory.usedJSHeapSize / 1048576; if (heapMB > 200) issues.push({ severity: 'high', msg: `Heap: ${heapMB.toFixed(0)}MB > 200MB` }); else if (heapMB > 100) issues.push({ severity: 'medium', msg: `Heap: ${heapMB.toFixed(0)}MB > 100MB` }); } - // Check 5: Logs + // Check 6: Logs try { const logContent = fs.readFile('/logs/current.log') || ''; const logLines = logContent.split('\n').filter(l => l).length; if (logLines > 500) issues.push({ severity: 'low', msg: `Logs: ${logLines} lines (consider logrotate)` }); } catch (e) {} - // Check 6: Errors in last 100 log lines + // Check 7: Errors in last 100 log lines try { const logContent = fs.readFile('/logs/current.log') || ''; const recent = logContent.split('\n').filter(l => l).slice(-100); @@ -6059,16 +6224,6 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm else if (errorCount > 0) issues.push({ severity: 'medium', msg: `Errors: ${errorCount} errors in last 100 log lines` }); } catch (e) {} - // Check 7: localStorage quota - let lsBytes = 0; - try { - for (let i = 0; i < localStorage.length; i++) { - const k = localStorage.key(i); - if (k) lsBytes += (k.length + (localStorage.getItem(k) || '').length) * 2; - } - } catch (e) {} - if (lsBytes > 4 * 1024 * 1024) issues.push({ severity: 'high', msg: `localStorage: ${(lsBytes/1024/1024).toFixed(1)}MB approaching 5MB quota` }); - // Check 8: Disabled modules let disabled = []; try { disabled = JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]'); } catch (e) {} @@ -6077,6 +6232,11 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } this.terminal.print('🩺 Doctor Report', 'header'); + this.terminal.print(` Measured: VFS ${vfsBytes === null ? 'unavailable' : (vfsBytes / MB).toFixed(2) + ' MB UTF-8'} · localStorage ${(lsBytes / MB).toFixed(2)} MB UTF-16 in ${lsKeys} keys`, 'info'); + this.terminal.print(` Network: ${net.state === 'reachable' ? 'reachable (' + net.detail + ')' : net.state + (net.detail ? ' — ' + net.detail : '')}`, 'info'); + if (originQuota > 0) { + this.terminal.print(` Origin usage ${(originUsage / MB).toFixed(1)} MB of ~${(originQuota / MB).toFixed(0)} MB quota (whole origin, mostly Cache Storage — localStorage has its own ~5 MB cap)`, 'info'); + } if (issues.length === 0) { this.terminal.print(' ✅ All systems healthy', 'success'); return; @@ -6085,20 +6245,21 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm const sevOrder = { high: 0, medium: 1, low: 2 }; issues.sort((a, b) => sevOrder[a.severity] - sevOrder[b.severity]); this.terminal.print(` Found ${issues.length} issue(s):`, 'warning'); - for (const i of issues.slice(0, 5)) { + for (const i of issues.slice(0, 3)) { const icon = i.severity === 'high' ? '🔴' : i.severity === 'medium' ? '🟡' : '🟢'; this.terminal.print(` ${icon} [${i.severity.toUpperCase()}] ${i.msg}`, i.severity === 'high' ? 'error' : 'warning'); } - if (issues.length > 5) this.terminal.print(` ... and ${issues.length - 5} more`, 'info'); + if (issues.length > 3) this.terminal.print(` ... and ${issues.length - 3} more`, 'info'); this.terminal.print('', ''); this.terminal.print(' Recommended actions:', 'info'); - if (issues.some(i => i.msg.startsWith('Network'))) this.terminal.print(' → Check internet connection'); + if (issues.some(i => i.msg.startsWith('Network: unreachable'))) this.terminal.print(' → Check connectivity; reload before trusting this probe'); + if (issues.some(i => i.msg.startsWith('Network: HTTP probe'))) this.terminal.print(' → Server answered the probe with an error status — check the server, not the network'); if (issues.some(i => i.msg.startsWith('Ollama'))) this.terminal.print(' → Check Ollama is running (ollama serve)'); - if (issues.some(i => i.msg.startsWith('VFS'))) this.terminal.print(' → Run reset-vfs --confirm to clear or checkpoint/rollback'); + if (issues.some(i => i.msg.startsWith('VFS'))) this.terminal.print(' → Delete stale snapshots (checkpoint-list, checkpoint-del <name>) or rollback; do NOT reset-vfs'); if (issues.some(i => i.msg.startsWith('Heap'))) this.terminal.print(' → Refresh the page to clear memory'); if (issues.some(i => i.msg.startsWith('Logs'))) this.terminal.print(' → Run logrotate'); if (issues.some(i => i.msg.startsWith('Errors'))) this.terminal.print(' → Run errors to see details, or bug-report to save state'); - if (issues.some(i => i.msg.startsWith('localStorage'))) this.terminal.print(' → Run factory-reset --confirm (last resort)'); + if (issues.some(i => i.msg.startsWith('localStorage'))) this.terminal.print(' → Free space by deleting stale VFS entries or checkpoints; do NOT factory-reset'); }; // v4.5.43: bug-report — generate markdown report to /tmp/ @@ -6112,7 +6273,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm lines.push('## System'); lines.push(`- Version: ${window.NEWOS4_VERSION || 'unknown'}`); lines.push(`- Cache key: ${localStorage.getItem('webx11_cache_ver') || 'unknown'}`); - lines.push(`- Online: ${navigator.onLine ? 'yes' : 'no'}`); + lines.push(`- Online: unknown (not probed; run doctor)`); lines.push(`- Browser: ${navigator.userAgent}`); lines.push(`- Screen: ${screen.width}x${screen.height}`); lines.push(`- Heap: ${performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'}`); @@ -6176,7 +6337,10 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm version: window.NEWOS4_VERSION || 'unknown', url: location.href, ua: navigator.userAgent, - online: navigator.onLine, + // Legacy key, kept so stored reports keep their shape. The old onLine + // boolean lied in Android WebView, so null = "not asserted". + online: null, + network: 'unknown', modules: Object.keys(this.terminal.moduleManager.modules).length, errors: [], }; @@ -7416,17 +7580,52 @@ class FileSystemModule { }; this.commands['vfs-size'] = () => { try { - const json = JSON.stringify(this.terminal.fs.root); - const totalSize = (json.length / 1024 / 1024).toFixed(2); + // Every number this command prints is STORED BYTES: the UTF-8 length of each + // file's content. It used to report JSON.stringify(root).length, which is UTF-16 + // string units plus every structural character of the serialisation — roughly 2x + // the real size of a plain-text tree, and wrong the other way for non-ASCII. + const utf8Len = (s) => { + if (typeof s !== 'string' || s.length === 0) return 0; + if (typeof Blob !== 'undefined') { + try { return new Blob([s]).size; } catch (e) { /* fall through */ } + } + if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(s).length; + return s.length; // last resort only: no Blob and no TextEncoder + }; + const fmtSize = (bytes) => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / 1024 / 1024).toFixed(2) + ' MB'; + }; + // Recursively total one subtree. Each file is visited exactly once, so a nested + // tree is never double counted and no directory node adds bytes of its own. + const measure = (node) => { + const out = { bytes: 0, files: 0 }; + if (!node || !node.content) return out; + for (const child of Object.values(node.content)) { + if (!child) continue; + if (child.type === 'file') { + out.bytes += utf8Len(child.content); + out.files += 1; + } else if (child.type === 'dir') { + const sub = measure(child); + out.bytes += sub.bytes; + out.files += sub.files; + } + } + return out; + }; const root = this.terminal.fs.root; + const total = measure(root); + // Per-file table, same byte metric. const sizeMap = []; function walk(node, path) { if (!node || !node.content) return; for (const [name, child] of Object.entries(node.content)) { + if (!child) continue; const fullPath = path + '/' + name; if (child.type === 'file') { - const size = (child.content ? child.content.length : 0); - sizeMap.push({ path: fullPath, size, sizeKB: (size / 1024).toFixed(1) }); + sizeMap.push({ path: fullPath, size: utf8Len(child.content) }); } else if (child.type === 'dir') { walk(child, fullPath); } @@ -7434,23 +7633,54 @@ class FileSystemModule { } walk(root, ''); sizeMap.sort((a, b) => b.size - a.size); + // Top-level groups, so the table accounts for the whole tree: one row per + // top-level directory plus a row for files stored directly at root. + const groups = []; + const rootFiles = { bytes: 0, files: 0 }; + for (const [name, child] of Object.entries((root && root.content) || {})) { + if (!child) continue; + if (child.type === 'dir') { + const m = measure(child); + groups.push({ name: '/' + name, bytes: m.bytes, files: m.files }); + } else if (child.type === 'file') { + rootFiles.bytes += utf8Len(child.content); + rootFiles.files += 1; + } + } + if (rootFiles.files > 0) groups.push({ name: '/(root files)', bytes: rootFiles.bytes, files: rootFiles.files }); + groups.sort((a, b) => b.bytes - a.bytes); const totalFiles = sizeMap.length; - const top5 = sizeMap.slice(0, 5); this.terminal.print('📊 VFS Size Report', 'text-muted'); - this.terminal.print(' Total: ' + totalSize + ' MB across ' + totalFiles + ' files'); - this.terminal.print(' Largest:'); - top5.forEach((f, i) => { - this.terminal.print(' ' + (i + 1) + '. ' + f.path + ' — ' + f.sizeKB + ' KB'); - }); - // Show system dir sizes - const sysDirs = ['/_snapshots', '/_sessions', '/.trash']; + this.terminal.print(' Total: ' + fmtSize(total.bytes) + ' (' + total.bytes + ' bytes) across ' + totalFiles + (totalFiles === 1 ? ' file' : ' files')); + if (totalFiles > 0) { + this.terminal.print(' Largest:'); + const shownFiles = sizeMap.slice(0, 10); + shownFiles.forEach((f, i) => { + this.terminal.print(' ' + (i + 1) + '. ' + f.path + ' — ' + fmtSize(f.size)); + }); + // The rows must add up to the total above, so the tail is folded into one + // row instead of dropped: this table used to be top-5 only, and the rest of + // the tree was missing from the report entirely. + if (totalFiles > shownFiles.length) { + let restBytes = 0; + for (const f of sizeMap.slice(shownFiles.length)) restBytes += f.size; + this.terminal.print(' (+' + (totalFiles - shownFiles.length) + ' more files — ' + fmtSize(restBytes) + ')'); + } + } + // Every top-level group is listed (root breadth, not depth), so these rows + // always sum to the total above. + this.terminal.print(' Top directories by stored bytes:', 'info'); + for (const g of groups) { + this.terminal.print(' ' + g.name + ' — ' + fmtSize(g.bytes) + ' (' + g.files + (g.files === 1 ? ' file)' : ' files)')); + } + // Show system dir sizes — real bytes, not JSON.stringify(list of entries).length + const sysDirs = ['/_snapshots', '/_sessions', '/.trash', '/_mods', '/.semantic_index']; for (const dir of sysDirs) { - const dirNode = this.terminal.fs.readDir(dir); + const name = dir.replace(/^\//, ''); + const dirNode = (root && root.content && root.content[name]) || null; if (dirNode) { - const cnt = dirNode.length; - // estimate dir size - const dirSize = JSON.stringify(dirNode).length; - this.terminal.print(' ' + dir + ': ' + cnt + ' files, ~' + (dirSize / 1024).toFixed(1) + ' KB', dirSize > 102400 ? 'warning' : 'text-muted'); + const m = measure(dirNode); + this.terminal.print(' ' + dir + ': ' + m.files + (m.files === 1 ? ' file, ' : ' files, ') + fmtSize(m.bytes), m.bytes > 102400 ? 'warning' : 'text-muted'); } } } catch (e) { @@ -8553,7 +8783,9 @@ class ServerMonitorModule { this.terminal.print('🌐 Network Test', 'header'); // 1. Browser online const online = navigator.onLine; - this.terminal.print(` ${online ? '✅' : '🔴'} Browser: ${online ? 'online' : 'offline'}`, online ? 'success' : 'error'); + // Reported, not measured: WebViews answer false while connected. The real verdict comes + // from the proxy probe below, so this line must not read as a failure. + this.terminal.print(` ${online ? '✅' : '❔'} Browser reports: ${online ? 'online' : 'offline (unreliable)'}`, online ? 'success' : 'warning'); // 2. Proxy API try { const t0 = Date.now(); |