@gehennawu/dsh-service 0.11.1

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/client.js ADDED
@@ -0,0 +1,1251 @@
1
+ // Browser half of @gehennawu/dsh-service
2
+ // 设置面板「服务控制」:版本信息 + 检查更新 + 一键重启
3
+ window.__ModuleLoader__.load({
4
+ id: '@gehennawu/dsh-service',
5
+ factory: (require) => {
6
+ var module = { exports: {} }
7
+ var exports = module.exports
8
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
9
+ const React = require('react')
10
+ const NS = 'dsh-service'
11
+ const zh = {
12
+ 'nav.label': '服务控制',
13
+ 'overlay.label': '服务重启状态',
14
+ 'recovery.waiting.title': '服务重启中…',
15
+ 'recovery.waiting.body': '正在等待新的 DSH Web 进程启动,已等待 {seconds} 秒。',
16
+ 'recovery.timeout.title': '服务尚未恢复',
17
+ 'recovery.timeout.body': '已等待 60 秒。请确认外部进程管理器已正确配置,或手动刷新页面重试。',
18
+ 'recovery.manual': '手动刷新',
19
+ 'health.title': '运行状况',
20
+ 'health.uptime': '运行时间',
21
+ 'health.rss': '内存 RSS',
22
+ 'health.liveSessions': '存活会话',
23
+ 'health.persistedSessions': '持久化会话',
24
+ 'health.activeAgents': '活跃 Agent',
25
+ 'health.activeJobs': '后台任务',
26
+ 'health.uptimeValue': '{hours} 小时 {minutes} 分钟',
27
+ 'health.error': '无法读取运行状况',
28
+ 'health.check': '立即健康检查',
29
+ 'health.checking': '检查中…',
30
+ 'health.overall.ok': '正常',
31
+ 'health.overall.warning': '警告',
32
+ 'health.overall.error': '错误',
33
+ 'health.alert.title': '健康提醒',
34
+ 'health.alert.diagnostics': '检查结果存在{status},请查看下方异常项。',
35
+ 'health.status.ok': '正常',
36
+ 'health.status.warning': '警告',
37
+ 'health.status.error': '错误',
38
+ 'health.status.info': '信息',
39
+ 'health.check.session-storage': '会话存储',
40
+ 'health.check.workspace-registry': '工作区注册表',
41
+ 'health.check.dsh-home': 'DSH_HOME',
42
+ 'health.check.backup-storage': '备份存储',
43
+ 'health.check.tar': 'tar',
44
+ 'health.check.permissions': '文件权限',
45
+ 'health.detail.session-storage.ok': '可用,共 {count} 个会话快照',
46
+ 'health.detail.workspace-registry.ok': '可用,共 {count} 个工作区',
47
+ 'health.detail.dsh-home.ok': '目录可访问,权限模式 {mode}',
48
+ 'health.detail.backup-storage.empty': '备份目录可用,当前暂无备份',
49
+ 'health.detail.backup-storage.ok': '备份目录可用,共 {count} 个备份,占用 {size}',
50
+ 'health.detail.tar.ok': 'tar 可执行文件可用',
51
+ 'health.detail.permissions.ok': '文件权限检查正常,未发现异常',
52
+ 'health.detail.permissions.warning': '发现 {count} 个文件或目录权限异常',
53
+ 'health.detail.generic': '{status}',
54
+ 'tabs.overview': '概览',
55
+ 'tabs.health': '健康诊断',
56
+ 'tabs.usage': '模型统计',
57
+ 'overview.container': '容器信息',
58
+ 'overview.errors': '报错信息',
59
+ 'tabs.backup': '备份维护',
60
+ 'tabs.restart': '重启',
61
+ 'tabs.alert.title': '服务控制提醒',
62
+ 'tabs.alert.body': '以下功能需要处理:{tabs}',
63
+ 'permissions.title': '文件权限',
64
+ 'permissions.description': '检查 Agent 是否能读取、写入并进入 DSH_HOME 和工作区。深检跳过 .git 内部文件;修复只补充当前用户所需权限并保留执行位,DSH 凭据文件固定为 600。',
65
+ 'permissions.target': '目标属主:{owner}',
66
+ 'permissions.repair': '修复权限',
67
+ 'permissions.repairing': '修复中…',
68
+ 'permissions.confirm': '确认修复',
69
+ 'permissions.confirmHint': '将跳过 .git,递归恢复当前用户属主并补充 Agent 读写权限,保留已有执行位。请确认后继续。',
70
+ 'permissions.cancel': '取消',
71
+ 'permissions.error': '权限操作失败',
72
+ 'permissions.summary.ok': '{count} 个根目录检查正常',
73
+ 'permissions.summary.warning': '发现 {count} 个根目录异常',
74
+ 'permissions.showDetails': '查看详情',
75
+ 'permissions.hideDetails': '隐藏详情',
76
+ 'permissions.deep': '深度检查',
77
+ 'permissions.deepChecking': '扫描中…',
78
+ 'permissions.deepSummary': '扫描 {scanned} 项,用时 {duration} ms;目录不可编辑 {directories},文件不可编辑 {files},无法读取 {unreadable}。',
79
+ 'backup.title': '备份管理',
80
+ 'backup.description': '备份会话、配置和插件 profile 清单;不会包含 node_modules 或凭据。备份不会自动清理,请自行管理磁盘空间。',
81
+ 'backup.create': '创建备份',
82
+ 'backup.creating': '创建中…',
83
+ 'backup.total': '总体积:{size}',
84
+ 'backup.empty': '还没有备份。',
85
+ 'backup.delete': '删除',
86
+ 'backup.confirm': '确认删除',
87
+ 'backup.confirmHint': '确认删除这个备份?此操作无法撤销。',
88
+ 'backup.cancel': '取消',
89
+ 'backup.error': '备份操作失败',
90
+ 'backup.import': '导入备份',
91
+ 'backup.importing': '导入中…',
92
+ 'backup.showRecords': '展开备份记录',
93
+ 'backup.hideRecords': '收起备份记录',
94
+ 'version.title': '版本信息',
95
+ 'version.current': 'DSH:',
96
+ 'version.plugin': 'dsh-service:',
97
+ 'version.loading': '加载中…',
98
+ 'update.check': '检查更新',
99
+ 'update.checking': '检查中…',
100
+ 'update.current': '已是最新版本',
101
+ 'update.available': '有新版本:{version}',
102
+ 'update.unavailable': '暂时无法检查最新版本',
103
+ 'update.unpublished': '尚未发布可检查版本',
104
+ 'health.recheck': '重新诊断',
105
+ 'update.badge': 'DSH 有更新',
106
+ 'update.details.title': 'DSH 更新可用',
107
+ 'update.details.current': '当前版本:{version}',
108
+ 'update.details.latest': '最新版本:{version}',
109
+ 'update.channels': 'latest:{latest} · next:{next}',
110
+ 'update.details.close': '关闭',
111
+ 'restart.title': '服务重启',
112
+ 'restart.description': '重启 dsh web 进程。运行中的工作会中断,持久化会话可恢复。也可在对话中输入 /restart。',
113
+ 'restart.button': '重启 dsh web',
114
+ 'restart.sending': '发送中…',
115
+ 'restart.confirm': '确认重启',
116
+ 'restart.force': '仍要重启',
117
+ 'restart.cancel': '取消',
118
+ 'restart.sent': '重启指令已发出。',
119
+ 'restart.sentHint': '页面连接即将断开,服务恢复后将自动刷新。',
120
+ 'restart.idleHint': '当前没有检测到运行中的工作。确认后将断开连接,等待服务自动重启。',
121
+ 'activity.agent': 'Agent',
122
+ 'activity.job': '后台任务',
123
+ 'activity.terminal': '终端',
124
+ 'activity.warning': '检测到 {count} 项运行中的工作,重启会立即中断它们。',
125
+ 'activity.item': '{type}:{label} ({status})',
126
+ 'error.update': '检查失败',
127
+ 'error.activity': '检查运行状态失败',
128
+ 'error.restart': '重启失败',
129
+ 'error.instance': '重启响应缺少进程实例标识',
130
+ 'usage.title': '模型使用',
131
+ 'usage.structure': 'token 结构',
132
+ 'usage.structureHint': '按日期展示输入、输出和缓存 token。',
133
+ 'usage.tooltip.date': '日期:{date}',
134
+ 'usage.tooltip.input': '输入 {value} token',
135
+ 'usage.tooltip.output': '输出 {value} token',
136
+ 'usage.tooltip.cache': '缓存命中 {value} token',
137
+ 'usage.axis': 'token 纵轴',
138
+ 'usage.models.more': '展开其余 {count} 个模型',
139
+ 'usage.models.less': '收起模型列表',
140
+ 'usage.refresh': '刷新统计',
141
+ 'usage.refreshing': '刷新中…',
142
+ 'usage.empty': '尚未建立使用统计索引。点击刷新统计开始只读建立索引。',
143
+ 'usage.error': '无法读取模型使用统计',
144
+ 'usage.allProjects': '全部项目',
145
+ 'usage.steps': '成功模型步骤',
146
+ 'usage.stepsValue': '{count} 次',
147
+ 'usage.modelLine': '{steps}次 · 缓存命中 {hitRate} · 输入 {input} token · 输出 {output} token',
148
+ 'usage.input': '输入 token',
149
+ 'usage.output': '输出 token',
150
+ 'usage.cache': '缓存 token',
151
+ 'usage.hitRate': '缓存命中率',
152
+ 'usage.today': '今天',
153
+ 'usage.sevenDays': '近 7 天',
154
+ 'usage.missing': '{count} 个步骤没有 token 数据',
155
+ 'usage.errors.title': '模型报错',
156
+ 'usage.errors.toggle': '模型报错({count} 类)',
157
+ 'usage.errors.recent': '最近 24 小时',
158
+ 'usage.errors.empty': '最近 24 小时没有记录到模型报错。',
159
+ 'usage.toolErrors.title': '工具报错',
160
+ 'usage.toolErrors.toggle': '工具报错({count} 类)',
161
+ 'usage.toolErrors.empty': '最近 24 小时没有记录到工具报错。',
162
+ 'usage.errors.count': '{count} 次',
163
+ }
164
+ const en = {
165
+ 'nav.label': 'Service Control',
166
+ 'overlay.label': 'Service restart status',
167
+ 'recovery.waiting.title': 'Restarting service…',
168
+ 'recovery.waiting.body': 'Waiting for a new DSH Web process. Elapsed: {seconds} seconds.',
169
+ 'recovery.timeout.title': 'Service has not recovered',
170
+ 'recovery.timeout.body': 'Waited 60 seconds. Check the external process manager, or refresh the page manually.',
171
+ 'recovery.manual': 'Manual reload',
172
+ 'health.title': 'Health',
173
+ 'health.uptime': 'Uptime',
174
+ 'health.rss': 'Memory RSS',
175
+ 'health.liveSessions': 'Live sessions',
176
+ 'health.persistedSessions': 'Persisted sessions',
177
+ 'health.activeAgents': 'Active agents',
178
+ 'health.activeJobs': 'Background jobs',
179
+ 'health.uptimeValue': '{hours} h {minutes} min',
180
+ 'health.error': 'Could not read health metrics',
181
+ 'health.check': 'Run health check',
182
+ 'health.checking': 'Checking…',
183
+ 'health.overall.ok': 'Healthy',
184
+ 'health.overall.warning': 'Warning',
185
+ 'health.overall.error': 'Error',
186
+ 'health.alert.title': 'Health alert',
187
+ 'health.alert.diagnostics': 'The health check reported {status}. Review the affected items below.',
188
+ 'health.status.ok': 'Healthy',
189
+ 'health.status.warning': 'Warning',
190
+ 'health.status.error': 'Error',
191
+ 'health.status.info': 'Info',
192
+ 'health.check.session-storage': 'Session storage',
193
+ 'health.check.workspace-registry': 'Workspace registry',
194
+ 'health.check.dsh-home': 'DSH_HOME',
195
+ 'health.check.backup-storage': 'Backup storage',
196
+ 'health.check.tar': 'tar',
197
+ 'health.check.permissions': 'File permissions',
198
+ 'health.detail.session-storage.ok': 'Available, with {count} session snapshots',
199
+ 'health.detail.workspace-registry.ok': 'Available, with {count} workspaces',
200
+ 'health.detail.dsh-home.ok': 'Directory is accessible with mode {mode}',
201
+ 'health.detail.backup-storage.empty': 'Backup directory is available; no backups exist yet',
202
+ 'health.detail.backup-storage.ok': 'Backup directory is available, with {count} backups using {size}',
203
+ 'health.detail.tar.ok': 'The tar executable is available',
204
+ 'health.detail.permissions.ok': 'Permission check passed with no anomalies',
205
+ 'health.detail.permissions.warning': 'Found {count} file or directory permission anomalies',
206
+ 'health.detail.generic': '{status}',
207
+ 'tabs.overview': 'Overview',
208
+ 'tabs.health': 'Health diagnostics',
209
+ 'tabs.usage': 'Model statistics',
210
+ 'overview.container': 'Container information',
211
+ 'overview.errors': 'Error information',
212
+ 'tabs.backup': 'Backup maintenance',
213
+ 'tabs.restart': 'Restart',
214
+ 'tabs.alert.title': 'Service control alert',
215
+ 'tabs.alert.body': 'These areas need attention: {tabs}',
216
+ 'permissions.title': 'File permissions',
217
+ 'permissions.description': 'Checks whether the Agent can read, write, and enter DSH_HOME and workspaces. Deep scans skip internal .git files; repair only adds permissions needed by the current user while preserving execute bits, and keeps the DSH credential file at 600.',
218
+ 'permissions.target': 'Target owner: {owner}',
219
+ 'permissions.repair': 'Repair permissions',
220
+ 'permissions.repairing': 'Repairing…',
221
+ 'permissions.confirm': 'Confirm repair',
222
+ 'permissions.confirmHint': 'This skips .git, restores ownership to the current user, and adds Agent read/write access while preserving existing execute bits. Confirm to continue.',
223
+ 'permissions.cancel': 'Cancel',
224
+ 'permissions.error': 'Permission operation failed',
225
+ 'permissions.summary.ok': '{count} root path(s) passed the check',
226
+ 'permissions.summary.warning': '{count} root path(s) need attention',
227
+ 'permissions.showDetails': 'Show details',
228
+ 'permissions.hideDetails': 'Hide details',
229
+ 'permissions.deep': 'Deep check',
230
+ 'permissions.deepChecking': 'Scanning…',
231
+ 'permissions.deepSummary': 'Scanned {scanned} entries in {duration} ms; non-editable directories {directories}, non-editable files {files}, unreadable {unreadable}.',
232
+ 'backup.title': 'Backup management',
233
+ 'backup.description': 'Backs up sessions, configuration, and plugin profile manifests. Credentials and node_modules are excluded. Backups are never auto-pruned; you are responsible for disk usage.',
234
+ 'backup.create': 'Create backup',
235
+ 'backup.creating': 'Creating…',
236
+ 'backup.total': 'Total size: {size}',
237
+ 'backup.empty': 'No backups yet.',
238
+ 'backup.delete': 'Delete',
239
+ 'backup.confirm': 'Confirm delete',
240
+ 'backup.confirmHint': 'Delete this backup? This cannot be undone.',
241
+ 'backup.cancel': 'Cancel',
242
+ 'backup.error': 'Backup operation failed',
243
+ 'backup.import': 'Import backup',
244
+ 'backup.importing': 'Importing…',
245
+ 'backup.showRecords': 'Show backup records',
246
+ 'backup.hideRecords': 'Hide backup records',
247
+ 'version.title': 'Version information',
248
+ 'version.current': 'DSH: ',
249
+ 'version.plugin': 'dsh-service: ',
250
+ 'version.loading': 'Loading…',
251
+ 'update.check': 'Check for updates',
252
+ 'update.checking': 'Checking…',
253
+ 'update.current': 'Up to date',
254
+ 'update.available': 'New version: {version}',
255
+ 'update.unavailable': 'Latest version is temporarily unavailable',
256
+ 'update.unpublished': 'No published version is available to check',
257
+ 'health.recheck': 'Run again',
258
+ 'update.badge': 'DSH update',
259
+ 'update.details.title': 'DSH update available',
260
+ 'update.details.current': 'Current version: {version}',
261
+ 'update.details.latest': 'Latest version: {version}',
262
+ 'update.channels': 'latest: {latest} · next: {next}',
263
+ 'update.details.close': 'Close',
264
+ 'restart.title': 'Service restart',
265
+ 'restart.description': 'Restart the dsh web process. Active work will be interrupted; persisted sessions can be resumed. You can also type /restart in a conversation.',
266
+ 'restart.button': 'Restart dsh web',
267
+ 'restart.sending': 'Sending…',
268
+ 'restart.confirm': 'Confirm restart',
269
+ 'restart.force': 'Force restart',
270
+ 'restart.cancel': 'Cancel',
271
+ 'restart.sent': 'Restart request sent.',
272
+ 'restart.sentHint': 'The connection will close shortly. The page will reload automatically after recovery.',
273
+ 'restart.idleHint': 'No active work was detected. Confirm to disconnect and wait for the service to restart.',
274
+ 'activity.agent': 'Agent',
275
+ 'activity.job': 'Background job',
276
+ 'activity.terminal': 'Terminal',
277
+ 'activity.warning': 'Detected {count} active item(s). Restarting will interrupt them immediately.',
278
+ 'activity.item': '{type}: {label} ({status})',
279
+ 'error.update': 'Update check failed',
280
+ 'error.activity': 'Could not check active work',
281
+ 'error.restart': 'Restart failed',
282
+ 'error.instance': 'Restart response is missing the process instance ID',
283
+ 'usage.title': 'Model usage',
284
+ 'usage.structure': 'token structure',
285
+ 'usage.structureHint': 'Input, output, and cache token by date.',
286
+ 'usage.tooltip.date': 'Date: {date}',
287
+ 'usage.tooltip.input': 'Input {value} token',
288
+ 'usage.tooltip.output': 'Output {value} token',
289
+ 'usage.tooltip.cache': 'Cache hit {value} token',
290
+ 'usage.axis': 'token vertical axis',
291
+ 'usage.models.more': 'Show {count} more models',
292
+ 'usage.models.less': 'Collapse model list',
293
+ 'usage.refresh': 'Refresh usage',
294
+ 'usage.refreshing': 'Refreshing…',
295
+ 'usage.empty': 'No usage index yet. Select Refresh usage to build the read-only index.',
296
+ 'usage.error': 'Could not read model usage',
297
+ 'usage.allProjects': 'All projects',
298
+ 'usage.steps': 'Successful model steps',
299
+ 'usage.stepsValue': '{count} times',
300
+ 'usage.modelLine': '{steps} times · Cache hit {hitRate} · Input {input} token · Output {output} token',
301
+ 'usage.input': 'Input token',
302
+ 'usage.output': 'Output token',
303
+ 'usage.cache': 'Cache token',
304
+ 'usage.hitRate': 'Cache hit rate',
305
+ 'usage.today': 'Today',
306
+ 'usage.sevenDays': 'Last 7 days',
307
+ 'usage.missing': '{count} step(s) have no token data',
308
+ 'usage.errors.title': 'Model errors',
309
+ 'usage.errors.toggle': 'Model errors ({count} types)',
310
+ 'usage.errors.recent': 'Last 24 hours',
311
+ 'usage.errors.empty': 'No model errors were recorded in the last 24 hours.',
312
+ 'usage.toolErrors.title': 'Tool errors',
313
+ 'usage.toolErrors.toggle': 'Tool errors ({count} types)',
314
+ 'usage.toolErrors.empty': 'No tool errors were recorded in the last 24 hours.',
315
+ 'usage.errors.count': '{count} occurrence(s)',
316
+ }
317
+
318
+ const inject = ['slots', 'connection', 'timer', 'locale']
319
+
320
+ function apply(ctx) {
321
+ const { useState, useEffect } = React
322
+ const svcStyle = document.createElement('style')
323
+ svcStyle.textContent = ':root{--dsh-svc-surface-bg:#f3f4f6}body[data-ds-dark-theme]{--dsh-svc-surface-bg:#1e1e20}'
324
+ document.head.appendChild(svcStyle)
325
+ ctx.effect(() => () => svcStyle.remove(), 'dsh-service theme styles')
326
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-service dictionaries')
327
+ const t = ctx.locale.bind(NS)
328
+ const useTranslation = () => {
329
+ const [, setSnapshot] = useState(ctx.locale.getSnapshot())
330
+ useEffect(() => ctx.locale.subscribe(() => setSnapshot(ctx.locale.getSnapshot())), [])
331
+ return t
332
+ }
333
+ const recoveryListeners = new Set()
334
+ const updateListeners = new Set()
335
+ let recoveryState = { status: 'idle', elapsedMs: 0 }
336
+ let recoveryGeneration = 0
337
+ let availableUpdate = null
338
+ let updateDetailsOpen = false
339
+
340
+ const setRecoveryState = (next) => {
341
+ recoveryState = next
342
+ for (const listener of recoveryListeners) listener(next)
343
+ }
344
+
345
+ const publishUpdateState = () => {
346
+ const snapshot = { update: availableUpdate, open: updateDetailsOpen }
347
+ for (const listener of updateListeners) listener(snapshot)
348
+ }
349
+ const setAvailableUpdate = (value) => {
350
+ availableUpdate = value
351
+ if (value === null) updateDetailsOpen = false
352
+ publishUpdateState()
353
+ }
354
+ const setUpdateDetailsOpen = (open) => {
355
+ updateDetailsOpen = open === true
356
+ publishUpdateState()
357
+ }
358
+ const useUpdateState = () => {
359
+ const [snapshot, setSnapshot] = useState({ update: availableUpdate, open: updateDetailsOpen })
360
+ useEffect(() => {
361
+ updateListeners.add(setSnapshot)
362
+ setSnapshot({ update: availableUpdate, open: updateDetailsOpen })
363
+ return () => updateListeners.delete(setSnapshot)
364
+ }, [])
365
+ return snapshot
366
+ }
367
+
368
+ const useRecoveryState = () => {
369
+ const [snapshot, setSnapshot] = useState(recoveryState)
370
+ useEffect(() => {
371
+ recoveryListeners.add(setSnapshot)
372
+ setSnapshot(recoveryState)
373
+ return () => recoveryListeners.delete(setSnapshot)
374
+ }, [])
375
+ return snapshot
376
+ }
377
+
378
+ const startRecovery = async (previousInstanceId) => {
379
+ const generation = ++recoveryGeneration
380
+ let elapsedMs = 0
381
+ let delayMs = 1000
382
+ setRecoveryState({ status: 'waiting', elapsedMs })
383
+
384
+ while (elapsedMs < 60000 && generation === recoveryGeneration) {
385
+ const waitMs = Math.min(delayMs, 60000 - elapsedMs)
386
+ try {
387
+ await ctx.timer.timeout(waitMs)
388
+ } catch (_) {
389
+ return
390
+ }
391
+ if (generation !== recoveryGeneration) return
392
+
393
+ elapsedMs += waitMs
394
+ setRecoveryState({ status: 'waiting', elapsedMs })
395
+ try {
396
+ const res = await ctx.connection.rpc.call('/dsh-service', 'version', {})
397
+ const nextInstanceId = res && res.ok ? res.value && res.value.instanceId : undefined
398
+ if (typeof nextInstanceId === 'string' && nextInstanceId.length > 0 && nextInstanceId !== previousInstanceId) {
399
+ window.location.reload()
400
+ return
401
+ }
402
+ } catch (_) {}
403
+
404
+ if (elapsedMs >= 60000) {
405
+ setRecoveryState({ status: 'timeout', elapsedMs })
406
+ return
407
+ }
408
+ delayMs = Math.min(delayMs * 2, 10000)
409
+ }
410
+ }
411
+
412
+ ctx.effect(() => () => {
413
+ recoveryGeneration += 1
414
+ recoveryListeners.clear()
415
+ updateListeners.clear()
416
+ }, 'dsh-service recovery')
417
+
418
+ function UpdateBadge() {
419
+ const state = useUpdateState()
420
+ const translate = useTranslation()
421
+ if (state.update === null) return null
422
+ return React.createElement('button', {
423
+ type: 'button',
424
+ onClick: () => setUpdateDetailsOpen(true),
425
+ style: { margin: '4px', padding: '5px 8px', borderRadius: '999px', border: 0, background: '#d80', color: '#fff', cursor: 'pointer', fontSize: '11px', fontWeight: 600 },
426
+ }, translate('update.badge'))
427
+ }
428
+
429
+ function ServiceOverlay() {
430
+ const recovery = useRecoveryState()
431
+ const updateState = useUpdateState()
432
+ const translate = useTranslation()
433
+ if (recovery.status === 'idle' && !updateState.open) return null
434
+ if (recovery.status === 'idle') {
435
+ const update = updateState.update
436
+ if (update === null) return null
437
+ return React.createElement('div', {
438
+ style: { position: 'fixed', inset: 0, zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px', background: 'rgba(12, 14, 20, 0.72)', backdropFilter: 'blur(4px)', pointerEvents: 'auto' },
439
+ }, React.createElement('div', {
440
+ style: { width: 'min(420px, 100%)', padding: '24px', borderRadius: '12px', background: 'var(--dsw-alias-bg-overlay)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l2)', boxShadow: '0 18px 60px rgba(0,0,0,0.35)', textAlign: 'center' },
441
+ },
442
+ React.createElement('div', { style: { fontSize: '18px', fontWeight: 700, marginBottom: '10px' } }, translate('update.details.title')),
443
+ React.createElement('p', { style: { margin: '4px 0', fontSize: '13px' } }, translate('update.details.current', { version: update.current })),
444
+ React.createElement('p', { style: { margin: '4px 0', fontSize: '13px' } }, translate('update.details.latest', { version: update.latest })),
445
+ React.createElement('p', { style: { margin: '4px 0', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, translate('update.channels', { latest: update.tags?.latest || '—', next: update.tags?.next || '—' })),
446
+ React.createElement('button', { style: { marginTop: '16px', padding: '7px 16px', borderRadius: '6px', border: 0, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-button-contrast-fill)', cursor: 'pointer' }, onClick: () => setUpdateDetailsOpen(false) }, translate('update.details.close'))))
447
+ }
448
+
449
+ const timedOut = recovery.status === 'timeout'
450
+ return React.createElement('div', {
451
+ style: {
452
+ position: 'fixed',
453
+ inset: 0,
454
+ zIndex: 10000,
455
+ display: 'flex',
456
+ alignItems: 'center',
457
+ justifyContent: 'center',
458
+ padding: '24px',
459
+ background: 'rgba(12, 14, 20, 0.72)',
460
+ backdropFilter: 'blur(4px)',
461
+ pointerEvents: 'auto',
462
+ },
463
+ },
464
+ React.createElement('div', {
465
+ style: {
466
+ width: 'min(420px, 100%)',
467
+ padding: '24px',
468
+ borderRadius: '12px',
469
+ background: 'var(--dsw-alias-bg-overlay)',
470
+ color: 'var(--dsw-alias-label-primary)',
471
+ border: '1px solid var(--dsw-alias-border-l2)',
472
+ boxShadow: '0 18px 60px rgba(0,0,0,0.35)',
473
+ textAlign: 'center',
474
+ },
475
+ },
476
+ React.createElement('div', { style: { fontSize: '18px', fontWeight: 700, marginBottom: '10px' } },
477
+ translate(timedOut ? 'recovery.timeout.title' : 'recovery.waiting.title')),
478
+ React.createElement('p', { style: { margin: 0, fontSize: '13px', lineHeight: 1.7, color: 'var(--dsw-alias-label-secondary)' } },
479
+ timedOut
480
+ ? translate('recovery.timeout.body')
481
+ : translate('recovery.waiting.body', { seconds: Math.floor(recovery.elapsedMs / 1000) })),
482
+ timedOut
483
+ ? React.createElement('button', {
484
+ style: { marginTop: '16px', padding: '7px 16px', borderRadius: '6px', border: 0, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-button-contrast-fill)', cursor: 'pointer' },
485
+ onClick: () => window.location.reload(),
486
+ }, translate('recovery.manual'))
487
+ : null))
488
+ }
489
+
490
+ function RestartOverlay() {
491
+ return React.createElement(ServiceOverlay, null)
492
+ }
493
+
494
+ function ServicePanel() {
495
+ const translate = useTranslation()
496
+ const [health, setHealth] = useState(null)
497
+ const [healthError, setHealthError] = useState(null)
498
+ const [diagnostics, setDiagnostics] = useState(null)
499
+ const [diagnosticsBusy, setDiagnosticsBusy] = useState(false)
500
+ const [diagnosticsLoadedAt, setDiagnosticsLoadedAt] = useState(0)
501
+ const [permissions, setPermissions] = useState(null)
502
+ const [permissionConfirm, setPermissionConfirm] = useState(false)
503
+ const [permissionBusy, setPermissionBusy] = useState(false)
504
+ const [permissionError, setPermissionError] = useState(null)
505
+ const [permissionDetails, setPermissionDetails] = useState(false)
506
+ const [permissionDeep, setPermissionDeep] = useState(null)
507
+ const [permissionDeepBusy, setPermissionDeepBusy] = useState(false)
508
+ const [backups, setBackups] = useState({ items: [], totalBytes: 0 })
509
+ const [backupBusy, setBackupBusy] = useState(false)
510
+ const [backupError, setBackupError] = useState(null)
511
+ const [backupDeleteId, setBackupDeleteId] = useState(null)
512
+ const [backupImportBusy, setBackupImportBusy] = useState(false)
513
+ const [backupDetails, setBackupDetails] = useState(false)
514
+ const [version, setVersion] = useState(null)
515
+ const [pluginVersion, setPluginVersion] = useState(null)
516
+ const [updateInfo, setUpdateInfo] = useState(null)
517
+ const [updateError, setUpdateError] = useState(null)
518
+ const [usage, setUsage] = useState(null)
519
+ const [usageBusy, setUsageBusy] = useState(false)
520
+ const [usageError, setUsageError] = useState(null)
521
+ const [hoveredUsageSegment, setHoveredUsageSegment] = useState(null)
522
+ const [usageProject, setUsageProject] = useState('all')
523
+ const [modelErrorsOpen, setModelErrorsOpen] = useState(false)
524
+ const [toolErrorsOpen, setToolErrorsOpen] = useState(false)
525
+ const [modelsOpen, setModelsOpen] = useState(false)
526
+ const [activeTab, setActiveTab] = useState('overview')
527
+ // 重启状态:0=初始,1=普通确认,2=已发出,3=检测到活动工作
528
+ const [stage, setStage] = useState(0)
529
+ const [activity, setActivity] = useState(null)
530
+ const [busy, setBusy] = useState(false)
531
+ const [error, setError] = useState(null)
532
+ const usageRequestPayload = { timezoneOffsetMinutes: new Date().getTimezoneOffset() }
533
+
534
+ // 进入面板时拉取当前版本和健康快照;健康数据每 5 秒刷新,卸载即停止。
535
+ useEffect(() => {
536
+ ctx.connection.rpc.call('/dsh-service', 'version', {}).then((res) => {
537
+ if (res && res.ok) {
538
+ setVersion(res.value.current)
539
+ setPluginVersion(res.value.pluginVersion || null)
540
+ }
541
+ }).catch(() => {})
542
+ }, [])
543
+ useEffect(() => {
544
+ let active = true
545
+ ctx.connection.rpc.call('/dsh-service', 'check-update', {}).then((res) => {
546
+ if (!active || !res || res.ok === false) { if (active) setUpdateError(translate('update.unavailable')); return }
547
+ setUpdateInfo(res.value)
548
+ setUpdateError(null)
549
+ setAvailableUpdate(res.value.dsh && !res.value.dsh.upToDate ? res.value.dsh : null)
550
+ }).catch(() => { if (active) setUpdateError(translate('update.unavailable')) })
551
+ return () => { active = false }
552
+ }, [])
553
+ useEffect(() => {
554
+ let active = true
555
+ ctx.connection.rpc.call('/dsh-service', 'permissions-plan', {}).then((res) => {
556
+ if (!active) return
557
+ if (!res || res.ok === false) setPermissionError(translate('permissions.error'))
558
+ else setPermissions(res.value)
559
+ }).catch(() => {
560
+ if (active) setPermissionError(translate('permissions.error'))
561
+ })
562
+ return () => { active = false }
563
+ }, [])
564
+ useEffect(() => {
565
+ let active = true
566
+ ctx.connection.rpc.call('/dsh-service', 'usage', usageRequestPayload).then(async (res) => {
567
+ if (!active) return
568
+ if (!res || res.ok === false) { setUsageError(translate('usage.error')); return }
569
+ setUsage(res.value)
570
+ if (res.value.updatedAt > 0 && Date.now() - res.value.updatedAt <= 300000) return
571
+ try {
572
+ const refreshed = await ctx.connection.rpc.call('/dsh-service', 'usage-refresh', usageRequestPayload)
573
+ if (active && refreshed && refreshed.ok) setUsage(refreshed.value)
574
+ } catch (_) {}
575
+ }).catch(() => {
576
+ if (active) setUsageError(translate('usage.error'))
577
+ })
578
+ return () => { active = false }
579
+ }, [])
580
+ useEffect(() => {
581
+ let active = true
582
+ ctx.connection.rpc.call('/dsh-service', 'backup-list', {}).then((res) => {
583
+ if (!active) return
584
+ if (!res || res.ok === false) setBackupError(translate('backup.error'))
585
+ else setBackups(res.value)
586
+ }).catch(() => {
587
+ if (active) setBackupError(translate('backup.error'))
588
+ })
589
+ return () => { active = false }
590
+ }, [])
591
+ useEffect(() => {
592
+ let active = true
593
+ let cancelNext = () => {}
594
+ const poll = async () => {
595
+ try {
596
+ const res = await ctx.connection.rpc.call('/dsh-service', 'health', {})
597
+ if (!active) return
598
+ if (!res || res.ok === false) throw new Error(res && res.error ? res.error : 'health failed')
599
+ setHealth(res.value)
600
+ setHealthError(null)
601
+ } catch (_) {
602
+ if (!active) return
603
+ setHealthError(translate('health.error'))
604
+ }
605
+ if (active) cancelNext = ctx.timer.timeout(poll, 5000)
606
+ }
607
+ poll()
608
+ return () => {
609
+ active = false
610
+ cancelNext()
611
+ }
612
+ }, [])
613
+
614
+ const runDiagnostics = async (force = false) => {
615
+ if (!force && diagnosticsLoadedAt > 0 && Date.now() - diagnosticsLoadedAt <= 30000) return
616
+ setDiagnosticsBusy(true)
617
+ try {
618
+ const res = await ctx.connection.rpc.call('/dsh-service', 'diagnostics', {})
619
+ if (!res || res.ok === false) throw new Error('diagnostics failed')
620
+ setDiagnostics(res.value)
621
+ setDiagnosticsLoadedAt(Date.now())
622
+ } catch (_) {
623
+ setHealthError(translate('health.error'))
624
+ } finally {
625
+ setDiagnosticsBusy(false)
626
+ }
627
+ }
628
+
629
+ const refreshUsage = async () => {
630
+ setUsageBusy(true)
631
+ setUsageError(null)
632
+ try {
633
+ const res = await ctx.connection.rpc.call('/dsh-service', 'usage-refresh', usageRequestPayload)
634
+ if (!res || res.ok === false) throw new Error('usage refresh failed')
635
+ setUsage(res.value)
636
+ } catch (_) {
637
+ setUsageError(translate('usage.error'))
638
+ } finally {
639
+ setUsageBusy(false)
640
+ }
641
+ }
642
+
643
+ const deepCheckPermissions = async () => {
644
+ if (!permissions || permissions.supported !== true) return
645
+ setPermissionDeepBusy(true)
646
+ setPermissionError(null)
647
+ try {
648
+ const res = await ctx.connection.rpc.call('/dsh-service', 'permissions-deep', { planId: permissions.planId })
649
+ if (!res || res.ok === false) throw new Error('deep permission check failed')
650
+ setPermissionDeep(res.value)
651
+ } catch (_) {
652
+ setPermissionError(translate('permissions.error'))
653
+ } finally {
654
+ setPermissionDeepBusy(false)
655
+ }
656
+ }
657
+
658
+ const repairPermissions = async () => {
659
+ if (!permissions || permissions.supported !== true) return
660
+ setPermissionBusy(true)
661
+ setPermissionError(null)
662
+ try {
663
+ const res = await ctx.connection.rpc.call('/dsh-service', 'permissions-repair', { planId: permissions.planId })
664
+ if (!res || res.ok === false) throw new Error('permission repair failed')
665
+ setPermissions(res.value)
666
+ setPermissionConfirm(false)
667
+ } catch (_) {
668
+ setPermissionError(translate('permissions.error'))
669
+ } finally {
670
+ setPermissionBusy(false)
671
+ }
672
+ }
673
+
674
+ const createBackup = async () => {
675
+ setBackupBusy(true)
676
+ setBackupError(null)
677
+ try {
678
+ const res = await ctx.connection.rpc.call('/dsh-service', 'backup-create', {})
679
+ if (!res || res.ok === false) throw new Error('backup failed')
680
+ setBackups({ items: res.value.items, totalBytes: res.value.totalBytes })
681
+ } catch (_) {
682
+ setBackupError(translate('backup.error'))
683
+ } finally {
684
+ setBackupBusy(false)
685
+ }
686
+ }
687
+
688
+ const deleteBackup = async (id) => {
689
+ setBackupBusy(true)
690
+ setBackupError(null)
691
+ try {
692
+ const res = await ctx.connection.rpc.call('/dsh-service', 'backup-delete', { id })
693
+ if (!res || res.ok === false) throw new Error('backup failed')
694
+ setBackups(res.value)
695
+ setBackupDeleteId(null)
696
+ } catch (_) {
697
+ setBackupError(translate('backup.error'))
698
+ } finally {
699
+ setBackupBusy(false)
700
+ }
701
+ }
702
+
703
+ const importBackup = (event) => {
704
+ const file = event.target.files && event.target.files[0]
705
+ event.target.value = ''
706
+ if (!file) return
707
+ setBackupImportBusy(true)
708
+ setBackupError(null)
709
+ const reader = new FileReader()
710
+ reader.onload = async () => {
711
+ try {
712
+ const bytes = new Uint8Array(reader.result)
713
+ let binary = ''
714
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000))
715
+ const res = await ctx.connection.rpc.call('/dsh-service', 'backup-import', { name: file.name, data: btoa(binary) })
716
+ if (!res || res.ok === false) throw new Error('backup import failed')
717
+ setBackups(res.value)
718
+ } catch (_) {
719
+ setBackupError(translate('backup.error'))
720
+ } finally {
721
+ setBackupImportBusy(false)
722
+ }
723
+ }
724
+ reader.onerror = () => {
725
+ setBackupImportBusy(false)
726
+ setBackupError(translate('backup.error'))
727
+ }
728
+ reader.readAsArrayBuffer(file)
729
+ }
730
+
731
+ const checkRestart = async () => {
732
+ setBusy(true)
733
+ setError(null)
734
+ try {
735
+ const res = await ctx.connection.rpc.call('/dsh-service', 'activity', {})
736
+ if (res && res.ok === false) {
737
+ console.error('dsh-service: activity check failed', res.error)
738
+ throw new Error(translate('error.activity'))
739
+ }
740
+ const nextActivity = res && res.value ? res.value : { hasActive: false, items: [] }
741
+ setActivity(nextActivity)
742
+ setStage(nextActivity.hasActive ? 3 : 1)
743
+ } catch (err) {
744
+ setError(err && err.message ? String(err.message) : String(err))
745
+ setStage(0)
746
+ } finally {
747
+ setBusy(false)
748
+ }
749
+ }
750
+
751
+ const restart = async (force) => {
752
+ setBusy(true)
753
+ setError(null)
754
+ try {
755
+ const res = await ctx.connection.rpc.call('/dsh-service', 'web', { force: force === true })
756
+ if (res && res.ok === false) {
757
+ if (res.error === 'active-work' && res.value) {
758
+ setActivity(res.value)
759
+ setStage(3)
760
+ return
761
+ }
762
+ console.error('dsh-service: restart failed', res.error)
763
+ throw new Error(translate('error.restart'))
764
+ }
765
+ setStage(2)
766
+ const previousInstanceId = res && res.value ? res.value.instanceId : undefined
767
+ if (typeof previousInstanceId !== 'string' || previousInstanceId.length === 0) {
768
+ throw new Error(translate('error.instance'))
769
+ }
770
+ startRecovery(previousInstanceId).catch((err) => console.error('dsh-service: recovery failed', err))
771
+ } catch (err) {
772
+ setError(err && err.message ? String(err.message) : String(err))
773
+ setStage(0)
774
+ } finally {
775
+ setBusy(false)
776
+ }
777
+ }
778
+
779
+ // 样式
780
+ const btn = { minHeight: '32px', padding: '6px 14px', borderRadius: '7px', border: '1px solid transparent', cursor: 'pointer', fontSize: '13px', fontWeight: 550, transition: 'border-color 120ms ease, color 120ms ease, background 120ms ease', lineHeight: '20px' }
781
+ const primary = { ...btn, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-button-contrast-fill)', borderColor: 'var(--dsw-alias-brand-primary)' }
782
+ const secondary = { ...btn, background: 'transparent', color: 'var(--dsw-alias-brand-primary)', borderColor: 'var(--dsw-alias-brand-primary)' }
783
+ const neutral = { ...btn, background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', borderColor: 'var(--dsw-alias-border-l2)' }
784
+ const danger = { ...btn, background: 'var(--dsw-alias-state-error-primary)', color: '#fff', borderColor: 'var(--dsw-alias-state-error-primary)' }
785
+ const dangerGhost = { ...btn, background: 'transparent', color: 'var(--dsw-alias-state-error-primary)', borderColor: 'var(--dsw-alias-state-error-primary)' }
786
+ const ghost = { ...btn, background: 'transparent', color: 'var(--dsw-alias-label-primary)', borderColor: 'var(--dsw-alias-border-l2)' }
787
+ const toggle = Object.assign({}, btn, { background: 'transparent', color: 'var(--dsw-alias-label-primary)', border: 0, borderTop: '1px solid var(--dsw-alias-border-l1)', borderRadius: 0, padding: '10px 2px', width: '100%', textAlign: 'left', fontWeight: 600 })
788
+ const row = { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '12px', flexWrap: 'wrap' }
789
+ const hint = { color: 'var(--dsw-alias-label-secondary)', fontSize: '12px', marginTop: '8px', lineHeight: 1.5 }
790
+ const card = { padding: '4px 0 14px', marginBottom: '12px', color: 'var(--dsw-alias-label-primary)' }
791
+ const displaySurface = { background: 'var(--dsh-svc-surface-bg)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px', padding: '10px' }
792
+ const tabPanel = { padding: '14px 2px 2px', color: 'var(--dsw-alias-label-primary)' }
793
+ const inlineTab = { background: 'transparent', color: 'var(--dsw-alias-label-secondary)', border: 0, borderBottom: '2px solid transparent', padding: '8px 14px', cursor: 'pointer', fontSize: '13px', fontWeight: 550, transition: 'color 120ms, border-color 120ms' }
794
+ const inlineTabActive = { color: 'var(--dsw-alias-brand-primary)', borderBottom: '2px solid var(--dsw-alias-brand-primary)', fontWeight: 700 }
795
+ const sectionTitle = { fontSize: '14px', fontWeight: 700, margin: '0 0 8px', color: 'var(--dsw-alias-label-primary)' }
796
+
797
+ const formatSize = (bytes) => {
798
+ const value = Number(bytes)
799
+ if (value < 1024) return value + ' B'
800
+ if (value < 1024 * 1024) return (Math.round(value / 102.4) / 10) + ' KB'
801
+ if (value < 1024 * 1024 * 1024) return (Math.round(value / (1024 * 1024) * 10) / 10) + ' MB'
802
+ return (Math.round(value / (1024 * 1024 * 1024) * 10) / 10) + ' GB'
803
+ }
804
+ const formatUptime = (seconds) => {
805
+ const totalMinutes = Math.floor(Number(seconds) / 60)
806
+ return translate('health.uptimeValue', {
807
+ hours: Math.floor(totalMinutes / 60),
808
+ minutes: totalMinutes % 60,
809
+ })
810
+ }
811
+ const formatBytes = (bytes) => {
812
+ const mb = Number(bytes) / (1024 * 1024)
813
+ return (Math.round(mb * 10) / 10).toLocaleString(undefined, { maximumFractionDigits: 1 }) + ' MB'
814
+ }
815
+ const metric = (labelKey, value) => React.createElement('div', {
816
+ key: labelKey,
817
+ style: { padding: '8px 10px', borderRadius: '6px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)' },
818
+ },
819
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', marginBottom: '2px' } }, translate(labelKey)),
820
+ React.createElement('div', { style: { fontSize: '14px', fontWeight: 600 } }, value))
821
+ const dateKey = (date) => {
822
+ const digits = (value) => String(value).padStart(2, '0')
823
+ return `${date.getFullYear()}-${digits(date.getMonth() + 1)}-${digits(date.getDate())}`
824
+ }
825
+ const usageDays = []
826
+ for (let offset = 6; offset >= 0; offset -= 1) {
827
+ const date = new Date()
828
+ date.setHours(0, 0, 0, 0)
829
+ date.setDate(date.getDate() - offset)
830
+ usageDays.push({ key: dateKey(date), label: `${date.getMonth() + 1}/${date.getDate()}` })
831
+ }
832
+ const usageSegments = [
833
+ ['inputTokens', 'usage.input', '#79aaf7'],
834
+ ['outputTokens', 'usage.output', '#f1b35b'],
835
+ ['cacheTokens', 'usage.cache', '#48c7b0'],
836
+ ]
837
+ const usageTotalsFor = (day) => {
838
+ const source = usage && usage.days ? usage.days[day] : null
839
+ if (!source) return { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
840
+ if (usageProject === 'all') return source.totals
841
+ const project = source.projects.find((item) => item.id === usageProject)
842
+ return project ? project.totals : { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
843
+ }
844
+ const modelCacheHitRate = (model) => {
845
+ const denominator = Number(model.inputTokens || 0) + Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
846
+ return denominator === 0 ? 0 : Number(model.cacheReadTokens || 0) / denominator
847
+ }
848
+ const usageValue = (totals, metricName) => metricName === 'cacheTokens'
849
+ ? totals.cacheReadTokens + totals.cacheWriteTokens
850
+ : totals[metricName] || 0
851
+ const formatTokenValue = (value) => {
852
+ const number = Number(value) || 0
853
+ if (number >= 1000000) return `${Math.round(number / 100000) / 10}M`
854
+ if (number >= 1000) return `${Math.round(number / 100) / 10}K`
855
+ return number.toLocaleString()
856
+ }
857
+ const formatUsageValue = (value, metricName) => metricName === 'cacheHitRate'
858
+ ? (Number(value) * 100).toFixed(1) + '%'
859
+ : metricName === 'steps'
860
+ ? translate('usage.stepsValue', { count: Number(value).toLocaleString() })
861
+ : formatTokenValue(value)
862
+ const chartTotals = usageDays.map((day) => usageTotalsFor(day.key))
863
+ const chartValues = chartTotals.map((totals) => usageSegments.reduce((sum, [metricName]) => sum + usageValue(totals, metricName), 0))
864
+ const chartMax = Math.max(1, ...chartValues)
865
+ const chartTicks = [chartMax, chartMax * 0.75, chartMax * 0.5, chartMax * 0.25, 0]
866
+ const todayTotals = usageTotalsFor(usageDays[6].key)
867
+ const sevenTotals = usageDays.reduce((total, day) => {
868
+ const source = usageTotalsFor(day.key)
869
+ total.steps += source.steps || 0
870
+ total.missingUsage += source.missingUsage || 0
871
+ total.inputTokens += source.inputTokens || 0
872
+ total.outputTokens += source.outputTokens || 0
873
+ total.cacheReadTokens += source.cacheReadTokens || 0
874
+ total.cacheWriteTokens += source.cacheWriteTokens || 0
875
+ return total
876
+ }, { steps: 0, missingUsage: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 })
877
+ const sevenDenominator = sevenTotals.inputTokens + sevenTotals.cacheReadTokens + sevenTotals.cacheWriteTokens
878
+ sevenTotals.cacheHitRate = sevenDenominator === 0 ? 0 : sevenTotals.cacheReadTokens / sevenDenominator
879
+ const selectedProjects = usageProject === 'all'
880
+ ? (usage?.projects || []).map((project) => project.id)
881
+ : [usageProject]
882
+ const modelTotals = new Map()
883
+ for (const day of usageDays) {
884
+ const source = usage?.days?.[day.key]
885
+ if (!source) continue
886
+ for (const project of source.projects) {
887
+ if (!selectedProjects.includes(project.id)) continue
888
+ for (const model of project.models) {
889
+ const existing = modelTotals.get(model.id) || { id: model.id, steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
890
+ existing.steps += model.totals.steps || 0
891
+ existing.inputTokens += model.totals.inputTokens || 0
892
+ existing.outputTokens += model.totals.outputTokens || 0
893
+ existing.cacheReadTokens += model.totals.cacheReadTokens || 0
894
+ existing.cacheWriteTokens += model.totals.cacheWriteTokens || 0
895
+ modelTotals.set(model.id, existing)
896
+ }
897
+ }
898
+ }
899
+ const diagnosticDetail = (check) => {
900
+ const detail = String(check.detail ?? '')
901
+ if (check.id === 'session-storage' && check.status === 'ok') return translate('health.detail.session-storage.ok', { count: detail })
902
+ if (check.id === 'workspace-registry' && check.status === 'ok') return translate('health.detail.workspace-registry.ok', { count: detail })
903
+ if (check.id === 'dsh-home' && check.status === 'ok') return translate('health.detail.dsh-home.ok', { mode: detail })
904
+ if (check.id === 'backup-storage') {
905
+ const [count = '0', size = '0'] = detail.split(':')
906
+ return Number(count) === 0
907
+ ? translate('health.detail.backup-storage.empty')
908
+ : translate('health.detail.backup-storage.ok', { count, size: formatSize(size) })
909
+ }
910
+ if (check.id === 'tar' && check.status === 'ok') return translate('health.detail.tar.ok')
911
+ if (check.id === 'permissions') return translate(check.status === 'ok' ? 'health.detail.permissions.ok' : 'health.detail.permissions.warning', { count: detail || '0' })
912
+ return translate('health.detail.generic', { status: translate(`health.status.${check.status}`) })
913
+ }
914
+ const summaryItems = (totals) => [
915
+ ...usageSegments.map(([metricName, label]) => [label, formatTokenValue(usageValue(totals, metricName))]),
916
+ ['usage.steps', formatUsageValue(totals.steps, 'steps')],
917
+ ['usage.hitRate', formatUsageValue(totals.cacheHitRate, 'cacheHitRate')],
918
+ ]
919
+ const summaryBlock = (id, title, totals) => React.createElement('div', { style: { padding: '10px', borderRadius: '7px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' } },
920
+ React.createElement('div', { style: { fontSize: '12px', fontWeight: 700, marginBottom: '5px' } }, translate(title)),
921
+ summaryItems(totals).map(([label, value], index) => React.createElement('div', { key: label, 'data-testid': `usage-summary-${id}-${index}`, style: { display: 'flex', justifyContent: 'space-between', gap: '12px', padding: '4px 0', fontSize: '12px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
922
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, translate(label)),
923
+ React.createElement('span', { style: { fontWeight: 650 } }, value)))
924
+ )
925
+ const sortedModels = [...modelTotals.values()].sort((a, b) => b.steps - a.steps || a.id.localeCompare(b.id))
926
+ const visibleModels = modelsOpen ? sortedModels : sortedModels.slice(0, 3)
927
+ const hiddenModelCount = Math.max(0, sortedModels.length - 3)
928
+ const selectedErrors = (list) => (list || [])
929
+ .filter((error) => usageProject === 'all' || error.projectId === usageProject)
930
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
931
+ const modelErrors = selectedErrors(usage?.errors?.models)
932
+ const toolErrors = selectedErrors(usage?.errors?.tools)
933
+ const errorList = (kind, errors) => React.createElement('div', { key: kind, style: { display: 'grid', gap: '6px', marginTop: '8px' } },
934
+ errors.length === 0
935
+ ? React.createElement('p', { style: hint }, translate(kind === 'model' ? 'usage.errors.empty' : 'usage.toolErrors.empty'))
936
+ : errors.map((failure) => React.createElement('div', { key: `${kind}:${failure.projectId}:${failure.key}`, style: { padding: '8px 10px', borderRadius: '6px', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-state-error-primary)', color: 'var(--dsw-alias-label-primary)', fontSize: '12px' } },
937
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: '8px', fontWeight: 600 } },
938
+ React.createElement('span', null, kind === 'model'
939
+ ? `${failure.provider}/${failure.model} · ${failure.code}${failure.status === null ? '' : ` · ${failure.status}`}`
940
+ : `${failure.tool} · ${failure.code}`),
941
+ React.createElement('span', null, translate('usage.errors.count', { count: failure.count }))),
942
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '3px', overflowWrap: 'anywhere' } }, failure.message))))
943
+ const usageBlock = React.createElement('div', { key: 'usage-section', 'data-testid': 'usage-card', style: card },
944
+ React.createElement('div', { style: sectionTitle }, translate('usage.structure')),
945
+ React.createElement('p', { style: Object.assign({}, hint, { marginTop: '-4px' }) }, translate('usage.structureHint')),
946
+ usage && usage.projects.length > 0
947
+ ? React.createElement('div', { 'data-testid': 'usage-project-tabs', style: { display: 'flex', flexWrap: 'wrap', gap: '14px', marginBottom: '12px', borderBottom: '1px solid var(--dsw-alias-border-l1)' } },
948
+ React.createElement('button', { style: Object.assign({}, inlineTab, usageProject === 'all' ? inlineTabActive : { color: 'var(--dsw-alias-label-secondary)', borderBottom: '2px solid transparent' }), onClick: () => setUsageProject('all') }, translate('usage.allProjects')),
949
+ usage.projects.map((project) => React.createElement('button', { key: project.id, style: Object.assign({}, inlineTab, usageProject === project.id ? inlineTabActive : { color: 'var(--dsw-alias-label-secondary)', borderBottom: '2px solid transparent' }), onClick: () => setUsageProject(project.id) }, project.title)))
950
+ : null,
951
+ usage && usage.indexedSessions > 0
952
+ ? React.createElement('div', { 'data-testid': 'usage-statistics-region', style: Object.assign({}, displaySurface, { padding: '12px', borderRadius: '9px' }) },
953
+ React.createElement('div', { 'data-testid': 'usage-chart', style: { position: 'relative', display: 'grid', gridTemplateColumns: '42px minmax(0, 1fr)', height: '180px', padding: '12px 10px 8px', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
954
+ React.createElement('div', { 'data-testid': 'usage-y-axis', 'aria-label': translate('usage.axis'), style: { position: 'relative', height: '136px', fontSize: '10px', color: 'var(--dsw-alias-label-secondary)' } },
955
+ chartTicks.map((tick, index) => React.createElement('span', { key: index, style: { position: 'absolute', right: '7px', top: `${index * 25}%`, transform: index === 4 ? 'translateY(-100%)' : 'translateY(-50%)' } }, formatTokenValue(tick)))),
956
+ React.createElement('div', { 'data-testid': 'usage-plot', style: { position: 'relative', height: '156px' } },
957
+ React.createElement('div', { style: { position: 'absolute', inset: '0 0 20px', display: 'flex', alignItems: 'end', gap: '8px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
958
+ chartTicks.map((_, index) => React.createElement('div', { key: index, 'data-testid': `usage-grid-${index}`, style: { position: 'absolute', left: 0, right: 0, top: `${index * 25}%`, borderTop: '1px solid var(--dsw-alias-border-l1)', pointerEvents: 'none' } })),
959
+ usageDays.map((day, index) => React.createElement('div', { key: day.key, style: { position: 'relative', zIndex: 1, flex: 1, minWidth: 0, alignSelf: 'end' } },
960
+ React.createElement('div', { 'data-testid': `usage-bar-${day.key}`, style: { height: `${Math.max(2, chartValues[index] / chartMax * 136)}px`, display: 'flex', flexDirection: 'column-reverse', justifyContent: 'flex-start', borderRadius: '4px 4px 0 0', overflow: 'hidden' } },
961
+ usageSegments.map(([metricName, label, color]) => {
962
+ const value = usageValue(chartTotals[index], metricName)
963
+ const segmentHeight = chartValues[index] === 0 ? 0 : value / chartValues[index] * 100
964
+ const segmentId = `${day.key}-${metricName}`
965
+ const active = hoveredUsageSegment && hoveredUsageSegment.id === segmentId
966
+ return React.createElement('div', {
967
+ key: metricName,
968
+ 'data-testid': `usage-segment-${segmentId}`,
969
+ 'data-value': value,
970
+ onMouseEnter: (event) => setHoveredUsageSegment({ id: segmentId, date: day.key, totals: chartTotals[index], x: event.clientX, y: event.clientY }),
971
+ onMouseMove: (event) => setHoveredUsageSegment((current) => current && current.id === segmentId ? Object.assign({}, current, { x: event.clientX, y: event.clientY }) : current),
972
+ onMouseLeave: () => setHoveredUsageSegment(null),
973
+ style: { height: `${segmentHeight}%`, minHeight: value > 0 ? '2px' : 0, background: color, opacity: hoveredUsageSegment && !active ? 0.42 : 1, cursor: value > 0 ? 'pointer' : 'default', transition: 'opacity 120ms ease' },
974
+ })
975
+ }))))),
976
+ React.createElement('div', { 'data-testid': 'usage-x-axis', style: { position: 'absolute', left: 0, right: 0, bottom: '0', height: '20px', display: 'flex', gap: '8px', alignItems: 'end' } },
977
+ usageDays.map((day) => React.createElement('div', { key: day.key, style: { flex: 1, minWidth: 0, textAlign: 'center', fontSize: '10px', color: 'var(--dsw-alias-label-secondary)' } }, day.label))))),
978
+ React.createElement('div', { style: { display: 'flex', gap: '16px', flexWrap: 'wrap', marginTop: '8px', fontSize: '11px' } },
979
+ usageSegments.map(([, label, color]) => React.createElement('span', { key: label, style: { display: 'inline-flex', alignItems: 'center', gap: '5px' } },
980
+ React.createElement('span', { style: { width: '9px', height: '9px', borderRadius: '2px', background: color } }),
981
+ translate(label)))),
982
+ hoveredUsageSegment ? React.createElement('div', { 'data-testid': 'usage-tooltip', style: { position: 'fixed', left: `${hoveredUsageSegment.x + 12}px`, top: `${hoveredUsageSegment.y + 12}px`, zIndex: 1000, pointerEvents: 'none', padding: '7px 9px', borderRadius: '6px', background: 'var(--dsw-alias-bg-overlay)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l2)', boxShadow: '0 4px 12px rgba(0,0,0,0.2)', fontSize: '12px', fontWeight: 600, whiteSpace: 'pre-line', textAlign: 'left' } }, `${translate('usage.tooltip.date', { date: hoveredUsageSegment.date })}\n${translate('usage.tooltip.input', { value: Number(hoveredUsageSegment.totals.inputTokens || 0).toLocaleString() })}\n${translate('usage.tooltip.output', { value: Number(hoveredUsageSegment.totals.outputTokens || 0).toLocaleString() })}\n${translate('usage.tooltip.cache', { value: Number((hoveredUsageSegment.totals.cacheReadTokens || 0) + (hoveredUsageSegment.totals.cacheWriteTokens || 0)).toLocaleString() })}`) : null,
983
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px', marginTop: '10px' } },
984
+ summaryBlock('today', 'usage.today', todayTotals),
985
+ summaryBlock('seven', 'usage.sevenDays', sevenTotals)),
986
+ sevenTotals.missingUsage > 0 ? React.createElement('p', { style: hint }, translate('usage.missing', { count: sevenTotals.missingUsage })) : null,
987
+ React.createElement('div', { 'data-testid': 'usage-model-list', style: { marginTop: '10px', padding: '8px 10px', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l1)' } },
988
+ visibleModels.map((model, index) => React.createElement('div', { key: model.id, style: { display: 'flex', justifyContent: 'space-between', gap: '12px', fontSize: '12px', padding: '8px 2px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
989
+ React.createElement('span', null, model.id),
990
+ React.createElement('span', { style: { textAlign: 'right' } }, translate('usage.modelLine', {
991
+ steps: Number(model.steps || 0).toLocaleString(),
992
+ hitRate: formatUsageValue(modelCacheHitRate(model), 'cacheHitRate'),
993
+ input: formatTokenValue(model.inputTokens),
994
+ output: formatTokenValue(model.outputTokens),
995
+ })))),
996
+ hiddenModelCount > 0 ? React.createElement('button', { style: Object.assign({}, toggle, { borderTop: '1px solid var(--dsw-alias-border-l1)', marginTop: '2px' }), onClick: () => setModelsOpen((value) => !value) }, `${modelsOpen ? '▾' : '▸'} ${translate(modelsOpen ? 'usage.models.less' : 'usage.models.more', { count: hiddenModelCount })}`) : null))
997
+ : React.createElement('p', { style: hint }, usageError || translate('usage.empty')),
998
+ React.createElement('div', { style: row }, React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: refreshUsage, disabled: usageBusy }, translate(usageBusy ? 'usage.refreshing' : 'usage.refresh'))))
999
+
1000
+ const containerInfoBlock = React.createElement('div', { key: 'container-info', style: { marginTop: '18px' } },
1001
+ React.createElement('div', { style: sectionTitle }, translate('overview.container')),
1002
+ health
1003
+ ? React.createElement('div', { 'data-testid': 'health-display', style: Object.assign({}, displaySurface, { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px' }) },
1004
+ metric('health.uptime', formatUptime(health.uptimeSeconds)),
1005
+ metric('health.rss', formatBytes(health.rssBytes)),
1006
+ metric('health.liveSessions', String(health.liveSessions)),
1007
+ metric('health.persistedSessions', String(health.persistedSessions)),
1008
+ metric('health.activeAgents', String(health.activeAgents)),
1009
+ metric('health.activeJobs', String(health.activeJobs)))
1010
+ : React.createElement('p', { style: hint }, healthError || translate('version.loading')))
1011
+ const overviewErrorsBlock = React.createElement('div', { key: 'overview-errors', style: { marginTop: '18px' } },
1012
+ React.createElement('div', { 'data-testid': 'overview-errors-title', style: sectionTitle }, translate('overview.errors')),
1013
+ React.createElement('div', { 'data-testid': 'overview-errors-region', style: displaySurface },
1014
+ React.createElement('div', { style: Object.assign({}, hint, { margin: '-3px 0 7px' }) }, translate('usage.errors.recent')),
1015
+ React.createElement('div', { style: { display: 'grid', gap: '7px' } },
1016
+ React.createElement('div', null,
1017
+ React.createElement('button', { style: toggle, onClick: () => setModelErrorsOpen((value) => !value) }, `${modelErrorsOpen ? '▾' : '▸'} ${translate('usage.errors.toggle', { count: modelErrors.length })}`),
1018
+ modelErrorsOpen ? React.createElement('div', { style: { padding: '0 2px 8px' } }, errorList('model', modelErrors)) : null),
1019
+ React.createElement('div', null,
1020
+ React.createElement('button', { style: toggle, onClick: () => setToolErrorsOpen((value) => !value) }, `${toolErrorsOpen ? '▾' : '▸'} ${translate('usage.toolErrors.toggle', { count: toolErrors.length })}`),
1021
+ toolErrorsOpen ? React.createElement('div', { style: { padding: '0 2px 8px' } }, errorList('tool', toolErrors)) : null))))
1022
+ const healthSummaryBlock = React.createElement('div', { 'data-testid': 'health-diagnostics-region', style: displaySurface },
1023
+ React.createElement('div', { style: row }, React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: () => runDiagnostics(true), disabled: diagnosticsBusy }, translate(diagnosticsBusy ? 'health.checking' : diagnostics ? 'health.recheck' : 'health.check'))),
1024
+ diagnostics && diagnostics.status !== 'ok'
1025
+ ? React.createElement('div', { style: { marginTop: '10px', padding: '9px 11px', borderRadius: '7px', background: diagnostics.status === 'error' ? 'rgba(211,51,51,0.1)' : 'rgba(198,128,0,0.12)', border: `1px solid ${diagnostics.status === 'error' ? 'rgba(211,51,51,0.3)' : 'rgba(198,128,0,0.3)'}` } },
1026
+ React.createElement('div', { style: { fontSize: '12px', fontWeight: 700 } }, translate('health.alert.title')),
1027
+ React.createElement('div', { style: hint }, translate('health.alert.diagnostics', { status: translate(`health.overall.${diagnostics.status}`) })))
1028
+ : null,
1029
+ diagnostics
1030
+ ? React.createElement('div', { 'data-testid': 'health-check-list', style: Object.assign({}, displaySurface, { marginTop: '10px', padding: '8px 10px' }) },
1031
+ diagnostics.checks.map((check, index) => React.createElement('div', { key: check.id, style: { display: 'flex', justifyContent: 'space-between', gap: '14px', fontSize: '12px', padding: '9px 2px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
1032
+ React.createElement('span', null, translate(`health.check.${check.id}`)),
1033
+ React.createElement('span', { style: { color: check.status === 'ok' ? 'var(--dsw-alias-state-success-primary)' : check.status === 'warning' ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)', textAlign: 'right' } }, diagnosticDetail(check)))))
1034
+ : null)
1035
+
1036
+ const permissionAbnormal = permissions && permissions.supported === true
1037
+ ? permissions.items.filter((item) => item.writable === false).length
1038
+ : 0
1039
+ const permissionNeedsRepair = permissionAbnormal > 0 || (permissionDeep && (permissionDeep.ownerIssues > 0 || permissionDeep.directoryModeIssues > 0 || permissionDeep.fileModeIssues > 0 || permissionDeep.unreadable > 0))
1040
+ const permissionBlock = permissions && permissions.supported === true
1041
+ ? React.createElement('div', { key: 'permissions-section', style: { marginTop: '18px' } },
1042
+ React.createElement('div', { style: sectionTitle }, translate('permissions.title')),
1043
+ React.createElement('div', { style: Object.assign({}, displaySurface, { marginTop: '4px' }) },
1044
+ React.createElement('p', { style: hint }, translate('permissions.description')),
1045
+ permissionAbnormal > 0 ? React.createElement('div', { style: { marginTop: '8px', padding: '9px 11px', borderRadius: '7px', background: 'rgba(198,128,0,0.12)', border: '1px solid rgba(198,128,0,0.3)' } },
1046
+ React.createElement('div', { style: { fontSize: '12px', fontWeight: 700 } }, translate('health.alert.title')),
1047
+ React.createElement('div', { style: hint }, translate('permissions.summary.warning', { count: permissionAbnormal }))) : null,
1048
+ React.createElement('p', { style: Object.assign({}, hint, { fontWeight: 600, color: permissionAbnormal > 0 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-label-primary)' }) }, translate(permissionAbnormal > 0 ? 'permissions.summary.warning' : 'permissions.summary.ok', { count: permissionAbnormal > 0 ? permissionAbnormal : permissions.items.length })),
1049
+ React.createElement('p', { style: Object.assign({}, hint, { fontWeight: 600 }) }, translate('permissions.target', { owner: permissions.targetOwner })),
1050
+ React.createElement('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
1051
+ React.createElement('button', { style: neutral, onClick: () => setPermissionDetails((value) => !value) }, translate(permissionDetails ? 'permissions.hideDetails' : 'permissions.showDetails')),
1052
+ React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: deepCheckPermissions, disabled: permissionDeepBusy }, translate(permissionDeepBusy ? 'permissions.deepChecking' : 'permissions.deep'))),
1053
+ permissionDeep ? React.createElement('p', { style: hint }, translate('permissions.deepSummary', { scanned: permissionDeep.scanned, duration: permissionDeep.durationMs, owner: permissionDeep.ownerIssues, directories: permissionDeep.directoryModeIssues, files: permissionDeep.fileModeIssues, unreadable: permissionDeep.unreadable })) : null,
1054
+ permissionDetails ? React.createElement('div', { style: { display: 'grid', gap: '8px', marginTop: '8px' } },
1055
+ permissions.items.map((item) => React.createElement('div', {
1056
+ key: item.path,
1057
+ style: { padding: '9px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l1)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
1058
+ },
1059
+ React.createElement('div', { style: { fontSize: '12px', fontWeight: 600 } }, item.label),
1060
+ React.createElement('div', { style: { fontFamily: 'monospace', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', overflowWrap: 'anywhere' } }, item.path),
1061
+ React.createElement('div', { style: { fontFamily: 'monospace', fontSize: '12px', marginTop: '3px' } }, `${item.owner} · ${item.mode}`)))) : null,
1062
+ permissionConfirm
1063
+ ? React.createElement('div', { style: { marginTop: '10px' } },
1064
+ React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, translate('permissions.confirmHint')),
1065
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
1066
+ React.createElement('button', { style: danger, disabled: permissionBusy, onClick: repairPermissions }, translate(permissionBusy ? 'permissions.repairing' : 'permissions.confirm')),
1067
+ React.createElement('button', { style: ghost, disabled: permissionBusy, onClick: () => setPermissionConfirm(false) }, translate('permissions.cancel'))))
1068
+ : permissionNeedsRepair
1069
+ ? React.createElement('button', { style: Object.assign({}, dangerGhost, { marginTop: '10px' }), 'data-variant': 'danger-filled', disabled: permissionBusy, onClick: () => setPermissionConfirm(true) }, translate('permissions.repair'))
1070
+ : null,
1071
+ permissionError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, permissionError) : null))
1072
+ : null
1073
+
1074
+ const healthBlock = React.createElement('div', { key: 'health-section', 'data-testid': 'health-card', style: card },
1075
+ React.createElement('div', { style: sectionTitle }, translate('tabs.health')),
1076
+ healthSummaryBlock,
1077
+ permissionBlock)
1078
+
1079
+ const backupBlock = React.createElement('div', { key: 'backup-section' },
1080
+ React.createElement('div', { style: sectionTitle }, translate('backup.title')),
1081
+ React.createElement('div', { style: Object.assign({}, displaySurface, { marginTop: '4px' }) },
1082
+ React.createElement('p', { style: hint }, translate('backup.description')),
1083
+ React.createElement('div', { style: { display: 'flex', gap: '8px', marginTop: '12px', flexWrap: 'wrap' } },
1084
+ React.createElement('button', { style: Object.assign({}, neutral, { flex: 1, minWidth: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }), onClick: createBackup, disabled: backupBusy }, translate(backupBusy ? 'backup.creating' : 'backup.create')),
1085
+ React.createElement('label', { style: Object.assign({}, neutral, { flex: 1, minWidth: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', cursor: backupImportBusy ? 'default' : 'pointer' }) },
1086
+ translate(backupImportBusy ? 'backup.importing' : 'backup.import'),
1087
+ React.createElement('input', { type: 'file', accept: '.tar.gz,application/gzip', disabled: backupImportBusy, onChange: importBackup, style: { display: 'none' } }))),
1088
+ React.createElement('p', { style: hint }, translate('backup.total', { size: formatSize(backups.totalBytes) })),
1089
+ backupError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, backupError) : null,
1090
+ backups.items.length === 0
1091
+ ? React.createElement('p', { style: hint }, translate('backup.empty'))
1092
+ : backups.items.length > 10
1093
+ ? React.createElement('button', { style: Object.assign({}, ghost, { marginTop: '8px' }), onClick: () => setBackupDetails((value) => !value) }, translate(backupDetails ? 'backup.hideRecords' : 'backup.showRecords'))
1094
+ : null,
1095
+ backups.items.length > 0 && (backups.items.length <= 10 || backupDetails)
1096
+ ? React.createElement('div', { style: { marginTop: '10px', display: 'grid', gap: '8px' } },
1097
+ backups.items.map((item) => React.createElement('div', {
1098
+ key: item.id,
1099
+ style: { padding: '9px 10px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l1)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
1100
+ },
1101
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: '12px', alignItems: 'flex-start' } },
1102
+ React.createElement('div', null,
1103
+ React.createElement('div', { style: { fontFamily: 'monospace', fontSize: '12px', overflowWrap: 'anywhere' } }, item.name),
1104
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', marginTop: '3px' } }, `${formatSize(item.sizeBytes)} · ${new Date(item.createdAt).toLocaleString()}`)),
1105
+ React.createElement('div', { style: { display: 'flex', gap: '6px', flexShrink: 0 } },
1106
+ backupDeleteId === item.id
1107
+ ? null
1108
+ : React.createElement('button', { style: Object.assign({}, dangerGhost, { minHeight: '28px', padding: '4px 9px' }), 'data-variant': 'danger-filled', disabled: backupBusy, onClick: () => setBackupDeleteId(item.id) }, translate('backup.delete')),
1109
+ )),
1110
+ backupDeleteId === item.id
1111
+ ? React.createElement('div', { style: { marginTop: '8px' } },
1112
+ React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)', margin: '0 0 6px' }) }, translate('backup.confirmHint')),
1113
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
1114
+ React.createElement('button', { style: danger, disabled: backupBusy, onClick: () => deleteBackup(item.id) }, translate('backup.confirm')),
1115
+ React.createElement('button', { style: ghost, disabled: backupBusy, onClick: () => setBackupDeleteId(null) }, translate('backup.cancel'))))
1116
+ : null)))
1117
+ : null))
1118
+
1119
+ const versionRow = (id, label, fallbackVersion, state) => React.createElement('div', { key: id, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '16px', padding: '10px 2px', borderTop: id === 'dsh' ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
1120
+ React.createElement('div', { style: { whiteSpace: 'nowrap' } },
1121
+ React.createElement('span', { style: { fontSize: '13px', fontWeight: 650 } }, `${label} `),
1122
+ state?.url
1123
+ ? React.createElement('a', { 'data-testid': `version-${id}-link`, href: state.url, target: '_blank', rel: 'noreferrer', style: { color: 'var(--dsw-alias-label-primary)', textDecoration: 'underline', fontSize: '12px', whiteSpace: 'nowrap' } }, state.current || fallbackVersion || translate('version.loading'))
1124
+ : React.createElement('code', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)' } }, state?.current || fallbackVersion || translate('version.loading'))),
1125
+ state?.tags ? React.createElement('span', { style: { marginLeft: '8px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', whiteSpace: 'nowrap' } }, translate('update.channels', { latest: state.tags.latest || '—', next: state.tags.next || '—' })) : null,
1126
+ React.createElement('div', { style: { textAlign: 'right', fontSize: '12px' } },
1127
+ React.createElement('div', { style: { color: !state ? 'var(--dsw-alias-label-secondary)' : state.upToDate ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warn-primary)', fontWeight: 600 } }, !state
1128
+ ? (updateError || translate('update.checking'))
1129
+ : state.status === 'unpublished' ? translate('update.unpublished')
1130
+ : state.status === 'unavailable' ? translate('update.unavailable')
1131
+ : state.upToDate ? translate('update.current') : translate('update.available', { version: state.latest }))))
1132
+ // 版本信息区块
1133
+ const versionBlock = React.createElement('div', { key: 'version-card', 'data-testid': 'version-card', style: card },
1134
+ React.createElement('div', { key: 'title', style: sectionTitle }, translate('version.title')),
1135
+ React.createElement('div', { style: displaySurface },
1136
+ versionRow('dsh', 'DSH', version, updateInfo?.dsh),
1137
+ versionRow('plugin', 'dsh-service', pluginVersion, updateInfo?.plugin)))
1138
+
1139
+ // 重启后提示
1140
+ if (stage === 2) {
1141
+ return React.createElement('div', null,
1142
+ healthBlock,
1143
+ backupBlock,
1144
+ versionBlock,
1145
+ React.createElement('div', { key: 'restart-section' },
1146
+ React.createElement('div', { style: sectionTitle }, translate('restart.title')),
1147
+ React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.sent')),
1148
+ React.createElement('p', { style: hint }, translate('restart.sentHint')))
1149
+ )
1150
+ }
1151
+
1152
+ const activityLabels = {
1153
+ agent: translate('activity.agent'),
1154
+ job: translate('activity.job'),
1155
+ terminal: translate('activity.terminal'),
1156
+ }
1157
+ const activityItems = activity && Array.isArray(activity.items) ? activity.items : []
1158
+ const activityWarning = stage === 3
1159
+ ? React.createElement('div', { style: { marginTop: '12px', padding: '10px 12px', borderRadius: '6px', background: 'rgba(211,51,51,0.1)', border: '1px solid rgba(211,51,51,0.35)' } },
1160
+ React.createElement('p', { style: { margin: '0 0 8px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } },
1161
+ translate('activity.warning', { count: activityItems.length })),
1162
+ React.createElement('ul', { style: { margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: 1.7 } },
1163
+ activityItems.map((item) => React.createElement('li', { key: item.type + ':' + item.id },
1164
+ translate('activity.item', {
1165
+ type: activityLabels[item.type] || item.type,
1166
+ label: item.label,
1167
+ status: item.status,
1168
+ }))))
1169
+ )
1170
+ : null
1171
+
1172
+ // 重启按钮区块
1173
+ const restartBlock = React.createElement('div', { key: 'restart-section', 'data-testid': 'restart-card', style: card },
1174
+ React.createElement('div', { style: sectionTitle }, translate('restart.title')),
1175
+ React.createElement('div', { 'data-testid': 'restart-region', style: displaySurface },
1176
+ React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.description')),
1177
+ activityWarning,
1178
+ React.createElement('div', { style: row },
1179
+ stage === 0
1180
+ ? React.createElement('button', { style: danger, 'data-variant': 'danger', onClick: checkRestart, disabled: busy }, translate(busy ? 'update.checking' : 'restart.button'))
1181
+ : stage === 1
1182
+ ? [
1183
+ React.createElement('button', { key: 'confirm', style: danger, onClick: () => restart(false), disabled: busy }, translate(busy ? 'restart.sending' : 'restart.confirm')),
1184
+ React.createElement('button', { key: 'cancel', style: ghost, onClick: () => { setActivity(null); setStage(0) }, disabled: busy }, translate('restart.cancel')),
1185
+ ]
1186
+ : stage === 3
1187
+ ? [
1188
+ React.createElement('button', { key: 'force', style: danger, onClick: () => restart(true), disabled: busy }, translate(busy ? 'restart.sending' : 'restart.force')),
1189
+ React.createElement('button', { key: 'cancel', style: ghost, onClick: () => { setActivity(null); setStage(0) }, disabled: busy }, translate('restart.cancel')),
1190
+ ]
1191
+ : null
1192
+ ),
1193
+ stage === 1 ? React.createElement('p', { style: hint }, translate('restart.idleHint')) : null,
1194
+ error ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, String(error)) : null)
1195
+ )
1196
+
1197
+ const overviewBlock = React.createElement('div', null, versionBlock, containerInfoBlock, overviewErrorsBlock)
1198
+ const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
1199
+ const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || (check.status === 'warning' && !(check.id === 'backup-storage' && String(check.detail || '').startsWith('0:')))) === true
1200
+ const tabWarnings = {
1201
+ overview: false,
1202
+ health: Boolean(healthError || permissionError || diagnosticFailure || permissionAbnormal > 0),
1203
+ usage: Boolean(usageError),
1204
+ backup: Boolean(backupError),
1205
+ restart: Boolean(error),
1206
+ }
1207
+ const tabs = [
1208
+ ['overview', 'tabs.overview'],
1209
+ ['health', 'tabs.health'],
1210
+ ['usage', 'tabs.usage'],
1211
+ ['backup', 'tabs.backup'],
1212
+ ['restart', 'tabs.restart'],
1213
+ ]
1214
+ const warningTabs = tabs.filter(([id]) => tabWarnings[id]).map(([, label]) => translate(label))
1215
+ const tabContent = activeTab === 'overview'
1216
+ ? overviewBlock
1217
+ : activeTab === 'health'
1218
+ ? healthBlock
1219
+ : activeTab === 'usage'
1220
+ ? usageBlock
1221
+ : activeTab === 'backup'
1222
+ ? maintenanceBlock
1223
+ : restartBlock
1224
+ return React.createElement('div', null,
1225
+ warningTabs.length > 0 ? React.createElement('div', { style: { marginBottom: '12px', padding: '11px 13px', borderRadius: '8px', background: 'rgba(198,128,0,0.16)', border: '1px solid rgba(198,128,0,0.48)', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' } },
1226
+ React.createElement('div', { style: { fontSize: '13px', fontWeight: 700 } }, translate('tabs.alert.title')),
1227
+ React.createElement('div', { style: Object.assign({}, hint, { marginTop: '3px' }) }, translate('tabs.alert.body', { tabs: warningTabs.join('、') }))) : null,
1228
+ React.createElement('div', { 'data-testid': 'tab-list', style: { display: 'flex', gap: '18px', flexWrap: 'wrap', borderBottom: '1px solid var(--dsw-alias-border-l1)' } },
1229
+ tabs.map(([id, label]) => React.createElement('button', { key: id, style: Object.assign({}, inlineTab, activeTab === id ? inlineTabActive : { color: tabWarnings[id] ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-label-secondary)', borderBottom: '2px solid transparent' }), onClick: () => { setActiveTab(id); if (id === 'health') runDiagnostics(false) } }, `${tabWarnings[id] ? '⚠ ' : ''}${translate(label)}`))),
1230
+ React.createElement('div', { 'data-testid': 'tab-panel', style: tabPanel }, tabContent))
1231
+ }
1232
+
1233
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(
1234
+ { name: 'sidebar.footer.action', id: 'dsh-service-update', order: 90, label: () => t('update.badge') },
1235
+ () => React.createElement(UpdateBadge, null),
1236
+ ))
1237
+ ctx.slots.inject('settings.section', () => ctx.slots.register(
1238
+ { name: 'settings.section', id: 'dsh-service', order: 99, label: () => t('nav.label') },
1239
+ () => React.createElement(ServicePanel, null),
1240
+ ))
1241
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register(
1242
+ { name: 'shell.overlay', id: 'dsh-service-restart', order: 100, label: () => t('overlay.label') },
1243
+ () => React.createElement(RestartOverlay, null),
1244
+ ))
1245
+ }
1246
+
1247
+ exports.inject = inject
1248
+ exports.apply = apply
1249
+ return module.exports
1250
+ },
1251
+ })