aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-06-02 18:00:37 +0200
committerAgando <roduccomm@gmail.com>2026-06-02 18:00:37 +0200
commitaafb0fbde5289a45f9f818a00141f80b149be283 (patch)
treed4d8f588187719c99240fbaef1940c068402b892 /modules/ext
parent6d41e590a38c73ef60f1c04beaf8f5438f350f46 (diff)
auto: 2026-06-02_18:00 changes
Diffstat (limited to 'modules/ext')
-rw-r--r--modules/ext/skill-pack-core.js284
-rw-r--r--modules/ext/webdav.js515
2 files changed, 666 insertions, 133 deletions
diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js
index cc8ae88..f731594 100644
--- a/modules/ext/skill-pack-core.js
+++ b/modules/ext/skill-pack-core.js
@@ -20,37 +20,51 @@ class SkillPackCoreModule {
getHelpInfo() {
return {
- 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, mindmap, etc.)',
+ 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, agents, webdav, security, etc.)',
};
}
_buildSkills() {
const s = (name, desc, tags, cat, steps) => ({
name, description: desc, tags, category: cat,
- content: `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: ${cat}\n---\n## Steps\n${steps}`
+ content: '---\nname: ' + name + '\ndescription: ' + desc + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + cat + '\n---\n## Steps\n' + steps
+ });
+
+ const sk = (name, desc, tags, cat, content) => ({
+ name, description: desc, tags, category: cat,
+ content: '---\nname: ' + name + '\ndescription: ' + desc + '\ntags: [' + tags.join(', ') + ']\ncategory: ' + cat + '\n---\n' + content
});
return [
s('navigating-the-terminal', 'How to explore the WebX11 terminal, find commands, check status, and navigate the filesystem', ['terminal', 'navigation', 'help', 'filesystem'], 'system',
`1. **Get an overview of available commands:**
- - Call get_help() to get a full command list sorted by category
- - Call get_status() to see filesystem state, loaded modules, Ollama status
- - For a specific command, use help <command> or check the help map
+ - Type "help" for a full command list sorted by category
+ - Type "status" to see filesystem state, loaded modules, Ollama status
+ - For a specific command, use help <command>
+ - Type "commands" for a compact list with module source info
+ - Type "check" for system health including module requirements
2. **Explore the filesystem:**
- - Use list_files(path?) to browse directories (omit path for current dir)
- - Use read_file(path) to view file contents
- - Use get_current_dir to see your working directory
- - Use run_command("ls") or run_command("tree") for visual output
+ - Use ls, ll, tree to browse directories
+ - Use cd <dir> to navigate, pwd to show current path
+ - Use cat or edit to view file contents
+ - Use find or grep to search files
+ - Use du to see disk usage
3. **Understand system state:**
- - run_command("mem") — memory usage
- - run_command("status") — network, files, modules, Ollama
- - run_command("modules") — module manager GUI
- - get_module_status() — loaded modules, URLs, local edits
-
-4. **When user asks a vague question:**
- - First call get_status() and get_help() to understand the current state
+ - mem — memory usage (auto-shown in header on boot)
+ - status — network, files, modules, Ollama
+ - modules — module manager GUI
+ - health-check — VFS size, modules, logs, trash, skills, tool failures
+ - checkpoint-list — VFS checkpoint snapshots
+
+4. **Agent / Orchestration state:**
+ - agent-list — registered agents (type, capabilities, status)
+ - orchestrate — list active orchestrated agents
+ - lease-list — show active leases with TTL
+
+5. **When user asks a vague question:**
+ - First run status and help to understand the current state
- Then ask clarifying questions based on what you found`),
s('memory-and-profile', 'How to use the user profile system, session memory, and pattern tracking for persistent context', ['memory', 'profile', 'session', 'persistence'], 'system',
@@ -67,26 +81,36 @@ class SkillPackCoreModule {
- session_recent(count) — show last N session summaries
- session_stats — aggregate totals across all sessions
- Recent sessions auto-load into context on first message
+ - systemd-style journal: use logs and logrotate commands for persistent logging
3. **Conversation Patterns:**
- Pattern tracking runs automatically — frequent topics surface as hints
- my-set, my-get, my-list store additional personal data
- AI conversation context persists within a session (use ai-clear to reset)
+ - debug command toggles real-time debug panel with Copy/Export/VFS buttons
-4. **When to use:**
+4. **Checkpoints (VFS snapshots):**
+ - checkpoint [name] — snapshot current VFS state
+ - checkpoint-list — list all checkpoints
+ - rollback <name> — restore from checkpoint
+ - checkpoint-del <name> — delete a checkpoint
+
+5. **When to use:**
- At start: call session_recent(3) to see what was worked on before
- After identifying user preferences: call user_set to store them
- - After complex tasks: call session_save with a brief summary`),
+ - After complex tasks: call session_save with a brief summary
+ - Before risky edits: call checkpoint to create a restore point`),
s('web-search-and-fetch', 'How to search the web and fetch URL content using the WebX11 proxy API', ['search', 'fetch', 'web', 'proxy', 'api'], 'research',
`1. **Search the web:**
- - Call web_search(query) — uses /api/search which queries SearXNG + DDG fallback
+ - Use search <query> or web_search(query) — uses /api/search which queries SearXNG + DDG fallback
- Results include: title, url, content snippet, source engine
- Always available (same-origin, no CORS issues)
- 5-minute server-side cache for repeated queries
+ - Alternative: use ddg or duck <query> for DuckDuckGo direct, or s <query> as alias
2. **Fetch URL content:**
- - Call web_fetch(url) — uses /api/extract which strips HTML, extracts clean text
+ - Use fetch <url> or web_fetch(url) — uses /api/extract which strips HTML, extracts clean text
- Returns: title, content (up to 4000 chars), word_count, extraction method
- SSRF protection blocks internal network addresses
- 15-minute server-side cache
@@ -96,19 +120,21 @@ class SkillPackCoreModule {
- search <query> --fetch — fetches content snippets for top 3 results
4. **When to use:**
- - User asks about current events, facts, research → call web_search
- - User says "read this article" or provides a URL → call web_fetch
+ - User asks about current events, facts, research → use search <query>
+ - User says "read this article" or provides a URL → use fetch <url>
- For in-depth analysis: search first, then fetch top results`),
s('module-management', 'How to view, edit, test, reload modules and manage startup configuration', ['modules', 'editing', 'code', 'self-improvement'], 'dev',
`1. **View module source:**
- view_module(name) — fetches original source or reads from _mods/ if edited
- Supports all loaded modules: SystemModule, FileSystemModule, SearXNGModule, etc.
+ - Or use modules command for GUI module manager
2. **Edit a module:**
- edit_module(name) — copies source to _mods/<name>.js and opens code editor
- Editor shows the full source in a textarea
- Click "Save & Test" to save and auto-load the edited version
+ - Checkpoint before editing: checkpoint mod-edit-<name>
3. **Test and reload:**
- test_module(name) — loads edited module from _mods/ for live testing
@@ -121,8 +147,15 @@ class SkillPackCoreModule {
5. **Manage startup:**
- startup-modules — opens the Startup Modules UI to select which modules load
- load-local <path> — load a module from VFS or URL without editing
+ - loadmod <url> — alternative: load module from URL
+ - removemod <name> — remove a loaded module
-6. **When to use:**
+6. **Agent modules (Phase 2):**
+ - module-agent-registry.js — Agent registry with agent-list, agent-register, agent-unregister, agent-status
+ - module-leases.js — Time-bound contracts with 5s watchdog, auto-expiry
+ - module-orchestrator.js — Spawn, delegate, kill, purge with heartbeat monitoring
+
+7. **When to use:**
- User reports a bug in a module → view_module, edit_module, test_module
- User asks "can you add X feature?" → same workflow
- Always save edits before testing. If test fails, the original module stays loaded.`),
@@ -142,42 +175,68 @@ class SkillPackCoreModule {
- config-export — dump all config as JSON
- config-reset — restore defaults (use before factory-reset if you want to save data)
-4. **Health checks (run automatically after complex tasks):**
+4. **Security level:**
+ - security-level <0-3> — set security level (0=Permissive, 1=Standard, 2=Paranoid, 3=Audit)
+ - security — show current security level, blocked count, and stats
+ - cmd-allow <cmd> — whitelist a command from security checks
+ - cmd-deny <cmd> — blacklist a command
+
+5. **Health checks (run automatically after complex tasks):**
- Check module loading status: loaded vs total
- Check for pending module edits
- Check memory usage (high if >100MB)
- Check skill count (0 skills after 2+ tool calls → suggest create_skill)
+ - health-check — full audit: VFS size, modules, logs, trash, skills, tool failures
-5. **Font size and display:**
+6. **Font size and display:**
- font-size <8-32> — set terminal font size, persists across sessions
- setfont — alias for font-size
-6. **When to use:**
- - System is slow or modules fail to load → check check_updates, memory
+7. **Checkpoints:**
+ - checkpoint [name] — snapshot current VFS state
+ - checkpoint-list — list all checkpoints
+ - rollback <name> — restore from checkpoint
+
+8. **When to use:**
+ - System is slow or modules fail to load → check check_updates, mem
- User says "reset everything" → factory-reset --confirm
- - User says "clear my files but keep settings" → reset-vfs --confirm`),
+ - User says "clear my files but keep settings" → reset-vfs --confirm
+ - User reports security concerns → security-level or cmd-allow/cmd-deny`),
s('backup-and-restore', 'How to backup, restore, and export WebX11 data', ['backup', 'restore', 'export', 'import', 'persistence'], 'system',
`1. **Backup VFS:**
- vfs-export — downloads the entire virtual filesystem as a dated JSON file
- backup — creates a full backup including VFS, modules, config (saves to _backups/)
-2. **Export/Import personal data:**
+2. **WebDAV cloud backup:**
+ - webdav-backup — push VFS snapshot to WebDAV server (auto-archive)
+ - webdav-backup-list — list remote WebDAV backups
+ - webdav-restore <file> — restore VFS from remote WebDAV snapshot
+ - webdav-push <local> <remote> — push file to WebDAV space
+ - webdav-pull <remote> <local> — pull file from WebDAV space
+ - webdav-ls [path] — list remote WebDAV directory contents
+ - webdav-status — check WebDAV connection and space usage
+ - webdav-explorer — GUI file browser for WebDAV
+ - webdav-browser — simpler file listing for WebDAV
+ - webdav-config — configure WebDAV endpoint and credentials
+
+3. **Export/Import personal data:**
- my-export — prints all my-set data as formatted JSON
- my-import <json> — bulk-import personal data from JSON string
-3. **Export/Import modules:**
+4. **Export/Import modules:**
- export-mods — exports module registry as JSON
- import-mods <json> — imports modules from JSON backup
-4. **Restore:**
+5. **Restore:**
- restore <json> — restores terminal state from a backup JSON
- Session logs are saved to /memory/ automatically on page unload
-5. **When to use:**
+6. **When to use:**
- Before factory-reset: call my-export, vfs-export, export-mods to save data
- When switching browsers or devices: export all, then import
- - User says "backup my data" or "save my files"`),
+ - User says "backup my data" or "save my files"
+ - For cloud backup: use webdav-backup (requires WebDAV server configured)`),
s('mindmap-workflow', 'How to create, view, edit, and manage brainstorming mindmaps', ['mindmap', 'brainstorming', 'planning', 'organization'], 'productivity',
`1. **Create a new mindmap:**
@@ -215,18 +274,19 @@ class SkillPackCoreModule {
s('skills-system-guide', 'How to use the skills system: load, create, patch, list, and audit skills for persistent procedural memory', ['skills', 'system', 'meta'], 'system',
`1. **Always load skills at task start:**
- - At the START of every task, call load_skills(query) to check for stored procedures
- - Example: load_skills("search the web") returns matching skill content
- - Example: load_skills("edit module") returns the module editing workflow
+ - At the START of every task, type or call "skill-view <name>" or "skill-list"
+ - Example: skill-view "module-management" — returns matching skill content
+ - Example: skill-list system — browse skills by category
2. **Create skills after complex tasks:**
- - After any task with 3+ tool calls, call create_skill to save the approach
+ - After any task with 3+ tool calls, call "skill-create"
- Required fields: name (kebab-case), description, content (markdown with YAML frontmatter)
- Optional: tags (comma-separated), category (e.g. research, system, dev)
- Skills persist in /skills/ as .md files with /skills/index.json for fast lookup
+ - Skill pack auto-seeds 8 core skills on first boot
3. **Patch skills when they're outdated:**
- - If a stored procedure has wrong steps, call patch_skill(name, newContent)
+ - If a stored procedure has wrong steps, call "skill-patch <name>"
- This replaces the entire skill content and updates the index
- Do NOT create a duplicate — always patch the existing skill
@@ -235,13 +295,139 @@ class SkillPackCoreModule {
- Named tool-<name>-workaround (e.g. tool-write_file-workaround)
5. **Audit and maintenance:**
- - list_skills(category?) — browse available skills, optionally filter
+ - skill-list <category?> — browse available skills, optionally filter
+ - skill-export <name> — download a skill as markdown
+ - skill-import <json> — bulk import skills
+ - skill-sync — re-index skills from /skills/ directory
- Skills are audited every 20 messages for staleness (empty content, >30 days old)
6. **YAML frontmatter format:**
- Each skill file must start with: ---\\nname: <name>\\ndescription: <text>\\ntags: [...]\\ncategory: <cat>\\n---\\n
- Steps follow under ## Steps heading
- Use numbered lists for sequential steps`),
+
+ sk('agent-orchestration', 'How to use the agent registry, leases, and orchestrator for multi-agent workflows', ['agents', 'orchestration', 'delegation', 'leases'], 'system',
+`## Steps
+1. **Agent Registry:**
+ - agent-list — list all registered agents (ID, type, capabilities, status)
+ - agent-register <type> <capabilities> — register a new agent (capabilities: comma-separated like "code,research")
+ - agent-unregister <id> — remove an agent
+ - agent-status <id> — detailed status including uptime, task history
+
+2. **Leases (time-bound contracts):**
+ - lease-create <agent-id> <ttl-seconds> — create a lease with watchdog timer
+ - lease-list — show active leases with remaining TTL
+ - lease-revoke <lease-id> — terminate a lease early
+ - lease-renew <lease-id> <ttl-seconds> — extend a lease
+
+3. **Orchestrator (spawn, delegate, kill):**
+ - orchestrator-spawn <type> <capabilities> — spawn a new agent automatically (registers + creates lease)
+ - orchestrator-kill <agent-id> — kill an agent (revokes lease + unregisters)
+ - orchestrate — list all active agents with their orchestrator state
+ - orchestrator-purge — kill all idle/expired agents
+ - orchestrator-status — full system status: active, idle, dead agents
+
+4. **AI tool integration:**
+ - AI tools list_agents, delegate_task, spawn_agent are available in the AI system prompt
+ - The AI can auto-spawn worker agents for sub-tasks
+ - Each agent gets its own VFS workspace at /agents/<agent-id>/
+
+5. **When to use:**
+ - User needs parallel task execution → orchestrator-spawn coder + researcher
+ - Time-bound automation → lease-create with short TTL
+ - System cleanup → orchestrator-purge to kill stale agents`),
+
+ sk('debug-panel-and-logs', 'How to use the debug panel, session logging, and terminal diagnostics', ['debug', 'logging', 'diagnostics', 'troubleshooting'], 'system',
+`## Steps
+1. **Toggle debug panel:**
+ - debug — opens real-time debug panel showing last AI tool call, response time, and context
+ - Debug panel includes Copy, Export, and Write to VFS buttons
+ - Export: saves tool call log as timestamped JSON to downloads
+ - Write to VFS: appends debug data to /debug.log (dedup by turn ID)
+
+2. **Session logging:**
+ - logs [n] [LEVEL] — view last n log entries (optionally filtered by INFO/WARN/ERROR)
+ - logrotate — rotate current.log to dated archive (auto-cleans old logs beyond 10)
+ - Logs stored in /logs/ directory: current.log + dated archives
+ - Health check runs automatically after complex tasks
+
+3. **Diagnostic commands:**
+ - health-check — full audit: VFS size, modules, logs, trash, skills
+ - check — quick system health including module requirement status
+ - mem — real-time memory usage (also in header)
+ - status — network, files, modules, Ollama connectivity
+
+4. **System journals:**
+ - debug.log — appended by debug panel VFS button (dedup by turn ID)
+ - current.log — system activity log with rotation
+ - Auto-save: terminal state auto-saves every 60s
+
+5. **When to use:**
+ - AI is behaving unexpectedly → debug to see tool calls
+ - User reports errors → logs 50 ERROR to see error entries
+ - Memory concerns → mem to check, health-check for full audit`),
+
+ sk('webdav-operations', 'How to use WebDAV cloud storage for backup, file sync, and remote file access', ['webdav', 'cloud', 'backup', 'sync', 'storage'], 'system',
+`## Steps
+1. **Configuration:**
+ - webdav-config — configure WebDAV endpoint URL and credentials
+ - Connection auto-tested on config; webdav-status shows connection health
+ - Uses per-user token stored in .token file on the server side
+
+2. **File operations:**
+ - webdav-ls [path] — list remote directory contents
+ - webdav-push <local-path> <remote-path> — upload file to WebDAV
+ - webdav-pull <remote-path> [local-path] — download file from WebDAV
+ - webdav-explorer — GUI file browser for WebDAV
+ - webdav-browser — CLI file browser for WebDAV
+
+3. **Backup & Restore:**
+ - webdav-backup — push full VFS snapshot to WebDAV (creates /backups/newos4-<date>.json)
+ - webdav-backup-list — list all remote backup snapshots
+ - webdav-restore <filename> — restore VFS from a remote backup snapshot
+
+4. **Status & Diagnostics:**
+ - webdav-status — connection status, last sync, space information
+ - If uploads fail: check webdav-config, ensure .token file is valid
+ - WebDAV runs on the same server (nginx with WebDAV module), no external dependency
+
+5. **When to use:**
+ - Before factory-reset: webdav-backup to store VFS remotely
+ - Cross-browser: webdav-push/pull to move files between instances
+ - User says "cloud backup" or "remote save" → webdav-backup`),
+
+ sk('security-and-safety', 'How to use WebX11 security levels, command blocking, and safety features', ['security', 'safety', 'blocking', 'permissions'], 'system',
+`## Steps
+1. **Security levels:**
+ - security-level <0-3> — set security level:
+ 0 = Permissive (no blocks, no confirms — anything runs)
+ 1 = Standard (unknown cmds → AI route, destructive → confirm, system → block)
+ 2 = Paranoid (unknown cmds blocked entirely, destructive blocked)
+ 3 = Audit (everything allowed, everything logged)
+ - Default: level 1
+ - security — show current level, blocked count, and stats
+
+2. **Command control:**
+ - cmd-allow <cmd> — whitelist a command from security checks
+ - cmd-deny <cmd> — blacklist/block a command entirely
+ - whitelist built-in: help, clear, ls, cat, pwd, whoami, df, free, top, ps, echo, date, env, history
+
+3. **What gets blocked at each level:**
+ - Unknown commands: Level 1 → AI routed, Level 2+ → blocked
+ - Destructive commands (delete, write, save, format, etc.): Level 1 → confirmation, Level 2+ → blocked
+ - System commands (reboot, shutdown, format, kill): Level 1+ → blocked
+ - Suspicious flags (--execute, --eval, --shell): Level 1+ → blocked
+
+4. **Safety checklist:**
+ - Check security: security shows your current level
+ - Before running risky commands: create a checkpoint first
+ - For temporary relax: security-level 0, then restore to 1
+ - For total lockdown before leaving: security-level 2
+
+5. **When to use:**
+ - User wants to lock down the terminal → security-level 2
+ - User wants to allow a blocked command → cmd-allow <cmd>
+ - Debugging strange behavior → verify security-level (user may have set it higher)`)
];
}
registerCommands() {
@@ -264,8 +450,9 @@ class SkillPackCoreModule {
console.log('[SkillPackCore] existing index keys:', Object.keys(index));
let seeded = 0;
for (const skill of this._skills) {
- if (!index[skill.name]) {
- fs.writeFile(`/skills/${skill.name}.md`, skill.content);
+ const existing = index[skill.name];
+ if (!existing) {
+ fs.writeFile('/skills/' + skill.name + '.md', skill.content);
index[skill.name] = {
name: skill.name,
description: skill.description,
@@ -275,14 +462,25 @@ class SkillPackCoreModule {
updated: new Date().toISOString()
};
seeded++;
+ } else {
+ // Update existing skill content on fresh load (cache-bust version bump triggers this)
+ fs.writeFile('/skills/' + skill.name + '.md', skill.content);
+ index[skill.name] = {
+ ...existing,
+ description: skill.description,
+ tags: skill.tags,
+ category: skill.category,
+ updated: new Date().toISOString()
+ };
}
}
if (seeded > 0) {
- fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print('📚 Seeded ' + seeded + ' new skill(s)', 'success');
}
+ fs.fsWriteJSON('/skills/index.json', index);
this._seeded = true;
}
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new SkillPackCoreModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/skill-pack-core.js');
-}
+} \ No newline at end of file
diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js
index b4dfa6f..eea513b 100644
--- a/modules/ext/webdav.js
+++ b/modules/ext/webdav.js
@@ -46,8 +46,11 @@ class WebDAVModule {
'webdav-backup': 'Backup entire VFS to WebDAV as a dated JSON snapshot',
'webdav-restore': 'Restore VFS from a WebDAV backup file',
'webdav-backup-list': 'List available backup files on WebDAV',
- 'webdav-explorer': 'webdav-explorer [path] — Interactive remote file browser',
- 'webdav-browser': 'webdav-browser [path] — Visual WebDAV file explorer modal',
+ 'webdav-browser': 'webdav-browser [path] — Modern WebDAV file-tree browser with bulk operations',
+ 'webdav-rm': 'webdav-rm <path> — Delete a remote file or directory',
+ 'webdav-mkdir': 'webdav-mkdir <path> — Create a remote directory',
+ 'webdav-rename': 'webdav-rename <old> <new> — Rename or move a remote file/dir',
+ 'webdav-move': 'webdav-move <old> <new> — Alias for webdav-rename',
};
}
@@ -127,7 +130,11 @@ class WebDAVModule {
}
async _listDir(path) {
- // Try PROPFIND first (works with WebDAV servers), fallback to nginx autoindex
+ // For WebX11 spaces, skip PROPFIND (nginx returns 302) and use __list__ directly
+ if (this._spaceName) {
+ return this._listDirViaAutoIndex(path);
+ }
+ // Non-nginx: try PROPFIND first, fallback to autoindex
try {
const response = await this._webdavRequest('PROPFIND', path, {
headers: { 'Depth': '1' }
@@ -137,15 +144,24 @@ class WebDAVModule {
return this._parsePropfind(text);
}
} catch (e) { /* PROPFIND failed, try autoindex */ }
- // Fallback: use nginx __list__ endpoint
return this._listDirViaAutoIndex(path);
}
async _listDirViaAutoIndex(path) {
- const baseUrl = this.config.url.replace(/\/+$/, '') + '/';
- const urlPath = path ? (path.startsWith('/') ? path : '/' + path) : '/';
- // Use __list__ for directory listing
- const listUrl = baseUrl.replace(/\/spaces\/([^/]+)\//, '/spaces/$1/__list__/') + urlPath.replace(/^\//, '');
+ if (!this._spaceName) {
+ throw new Error('WebDAV: cannot use __list__ without space name');
+ }
+ // Derive host from configured URL to support different environments
+ let host = 'https://webx11.duckdns.org';
+ if (this.config && this.config.url) {
+ try {
+ const u = new URL(this.config.url);
+ host = u.origin;
+ } catch (e) {}
+ }
+ const cleanPath = path ? (path.startsWith('/') ? path : '/' + path) : '/';
+ // Avoid double slash when path starts with /
+ const listUrl = host + '/spaces/' + this._spaceName + '/__list__' + (cleanPath === '/' ? '' : cleanPath.replace(/\/{2,}/g, '/'));
try {
const resp = await fetch(listUrl, { method: 'GET' });
if (!resp.ok) throw new Error('Autoindex failed: HTTP ' + resp.status);
@@ -158,15 +174,14 @@ class WebDAVModule {
_parseAutoindex(html) {
const entries = [];
- // Parse nginx autoindex HTML format
- const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>\s+(\d{2}-[A-Za-z]{3}-\d{4}\s+\d{2}:\d{2})\s+([\d\w.-]+)/g;
+ // Parse nginx autoindex HTML format — flexible regex that doesn't depend on date locale
+ const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>\s+([\w]{2}-[\w]{3}-\d{4}\s+\d{2}:\d{2})\s+([\d\w.-]+)/g;
let match;
while ((match = linkRegex.exec(html)) !== null) {
const href = match[1];
const name = match[2].replace(/\/$/, '');
const sizeStr = match[4];
- if (name === '..' || name === '../') continue;
- if (name === '__list__') continue;
+ if (name === '..' || name === '../' || name === '__list__') continue;
const isDir = href.endsWith('/');
let size = 0;
if (!isDir && sizeStr !== '-') {
@@ -181,22 +196,40 @@ class WebDAVModule {
}
// Fallback: if regex failed, try simpler parse
if (entries.length === 0) {
- const simpleRegex = /<a href="([^"]+)">/g;
+ // Try broadest: extract all hrefs
+ const broadRegex = /<a href="([^"]+)"[^>]*>/g;
const names = [];
- while ((match = simpleRegex.exec(html)) !== null) {
+ while ((match = broadRegex.exec(html)) !== null) {
const href = match[1];
if (href === '../' || href === '/' || href === './') continue;
if (href.startsWith('?')) continue;
names.push(href);
}
- for (const href of names) {
- const isDir = href.endsWith('/');
- entries.push({
- name: decodeURIComponent(href.replace(/\/$/, '').split('/').pop() || href),
- type: isDir ? 'dir' : 'file',
- size: 0,
- href: href
- });
+ if (names.length > 0) {
+ for (const href of names) {
+ const isDir = href.endsWith('/');
+ entries.push({
+ name: decodeURIComponent(href.replace(/\/$/, '').split('/').pop() || href),
+ type: isDir ? 'dir' : 'file',
+ size: 0,
+ href: href
+ });
+ }
+ } else {
+ // Last resort: match any <a href="...">
+ const lastRegex = /<a href="([^"]+)"/g;
+ while ((match = lastRegex.exec(html)) !== null) {
+ const href = match[1];
+ if (href === '../' || href === '/' || href === './') continue;
+ if (href.startsWith('?')) continue;
+ const isDir = href.endsWith('/');
+ entries.push({
+ name: decodeURIComponent(href.replace(/\/$/, '').split('/').pop() || href),
+ type: isDir ? 'dir' : 'file',
+ size: 0,
+ href: href
+ });
+ }
}
}
return entries;
@@ -529,16 +562,27 @@ class WebDAVModule {
try {
await self._ensureDir('backups');
// Build structured export: root + currentPath + aliases + moduleRegistry
- const exportData = {
- type: 'webx11-backup',
- version: 2,
- savedAt: new Date().toISOString(),
- root: self.terminal.fs.root,
- currentPath: self.terminal.fs.currentPath,
- aliases: self.terminal.moduleManager.aliases || {},
- moduleRegistry: self.terminal.moduleManager.moduleRegistry || []
- };
+ let exportData;
+ try {
+ exportData = {
+ type: 'webx11-backup',
+ version: 2,
+ savedAt: new Date().toISOString(),
+ root: self.terminal.fs.root,
+ currentPath: self.terminal.fs.currentPath,
+ aliases: self.terminal.moduleManager.aliases || {},
+ moduleRegistry: self.terminal.moduleManager.moduleRegistry || []
+ };
+ } catch (e) {
+ self.terminal.print('Failed to read VFS: ' + e.message, 'error');
+ return;
+ }
const serialized = JSON.stringify(exportData, null, 2);
+ // Size check: abort if >5MB
+ if (serialized.length > 5 * 1024 * 1024) {
+ self.terminal.print('Backup too large: ' + self._formatSize(serialized.length) + ' (>5MB limit). Try reducing VFS size first.', 'error');
+ return;
+ }
// Gzip payload
let payload = serialized;
let isGzipped = false;
@@ -550,7 +594,9 @@ class WebDAVModule {
const buf = await resp.arrayBuffer();
payload = new Uint8Array(buf);
isGzipped = true;
- } catch (e) { /* fallback to plain */ }
+ } catch (e) { self.terminal.print('Gzip unavailable, falling back to plain JSON', 'info'); }
+ } else {
+ self.terminal.print('CompressionStream not supported, saving as plain JSON', 'info');
}
// Encrypt if requested
if (encryptPass) {
@@ -671,37 +717,74 @@ class WebDAVModule {
}
};
- // --- webdav-browser: visual modal file browser ---
+ // --- webdav-browser: modern visual file browser ---
this.commands['webdav-browser'] = async (args) => {
const startPath = args[0] || self._browsingPath || '/';
await self.openWebdavBrowser(startPath);
};
- // --- webdav-explorer: interactive file browser via WebDAV ---
- this.commands['webdav-explorer'] = async (args) => {
- const startPath = args[0] || '/';
+ // --- webdav-rm: delete remote file/dir ---
+ this.commands['webdav-rm'] = async (args) => {
+ if (!args[0]) {
+ self.terminal.print('Usage: webdav-rm <remote-path>', 'warning');
+ return;
+ }
try {
- const entries = await self._listDir(startPath);
- self.terminal.print('📁 WebDAV: ' + startPath, 'info');
- if (entries.length === 0) {
- self.terminal.print('(empty)', 'info');
- return;
+ const ok = await self._deleteFile(args[0]);
+ if (ok) {
+ self.terminal.print('Deleted: ' + args[0], 'success');
+ } else {
+ self.terminal.print('Delete failed: resource may not exist', 'error');
}
- entries.forEach(e => {
- const icon = e.type === 'dir' ? '📁' : '📄';
- const size = e.type === 'file' ? ' (' + self._formatSize(e.size) + ')' : '/';
- self.terminal.print(icon + ' ' + e.name + (e.type === 'dir' ? '/' : '') + size);
+ } catch (e) {
+ self.terminal.print('Delete error: ' + e.message, 'error');
+ }
+ };
+
+ // --- webdav-mkdir: create remote directory ---
+ this.commands['webdav-mkdir'] = async (args) => {
+ if (!args[0]) {
+ self.terminal.print('Usage: webdav-mkdir <remote-path>', 'warning');
+ return;
+ }
+ try {
+ const ok = await self._ensureDir(args[0]);
+ if (ok) {
+ self.terminal.print('Created directory: ' + args[0], 'success');
+ } else {
+ self.terminal.print('Failed to create directory', 'error');
+ }
+ } catch (e) {
+ self.terminal.print('Mkdir error: ' + e.message, 'error');
+ }
+ };
+
+ // --- webdav-rename: rename/move remote file or dir (MOVE method) ---
+ this.commands['webdav-rename'] = async (args) => {
+ if (!args[0] || !args[1]) {
+ self.terminal.print('Usage: webdav-rename <old-path> <new-path>', 'warning');
+ self.terminal.print('Renames or moves a file/directory using WebDAV MOVE', 'info');
+ return;
+ }
+ try {
+ const resp = await self._webdavRequest('MOVE', args[0], {
+ headers: { 'Destination': args[1] }
});
- self.terminal.print('---');
- self.terminal.print('↳ Navigate: webdav-explorer <dir> | Pull: webdav-pull <file>', 'text-muted');
- self.terminal.print(entries.length + ' item(s)', 'text-muted');
+ if (resp.ok || resp.status === 201 || resp.status === 204) {
+ self.terminal.print('Renamed: ' + args[0] + ' → ' + args[1], 'success');
+ } else {
+ self.terminal.print('Rename failed: HTTP ' + resp.status, 'error');
+ }
} catch (e) {
- self.terminal.print('WebDAV error: ' + e.message, 'error');
+ self.terminal.print('Rename error: ' + e.message, 'error');
}
};
+
+ // --- webdav-move: alias for webdav-rename ---
+ this.commands['webdav-move'] = this.commands['webdav-rename'];
}
- /* ── Visual WebDAV Browser Modal ── */
+ /* ── Modern WebDAV File-Tree Browser with Bulk Ops ── */
async openWebdavBrowser(path) {
if (!this.config || !this.config.url) {
@@ -710,6 +793,7 @@ class WebDAVModule {
}
this._browsingPath = path;
const self = this;
+ this._selected = {};
// Remove existing modal if any
const existing = document.getElementById('webdav-browser-modal');
@@ -724,6 +808,12 @@ class WebDAVModule {
errorMsg = e.message;
}
+ // Sort: dirs first, then files, alphabetically
+ entries.sort(function(a, b) {
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
+ return a.name.localeCompare(b.name);
+ });
+
// Build modal
const overlay = document.createElement('div');
overlay.id = 'webdav-browser-modal';
@@ -733,73 +823,237 @@ class WebDAVModule {
const content = document.createElement('div');
content.className = 'modal-content';
- content.style.cssText = 'max-width:650px; max-height:80vh; width:92%;';
+ content.style.cssText = 'max-width:750px; max-height:85vh; width:94%;';
// Header
const header = document.createElement('div');
header.className = 'modal-header';
- header.innerHTML = '<span>📁 WebDAV Browser</span><span style="cursor:pointer" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.remove();var t=window.webx11;if(t&&t.inputField)t.inputField.focus();">✕</span>';
+ header.innerHTML = '<span>📁 WebDAV File Tree</span>' +
+ '<span style="display:flex;gap:6px;align-items:center">' +
+ '<span id="sel-count" style="font-size:0.8em;color:var(--text-muted)"></span>' +
+ '<span style="cursor:pointer" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.remove();var t=window.webx11;if(t&&t.inputField)t.inputField.focus();">✕</span></span>';
content.appendChild(header);
// Body
const body = document.createElement('div');
body.className = 'modal-body';
- body.style.cssText = 'padding:8px; overflow-y:auto; flex-grow:1;';
+ body.style.cssText = 'padding:0; overflow-y:auto; flex-grow:1;';
if (errorMsg) {
const errDiv = document.createElement('div');
errDiv.style.cssText = 'color:var(--error-color); padding:16px; text-align:center;';
errDiv.textContent = '❌ ' + errorMsg;
body.appendChild(errDiv);
- } else if (entries.length === 0) {
- const emptyDiv = document.createElement('div');
- emptyDiv.className = 'text-center';
- emptyDiv.style.cssText = 'padding:20px; color:var(--text-muted);';
- emptyDiv.textContent = '(empty) — upload files using the button below';
- body.appendChild(emptyDiv);
} else {
- // Sort: dirs first, then files, alphabetically
- entries.sort(function(a, b) {
- if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
- return a.name.localeCompare(b.name);
- });
+ // Select-all row
+ const selectAllRow = document.createElement('div');
+ selectAllRow.style.cssText = 'display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid #222;font-size:0.8em;color:var(--text-muted);gap:8px;';
+ const selAllCheck = document.createElement('input');
+ selAllCheck.type = 'checkbox';
+ selAllCheck.style.cssText = 'accent-color:var(--success-color);width:16px;height:16px;cursor:pointer;';
+ selAllCheck.onchange = function() {
+ const checked = this.checked;
+ entries.forEach(function(e) { self._selected[e.name] = checked; });
+ document.querySelectorAll('.wdav-checkbox').forEach(function(c) { c.checked = checked; });
+ self._updateSelectionCount();
+ };
+ selectAllRow.appendChild(selAllCheck);
+ selectAllRow.appendChild(document.createTextNode('Select All (' + entries.length + ' items)'));
+ body.appendChild(selectAllRow);
- var frag = document.createDocumentFragment();
+ // File rows
+ const listContainer = document.createElement('div');
+ let rowIndex = 0;
entries.forEach(function(entry) {
- var item = document.createElement('div');
- item.className = entry.type === 'dir' ? 'file-item dir' : 'file-item';
- item.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:8px; border-bottom:1px solid #111; cursor:pointer; min-height:36px;';
- item.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; };
- item.onmouseout = function() { this.style.background = ''; };
+ var row = document.createElement('div');
+ row.className = 'wdav-row';
+ row.style.cssText = 'display:flex;align-items:center;padding:7px 10px;border-bottom:1px solid #1a1a1a;cursor:default;min-height:38px;gap:8px;';
+ row.onmouseover = function() { this.style.background = 'var(--bg-tertiary)'; };
+ row.onmouseout = function() { this.style.background = ''; };
+ row.dataset.index = rowIndex++;
+
+ // Checkbox
+ var cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.className = 'wdav-checkbox';
+ cb.style.cssText = 'accent-color:var(--success-color);width:15px;height:15px;cursor:pointer;flex-shrink:0;';
+ cb.onchange = function(e) {
+ self._selected[entry.name] = this.checked;
+ self._updateSelectionCount();
+ e.stopPropagation();
+ };
+ row.appendChild(cb);
+ // Icon
+ var iconSpan = document.createElement('span');
+ iconSpan.textContent = entry.type === 'dir' ? '📁' : '📄';
+ iconSpan.style.cssText = 'flex-shrink:0;font-size:1.1em;';
+
+ // Filename (editable on dblclick)
var nameSpan = document.createElement('span');
- var icon = entry.type === 'dir' ? '📁' : '📄';
- nameSpan.textContent = icon + ' ' + entry.name + (entry.type === 'dir' ? '/' : '');
+ nameSpan.textContent = entry.name + (entry.type === 'dir' ? '/' : '');
+ nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.9em;cursor:pointer;';
+ nameSpan.title = entry.name;
+ nameSpan.ondblclick = function(e) {
+ e.stopPropagation();
+ var input = document.createElement('input');
+ input.type = 'text';
+ input.value = entry.name;
+ input.style.cssText = 'width:100%;background:#1a1a1a;border:1px solid var(--success-color);color:var(--text-color);padding:2px 6px;border-radius:3px;font-size:0.9em;';
+ input.onblur = function() {
+ var newName = this.value.trim();
+ if (newName && newName !== entry.name) {
+ var parentPath = path.replace(/\/+$/, '') || '';
+ var oldFull = parentPath + '/' + entry.name;
+ var newFull = parentPath + '/' + newName;
+ self._webdavRequest('MOVE', oldFull, { headers: { 'Destination': newFull } })
+ .then(function(r) {
+ if (r.ok || r.status === 201 || r.status === 204) {
+ self.terminal.print('Renamed: ' + entry.name + ' → ' + newName, 'success');
+ self.openWebdavBrowser(path);
+ } else {
+ self.terminal.print('Rename failed: HTTP ' + r.status, 'error');
+ nameSpan.textContent = entry.name;
+ }
+ });
+ } else {
+ nameSpan.textContent = entry.name;
+ }
+ };
+ input.onkeydown = function(ev) {
+ if (ev.key === 'Enter') this.blur();
+ if (ev.key === 'Escape') { nameSpan.textContent = entry.name; }
+ };
+ nameSpan.textContent = '';
+ nameSpan.appendChild(input);
+ input.focus();
+ input.select();
+ };
+ // Click = navigate dir / preview file
+ if (entry.type === 'dir') {
+ row.onclick = function(e) {
+ if (e.target.type === 'checkbox') return;
+ self.openWebdavBrowser(entry.href);
+ };
+ } else {
+ row.onclick = function(e) {
+ if (e.target.type === 'checkbox') return;
+ self.previewWebdavFile(entry);
+ };
+ }
+ // Size
var sizeSpan = document.createElement('span');
- sizeSpan.style.cssText = 'font-size:0.8em; color:var(--text-muted);';
+ sizeSpan.style.cssText = 'font-size:0.75em;color:var(--text-muted);flex-shrink:0;min-width:60px;text-align:right;';
if (entry.type === 'file') {
sizeSpan.textContent = self._formatSize(entry.size);
}
- item.appendChild(nameSpan);
- item.appendChild(sizeSpan);
+ // Context menu on right-click
+ row.oncontextmenu = function(e) {
+ e.preventDefault();
+ self._showContextMenu(e, entry, path);
+ };
- if (entry.type === 'dir') {
- item.onclick = function() { self.openWebdavBrowser(entry.href); };
- } else {
- item.onclick = function() { self.previewWebdavFile(entry); };
- // Right-click or context menu could download — but for simplicity, long tap or regular click previews
- // Double-click to download
- item.ondblclick = function() { self.downloadFromWebdav(entry); };
- }
- frag.appendChild(item);
+ row.appendChild(iconSpan);
+ row.appendChild(nameSpan);
+ row.appendChild(sizeSpan);
+ listContainer.appendChild(row);
});
- body.appendChild(frag);
+ body.appendChild(listContainer);
+
+ // Keyboard navigation
+ body.tabIndex = -1;
+ body.style.outline = 'none';
+ var focusIdx = 0;
+ body.onkeydown = function(e) {
+ var rows = listContainer.querySelectorAll('.wdav-row');
+ if (e.key === 'ArrowDown' || e.key === 'j') {
+ e.preventDefault();
+ focusIdx = Math.min(focusIdx + 1, rows.length - 1);
+ rows[focusIdx].scrollIntoView({ block: 'nearest' });
+ rows.forEach(function(r, i) { r.style.borderLeft = i === focusIdx ? '2px solid var(--success-color)' : ''; });
+ } else if (e.key === 'ArrowUp' || e.key === 'k') {
+ e.preventDefault();
+ focusIdx = Math.max(focusIdx - 1, 0);
+ rows[focusIdx].scrollIntoView({ block: 'nearest' });
+ rows.forEach(function(r, i) { r.style.borderLeft = i === focusIdx ? '2px solid var(--success-color)' : ''; });
+ } else if (e.key === ' ') {
+ e.preventDefault();
+ var cb = rows[focusIdx]?.querySelector('.wdav-checkbox');
+ if (cb) { cb.checked = !cb.checked; cb.onchange({ stopPropagation: function(){} }); }
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ rows[focusIdx]?.click();
+ }
+ };
+ setTimeout(function() { body.focus(); }, 100);
}
content.appendChild(body);
+ // Bulk action bar
+ const bulkBar = document.createElement('div');
+ bulkBar.id = 'wdav-bulk-bar';
+ bulkBar.style.cssText = 'display:none;padding:8px 10px;border-top:1px solid var(--success-color);background:var(--bg-tertiary);gap:6px;align-items:center;flex-wrap:wrap;';
+ bulkBar.innerHTML = '<span id="bulk-count" style="font-size:0.85em;color:var(--text-color);flex:1;">0 selected</span>' +
+ '<button class="btn" id="bulk-delete" style="font-size:0.8em;padding:3px 10px;">🗑 Delete</button>' +
+ '<button class="btn" id="bulk-move" style="font-size:0.8em;padding:3px 10px;">📦 Move</button>' +
+ '<button class="btn" id="bulk-download" style="font-size:0.8em;padding:3px 10px;">⬇ Download</button>';
+ self._updateSelectionCount = function() {
+ var count = Object.values(self._selected).filter(Boolean).length;
+ var el = document.getElementById('sel-count');
+ if (el) el.textContent = count > 0 ? count + ' selected' : '';
+ var bar = document.getElementById('wdav-bulk-bar');
+ var cnt = document.getElementById('bulk-count');
+ if (bar) bar.style.display = count > 0 ? 'flex' : 'none';
+ if (cnt) cnt.textContent = count + ' selected';
+ };
+
+ // Wire bulk actions
+ setTimeout(function() {
+ document.getElementById('bulk-delete')?.addEventListener('click', function() {
+ var names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; });
+ if (!names.length || !confirm('Delete ' + names.length + ' item(s) from ' + path + '?')) return;
+ names.forEach(function(name) {
+ var fullPath = (path.replace(/\/+$/, '') || '') + '/' + name;
+ self._deleteFile(fullPath).then(function(ok) {
+ if (ok) self.terminal.print('Deleted: ' + name, 'success');
+ else self.terminal.print('Delete failed: ' + name, 'error');
+ });
+ });
+ setTimeout(function() { self.openWebdavBrowser(path); }, 500);
+ });
+ document.getElementById('bulk-move')?.addEventListener('click', function() {
+ var names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; });
+ if (!names.length) return;
+ var target = prompt('Move ' + names.length + ' item(s) to directory:', '/');
+ if (!target) return;
+ target = target.replace(/\/+$/, '');
+ names.forEach(function(name) {
+ var oldFull = (path.replace(/\/+$/, '') || '') + '/' + name;
+ var newFull = target + '/' + name;
+ self._webdavRequest('MOVE', oldFull, { headers: { 'Destination': newFull } })
+ .then(function(r) {
+ if (r.ok || r.status === 201 || r.status === 204) self.terminal.print('Moved: ' + name, 'success');
+ else self.terminal.print('Move failed: ' + name + ' (HTTP ' + r.status + ')', 'error');
+ });
+ });
+ setTimeout(function() { self.openWebdavBrowser(path); }, 500);
+ });
+ document.getElementById('bulk-download')?.addEventListener('click', function() {
+ var names = Object.entries(self._selected).filter(function(e) { return e[1]; }).map(function(e) { return e[0]; });
+ names.forEach(function(name) {
+ var fullPath = (path.replace(/\/+$/, '') || '') + '/' + name;
+ // Find entry from entries
+ var entry = entries.find(function(e) { return e.name === name; });
+ if (entry && entry.type === 'file') self.downloadFromWebdav(entry);
+ });
+ });
+ }, 50);
+
+ content.appendChild(bulkBar);
+
// Footer with breadcrumb + buttons
const footer = document.createElement('div');
footer.className = 'modal-footer';
@@ -815,17 +1069,30 @@ class WebDAVModule {
for (var i = 0; i < parts.length; i++) {
cumulative += '/' + parts[i];
var seg = parts[i];
+ breadHtml += ' <span style="color:var(--text-muted);font-size:0.7em;">▸</span> ';
breadHtml += '<span style="cursor:pointer;text-decoration:underline;" onclick="var m=document.getElementById(\'webdav-browser-modal\');if(m)m.__nav(\'' + cumulative.replace(/'/g, "\\'") + '/\');return false;">' + seg + '</span>';
- if (i < parts.length - 1) breadHtml += ' ▸ ';
}
breadDiv.innerHTML = breadHtml;
- // Attach nav function to modal element for breadcrumb clicks
overlay.__nav = function(p) { self.openWebdavBrowser(p); };
// Button group
var btnGroup = document.createElement('div');
btnGroup.style.cssText = 'display:flex; gap:4px; flex-shrink:0;';
+ var mkdirBtn = document.createElement('button');
+ mkdirBtn.className = 'btn';
+ mkdirBtn.textContent = '📁+';
+ mkdirBtn.title = 'New Directory';
+ mkdirBtn.onclick = function() {
+ var name = prompt('Directory name:');
+ if (name) {
+ self._ensureDir((path.replace(/\/+$/, '') || '') + '/' + name).then(function(ok) {
+ if (ok) { self.terminal.print('Created: ' + name, 'success'); self.openWebdavBrowser(path); }
+ else self.terminal.print('Failed to create directory', 'error');
+ });
+ }
+ };
+
var uploadBtn = document.createElement('button');
uploadBtn.className = 'btn';
uploadBtn.textContent = '⬆ Upload';
@@ -842,6 +1109,7 @@ class WebDAVModule {
closeBtn.textContent = 'Close';
closeBtn.onclick = function() { self.closeWebdavBrowser(); };
+ btnGroup.appendChild(mkdirBtn);
btnGroup.appendChild(uploadBtn);
btnGroup.appendChild(refreshBtn);
btnGroup.appendChild(closeBtn);
@@ -853,7 +1121,71 @@ class WebDAVModule {
overlay.appendChild(content);
document.body.appendChild(overlay);
- this.terminal.print('📂 Opened WebDAV browser: ' + path, 'info');
+ this.terminal.print('📂 Opened WebDAV file browser: ' + path, 'info');
+ }
+
+ _showContextMenu(e, entry, currentPath) {
+ var existing = document.getElementById('wdav-context-menu');
+ if (existing) existing.remove();
+
+ var menu = document.createElement('div');
+ menu.id = 'wdav-context-menu';
+ menu.style.cssText = 'position:fixed;left:' + e.clientX + 'px;top:' + e.clientY + 'px;background:#111;border:1px solid #333;border-radius:6px;padding:4px 0;z-index:1001;min-width:160px;box-shadow:0 4px 12px rgba(0,0,0,0.5);';
+ menu.onclick = function() { this.remove(); };
+
+ var items = [];
+ if (entry.type === 'file') {
+ items.push({ label: '📄 Preview', fn: function() { this.previewWebdavFile(entry); }.bind(this) });
+ items.push({ label: '⬇ Download', fn: function() { this.downloadFromWebdav(entry); }.bind(this) });
+ items.push({ label: '📋 Copy Path', fn: function() {
+ var full = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name;
+ navigator.clipboard.writeText(full).then(function() { this.terminal.print('Path copied: ' + full, 'success'); }.bind(this));
+ }.bind(this) });
+ } else {
+ items.push({ label: '📂 Open', fn: function() { this.openWebdavBrowser(entry.href); }.bind(this) });
+ items.push({ label: '📋 Copy Path', fn: function() {
+ var full = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name + '/';
+ navigator.clipboard.writeText(full).then(function() { this.terminal.print('Path copied: ' + full, 'success'); }.bind(this));
+ }.bind(this) });
+ }
+ items.push({ label: '✏️ Rename', fn: function() {
+ var newName = prompt('Rename "' + entry.name + '" to:');
+ if (newName && newName !== entry.name) {
+ var oldFull = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name;
+ var newFull = (currentPath.replace(/\/+$/, '') || '') + '/' + newName;
+ this._webdavRequest('MOVE', oldFull, { headers: { 'Destination': newFull } }).then(function(r) {
+ if (r.ok || r.status === 201 || r.status === 204) {
+ this.terminal.print('Renamed: ' + entry.name + ' → ' + newName, 'success');
+ this.openWebdavBrowser(currentPath);
+ } else { this.terminal.print('Rename failed: HTTP ' + r.status, 'error'); }
+ }.bind(this));
+ }
+ }.bind(this) });
+ items.push({ label: '🗑 Delete', fn: function() {
+ if (!confirm('Delete ' + entry.name + '?')) return;
+ var full = (currentPath.replace(/\/+$/, '') || '') + '/' + entry.name;
+ this._deleteFile(full).then(function(ok) {
+ if (ok) { this.terminal.print('Deleted: ' + entry.name, 'success'); this.openWebdavBrowser(currentPath); }
+ else { this.terminal.print('Delete failed', 'error'); }
+ }.bind(this));
+ }.bind(this) });
+
+ items.forEach(function(item) {
+ var div = document.createElement('div');
+ div.textContent = item.label;
+ div.style.cssText = 'padding:6px 14px;cursor:pointer;font-size:0.85em;';
+ div.onmouseover = function() { this.style.background = '#222'; };
+ div.onmouseout = function() { this.style.background = ''; };
+ div.onclick = function(e) { e.stopPropagation(); menu.remove(); item.fn(); };
+ menu.appendChild(div);
+ });
+
+ document.body.appendChild(menu);
+ // Close on click outside
+ var closeMenu = function(ev) {
+ if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('click', closeMenu); }
+ };
+ setTimeout(function() { document.addEventListener('click', closeMenu); }, 10);
}
closeWebdavBrowser() {
@@ -949,7 +1281,10 @@ class WebDAVModule {
async downloadFromWebdav(entry) {
try {
- var resp = await this._webdavRequest('GET', entry.href);
+ // Construct path from current browsing path + entry name (avoid autoindex href fragility)
+ var basePath = (this._browsingPath || '/').replace(/\/+$/, '');
+ var fullPath = basePath + '/' + entry.name;
+ var resp = await this._webdavRequest('GET', fullPath);
if (!resp.ok) { this.terminal.print('Download failed: HTTP ' + resp.status, 'error'); return; }
var content = await resp.text();
var blob = new Blob([content], { type: 'application/octet-stream' });
@@ -957,7 +1292,7 @@ class WebDAVModule {
var a = document.createElement('a');
a.href = url;
a.download = entry.name;
- a.style.display = 'none';
+ a.style.cssText = 'display:none;';
document.body.appendChild(a);
a.click();
setTimeout(function() {