diff options
| -rw-r--r-- | api/debug/report.php | 13 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 35 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-system.js | 14 | ||||
| -rw-r--r-- | modules/core/virtual-fs.js | 21 | ||||
| -rw-r--r-- | modules/ext/course.js | 5 | ||||
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 13 | ||||
| -rw-r--r-- | modules/ext/filemgr.js | 13 |
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, @@ -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() { |