dsh-remote 0.6.0: other machines' workspaces in the Workspaces list, richer Devices tab

- sidebar: workspaces (+ sessions) of every other device appear inside the stock
  Workspaces list with a server icon and the device name; click folds sessions,
  hover + creates a session there, a session opens as the remote conversation view
  (/r/<device>/#/session/<id>); the viewed device's own rows stay native
- Devices tab: every device with its state (online/offline, last seen, platform,
  workspace/session/running counts), offline and revoked devices included
- deploy/e2e-dom.mjs, e2e-eval.mjs: DOM dump / evaluate-in-page helpers
This commit is contained in:
lyuksovannyy committed 2026-10-12 09:50:13 +00:00
1 parent bfbc3c66e4
commit bf99a952fb
5 files changed
+262 -27

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Dump the sidebar DOM (structure only) of the local DSH, to build UI integrations against.
// node deploy/e2e-dom.mjs <chrome> [port] [selector]
import { spawn } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { cookieName, mintCookie } from '../lib/auth-cookie.mjs'
const [chrome, port = '3081', selector = 'aside, nav, [class*=sidebar]'] = process.argv.slice(2)
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
const secret = fs.readFileSync(path.join(home, '.credentials.yaml'), 'utf8').match(/browser-session:[\s\S]*?secret:\s*([A-Za-z0-9_-]{43})/)[1]
const authority = `127.0.0.1:${port}`
const prof = fs.mkdtempSync(path.join(os.tmpdir(), 'dshr-dom-'))
const proc = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', `--user-data-dir=${prof}`, '--remote-debugging-port=0', '--window-size=1400,900', 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] })
const wsUrl = await new Promise((resolve, reject) => {
let buf = ''
proc.stderr.on('data', (d) => { buf += d; const m = buf.match(/DevTools listening on (ws:\/\/\S+)/); if (m) resolve(m[1]) })
setTimeout(() => reject(new Error('chrome did not start')), 15000)
})
const page = (await (await fetch(wsUrl.replace(/^ws/, 'http').replace(/\/devtools\/browser\/.*/, '/json/list'))).json()).find((t) => t.type === 'page')
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((r) => ws.addEventListener('open', r))
let id = 0
const pending = new Map()
ws.addEventListener('message', (ev) => { const m = JSON.parse(ev.data); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id) } })
const send = (method, params = {}) => new Promise((resolve) => { const i = ++id; pending.set(i, resolve); ws.send(JSON.stringify({ id: i, method, params })) })
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable')
await send('Network.setCookie', { name: cookieName(authority), value: mintCookie(authority, secret, 1).split('=').slice(1).join('='), domain: '127.0.0.1', path: '/', httpOnly: true })
await send('Page.navigate', { url: `http://${authority}/` })
await sleep(10000)
const expr = `(() => {
const root = [...document.querySelectorAll(${JSON.stringify(selector)})].find((e) => /Workspaces/.test(e.innerText || '')) || document.body
const dump = (el, d = 0) => {
if (d > 9) return ''
const attrs = [...el.attributes].filter((a) => /^(class|role|data-|aria-label|href|title|type)/.test(a.name)).map((a) => a.name + '="' + String(a.value).slice(0, 70) + '"').join(' ')
const own = [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent.trim()).filter(Boolean).join(' ').slice(0, 40)
const kids = [...el.children].map((c) => dump(c, d + 1)).join('')
return ' '.repeat(d) + '<' + el.tagName.toLowerCase() + (attrs ? ' ' + attrs : '') + '>' + (own ? ' ' + own : '') + '\\n' + kids
}
return dump(root)
})()`
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true })
console.log(r.result?.result?.value || JSON.stringify(r.result))
ws.close(); proc.kill('SIGKILL')
try { fs.rmSync(prof, { recursive: true, force: true }) } catch { /* chrome may still hold files */ }
process.exit(0)