diff options
Diffstat (limited to 'modules.html')
| -rw-r--r-- | modules.html | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/modules.html b/modules.html new file mode 100644 index 0000000..521f92f --- /dev/null +++ b/modules.html @@ -0,0 +1,386 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> +<meta name="theme-color" content="#0a0a14"> +<title>WebX11 — Modules</title> +<script>!function(){var v=localStorage.getItem('webx11_cache_ver');if(!v)return;var m=v.match(/v([\d.]+)/);if(!m)return;document.title='WebX11 v'+m[1]+' — Modules';document.querySelector('.badge-v')&&(document.querySelector('.badge-v').textContent='v'+m[1])}()</script> +<style> +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent} +:root{--bg:#0a0a14;--bg2:#141428;--bg3:#1c1c36;--bg4:#24244a;--text:#e8e8ff;--text2:#b0b0cc;--accent:#818cf8;--accent2:#a78bfa;--green:#34d399;--cyan:#22d3ee;--amber:#fbbf24;--rose:#fb7185;--pink:#e879f9;--border:#3a3a5a;--radius:12px;--font-mono:'JetBrains Mono','Fira Code','Cascadia Code','Courier New',monospace;--font-sans:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter','SF Pro',Roboto,sans-serif} +body{background:var(--bg);color:var(--text);font-family:var(--font-sans);line-height:1.6;padding:0 16px 80px;max-width:1100px;margin:0 auto;min-height:100vh} +header{padding:24px 0 16px;border-bottom:1px solid var(--border);margin-bottom:20px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} +h1{font-size:1.5rem;font-weight:600;letter-spacing:-0.02em} +.badge-v{background:var(--bg4);color:var(--accent);font-family:var(--font-mono);font-size:.75rem;padding:2px 10px;border-radius:20px;border:1px solid var(--border)} +.live-badge{font-size:.72rem;padding:3px 10px;border-radius:12px;font-family:var(--font-mono);border:1px solid var(--border);cursor:pointer;transition:all .2s;background:var(--bg3);color:var(--text2)} +.live-badge.active{background:#0d3320;border-color:var(--green);color:var(--green)} +.live-badge.connecting{background:#332a0d;border-color:var(--amber);color:var(--amber)} +.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center} +#search{flex:1;min-width:160px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 14px;color:var(--text);font-size:.9rem;font-family:var(--font-sans);outline:none;transition:border-color .2s} +#search:focus{border-color:var(--accent)} +#search::placeholder{color:var(--text2)} +.tabs{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:12px} +.tab{background:var(--bg3);color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:6px 14px;font-size:.82rem;cursor:pointer;transition:all .2s;font-family:inherit} +.tab:hover{border-color:var(--accent);color:var(--text)} +.tab.active{background:var(--accent);color:#fff;border-color:var(--accent)} +.stats{color:var(--text2);font-size:.82rem;margin-bottom:12px;display:flex;gap:16px;flex-wrap:wrap} +.module-row{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:6px;overflow:hidden} +.module-header{display:flex;align-items:center;gap:10px;padding:12px 14px;cursor:pointer;transition:background .2s;-webkit-user-select:none;user-select:none} +.module-header:hover{background:var(--bg3)} +.module-header .name{font-size:.92rem;font-weight:500;flex:1} +.module-header .class-name{color:var(--text2);font-size:.78rem;font-family:var(--font-mono)} +.badge{font-size:.68rem;padding:2px 8px;border-radius:8px;font-family:var(--font-mono);white-space:nowrap} +.badge-core{background:var(--bg4);color:var(--accent);border:1px solid var(--accent)} +.badge-ext{background:#1a2a1a;color:var(--green);border:1px solid var(--green)} +.badge-infra{background:#1a1a2a;color:var(--cyan);border:1px solid var(--cyan)} +.badge-unknown{background:#2a1a1a;color:var(--rose);border:1px solid var(--rose)} +.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0} +.status-loaded{background:var(--green);box-shadow:0 0 4px var(--green)} +.status-unloaded{background:var(--text2)} +.status-modified{background:var(--amber);box-shadow:0 0 4px var(--amber)} +.expand-icon{color:var(--text2);font-size:.7rem;transition:transform .2s} +.expand-icon.open{transform:rotate(90deg)} +.module-detail{display:none;padding:0 14px 14px;border-top:1px solid var(--border)} +.module-detail.open{display:block} +.detail-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;font-size:.82rem;margin-bottom:10px;margin-top:10px} +.dt{color:var(--text2);white-space:nowrap} +.dd{color:var(--text)} +.dd .cmd-tag{display:inline-block;background:var(--bg3);padding:2px 8px;border-radius:6px;margin:2px 2px;font-family:var(--font-mono);font-size:.75rem;color:var(--cyan)} +.dd .cmd-tag.missing{color:var(--rose);opacity:.7} +.dd .cmd-tag.loaded{color:var(--green)} +.dd a{color:var(--accent);text-decoration:none} +.dd a:hover{text-decoration:underline} +.cmd-section{margin-top:8px} +.cmd-section h4{font-size:.82rem;font-weight:500;color:var(--text2);margin-bottom:4px} +.command-bar{display:flex;flex-wrap:wrap;gap:4px} +.error-banner{background:#4a1a1a;border:1px solid var(--rose);border-radius:var(--radius);padding:16px;text-align:center;color:var(--rose);margin-bottom:16px} +.error-banner button{background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 20px;margin-top:8px;cursor:pointer;font-family:inherit} +.empty-state{text-align:center;color:var(--text2);padding:40px 20px;font-size:.9rem} +#loading{text-align:center;color:var(--text2);padding:40px} +.live-indicator{font-size:.75rem;display:flex;align-items:center;gap:4px;padding:2px 8px;border-radius:10px} +.live-connected{color:var(--green)} +.live-offline{color:var(--text2)} +footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--border);text-align:center;font-size:.8rem;color:var(--text2)} +footer a{color:var(--accent);text-decoration:none} +.hidden{display:none!important} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} +.connecting .live-indicator{animation:pulse 1.2s ease-in-out infinite} +@media(max-width:600px){.module-header{flex-wrap:wrap;gap:6px}body{padding:0 12px 60px}} +</style> +</head> +<body> +<header> + <h1>🧩 <span class="badge-v"></span> Modules</h1> + <span class="live-badge" id="liveBtn" onclick="toggleLive()">⚡ Offline</span> +</header> +<div class="tabs" id="tabs"></div> +<div class="toolbar"> + <input type="text" id="search" placeholder="Search modules by name, class, command, description..."> +</div> +<div class="stats" id="stats"></div> +<div id="loading">Loading module index...</div> +<div id="error" class="error-banner hidden"> + <div id="errorMsg"></div> + <button onclick="loadModules()">Retry</button> +</div> +<div id="empty" class="empty-state hidden">No modules match your filters.</div> +<div id="moduleList"></div> +<footer><a href="skills.html">Skills</a> · <a href="help.html">Help</a> · <a href="guide.html">Guide</a></footer> +<script> +const INDEX_URL = '/spaces/newos4/modules-index.json'; +const PKGS_URL = '/spaces/newos4/packages.json'; +let allModules = []; +let packages = {}; +let liveState = {}; +let currentTab = 'all'; +let liveMode = false; + +function getCacheVer() { return localStorage.getItem('webx11_cache_ver') || ''; } + +function groupByType(mods) { + const groups = { core: [], ext: [], infra: [], unknown: [] }; + mods.forEach(m => { + const t = (m.type || 'unknown').toLowerCase(); + const l = (m.layer || '').toLowerCase(); + if (l === 'infrastructure') groups.infra.push(m); + else if (t === 'core') groups.core.push(m); + else if (t === 'ext') groups.ext.push(m); + else groups.unknown.push(m); + }); + return groups; +} + +function getSortedModules(mods) { + return mods.sort((a,b) => { + const aName = a.name || a.class || ''; + const bName = b.name || b.class || ''; + return aName.localeCompare(bName); + }); +} + +async function loadModules() { + const list = document.getElementById('moduleList'); + const loading = document.getElementById('loading'); + const error = document.getElementById('error'); + loading.classList.remove('hidden'); + error.classList.add('hidden'); + list.innerHTML = ''; + + try { + const [idxResp, pkgResp] = await Promise.all([ + fetch(INDEX_URL + '?v=' + getCacheVer()), + fetch(PKGS_URL + '?v=' + getCacheVer()).catch(() => null) + ]); + if (!idxResp.ok) throw new Error('HTTP ' + idxResp.status); + const idx = await idxResp.json(); + if (pkgResp && pkgResp.ok) { + packages = (await pkgResp.json()).packages || {}; + } + + allModules = []; + for (const [key, meta] of Object.entries(idx.modules || {})) { + const pkgInfo = packages[key] || {}; + allModules.push({ + key: key, + name: meta.name || key, + file: meta.file || '', + type: meta.type || 'unknown', + class: meta.class || '', + auto_load: meta.auto_load, + commands: meta.commands || [], + description: meta.description || pkgInfo.desc || '', + dependencies: meta.dependencies || [], + version: meta.version || '', + updated: meta.updated || '', + layer: meta.layer || '', + pkgName: pkgInfo.name || '', + pkgCategory: pkgInfo.category || '' + }); + } + + renderTabs(); + applyFilters(); + loading.classList.add('hidden'); + } catch(e) { + loading.classList.add('hidden'); + document.getElementById('errorMsg').textContent = 'Failed to load modules: ' + e.message; + error.classList.remove('hidden'); + } +} + +function renderTabs() { + const groups = groupByType(allModules); + const counts = { all: allModules.length, core: groups.core.length, ext: groups.ext.length, infra: groups.infra.length, unknown: groups.unknown.length }; + const container = document.getElementById('tabs'); + container.innerHTML = ['all', 'core', 'ext', 'infra', 'unknown'].map(t => + `<button class="tab${t === currentTab ? ' active' : ''}" data-tab="${t}" onclick="setTab('${t}')">${t} (${counts[t]})</button>` + ).join(''); +} + +function setTab(tab) { + currentTab = tab; + document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab)); + applyFilters(); +} + +function applyFilters() { + const q = document.getElementById('search').value.toLowerCase().trim(); + let filtered = allModules; + + if (currentTab !== 'all') { + const isInfra = currentTab === 'infra'; + filtered = filtered.filter(m => { + const ml = (m.layer || '').toLowerCase(); + const mt = (m.type || '').toLowerCase(); + if (isInfra) return ml === 'infrastructure'; + return mt === currentTab && ml !== 'infrastructure'; + }); + } + + if (q) { + filtered = filtered.filter(m => + (m.name || '').toLowerCase().includes(q) || + (m.class || '').toLowerCase().includes(q) || + (m.description || '').toLowerCase().includes(q) || + (m.commands || []).some(c => c.toLowerCase().includes(q)) || + (m.key || '').toLowerCase().includes(q) + ); + } + + filtered = getSortedModules(filtered); + renderList(filtered); +} + +function renderList(modules) { + const container = document.getElementById('moduleList'); + const stats = document.getElementById('stats'); + const empty = document.getElementById('empty'); + + stats.textContent = modules.length + ' of ' + allModules.length + ' modules'; + if (liveMode) { + const liveCount = Object.keys(liveState).length; + stats.textContent += ' · ⚡ Live: ' + liveCount + ' running instances'; + } + + if (modules.length === 0) { + container.innerHTML = ''; + empty.classList.remove('hidden'); + return; + } + empty.classList.add('hidden'); + + container.innerHTML = modules.map((m, idx) => { + const isLive = liveMode && liveState[m.class]; + const liveInfo = isLive ? liveState[m.class] : null; + const statusClass = liveInfo ? (liveInfo.modified ? 'status-modified' : 'status-loaded') : 'status-unloaded'; + const badgeType = (m.layer || '').toLowerCase() === 'infrastructure' ? 'infra' : (m.type || 'unknown'); + const badgeClass = 'badge-' + badgeType; + const filePath = m.file ? '/spaces/newos4/' + m.file : ''; + const cmdCount = m.commands.length; + const cmdList = m.commands.slice(0, 8); + const cmdExtra = cmdCount - 8; + + return `<div class="module-row"> + <div class="module-header" onclick="toggleDetail(this)"> + <span class="status-dot ${statusClass}" title="${liveInfo ? (liveInfo.modified ? 'Has local edits' : 'Loaded') : 'Not loaded'}"></span> + <span class="name">${m.name}</span> + <span class="class-name">${m.class || m.key}</span> + <span class="badge ${badgeClass}">${badgeType}</span> + <span class="expand-icon">▶</span> + </div> + <div class="module-detail" id="detail-${idx}"> + <div class="detail-grid"> + <span class="dt">Commands</span> + <span class="dd">${cmdCount} ${cmdCount === 1 ? 'command' : 'commands'}</span> + + ${m.description ? '<span class="dt">Desc</span><span class="dd">' + m.description + '</span>' : ''} + ${m.file ? '<span class="dt">File</span><span class="dd"><a href="' + filePath + '" target="_blank">' + m.file + '</a></span>' : ''} + <span class="dt">Class</span><span class="dd">${m.class || '—'}</span> + ${m.version ? '<span class="dt">Version</span><span class="dd">' + m.version + '</span>' : ''} + <span class="dt">Auto-load</span><span class="dd">${m.auto_load ? '✅ Yes' : '❌ No'}</span> + ${m.dependencies && m.dependencies.length ? '<span class="dt">Deps</span><span class="dd">' + m.dependencies.join(', ') + '</span>' : ''} + ${liveInfo ? '<span class="dt">Status</span><span class="dd">' + (liveInfo.modified ? '🟡 Modified locally' : '✅ Loaded') + '</span>' : ''} + ${liveInfo && liveInfo.commands ? '<span class="dt">Live</span><span class="dd">' + liveInfo.commands.length + ' active commands</span>' : ''} + </div> + <div class="cmd-section"> + <h4>Commands</h4> + <div class="command-bar">${m.commands.map(c => { + const liveAvailable = liveInfo && liveInfo.commands && liveInfo.commands.includes(c); + const cls = liveMode ? (liveAvailable ? 'cmd-tag loaded' : 'cmd-tag missing') : 'cmd-tag'; + return '<span class="' + cls + '">' + c + '</span>'; + }).join('')}</div> + </div> + </div> + </div>`; + }).join(''); +} + +function toggleDetail(el) { + const detail = el.nextElementSibling; + const icon = el.querySelector('.expand-icon'); + if (!detail) return; + detail.classList.toggle('open'); + icon.classList.toggle('open'); +} + +async function toggleLive() { + const btn = document.getElementById('liveBtn'); + if (liveMode) { + liveMode = false; + btn.textContent = '⚡ Offline'; + btn.classList.remove('active', 'connecting'); + liveState = {}; + applyFilters(); + return; + } + + btn.textContent = '⏳ Connecting...'; + btn.classList.add('connecting'); + + liveState = {}; + // Try 3 approaches to get live state + try { + // Approach 1: parent window (if loaded in iframe) + if (window.parent && window.parent !== window && window.parent.webx11) { + const mm = window.parent.webx11.moduleManager; + if (mm && mm.modules) { + for (const [className, instance] of mm.modules) { + const cmds = instance.commands ? Object.keys(instance.commands) : []; + const modified = !!(window.parent.webx11.terminal && window.parent.webx11.terminal.fs + && window.parent.webx11.terminal.fs.readFile('_mods/' + className + '.js')); + liveState[className] = { commands: cmds, modified: !!modified, count: cmds.length }; + } + } + } + // Approach 2: direct window (if somehow same-context) + if (Object.keys(liveState).length === 0 && window.webx11) { + const mm = window.webx11.moduleManager; + if (mm && mm.modules) { + for (const [className, instance] of mm.modules) { + const cmds = instance.commands ? Object.keys(instance.commands) : []; + const modified = !!(window.webx11.terminal && window.webx11.terminal.fs + && window.webx11.terminal.fs.readFile('_mods/' + className + '.js')); + liveState[className] = { commands: cmds, modified: !!modified, count: cmds.length }; + } + } + } + } catch(e) { console.warn('Live mode probe failed:', e); } + + const count = Object.keys(liveState).length; + if (count > 0) { + liveMode = true; + btn.textContent = '⚡ Live (' + count + ')'; + btn.classList.remove('connecting'); + btn.classList.add('active'); + } else { + btn.textContent = '⚡ Offline (no terminal)'; + btn.classList.remove('connecting'); + // Try approach 3: open an iframe to probe + tryProbeIFrame(btn); + return; + } + applyFilters(); +} + +function tryProbeIFrame(btn) { + // Approach 3: create hidden iframe with main.html and wait for ready + const iframe = document.createElement('iframe'); + iframe.style.cssText = 'position:fixed;left:-9999px;top:0;width:1px;height:1px;border:none'; + iframe.src = '/spaces/newos4/main.html?embed=1'; + iframe.onload = function() { + try { + const mm = iframe.contentWindow?.webx11?.moduleManager; + if (mm && mm.modules) { + for (const [className, instance] of mm.modules) { + const cmds = instance.commands ? Object.keys(instance.commands) : []; + liveState[className] = { commands: cmds, count: cmds.length }; + } + } + } catch(e) { /* cross-origin or timeout */ } + document.body.removeChild(iframe); + const count = Object.keys(liveState).length; + if (count > 0) { + liveMode = true; + btn.textContent = '⚡ Live (' + count + ')'; + btn.classList.remove('connecting'); + btn.classList.add('active'); + applyFilters(); + } else { + btn.textContent = '⚡ Offline'; + btn.classList.remove('connecting'); + } + }; + iframe.onerror = function() { + document.body.removeChild(iframe); + btn.textContent = '⚡ Offline'; + btn.classList.remove('connecting'); + }; + document.body.appendChild(iframe); +} + +document.getElementById('search').addEventListener('input', applyFilters); +document.addEventListener('DOMContentLoaded', loadModules); +</script> +</body> +</html>
\ No newline at end of file |