diff options
| author | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-12 16:39:12 +0100 |
|---|---|---|
| committer | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-12 16:39:12 +0100 |
| commit | 228ad621e2631487e74acdf9bf58e797f60ab745 (patch) | |
| tree | 6372c40b0ec6ec1470999b0e8e39cd267f24e979 /modules/core/module-fs.js | |
| parent | cbaa678ae624860ce4f918428a7b09d5f0e0ae2f (diff) | |
fix(newos4): honest size/network measurements + checkpoint and phantom-delete fixes (v4.6.160)
Applies the three 2026-11-09 patches after review, and fixes two more
navigator.onLine lies found while verifying them. Each patch was checked
against the live code first; one of its claims did not survive.
1. virtual-fs — phantom deletes and checkpoint bloat
- deleteCheckpoint('nope') returned true and printed "Deleted checkpoint"
for a name that did not exist: _resolvePath() validates intermediate
segments only, so the leaf was never checked. New _requireLeaf() refuses
a resolved path whose leaf is absent, and deleteCheckpoint uses it.
- CHECKPOINT SIZE: checkpoint() cloned the whole root minus
_snapshots/_sessions/.trash, so _mods (module originals) and
.semantic_index were duplicated into EVERY checkpoint — measured
2,101,686 bytes for a 2 MiB _mods. Both are now excluded on write and
preserved across rollback (without the keep-list change a rollback would
delete the live module originals). Measured after: 185 bytes.
- Claim corrected: the patch also claimed deleteFile() reported success on
a phantom path. It did not — live code already required
`node && node.type === 'file'` and returned false. The guard is kept as
defence-in-depth, and the test asserts the real behaviour rather than the
claimed one.
2. module-fs — vfs-size reported ~2x the real size
- It measured JSON.stringify(root).length: UTF-16 code units PLUS every
structural character of the serialisation. A 1046-byte tree printed as
1961; anything under ~5 KB printed as "0.00 MB".
- Every number is now stored bytes (UTF-8 of file content): one measure()
walk for the total, the per-file table, the top-directory table and the
system dirs, so the rows add up to the total. The patch's arithmetic was
still off in seven places, all fixed here: a UTF-16 fallback that
reintroduced the bug when Blob was missing, a top-5 table that could
never sum to the total (now top 10 + an aggregated remainder row), the
top-dirs table dropping files stored at root (now a "/(root files)" row),
a 2-decimal MB total that printed 0.00 for small VFS, three duplicate
hand-rolled walks, readDir() used for dir sizes, and "1 files".
3. module-system — status/doctor no longer trust navigator.onLine
- status printed "Network: Offline" from navigator.onLine, which an
Android WebView reports false while fully connected.
- doctor now awaits a real HEAD probe (AbortController, 4 s): only
'reachable' is healthy, a throw is HIGH "Network: unreachable", a non-ok
answer is MEDIUM and names the status (405 to HEAD is not "offline").
- THE PATCH'S THRESHOLDS COULD NEVER FIRE: it compared localStorage bytes
against navigator.storage.estimate().quota, which is the whole-origin
quota (GBs, mostly Cache Storage), while localStorage has its own ~5 MB
cap — a 4.5 MB localStorage reported "All systems healthy" against a
2 GB quota. Now: localStorage measured as (key+value)*2 UTF-16 bytes with
HIGH > 4 MiB / MEDIUM > 3 MiB, VFS measured in UTF-8 bytes with
HIGH > 3 MiB and > 80% of the measured localStorage bytes. The origin
quota appears only in one informational line and decides no severity.
- Also removed navigator.onLine from version, bug-report and bug (a grep
for it is now 0 in the module), and added a "Measured:" line so a clean
report still shows what was measured.
4. terminal.js / module-servermon.js — the same lie, where the user looks
- The status bar printed "❌ Offline" in red from navigator.onLine; on a
WebView that is false while online. It now shows "📶 Network?" with a
title pointing at `doctor` unless the browser claims online.
- `net` test line now reads "Browser reports: offline (unreliable)" as a
warning instead of a red failure — the real verdict comes from the
probes that follow.
Verified: 40/40 node tests (3 new files: 36 + 53 + 34 assertions, each proven
red against the pre-patch code; the doctor test runs against the PATCH's
threshold logic and shows the 4.5 MB case staying silent). An independent
orchestrator probe re-derived all of it: phantom delete false, 2 MB _mods
absent from the checkpoint and preserved through rollback, vfs-size exact
bytes for ASCII/é/☃ nested trees, 4.5 MB localStorage HIGH despite a 2 GB
quota, failing probe unreachable, status never claiming Online/Offline.
Served bytes identical for core-bundle.js / main.html / module-system.js /
virtual-fs.js. Live Chromium on v4.6.160: vfs-size "85.8 KB (87880 bytes)
across 40 files", doctor "Measured: VFS 0.08 MB UTF-8 · localStorage 0.07 MB
UTF-16 in 14 keys", a stubbed failing probe reports HIGH unreachable, the
status bar reads "📶 Network?" when the browser lies, deleteCheckpoint on a
missing name returns false, and the console has 0 warnings and 0 JS errors.
Diffstat (limited to 'modules/core/module-fs.js')
| -rw-r--r-- | modules/core/module-fs.js | 100 |
1 files changed, 83 insertions, 17 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js index 0dea0d5..b6cf272 100644 --- a/modules/core/module-fs.js +++ b/modules/core/module-fs.js @@ -730,17 +730,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); } @@ -748,23 +783,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) { |