aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-18 17:19:14 +0200
committerAgando <roduccomm@gmail.com>2026-10-18 17:19:14 +0200
commit7e904aec06c09f7fdef0938155d2b9a19d36e314 (patch)
treef859e146ad9e34c75d86ebcabefdb70a07ec4db9
parent267b50588c736fdfec9a53488a2a39c608ebd7d4 (diff)
newOS4 v4.6.40: review fixes (bug diagnostics, trash tree, dir paste, stale capture, preview)
-rw-r--r--api/debug/report.php13
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js35
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-system.js14
-rw-r--r--modules/core/virtual-fs.js21
-rw-r--r--modules/ext/course.js5
-rw-r--r--modules/ext/critical-ext-bundle.js13
-rw-r--r--modules/ext/filemgr.js13
9 files changed, 77 insertions, 45 deletions
diff --git a/api/debug/report.php b/api/debug/report.php
index a4cf9f1..cefcae6 100644
--- a/api/debug/report.php
+++ b/api/debug/report.php
@@ -61,6 +61,13 @@ foreach (array_slice(array_reverse($existing), 0, 100) as $f) {
$d = json_decode(@file_get_contents($f), true);
if (!is_array($d)) continue;
if (($d['fingerprint'] ?? '') === $fingerprint && strtotime($d['created_at'] ?? '') > $now - 86400) {
+ // Don't discard user content on a duplicate: keep it as an appended note.
+ $d['duplicate_count'] = (int)($d['duplicate_count'] ?? 0) + 1;
+ if ($body !== '' && ($d['body'] ?? '') !== $body) {
+ $d['body'] = trim((string)($d['body'] ?? '') . "\n---\n[duplicate " . gmdate('c') . "] " . $body);
+ $json = json_encode($d, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ if ($json !== false) { @file_put_contents($f, $json); @chmod($f, 0664); }
+ }
json_response([
'success' => true,
'id' => $d['id'],
@@ -91,6 +98,12 @@ if ($json === false || file_put_contents($file, $json) === false) {
}
@chmod($file, 0664);
+// Regenerate the public page immediately so the URL we print is never stale.
+$generator = realpath(__DIR__ . '/../../scripts/gen-bug-report.py');
+if ($generator) {
+ @exec('/usr/bin/python3 ' . escapeshellarg($generator) . ' 2>&1', $genOut, $genRc);
+}
+
json_response([
'success' => true,
'id' => $id,
diff --git a/main.html b/main.html
index 1a0c622..f3564e6 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.38"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.40"></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.38"></script>
+<script src="modules/core/module-loader.js?v=v4.6.40"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.38"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.40"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 025572c..f275a1f 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -615,20 +615,13 @@ class VirtualFileSystem {
this.root.content['.trash'] = { type: 'dir', content: {} };
}
const trash = this.root.content['.trash'];
- const flatten = (n, prefix) => {
- if (n.type === 'file') {
- const flatName = prefix + '-' + name;
- let ctr = 0;
- let unique = flatName;
- while (trash.content[unique]) { unique = flatName + '-' + (++ctr); }
- trash.content[unique] = JSON.parse(JSON.stringify(n));
- } else if (n.type === 'dir') {
- for (const [childName, child] of Object.entries(n.content || {})) {
- flatten(child, prefix + '-' + childName);
- }
- }
- };
- flatten(node, ts + '-dir-' + name);
+ // Keep the subtree intact so it can be restored later; the old flatten loop
+ // turned every descendant into a sibling file and lost empty dirs entirely.
+ const baseName = ts + '-dir-' + name;
+ let unique = baseName;
+ let ctr = 0;
+ while (trash.content[unique]) { unique = baseName + '-' + (++ctr); }
+ trash.content[unique] = JSON.parse(JSON.stringify(node));
delete resolved.dir.content[resolved.name];
this.saveToStorage();
this._emitChange(path);
@@ -5758,8 +5751,18 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
errors: [],
};
try {
- const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
- const list = Array.isArray(raw) ? raw : (raw.errors || []);
+ // Prefer the LIVE error DB: localStorage writes are debounced, so a report
+ // filed right after an error would otherwise carry an empty list.
+ let list = [];
+ try {
+ const ds = Object.values(this.terminal.moduleManager.modules)
+ .find(m => m && m._errorDB && m._errorDB.errors);
+ if (ds) list = Object.values(ds._errorDB.errors);
+ } catch (_e) {}
+ if (!list.length) {
+ const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
+ list = Array.isArray(raw) ? raw : Object.values((raw && raw.errors) || {});
+ }
diag.errors = list.slice(-20).map(e => ({
message: String(e.message || e.msg || '').slice(0, 300),
source: String(e.source || e.url || '').slice(0, 200),
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index f9b4c9b..0913d6a 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.38';
+ const LATEST_VER = 'v4.6.40';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index 1a9ad05..bedceef 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -318,8 +318,18 @@
errors: [],
};
try {
- const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
- const list = Array.isArray(raw) ? raw : (raw.errors || []);
+ // Prefer the LIVE error DB: localStorage writes are debounced, so a report
+ // filed right after an error would otherwise carry an empty list.
+ let list = [];
+ try {
+ const ds = Object.values(this.terminal.moduleManager.modules)
+ .find(m => m && m._errorDB && m._errorDB.errors);
+ if (ds) list = Object.values(ds._errorDB.errors);
+ } catch (_e) {}
+ if (!list.length) {
+ const raw = JSON.parse(localStorage.getItem('webx11_debug_db') || '[]');
+ list = Array.isArray(raw) ? raw : Object.values((raw && raw.errors) || {});
+ }
diag.errors = list.slice(-20).map(e => ({
message: String(e.message || e.msg || '').slice(0, 300),
source: String(e.source || e.url || '').slice(0, 200),
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index d8b5098..8f11d25 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -613,20 +613,13 @@ class VirtualFileSystem {
this.root.content['.trash'] = { type: 'dir', content: {} };
}
const trash = this.root.content['.trash'];
- const flatten = (n, prefix) => {
- if (n.type === 'file') {
- const flatName = prefix + '-' + name;
- let ctr = 0;
- let unique = flatName;
- while (trash.content[unique]) { unique = flatName + '-' + (++ctr); }
- trash.content[unique] = JSON.parse(JSON.stringify(n));
- } else if (n.type === 'dir') {
- for (const [childName, child] of Object.entries(n.content || {})) {
- flatten(child, prefix + '-' + childName);
- }
- }
- };
- flatten(node, ts + '-dir-' + name);
+ // Keep the subtree intact so it can be restored later; the old flatten loop
+ // turned every descendant into a sibling file and lost empty dirs entirely.
+ const baseName = ts + '-dir-' + name;
+ let unique = baseName;
+ let ctr = 0;
+ while (trash.content[unique]) { unique = baseName + '-' + (++ctr); }
+ trash.content[unique] = JSON.parse(JSON.stringify(node));
delete resolved.dir.content[resolved.name];
this.saveToStorage();
this._emitChange(path);
diff --git a/modules/ext/course.js b/modules/ext/course.js
index d3b96de..8c615ec 100644
--- a/modules/ext/course.js
+++ b/modules/ext/course.js
@@ -177,7 +177,10 @@ class CourseModule {
if (cmd === 'course') return;
// Only capture while an exercise is actually running. Capturing every command
// swallowed all terminal output (pwd/help/version printed nothing).
- if (!this._active || !this._active.lesson || !this._active.lesson.exercise) return;
+ if (!this._active || !this._active.lesson || !this._active.lesson.exercise) {
+ this._capOut = ''; // never let a previous command's output satisfy an output-check exercise
+ return;
+ }
if (this._capActive) {
try { this.terminal._popPrintCapture(); } catch (e) {}
if (this._capOut) { try { this.terminal.print(this._capOut.replace(/\n+$/, '')); } catch (e) {} }
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
index 504dfa8..8f63081 100644
--- a/modules/ext/critical-ext-bundle.js
+++ b/modules/ext/critical-ext-bundle.js
@@ -1737,6 +1737,10 @@ class FileManagerModule {
const name = src.split('/').pop();
const destPath = dest === '/' ? '/' + name : dest + '/' + name;
if (destPath === src || destPath.startsWith(src + '/')) { skipped++; return; }
+ // Directory → existing directory would REPLACE the whole subtree silently; skip it.
+ let srcIsDir = false;
+ try { srcIsDir = typeof fs.readDir === 'function' && fs.readDir(src) !== null; } catch (_e) {}
+ if (this._exists(destPath) && srcIsDir) { skipped++; return; }
if (this._exists(destPath) && !confirm('Overwrite ' + destPath + '?')) { skipped++; return; }
try {
const ok = this._clipboard.mode === 'copy' ? fs.copy(src, destPath) : fs.move(src, destPath);
@@ -1754,8 +1758,8 @@ class FileManagerModule {
_batchDelete() {
const paths = this._getSelectedPaths();
- if (!paths.length) { this._updateStatus('No files selected'); return; }
- if (!confirm('Delete ' + paths.length + ' item(s)?')) return;
+ if (!paths.length) { this._updateStatus('No files selected'); return false; }
+ if (!confirm('Delete ' + paths.length + ' item(s)?')) return false;
const fs = this.terminal.fs;
let count = 0, failed = 0;
paths.forEach(p => {
@@ -1768,6 +1772,7 @@ class FileManagerModule {
this._selectedFile = null;
this._refreshFileList();
this._updateStatus('🗑️ Deleted ' + count + ' item(s)' + (failed ? ' — ' + failed + ' failed' : ''));
+ return count > 0;
}
_batchRename() {
@@ -1880,8 +1885,8 @@ class FileManagerModule {
if (!this._getSelectedPaths().length && this._selectedFile) {
this._multiSelect.add(this._selectedFile);
}
- this._batchDelete();
- this._clearPreview();
+ // Only clear the preview when something was actually deleted (not on cancel).
+ if (this._batchDelete()) this._clearPreview();
}
_actionDownload() {
diff --git a/modules/ext/filemgr.js b/modules/ext/filemgr.js
index 28c62ce..e099a34 100644
--- a/modules/ext/filemgr.js
+++ b/modules/ext/filemgr.js
@@ -804,6 +804,10 @@ class FileManagerModule {
const name = src.split('/').pop();
const destPath = dest === '/' ? '/' + name : dest + '/' + name;
if (destPath === src || destPath.startsWith(src + '/')) { skipped++; return; }
+ // Directory → existing directory would REPLACE the whole subtree silently; skip it.
+ let srcIsDir = false;
+ try { srcIsDir = typeof fs.readDir === 'function' && fs.readDir(src) !== null; } catch (_e) {}
+ if (this._exists(destPath) && srcIsDir) { skipped++; return; }
if (this._exists(destPath) && !confirm('Overwrite ' + destPath + '?')) { skipped++; return; }
try {
const ok = this._clipboard.mode === 'copy' ? fs.copy(src, destPath) : fs.move(src, destPath);
@@ -821,8 +825,8 @@ class FileManagerModule {
_batchDelete() {
const paths = this._getSelectedPaths();
- if (!paths.length) { this._updateStatus('No files selected'); return; }
- if (!confirm('Delete ' + paths.length + ' item(s)?')) return;
+ if (!paths.length) { this._updateStatus('No files selected'); return false; }
+ if (!confirm('Delete ' + paths.length + ' item(s)?')) return false;
const fs = this.terminal.fs;
let count = 0, failed = 0;
paths.forEach(p => {
@@ -835,6 +839,7 @@ class FileManagerModule {
this._selectedFile = null;
this._refreshFileList();
this._updateStatus('🗑️ Deleted ' + count + ' item(s)' + (failed ? ' — ' + failed + ' failed' : ''));
+ return count > 0;
}
_batchRename() {
@@ -947,8 +952,8 @@ class FileManagerModule {
if (!this._getSelectedPaths().length && this._selectedFile) {
this._multiSelect.add(this._selectedFile);
}
- this._batchDelete();
- this._clearPreview();
+ // Only clear the preview when something was actually deleted (not on cancel).
+ if (this._batchDelete()) this._clearPreview();
}
_actionDownload() {