You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
313 lines
18 KiB
313 lines
18 KiB
'use strict'; |
|
(function () { |
|
var UI_ID = 'dshmem-panel'; |
|
var LOG_CAP = 60; |
|
var state = { |
|
phase: 'off', |
|
connected: false, |
|
loopsLeft: 0, loopsMax: 0, |
|
dailyCount: 0, dailyMax: 0, |
|
resetCbs: [], stopCbs: [], saveCbs: [], startCbs: [], dotMode: null, transport: 'dom', accLimit: 900000, lastMetrics: null, forceInject: false, mcpTimer: null, togglePending: false |
|
}; |
|
var el = {}; |
|
function byId(id) { return document.getElementById(id); } |
|
function mk(tag, cls, txt) { |
|
var n = document.createElement(tag); |
|
if (cls) n.className = cls; |
|
if (txt !== undefined) n.textContent = txt; |
|
return n; |
|
} |
|
function build() { |
|
if (byId(UI_ID)) return; |
|
var root = mk('div', 'dshmem-panel'); |
|
root.id = UI_ID; |
|
var head = mk('div', 'dshmem-head'); |
|
el.dot = mk('span', 'dshmem-dot'); |
|
el.dot.id = 'dshmem-dot'; |
|
var title = mk('span', 'dshmem-title', 'DSH Memory'); |
|
el.phase = mk('span', 'dshmem-phase', 'Off'); |
|
el.phase.id = 'dshmem-phase'; |
|
el.collapse = mk('span', 'dshmem-toggle', '-'); |
|
head.appendChild(el.dot); head.appendChild(title); head.appendChild(el.phase); head.appendChild(el.collapse); |
|
el.body = mk('div', 'dshmem-body'); |
|
root.appendChild(head); root.appendChild(el.body); |
|
el.root = root; el.head = head; |
|
var row1 = mk('div', 'dshmem-row'); |
|
el.toggle = mk('button', 'dshmem-btn primary', 'Start'); el.restart = mk('button', 'dshmem-btn', 'Restart'); |
|
row1.appendChild(el.toggle); row1.appendChild(el.restart); |
|
el.body.appendChild(row1); el.stats = mk('div','dshmem-stats'); el.stats.id='dshmem-stats'; el.body.appendChild(el.stats); el.metrics = mk('div','dshmem-stats'); el.metrics.id='dshmem-metrics'; el.metrics.textContent='metrics: —'; el.body.appendChild(el.metrics); |
|
var setBox = mk('div', 'dshmem-fold-box'); |
|
el.settingsHead = mk('div', 'dshmem-fold-head'); |
|
el.settingsArrow = mk('span', 'dshmem-fold-arrow', '\u25B6'); |
|
el.settingsHead.appendChild(el.settingsArrow); |
|
el.settingsHead.appendChild(mk('span', '', 'Settings')); |
|
setBox.appendChild(el.settingsHead); |
|
var set = mk('div', 'dshmem-settings dshmem-fold-body'); el.settings = set; el.settingsBox = setBox; |
|
var lbl = mk('div', 'dshmem-label', 'Token (save, then reload page)'); |
|
el.token = mk('input', 'dshmem-input'); |
|
el.token.type = 'text'; el.token.autocomplete = 'off'; el.token.setAttribute('autocomplete', 'off'); |
|
el.token.placeholder = 'dshmem-bridge-token'; |
|
var row2 = mk('div', 'dshmem-row'); |
|
el.save = mk('button', 'dshmem-btn primary', 'Save'); |
|
el.health = mk('button', 'dshmem-btn', 'Health'); |
|
row2.appendChild(el.save); row2.appendChild(el.health); el.stg = mk('button', 'dshmem-btn stg', 'STG'); row2.appendChild(el.stg); el.transportToggle = mk('button', 'dshmem-btn danger', 'DOM'); row2.appendChild(el.transportToggle); var lblAcc = mk('div', 'dshmem-label', 'Acc limit (100%)'); set.appendChild(lblAcc); el.accLimit = mk('input', 'dshmem-input'); el.accLimit.type = 'number'; el.accLimit.autocomplete = 'off'; el.accLimit.setAttribute('autocomplete', 'off'); el.accLimit.autocomplete = 'off'; el.accLimit.setAttribute('autocomplete', 'off'); el.accLimit.placeholder = '900000'; el.accLimit.value = '900000'; set.appendChild(el.accLimit); |
|
set.appendChild(lbl); set.appendChild(el.token); set.appendChild(row2); |
|
setBox.appendChild(set); |
|
el.body.appendChild(setBox); |
|
el.log = mk('div', 'dshmem-log'); |
|
el.log.id = 'dshmem-log'; |
|
el.body.appendChild(el.log); |
|
el.preview = mk('div', 'dshmem-preview'); |
|
el.preview.style.display = 'none'; |
|
el.body.appendChild(el.preview); |
|
el.confirm = mk('div', 'dshmem-confirm-box'); |
|
el.confirm.style.display = 'none'; |
|
el.body.appendChild(el.confirm); |
|
// ArtCraft MCP box |
|
el.mcpBox = mk('div', 'dshmem-mcp-box'); |
|
el.mcpHead = mk('div', 'dshmem-mcp-head'); |
|
el.mcpArrow = mk('span', 'dshmem-mcp-arrow', '\u25B6'); |
|
el.mcpHead.appendChild(el.mcpArrow); |
|
el.mcpHead.appendChild(mk('span', '', 'ArtCraft')); |
|
el.mcpCount = mk('span', 'dshmem-mcp-count', ''); |
|
el.mcpHead.appendChild(el.mcpCount); |
|
el.mcpBox.appendChild(el.mcpHead); |
|
el.mcpBody = mk('div', 'dshmem-mcp-body'); |
|
var pathRow = mk('div', 'dshmem-mcp-pathrow'); |
|
el.mcpPath = mk('input', 'dshmem-input'); |
|
el.mcpPath.type = 'text'; el.mcpPath.placeholder = 'V:\\artcraft'; |
|
el.mcpRescan = mk('button', 'dshmem-btn', '\u21BB'); |
|
el.mcpRescan.title = 'Rescan folder'; |
|
pathRow.appendChild(el.mcpPath); pathRow.appendChild(el.mcpRescan); |
|
el.mcpBody.appendChild(pathRow); |
|
el.mcpList = mk('div', 'dshmem-mcp-list'); |
|
el.mcpList.textContent = 'click to load'; |
|
el.mcpBody.appendChild(el.mcpList); |
|
el.mcpBox.appendChild(el.mcpBody); |
|
el.body.appendChild(el.mcpBox); |
|
document.body.appendChild(root); |
|
} |
|
function wire() { |
|
el.head.addEventListener('click', function () { |
|
var min = el.root.classList.toggle('dshmem-min'); |
|
if (el.collapse) el.collapse.textContent = min ? '+' : '-'; |
|
}); |
|
el.settingsHead.addEventListener('click', function () { |
|
var open = el.settingsBox.classList.toggle('open'); |
|
el.settingsArrow.textContent = open ? '\u25BC' : '\u25B6'; |
|
}); el.transportToggle.addEventListener('click', function () { state.transport = state.transport === 'dom' ? 'sse' : 'dom'; el.transportToggle.textContent = state.transport === 'dom' ? 'DOM' : 'SSE'; try { chrome.storage.local.set({ 'dshmem-transport': state.transport }) } catch (e) { } }); |
|
el.toggle.addEventListener('click', function () { |
|
if (state.togglePending) return; |
|
state.togglePending = true; |
|
setTimeout(function () { state.togglePending = false; }, 400); |
|
var running = (state.phase && state.phase !== 'off'); |
|
if (running) { |
|
state.stopCbs.forEach(function (cb) { try { cb(); } catch (e) {} }); |
|
} else { |
|
state.startCbs.forEach(function (cb) { try { cb(); } catch (e) {} }); |
|
} |
|
}); |
|
el.restart.addEventListener('click', function () { API.restartBridge(); }); |
|
el.accLimit.addEventListener('change', function () { var n = parseInt(el.accLimit.value, 10); if (!n || n < 1000) n = 900000; state.accLimit = n; try { chrome.storage.local.set({ 'dshmem-acc-limit': n }) } catch (e) { } API.setMetrics(state.lastMetrics); }); el.save.addEventListener('click', function () { var v = el.token.value || ''; if (!v) { API.log('token not changed (empty)'); return; } state.saveCbs.forEach(function (cb) { try { cb(v); } catch (e) {} }); }); |
|
el.stg.addEventListener('click', function () { API.toggleForceInject(); }); |
|
el.health.addEventListener('click', function () { |
|
API.log('health: click'); |
|
if (!globalThis.DSH_BRIDGE) { API.log('health: DSH_BRIDGE undefined (bridge-client.js not loaded?)'); return; } |
|
if (typeof globalThis.DSH_BRIDGE.health !== 'function') { API.log('health: DSH_BRIDGE.health is not a function'); return; } |
|
try { chrome.storage.local.get('dshmem-bridge-token', function (v) { |
|
var t = v && v['dshmem-bridge-token'] ? String(v['dshmem-bridge-token']) : ''; |
|
API.log('health: token in storage len=' + t.length + (t ? ' head=' + t.slice(0, 8) + '...' : ' (empty)')); |
|
globalThis.DSH_BRIDGE.health().then(function (r) { |
|
if (r && r.ok) { API.setConnected(true); API.log('health ok, bridge up'); } |
|
else { API.setConnected(false); API.log('health fail: ' + ((r && r.error && (r.error.code + ' ' + r.error.message)) || '?')); } |
|
}).catch(function (e) { API.log('health promise reject: ' + String(e)); }); |
|
}); } catch (e) { API.log('health throw: ' + String(e)); } |
|
}); |
|
el.mcpHead.addEventListener('click', function () { |
|
var open = el.mcpBox.classList.toggle('open'); |
|
el.mcpArrow.textContent = open ? '\u25BC' : '\u25B6'; |
|
if (open) { |
|
API.mcpRefresh(); |
|
if (state.mcpTimer) clearInterval(state.mcpTimer); |
|
state.mcpTimer = setInterval(function () { API.mcpRefresh(); }, 5000); |
|
} else if (state.mcpTimer) { |
|
clearInterval(state.mcpTimer); state.mcpTimer = null; |
|
} |
|
}); |
|
el.mcpRescan.addEventListener('click', function () { API.mcpRescan(); }); |
|
} |
|
var API = { |
|
setPhase: function (p) { |
|
state.phase = p; |
|
if (el.phase) el.phase.textContent = p; |
|
if (el.toggle) { |
|
var running = (p && p !== 'off'); |
|
el.toggle.textContent = running ? 'Stop' : 'Start'; |
|
el.toggle.className = 'dshmem-btn ' + (running ? 'danger' : 'primary'); |
|
} |
|
}, |
|
setConnected: function (b) { state.connected = !!b; API._applyDot(); }, setDot: function (m) { state.dotMode = m || null; API._applyDot(); }, _applyDot: function () { if (!el.dot) return; var cls = 'dshmem-dot'; if (!state.connected) { cls += ' err'; } else { var m = state.dotMode || 'off'; if (m === 'busy' || m === 'ok-busy') cls += ' busy'; else if (m === 'error') cls += ' error'; else if (m === 'stopped') cls += ' stopped'; else if (m === 'ok') cls += ' ok'; else cls += ' off'; } el.dot.className = cls; }, |
|
setStartEnabled: function (b) { if (el.toggle) el.toggle.disabled = !b; }, setStats: function (ws, memN, skillN) { if (!el.stats) return; el.stats.textContent = 'WS: ' + (ws || '?') + ' mem: ' + (memN || 0) + ' skills: ' + (skillN || 0); }, setMetrics: function (m) { if (!el.metrics) return; if (!m) { el.metrics.textContent = 'metrics: —'; return; } state.lastMetrics = m; var __pct = 0; if (state.accLimit > 0) __pct = Math.round((m.accum || 0) * 100 / state.accLimit); var __cls = 'dshmem-acc-ok'; if (__pct > 100) __cls = 'dshmem-acc-crit'; else if (__pct >= 80) __cls = 'dshmem-acc-err'; else if (__pct >= 40) __cls = 'dshmem-acc-warn'; el.metrics.className = 'dshmem-stats ' + __cls; el.metrics.textContent = 'acc: ' + m.accum + ' · turn: ' + m.delta + ' · msg: ' + m.chars + 'ch (~' + m.est_tok + 't) ctx ' + (m.ctx_tok || 0) + 't ' + (m.ctx_pct || 0) + 'pct'; }, |
|
toggleForceInject: function () { |
|
state.forceInject = !state.forceInject; |
|
try { chrome.storage.local.set({ 'dshmem-force-inject': state.forceInject }) } catch (e) { } |
|
API._applyStg(); |
|
API.log('force inject: ' + (state.forceInject ? 'ARMED - next send will inject' : 'off')); |
|
}, |
|
_applyStg: function () { |
|
if (!el.stg) return; |
|
var cls = 'dshmem-btn stg' + (state.forceInject ? ' armed' : ''); |
|
el.stg.className = cls; |
|
}, |
|
log: function (m) { |
|
if (!el.log) return; |
|
var t = new Date().toLocaleTimeString(); |
|
var line = '[' + t + '] ' + m; |
|
var txt = (line + String.fromCharCode(10) + (el.log.textContent || '')).split(String.fromCharCode(10)).slice(0, LOG_CAP).join(String.fromCharCode(10)); |
|
el.log.textContent = txt; |
|
}, |
|
mcpRender: function (data) { |
|
if (!el.mcpList) return; |
|
var apps = (data && data.apps) || []; |
|
if (data && data.root && el.mcpPath && !el.mcpPath.value) el.mcpPath.value = data.root; |
|
if (el.mcpCount) el.mcpCount.textContent = apps.length ? (apps.length + ' app' + (apps.length === 1 ? '' : 's')) : ''; |
|
el.mcpList.textContent = ''; |
|
if (!apps.length) { el.mcpList.appendChild(mk('div', 'dshmem-mcp-empty', 'no apps found')); return; } |
|
apps.forEach(function (a) { |
|
var row = mk('div', 'dshmem-mcp-row'); |
|
var dotCls = 'dshmem-mcp-dot'; |
|
if (a.launching || a.starting) dotCls += ' busy'; |
|
else if (a.lastError) dotCls += ' err'; |
|
else if (a.guiUp || a.running) dotCls += ' on'; |
|
var dot = mk('span', dotCls); |
|
row.appendChild(dot); |
|
var nameEl = mk('span', 'dshmem-mcp-name', a.name); |
|
var parts = [a.name]; |
|
if (a.port != null) parts.push('port ' + a.port); |
|
if (a.guiUp) parts.push('window up'); |
|
if (a.running) parts.push('client up, ' + a.tools + ' tools'); |
|
if (!a.connectKnown) parts.push('no live control (headless)'); |
|
if (a.lastError) parts.push('error: ' + a.lastError); |
|
nameEl.title = parts.join(' | '); |
|
row.appendChild(nameEl); |
|
var statusTxt = ''; |
|
if (a.running) statusTxt = a.tools + 't'; |
|
else if (a.guiUp) statusTxt = 'win'; |
|
else if (a.lastError) statusTxt = 'err'; |
|
row.appendChild(mk('span', 'dshmem-mcp-tools', statusTxt)); |
|
if (a.canLaunch) { |
|
var lb = mk('button', 'dshmem-mcp-btn', a.launching ? '...' : (a.guiUp ? '\u2713' : '\u23FB')); |
|
lb.title = a.guiUp ? (a.name + ' window is already open on port ' + a.port) : ('Launch ' + a.name + ' window (port ' + a.port + ')'); |
|
lb.disabled = a.launching; |
|
lb.addEventListener('click', function (ev) { ev.stopPropagation(); API.mcpLaunch(a.name); }); |
|
row.appendChild(lb); |
|
} |
|
var sb = mk('button', 'dshmem-mcp-btn', a.running ? 'stop' : (a.guiUp ? 'close' : 'stop')); |
|
sb.title = a.running ? 'Close MCP client (window stays)' : (a.guiUp ? 'Close the ' + a.name + ' window' : 'Nothing to close'); |
|
sb.disabled = !a.running && !a.starting && !a.guiUp; |
|
sb.addEventListener('click', function (ev) { ev.stopPropagation(); API.mcpStop(a.name); }); |
|
row.appendChild(sb); |
|
el.mcpList.appendChild(row); |
|
if (a.lastError) el.mcpList.appendChild(mk('div', 'dshmem-mcp-err', a.lastError.slice(0, 160))); |
|
}); |
|
}, |
|
mcpRefresh: function () { |
|
try { |
|
globalThis.DSH_BRIDGE.fetchBridge('GET', '/mcp/apps').then(function (r) { |
|
if (r && r.ok) { API.mcpRender(r); } |
|
else { if (el.mcpList) el.mcpList.textContent = 'bridge: ' + ((r && r.error && r.error.message) || 'fail'); } |
|
}); |
|
} catch (e) { if (el.mcpList) el.mcpList.textContent = 'error: ' + String(e); } |
|
}, |
|
mcpRescan: function () { |
|
var p = el.mcpPath && el.mcpPath.value ? String(el.mcpPath.value).trim() : ''; |
|
var call = p ? globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/root', { path: p }) : globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/rescan'); |
|
Promise.resolve(call).then(function (r) { |
|
if (r && r.ok) { API.mcpRender({ root: r.root, apps: r.apps }); API.log('mcp rescan: ' + ((r.apps || []).length) + ' apps'); } |
|
else { API.log('mcp rescan fail: ' + ((r && r.error && r.error.message) || '?')); } |
|
}); |
|
}, |
|
mcpLaunch: function (name) { |
|
API.log('mcp launch: ' + name); |
|
globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/launch', { app: name }).then(function (r) { |
|
if (r && r.ok) API.log('mcp launch ' + name + ': ' + (r.already ? 'already up' : 'started') + (r.port != null ? ' port ' + r.port : '')); |
|
else API.log('mcp launch ' + name + ' fail: ' + ((r && r.error && r.error.message) || '?')); |
|
API.mcpRefresh(); |
|
}); |
|
}, |
|
mcpStop: function (name) { |
|
globalThis.DSH_BRIDGE.fetchBridge('POST', '/mcp/stop', { app: name }).then(function (r) { |
|
if (!r || !r.ok) { API.log('mcp stop ' + name + ' fail: ' + ((r && r.error && r.error.message) || '?')); API.mcpRefresh(); return; } |
|
var what = []; |
|
if (r.stopped) what.push('client'); |
|
if (r.windowClosed) what.push('window'); |
|
if (what.length === 0) what.push('nothing'); |
|
API.log('mcp stop ' + name + ': closed ' + what.join(' + ') + (r.warning ? ' | ' + r.warning : '')); |
|
API.mcpRefresh(); |
|
}); |
|
}, |
|
restartBridge: function () { |
|
API.log('restart: sending shutdown with restart=1'); |
|
globalThis.DSH_BRIDGE.fetchBridge('POST', '/shutdown', { restart: true }).then(function (r) { |
|
if (r && r.ok) { API.log('restart: bridge is going down, supervisor will bring it back in ~5s'); } |
|
else { API.log('restart fail: ' + ((r && r.error && r.error.message) || 'bridge unreachable')); } |
|
var tries = 0; |
|
var iv = setInterval(function () { |
|
tries++; |
|
globalThis.DSH_BRIDGE.health().then(function (h) { |
|
if (h && h.ok) { clearInterval(iv); API.setConnected(true); API.log('restart: bridge is back'); } |
|
else if (tries >= 8) { clearInterval(iv); API.log('restart: still down after ~24s, check the supervisor window'); } |
|
}); |
|
}, 3000); |
|
}); |
|
}, |
|
onResetLoops: function (cb) { state.resetCbs.push(cb); }, |
|
onStop: function (cb) { state.stopCbs.push(cb); }, onStart: function (cb) { state.startCbs.push(cb); }, |
|
onSaveToken: function (cb) { state.saveCbs.push(cb); }, |
|
getToken: function () { return el.token ? el.token.value : ''; }, getTransport: function () { return state.transport; }, setTransport: function (v) { state.transport = v === 'sse' ? 'sse' : 'dom'; if (el.transportToggle) el.transportToggle.textContent = state.transport === 'dom' ? 'DOM' : 'SSE'; }, |
|
setToken: function (v) { if (el.token) el.token.value = v || ''; }, |
|
showConfirm: function (opt) { |
|
if (!el.confirm) return; |
|
opt = opt || {}; |
|
el.confirm.textContent = ''; |
|
el.confirm.appendChild(mk('div', 'dshmem-confirm-title', opt.title || 'Confirm')); |
|
el.confirm.appendChild(mk('div', 'dshmem-label', opt.detail || '')); |
|
var r = mk('div', 'dshmem-row'); |
|
var ok = mk('button', 'dshmem-btn primary', 'Allow'); |
|
var no = mk('button', 'dshmem-btn danger', 'Deny'); |
|
ok.addEventListener('click', function () { API.hideConfirm(); if (opt.onAllow) opt.onAllow(); }); |
|
no.addEventListener('click', function () { API.hideConfirm(); if (opt.onDeny) opt.onDeny(); }); |
|
r.appendChild(ok); r.appendChild(no); el.confirm.appendChild(r); |
|
el.confirm.style.display = 'block'; |
|
}, |
|
hideConfirm: function () { if (el.confirm) el.confirm.style.display = 'none'; }, |
|
showPreview: function (opt) { |
|
if (!el.preview) return; |
|
opt = opt || {}; |
|
var sec = opt.seconds || 2; |
|
var text = opt.text || ''; |
|
var cancelled = false; |
|
el.preview.style.display = 'block'; |
|
var render = function () { el.preview.textContent = 'Sending in ' + sec + 's (click to cancel):' + String.fromCharCode(10) + text.slice(0, 500); }; |
|
render(); |
|
var iv = setInterval(function () { |
|
sec--; |
|
if (sec <= 0) { clearInterval(iv); el.preview.style.display = 'none'; el.preview.onclick = null; if (!cancelled && opt.onSend) opt.onSend(); } |
|
else render(); |
|
}, 1000); |
|
el.preview.onclick = function () { cancelled = true; clearInterval(iv); el.preview.style.display = 'none'; el.preview.onclick = null; if (opt.onCancel) opt.onCancel(); }; |
|
}, |
|
hidePreview: function () { if (el.preview) el.preview.style.display = 'none'; }, |
|
init: function () { |
|
try { build(); } catch (e) { console.error('[dshmem] build failed: ' + e); } |
|
try { wire(); } catch (e) { console.error('[dshmem] wire failed: ' + e); API.log('wire failed: ' + String(e)); } |
|
try { chrome.storage.local.get('dshmem-bridge-token', function (v) { var t = v && v['dshmem-bridge-token'] ? String(v['dshmem-bridge-token']) : ''; if (t && el.token) el.token.value = t; API.log('panel loaded, token in storage len=' + t.length); }) } catch (e) { } |
|
try { chrome.storage.local.get('dshmem-transport', function (v) { if (v && v['dshmem-transport']) { var tv = v['dshmem-transport']; if (tv === 'msg') tv = 'dom'; if (tv === 'xhr') tv = 'sse'; API.setTransport(tv) } }) } catch (e) { } try { chrome.storage.local.get('dshmem-acc-limit', function (v) { var n = v && v['dshmem-acc-limit'] ? parseInt(v['dshmem-acc-limit'], 10) : 0; if (n && n >= 1000) { state.accLimit = n; if (el.accLimit) el.accLimit.value = String(n); API.setMetrics(state.lastMetrics); } }) } catch (e) { } |
|
try { chrome.storage.local.get('dshmem-force-inject', function (v) { state.forceInject = Boolean(v && v['dshmem-force-inject']); API._applyStg(); }) } catch (e) { } |
|
try { chrome.storage.onChanged.addListener(function (ch, area) { if (area !== 'local') return; if (ch['dshmem-force-inject']) { state.forceInject = Boolean(ch['dshmem-force-inject'].newValue); API._applyStg(); } }); } catch (e) { } } |
|
}; |
|
globalThis.DSHMEM = API; |
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', function () { API.init(); }); |
|
} else { API.init(); } |
|
})(); |