dsh-mesh-ui 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +68 -17
- package/lib/collect.js +31 -0
- package/lib/delegations.js +62 -39
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
var type = deviceType(name)
|
|
43
43
|
return type ? label + ' · ' + type : label
|
|
44
44
|
}
|
|
45
|
-
// 同一台设备可能有多个 peer 记录(不同 token/入口:
|
|
45
|
+
// 同一台设备可能有多个 peer 记录(不同 token/入口:peer-node / peer-node-lan / peer-node-exec)。
|
|
46
46
|
// AI 网络拓扑按「设备」展示:去掉后缀归并,取可达且延迟最优的那条。
|
|
47
47
|
var PEER_SUFFIX = /-(lan|wan|exec|exe|import|local|ctl)$/i
|
|
48
48
|
function peerDeviceKey(name) {
|
|
@@ -96,6 +96,7 @@ function nodeLabel(name) { return NODE_LABELS[String(name || '').toLowerCase()]
|
|
|
96
96
|
return keys
|
|
97
97
|
}
|
|
98
98
|
// 2026-11-10:用户手工调整后认可的默认布局(世界坐标,10000×6000 画布内)
|
|
99
|
+
// 设备按设备名(home/peer-node/company/peer-node/peer-node)映射,各机通用;服务器两张卡单独一张表
|
|
99
100
|
var DEFAULT_DEVICE_POS = {}
|
|
100
101
|
var DEFAULT_INFRA_POS = {
|
|
101
102
|
aliyun: [4887.59, 2856.78],
|
|
@@ -677,7 +678,7 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
|
677
678
|
h('span', null, '⋯')
|
|
678
679
|
)
|
|
679
680
|
),
|
|
680
|
-
h('div', { className: 'dm-hint' }, '拓扑
|
|
681
|
+
h('div', { className: 'dm-hint' }, '拓扑 v19 · 内容 ' + Math.round(layout.W) + '×' + Math.round(layout.H) + 'px · 卡 ' + Math.round(layout.cardW) + '×' + Math.round(layout.cardH) + ' · 缩放 ' + Math.round(view.scale * 100) + '% · 滚轮缩放 / 拖拽平移')
|
|
681
682
|
)
|
|
682
683
|
}
|
|
683
684
|
|
|
@@ -811,6 +812,7 @@ var DOWN_COLOR = "rgba(209,75,65,.7)"
|
|
|
811
812
|
// 各机 DSH 安装形态(实测/档案)
|
|
812
813
|
var DEVICE_DSH = {}
|
|
813
814
|
function dshTypesFor(devKey) { return DEVICE_DSH[peerDeviceKey(devKey)] || [] }
|
|
815
|
+
// LAN peer 判定:-lan 后缀 / 内网 IP / kind=lan(实际数据里 peer-node-lan 的 kind 是 frp)
|
|
814
816
|
function isLanPeer(peer) {
|
|
815
817
|
var t = (peer && peer.tunnel) || {}
|
|
816
818
|
var name = String((peer && peer.name) || '')
|
|
@@ -835,9 +837,11 @@ function sakuraTunnelOf(data, devKey) {
|
|
|
835
837
|
function deviceLinkState(data, peer, lanMap) {
|
|
836
838
|
var dk = peerDeviceKey(peer.name)
|
|
837
839
|
var t = peer.tunnel || {}
|
|
838
|
-
|
|
840
|
+
// 2026-11-10:蓝线状态以隧道端口 TCP 探测为准(HTTP /health 对高延迟节点会误判断连)
|
|
841
|
+
var probe = t.probe && typeof t.probe.up === "boolean" ? t.probe : null
|
|
842
|
+
var publicUp = (probe ? probe.up === true : false) || Boolean(peer.up)
|
|
839
843
|
var lan = lanMap ? lanMap[dk] : (t.kind === 'lan' ? { up: Boolean(peer.up), host: t.host, port: t.port, ip: t.ip || '' } : null)
|
|
840
|
-
var lanUp = Boolean(lan && lan.up)
|
|
844
|
+
var lanUp = Boolean(lan && (lan.probe && lan.probe.up === true ? true : lan.up))
|
|
841
845
|
var st = sakuraTunnelOf(data, dk)
|
|
842
846
|
var sakuraUp = st ? st.online === true : null
|
|
843
847
|
var anyUp = publicUp || sakuraUp === true || lanUp
|
|
@@ -858,7 +862,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
858
862
|
var localKey = peerDeviceKey(data.nodeName)
|
|
859
863
|
var devs = [{ devKey: localKey, name: nodeDisplay(data.nodeName) + "(本机)", up: true, port: localPort, host: localHost }].concat(peers.filter(function (p) { return !isLanPeer(p) }).map(function (p) {
|
|
860
864
|
var tu = p.tunnel || {}
|
|
861
|
-
return { devKey: peerDeviceKey(p.name), name: nodeDisplay(peerDeviceKey(p.name)), up: Boolean(p.up), port: tu.port || null, host: tu.host || 'relay.example.com' }
|
|
865
|
+
return { devKey: peerDeviceKey(p.name), name: nodeDisplay(peerDeviceKey(p.name)), up: (tu.probe ? Boolean(tu.probe.up) : Boolean(p.up)), port: tu.port || null, host: tu.host || 'relay.example.com' }
|
|
862
866
|
}))
|
|
863
867
|
var aliyunRows = devs.filter(function (d) { return d.port }).map(function (d) {
|
|
864
868
|
return h('div', { key: d.devKey, className: 'dm-device-row' },
|
|
@@ -997,7 +1001,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
997
1001
|
var lanMap = {}
|
|
998
1002
|
filterSelfPeers(data.peers || [], data.nodeName).forEach(function (p) {
|
|
999
1003
|
var t = p.tunnel || {}
|
|
1000
|
-
if (isLanPeer(p)) lanMap[peerDeviceKey(p.name)] = { up: Boolean(p.up), host: t.host, port: t.port, ip: t.ip || '' }
|
|
1004
|
+
if (isLanPeer(p)) lanMap[peerDeviceKey(p.name)] = { up: Boolean(p.up), host: t.host, port: t.port, ip: t.ip || '', probe: t.probe || null }
|
|
1001
1005
|
})
|
|
1002
1006
|
var nodes = [{
|
|
1003
1007
|
key: 'local',
|
|
@@ -1418,17 +1422,62 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1418
1422
|
var s2 = useState(''); var error = s2[0]; var setError = s2[1]
|
|
1419
1423
|
var s3 = useState(0); var tick = s3[0]; var setTick = s3[1]
|
|
1420
1424
|
var s4 = useState(''); var copied = s4[0]; var setCopied = s4[1]
|
|
1425
|
+
// 2026-11-10:真刷新 —— 点击后等待后台采集完成再渲染;本地冷却 + 忙锁防止连点刷爆请求
|
|
1426
|
+
var s6 = useState(""); var refreshHint = s6[0]; var setRefreshHint = s6[1]
|
|
1427
|
+
var forceRef = useRef({ at: 0, busy: false })
|
|
1421
1428
|
var s5 = useState('local'); var selectedKey = s5[0]; var setSelectedKey = s5[1]
|
|
1422
1429
|
|
|
1423
1430
|
function load(force) {
|
|
1431
|
+
if (!force) {
|
|
1432
|
+
setLoading(true)
|
|
1433
|
+
fetchStatus(false)
|
|
1434
|
+
.then(function (json) {
|
|
1435
|
+
setData(json)
|
|
1436
|
+
setError(json && json.ok === false && !json.warming ? json.error || "mesh-health error" : "")
|
|
1437
|
+
})
|
|
1438
|
+
.catch(function (err) { setError(String((err && err.message) || err)) })
|
|
1439
|
+
.finally(function () { setLoading(false) })
|
|
1440
|
+
return
|
|
1441
|
+
}
|
|
1442
|
+
var st = forceRef.current
|
|
1443
|
+
var now = Date.now()
|
|
1444
|
+
if (st.busy) return
|
|
1445
|
+
if (now - st.at < 8000) {
|
|
1446
|
+
var waitSec = Math.ceil((8000 - (now - st.at)) / 1000)
|
|
1447
|
+
setRefreshHint("刚采集过," + waitSec + "s 后可再刷新")
|
|
1448
|
+
setTimeout(function () { setRefreshHint("") }, 2200)
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
st.busy = true
|
|
1452
|
+
st.at = now
|
|
1424
1453
|
setLoading(true)
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1454
|
+
setRefreshHint("采集中…")
|
|
1455
|
+
var prevAt = data && data.at
|
|
1456
|
+
var attempts = 0
|
|
1457
|
+
function poll() {
|
|
1458
|
+
attempts++
|
|
1459
|
+
fetchStatus(false).then(function (json) {
|
|
1460
|
+
var fresh = json && json.refreshing === false && json.at && json.at !== prevAt
|
|
1461
|
+
if (fresh || attempts >= 15) {
|
|
1462
|
+
setData(json)
|
|
1463
|
+
setError(json && json.ok === false && !json.warming ? json.error || "mesh-health error" : "")
|
|
1464
|
+
setLoading(false)
|
|
1465
|
+
st.busy = false
|
|
1466
|
+
setRefreshHint(fresh ? "已刷新" : "无新数据(采集超时)")
|
|
1467
|
+
setTimeout(function () { setRefreshHint("") }, 2000)
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
setTimeout(poll, 800)
|
|
1471
|
+
}).catch(function () {
|
|
1472
|
+
st.busy = false
|
|
1473
|
+
setLoading(false)
|
|
1474
|
+
setRefreshHint("刷新失败")
|
|
1475
|
+
setTimeout(function () { setRefreshHint("") }, 2500)
|
|
1429
1476
|
})
|
|
1430
|
-
|
|
1431
|
-
|
|
1477
|
+
}
|
|
1478
|
+
fetchStatus(true)
|
|
1479
|
+
.catch(function () { /* continue polling */ })
|
|
1480
|
+
.then(function () { setTimeout(poll, 500) })
|
|
1432
1481
|
}
|
|
1433
1482
|
|
|
1434
1483
|
useEffect(function () {
|
|
@@ -1437,7 +1486,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1437
1486
|
return function () { clearInterval(timer) }
|
|
1438
1487
|
}, [])
|
|
1439
1488
|
|
|
1440
|
-
useEffect(function () { if (tick > 0) load(false) }, [tick])
|
|
1489
|
+
useEffect(function () { if (tick > 0 && !forceRef.current.busy) load(false) }, [tick])
|
|
1441
1490
|
|
|
1442
1491
|
function copyJson() {
|
|
1443
1492
|
var text = JSON.stringify(data || {}, null, 2)
|
|
@@ -1468,7 +1517,8 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1468
1517
|
h('span', { style: { flex: 1 } }),
|
|
1469
1518
|
h('a', { className: 'dm-btn', href: API, target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none' } }, '原始 JSON'),
|
|
1470
1519
|
h('button', { className: 'dm-btn', onClick: copyJson }, copied || '复制诊断'),
|
|
1471
|
-
h('button', { className: 'dm-btn primary', onClick: function () { load(true) } }, loading ? '
|
|
1520
|
+
h('button', { className: 'dm-btn primary', onClick: function () { load(true) }, title: '立即采集一次并等待新快照(8s 冷却)' }, loading ? '采集中…' : '刷新'),
|
|
1521
|
+
refreshHint ? h('span', { className: 'dm-sub' }, refreshHint) : null
|
|
1472
1522
|
),
|
|
1473
1523
|
error ? h('div', { className: 'dm-err' }, error) : null,
|
|
1474
1524
|
|
|
@@ -1971,6 +2021,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
1971
2021
|
loading ? h('span', { className: 'dm-dg-loading' }, h('span', { className: 'dm-dg-spin' }), '刷新中') : '刷新')
|
|
1972
2022
|
),
|
|
1973
2023
|
notice ? h('div', { className: 'dm-dg-notice ' + notice.type }, notice.text) : null,
|
|
2024
|
+
selected && selected.staleActive ? h('div', { className: 'dm-dg-notice warn' }, '该任务已超过 ' + Math.round(selected.staleAfterSec / 60) + ' 分钟没有更新,可能已失联;可点「刷新状态」再查对端,或重新发起委托。') : null,
|
|
1974
2025
|
h('div', { className: 'dm-dg-body', style: { '--dm-dg-list-w': listWidth + 'px' } },
|
|
1975
2026
|
h('section', { className: 'dm-dg-pane' },
|
|
1976
2027
|
h('div', { className: 'dm-dg-pane-head' }, '委托列表', h('span', { className: 'dm-dg-sub' }, '(' + visibleTasks.length + (visibleTasks.length !== tasks.length ? '/' + tasks.length : '') + ')')),
|
|
@@ -1992,7 +2043,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
1992
2043
|
key: task.taskId,
|
|
1993
2044
|
className: 'dm-dg-row ' + tone + (task.taskId === selectedId ? ' active' : ''),
|
|
1994
2045
|
onClick: function () { if (task.taskId !== selectedId) selectTask(task) },
|
|
1995
|
-
title: task.goal || ''
|
|
2046
|
+
title: task.staleActive ? '超过 ' + Math.round(task.staleAfterSec / 60) + ' 分钟无更新(疑似失联,可点刷新状态)' : (task.goal || '')
|
|
1996
2047
|
},
|
|
1997
2048
|
h('div', { className: 'dm-dg-row-top' },
|
|
1998
2049
|
task.activeFresh ? h(LoadingSpinner, { size: 12 }) : null,
|
|
@@ -2002,7 +2053,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2002
2053
|
h('span', { className: 'dm-dg-arrow' }, '→'),
|
|
2003
2054
|
h('span', { className: 'dm-dg-to' }, (task.nodeLabel || task.node || '?') + (task.instance ? ' · ' + task.instance : ''))
|
|
2004
2055
|
),
|
|
2005
|
-
dgChip(task.state),
|
|
2056
|
+
dgChip(task.displayState || task.state),
|
|
2006
2057
|
h('span', { className: 'dm-dg-row-time' }, (task.isActive ? '更新 ' : '完成 ') + dgClock(task.displayAt || task.updatedAt))
|
|
2007
2058
|
),
|
|
2008
2059
|
h('div', { className: 'dm-dg-goal' }, task.goal || '(无目标)'),
|
|
@@ -2036,7 +2087,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2036
2087
|
h('span', { className: 'dm-dg-k' }, '状态'),
|
|
2037
2088
|
h('span', { className: 'dm-dg-v' },
|
|
2038
2089
|
selected.activeFresh ? h(LoadingSpinner, { size: 13 }) : null,
|
|
2039
|
-
dgChip(selected.state)
|
|
2090
|
+
dgChip(selected.displayState || selected.state)
|
|
2040
2091
|
)
|
|
2041
2092
|
),
|
|
2042
2093
|
kv('委托方', (selected.fromLabel || selected.from || '—') + ((selected.fromIsLocal || (localNode && selected.from === localNode)) ? '(本机)' : '')),
|
package/lib/collect.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { execFile } from 'node:child_process'
|
|
17
17
|
import { existsSync, readdirSync, readFileSync, statfsSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { lookup as dnsLookup } from 'node:dns/promises'
|
|
19
|
+
import { connect as netConnect } from 'node:net'
|
|
19
20
|
import { arch as osArch, cpus as osCpus, freemem as osFreemem, homedir, platform as osPlatform, release as osRelease, totalmem as osTotalmem } from 'node:os'
|
|
20
21
|
import { join } from 'node:path'
|
|
21
22
|
|
|
@@ -505,6 +506,36 @@ export async function collect(repo) {
|
|
|
505
506
|
} catch { /* 解析失败则保持主机名 */ }
|
|
506
507
|
}))
|
|
507
508
|
|
|
509
|
+
// 2026-11-10:为每条 peer 隧道做 TCP 端口探测(阿里云 701x / LAN 8124),面板连线状态以此为准,
|
|
510
|
+
// 而不是用 peer.up(HTTP /health 探测,高延迟节点偶发超时会造成"隧道通却显示断连"的误判)
|
|
511
|
+
function tcpProbe(host, port, timeoutMs) {
|
|
512
|
+
return new Promise((resolve) => {
|
|
513
|
+
const started = Date.now()
|
|
514
|
+
let done = false
|
|
515
|
+
const socket = netConnect({ host, port })
|
|
516
|
+
const finish = (up, error) => {
|
|
517
|
+
if (done) return
|
|
518
|
+
done = true
|
|
519
|
+
try { socket.destroy() } catch { /* ignore */ }
|
|
520
|
+
resolve({ up, latencyMs: Date.now() - started, error: up ? null : error || "timeout" })
|
|
521
|
+
}
|
|
522
|
+
socket.setTimeout(timeoutMs)
|
|
523
|
+
socket.once("connect", () => finish(true, null))
|
|
524
|
+
socket.once("timeout", () => finish(false, "timeout"))
|
|
525
|
+
socket.once("error", (err) => finish(false, err?.code || "error"))
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
await Promise.all(peers.map(async (peer) => {
|
|
529
|
+
const tunnel = peer.tunnel || {}
|
|
530
|
+
if (!tunnel.host || !tunnel.port) return
|
|
531
|
+
tunnel.probe = await tcpProbe(tunnel.host, Number(tunnel.port), 3000)
|
|
532
|
+
if (!tunnel.probe.up) {
|
|
533
|
+
const retry = await tcpProbe(tunnel.host, Number(tunnel.port), 3000)
|
|
534
|
+
if (retry.up) tunnel.probe = retry
|
|
535
|
+
}
|
|
536
|
+
}))
|
|
537
|
+
|
|
538
|
+
|
|
508
539
|
return {
|
|
509
540
|
ok: true,
|
|
510
541
|
generatedAt,
|
package/lib/delegations.js
CHANGED
|
@@ -26,53 +26,76 @@ function readJson(file, fallback = null) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** 委托列表(本地台账,按更新时间倒序)。
|
|
29
|
+
/** 委托列表(本地台账,按更新时间倒序)。
|
|
30
|
+
* - 过滤 selftest 污染记录(selftest 曾把测试任务写进真实台账)
|
|
31
|
+
* - 活跃任务超过阈值未更新 → displayState=stalled(UI 不再显示无限 loading)
|
|
32
|
+
*/
|
|
30
33
|
export function listDelegations(limit = 50) {
|
|
31
34
|
const data = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
|
|
32
35
|
const localNode = readJson(join(meshHome(), 'config.json'), {})?.node?.name || ''
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
const ACTIVE_STATES = ['working', 'submitted', 'accepted', 'question', 'provisioning', 'running']
|
|
37
|
+
// 超过这些时间没有 updatedAt 更新,就认为“疑似失联”(不直接改文件,UI 只展示)
|
|
38
|
+
const STALE_AFTER_MS = {
|
|
39
|
+
submitted: 15 * 60 * 1000,
|
|
40
|
+
accepted: 15 * 60 * 1000,
|
|
41
|
+
provisioning: 15 * 60 * 1000,
|
|
42
|
+
working: 60 * 60 * 1000,
|
|
43
|
+
running: 60 * 60 * 1000,
|
|
44
|
+
question: 6 * 60 * 60 * 1000
|
|
45
|
+
}
|
|
46
|
+
const nowMs = Date.now()
|
|
47
|
+
const tasks = Object.values(data?.tasks || {})
|
|
48
|
+
.filter((task) => task && task.from !== 'selftest')
|
|
49
|
+
.map((task) => {
|
|
50
|
+
const start = Date.parse(task.createdAt || '')
|
|
51
|
+
const end = Date.parse(task.updatedAt || '') || nowMs
|
|
52
|
+
const from = task.from || ''
|
|
53
|
+
const state = task.state || 'unknown'
|
|
54
|
+
const isActive = ACTIVE_STATES.indexOf(state) >= 0
|
|
55
|
+
const updatedMs = Date.parse(task.updatedAt || task.createdAt || '')
|
|
56
|
+
const ageMs = Number.isFinite(updatedMs) ? nowMs - updatedMs : Infinity
|
|
57
|
+
const staleAfter = STALE_AFTER_MS[state] ?? 60 * 60 * 1000
|
|
58
|
+
const staleActive = isActive && ageMs > staleAfter
|
|
59
|
+
return {
|
|
60
|
+
taskId: task.taskId || '',
|
|
61
|
+
conversationId: task.conversationId || '',
|
|
62
|
+
sessionId: task.sessionId || '',
|
|
63
|
+
from,
|
|
64
|
+
fromLabel: nodeLabel(from),
|
|
65
|
+
fromIsLocal: Boolean(from) && from === localNode,
|
|
66
|
+
fromInstance: task.fromInstance || '',
|
|
67
|
+
node: task.node || '',
|
|
68
|
+
nodeLabel: nodeLabel(task.node),
|
|
69
|
+
toIsLocal: Boolean(task.node) && task.node === localNode,
|
|
70
|
+
instance: task.instance || '',
|
|
71
|
+
workspace: task.workspace || '',
|
|
72
|
+
dedicated: Boolean(task.dedicated),
|
|
73
|
+
state,
|
|
74
|
+
displayState: staleActive ? 'stalled' : state,
|
|
75
|
+
isActive,
|
|
76
|
+
staleActive,
|
|
77
|
+
staleAfterSec: Math.round(staleAfter / 1000),
|
|
78
|
+
activeFresh: isActive && !staleActive,
|
|
79
|
+
displayAt: task.finishedAt || task.updatedAt || '',
|
|
80
|
+
policy: task.requestedPolicy || task.policy || '',
|
|
81
|
+
goal: String(task.goal || ''),
|
|
82
|
+
createdAt: task.createdAt || '',
|
|
83
|
+
updatedAt: task.updatedAt || '',
|
|
84
|
+
durationSec: (() => {
|
|
85
|
+
if (!Number.isFinite(start)) return null
|
|
86
|
+
const value = Math.max(0, Math.round((end - start) / 1000))
|
|
87
|
+
return value > 12 * 3600 ? null : value // 台账 updatedAt 会被轮询刷新,超长值不可信
|
|
88
|
+
})(),
|
|
89
|
+
stalledReason: task.stalledReason || '',
|
|
90
|
+
lastSummary: task.summary || ''
|
|
91
|
+
}
|
|
92
|
+
})
|
|
70
93
|
tasks.sort((a, b) => {
|
|
71
94
|
if (a.activeFresh !== b.activeFresh) return a.activeFresh ? -1 : 1
|
|
72
95
|
return String(b.displayAt || b.updatedAt || '').localeCompare(String(a.displayAt || a.updatedAt || ''))
|
|
73
96
|
})
|
|
74
97
|
const byState = {}
|
|
75
|
-
for (const task of tasks) byState[task.
|
|
98
|
+
for (const task of tasks) byState[task.displayState] = (byState[task.displayState] || 0) + 1
|
|
76
99
|
return { total: tasks.length, byState, localNode, tasks: tasks.slice(0, Math.max(1, Math.min(limit, 200))) }
|
|
77
100
|
}
|
|
78
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mesh-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "DSH settings-page panel for dsh-mesh: AI network topology, peer health, and delegation panel. Read-only; data is collected from the local mesh CLI.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|