dsh-token-use 0.3.1 → 0.3.3
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/README.en.md +11 -5
- package/README.md +11 -5
- package/assets/tab-config.jpg +0 -0
- package/assets/tab-models.jpg +0 -0
- package/assets/tab-overview.jpg +0 -0
- package/assets/tab-projects.jpg +0 -0
- package/assets/tab-selfcheck.jpg +0 -0
- package/client/client.js +282 -48
- package/lib/index.js +31 -3
- package/lib/power.js +127 -0
- package/lib/versions.js +60 -75
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -13,7 +13,7 @@ Four tabs across the top of the panel: **Overview / By model / By project / Conf
|
|
|
13
13
|
- **Overview** — the cost and totals cards, the trend chart, and a **usage heatmap** (last 12 months, one cell per day, coloured by total tokens or by cost; hovering shows that day's tokens, cost and calls **instantly**, without the native tooltip's one-second delay). Range and model filters apply here.
|
|
14
14
|
- **By model** — every model's usage and amount, with the same range query (day / month / year / date range / all).
|
|
15
15
|
- **By project** — every project's usage and amount, same range query.
|
|
16
|
-
- **Configuration** — versions (`dsh-service`, `dsh`, `dsh-base` and this plugin: installed vs the newest release **in the channel you run**, rc/alpha/stable), runtime facts (platform version, Node version, PID, port, uptime, `DSH_HOME`), a **health check
|
|
16
|
+
- **Configuration** — versions (`dsh-service`, `dsh`, `dsh-base` and this plugin: installed vs the newest release **in the channel you run**, rc/alpha/stable), runtime facts (platform version, Node version, PID, port, uptime, `DSH_HOME`), a **health check**, a confirmed **one-click restart** and a confirmed **shutdown**.
|
|
17
17
|
- **Self-check** (the health check button) reports three sections: **endpoint** (latency, version, history scan, price data, buckets, last update), **client bundles** (each plugin probed at the exact revisioned URL from this page's boot manifest, i.e. what the browser really loads) and **harness** (profile integrity — `node_modules`, lockfile, boot graph; plugin package integrity — entry file, `dsh.bundle.patch`, client bundle; dependency resolution; single cordis instance at one version; service bundle vs the running CLI). When a freshly installed plugin refuses to start, this names the package, the missing file and the fix.
|
|
18
18
|
- **Cost card** — spend estimated live from DeepSeek's published prices × the usage recorded here, with peak/off-peak rates. Every card (totals, trend, by model, by project) carries an amount.
|
|
19
19
|
- **Daily price book** — the official pricing page is fetched once a day at **12:00** and cached locally, so restarts keep it and an offline machine falls back to a built-in snapshot.
|
|
@@ -25,9 +25,13 @@ Four tabs across the top of the panel: **Overview / By model / By project / Conf
|
|
|
25
25
|
- **Unit switch** — 亿 / 万 / 千 in Chinese, B / M / K in English; remembered per browser. The tab bar and the unit switch stay pinned at the top, and the tab you were on is remembered.
|
|
26
26
|
- **Detail tables** — by model and by project (the day dimension lives in the heatmap), each with an amount column (hover a row for its rates) and a total column.
|
|
27
27
|
|
|
28
|
-
### One-click restart
|
|
28
|
+
### One-click restart / shutdown
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Both actions at the bottom of the configuration tab share one **full-screen transition**: a power ring with phase copy (preparing → stopping the process → recovering → new instance ready), drawn over the whole page without disturbing the rest of the host.
|
|
31
|
+
|
|
32
|
+
- **One-click restart** — restarts the dsh web process serving the page. It replays the current command line (`process.argv` plus the working directory), so custom ports and flags survive; it asks for confirmation first, and the page reconnects and reloads itself about 10-30 seconds later. The script lands in `$DSH_HOME/dsh-token-use/restart.sh` and its output in `restart.log`.
|
|
33
|
+
- **Shutdown** — stops that process and does **not** bring it back; the transition ends on "you can close this page now", and you start `dsh web` again from a terminal when you need it. Its confirm button is a solid red danger button so it cannot be hit by accident.
|
|
34
|
+
- When a launch cannot be replayed (no executable entry point), the restart button is disabled with a reason instead.
|
|
31
35
|
|
|
32
36
|
## The amounts are estimates
|
|
33
37
|
|
|
@@ -78,7 +82,7 @@ Every project's usage and amount, same range query.
|
|
|
78
82
|
|
|
79
83
|
### Configuration
|
|
80
84
|
|
|
81
|
-
Versions (compared inside your release channel), runtime facts, the health check
|
|
85
|
+
Versions (compared inside your release channel), runtime facts, the health check, the one-click restart and the shutdown — the last two ask for confirmation and run a full-screen transition.
|
|
82
86
|
|
|
83
87
|

|
|
84
88
|
|
|
@@ -104,7 +108,7 @@ dsh plugin --profile web add /path/to/dsh-token-use
|
|
|
104
108
|
- The host side **never polls and sets no periodic timer** other than the daily price refresh: it folds usage in O(1) increments from the `session/event` bus (one dictionary addition and one multiplication per `assistant/message`).
|
|
105
109
|
- History is rebuilt **once** at boot by streaming `$DSH_HOME/sessions/**/session.jsonl.zstd` (native zstd from `node:zlib`), yielding the event loop between files (`scheduler.yield()`) so session handling is never blocked. A per-session sequence watermark de-duplicates the rebuild against live events, whatever order they arrive in.
|
|
106
110
|
- **Price refresh**: one HTTPS GET per day at 12:00 local time (15 s timeout, retried an hour later on failure), stored atomically in `$DSH_HOME/dsh-token-use/pricing.json` (up to 30 snapshots). A restart reads the cache synchronously and does not re-fetch; a failed fetch falls back to the built-in snapshot, so the panel always shows numbers.
|
|
107
|
-
-
|
|
111
|
+
- Five routes are exposed (loopback only, `no-store`): `GET /dsh-token-use` for the snapshot, `GET /dsh-token-use/config` for versions and runtime facts, `GET /dsh-token-use/health` for the harness self-check, `POST /dsh-token-use/restart` for the confirmed restart and `POST /dsh-token-use/shutdown` for the confirmed shutdown (neither POST answers a GET).
|
|
108
112
|
- **How the self-check probes**: client bundles are fetched at the revisioned URL from this page's boot manifest (a plugin URL embeds a build revision, so only the manifest can address it); the harness walk reads the profile directory, resolves each real entry file through `exports["."]` / `exports["./client"]` / `main`, and walks up from every plugin directory to spot duplicate `@deepseek-ai/cordis` copies — all read-only, never loading or executing the plugins it inspects. The report carries structured fields plus paths and versions; the panel renders the wording in your language.
|
|
109
113
|
- **Version checks** read the registry's abbreviated packument (`versions` + `dist-tags`) and compare against the highest release **in the channel you run** (rc / alpha / stable) — `@deepseek-ai/dsh-web-app` keeps a stale `0.0.1-rc.1` on its `latest` tag, so a plain `latest` comparison would report a false upgrade. The answer is cached for 6 hours in `$DSH_HOME/dsh-token-use/versions.json`.
|
|
110
114
|
|
|
@@ -124,6 +128,8 @@ curl http://127.0.0.1:3080/dsh-token-use/config
|
|
|
124
128
|
curl http://127.0.0.1:3080/dsh-token-use/health
|
|
125
129
|
# restart the service (the configuration tab's button; degrades after 2 s and replays the same command)
|
|
126
130
|
curl -X POST http://127.0.0.1:3080/dsh-token-use/restart
|
|
131
|
+
# shut it down (the configuration tab's button; stops and does not come back)
|
|
132
|
+
curl -X POST http://127.0.0.1:3080/dsh-token-use/shutdown
|
|
127
133
|
```
|
|
128
134
|
|
|
129
135
|
Every bucket carries a `cost` (the estimate); `pricing` holds the current price book, when it was fetched and when it refreshes next; `modelPricing` maps each observed model to its rates or to the reason it carries no amount. `trend` spans the last 366 days — the chart takes the last 7/30/90 of it and the heatmap takes the whole run.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ DeepSeek Harness 实时 Token 用量与消费金额插件:安装后在 **设
|
|
|
13
13
|
- **概览**:金额与总量卡片、用量趋势图、**使用热力图**(近 12 个月,一格一天,可按「总计 / 金额」着色;悬停**即时**弹出当天「总计 / 金额 / 调用次数」,无原生 tooltip 的 1 秒延迟)。支持范围与模型筛选。
|
|
14
14
|
- **模型统计**:所有模型的用量与金额明细表,支持范围查询(按天 / 按月 / 按年 / 时间段 / 全部)。
|
|
15
15
|
- **项目统计**:所有项目的用量与金额明细表,同样支持范围查询。
|
|
16
|
-
- **配置信息**:版本信息(`dsh-service`、`dsh`、`dsh-base`、本插件各自的已安装版本 vs 该版本**通道**(rc/alpha/stable)的最新版)、运行环境(平台版本、Node 版本、进程 PID、监听端口、已运行时长、`DSH_HOME
|
|
16
|
+
- **配置信息**:版本信息(`dsh-service`、`dsh`、`dsh-base`、本插件各自的已安装版本 vs 该版本**通道**(rc/alpha/stable)的最新版)、运行环境(平台版本、Node 版本、进程 PID、监听端口、已运行时长、`DSH_HOME`),以及**健康检查**、**一键重启**与**关机**(后两者都带二次确认)。
|
|
17
17
|
- **健康检查(自检)**:分三段报告——**接口**(响应耗时、接口版本、历史扫描、定价数据、数据桶、数据更新时间)、**客户端 bundle**(逐个插件探测浏览器实际能否加载,用的是当前页面启动清单里带版本号的真实 URL)、**Harness 自检**(profile 完整性:`node_modules` / 锁文件 / 启动图;插件包完整性:入口文件、`dsh.bundle.patch`、前端 bundle 是否齐全;依赖解析;cordis 是否单实例同版本;服务端 bundle 与正在运行的 CLI 版本是否一致)。装完插件起不来时,这里会直接指出是哪个包、缺什么文件、怎么修。
|
|
18
18
|
- **金额卡片**:按 DeepSeek 官方定价 × 本地用量实时估算消费金额(含高峰/空闲时段倍率),每张卡片(总量、趋势、按模型、按项目)都带金额字段。
|
|
19
19
|
- **官方定价自动更新**:每天 **12:00** 抓取一次官方定价页并缓存到本地,重启不丢、断网可用内置快照兜底。
|
|
@@ -25,9 +25,13 @@ DeepSeek Harness 实时 Token 用量与消费金额插件:安装后在 **设
|
|
|
25
25
|
- **单位切换**:中文(亿 / 万 / 千)与英文(B / M / K)一键切换,记忆选择;tab 与单位切换栏常驻顶部,每个 tab 记忆上次停留位置。
|
|
26
26
|
- **明细表**:按模型、按项目两张表(按日期维度由热力图承担),含金额列(悬停显示该模型单价)与总计列。
|
|
27
27
|
|
|
28
|
-
### 一键重启
|
|
28
|
+
### 一键重启 / 关机
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
「配置信息」底部的两个动作都走同一个**全屏过渡**:电源环 + 阶段文案(准备 → 正在结束进程 → 恢复中 → 新实例已就绪),占满页面、不遮挡宿主其它区域。
|
|
31
|
+
|
|
32
|
+
- **一键重启**:重启正在提供页面的 dsh web 进程。它按当前进程的启动命令(`process.argv` + 工作目录)原样重新拉起,因此自定义端口、参数都能保持;点击后弹窗二次确认,页面断开约 10-30 秒,恢复后自动重新加载。脚本写在 `$DSH_HOME/dsh-token-use/restart.sh`,日志在 `$DSH_HOME/dsh-token-use/restart.log`。
|
|
33
|
+
- **关机**:停止该进程且**不会自动拉起**,过渡结束时提示「可以关闭此页面了」,需要时在终端重新运行 `dsh web`。确认按钮是红色实心,避免误点。
|
|
34
|
+
- 若启动方式无法原样重放(拿不到可执行的入口),重启按钮会禁用并说明原因。
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
## 金额是估算值
|
|
@@ -79,7 +83,7 @@ DeepSeek Harness 实时 Token 用量与消费金额插件:安装后在 **设
|
|
|
79
83
|
|
|
80
84
|
### 配置信息
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
版本信息(按通道对比最新版)、运行环境、健康检查、一键重启与关机(均带二次确认,动作时有全屏过渡动画)。
|
|
83
87
|
|
|
84
88
|

|
|
85
89
|
|
|
@@ -105,7 +109,7 @@ dsh plugin --profile web add /解压路径/dsh-token-use
|
|
|
105
109
|
- 宿主侧 **不轮询、不写盘、不加定时器**(除了每天一次定价刷新):通过 `session/event` 事件总线做 O(1) 增量累加(每条 `assistant/message` 一次字典加法 + 一次金额乘法)。
|
|
106
110
|
- 启动时做 **一次性**历史重建:流式解压 `$DSH_HOME/sessions/**/session.jsonl.zstd`(`node:zlib` 原生 zstd),每读一个文件主动让出事件循环(`scheduler.yield()`),不阻塞会话处理;重建与实时事件用「会话 seq 水位」去重,任意先后顺序都不会重复计数。
|
|
107
111
|
- **定价刷新**:每天 12:00 一次 HTTPS GET(15 秒超时,失败 1 小时后重试),结果写入 `$DSH_HOME/dsh-token-use/pricing.json`(原子替换,最多保留 30 份快照);进程重启时同步读缓存,不重复联网;联网失败自动退回内置快照,面板照常显示。
|
|
108
|
-
-
|
|
112
|
+
- 只暴露五个只读/受控接口(仅回环可访问,内存快照,`no-store`):`GET /dsh-token-use` 数据快照、`GET /dsh-token-use/config` 版本与运行环境、`GET /dsh-token-use/health` Harness 自检、`POST /dsh-token-use/restart` 重启服务、`POST /dsh-token-use/shutdown` 关机(后两个都需前端二次确认,且不接受 GET)。
|
|
109
113
|
- **自检怎么探**:客户端 bundle 用当前页面启动清单里的 `rev` 拉真实 URL(插件 URL 带构建版本号,只有启动清单拼得对);Harness 自检读 profile 目录、按 `exports["."]` / `exports["./client"]` / `main` 找真实入口、从各插件目录向上解析 `@deepseek-ai/cordis` 判断是否重复——全程只读,不加载也不执行被检查的插件。报告里只有结构化字段与路径/版本,文案由前端按语言渲染。
|
|
110
114
|
- **版本检查**:`/config` 读 npm registry 的精简 packument(`versions` + `dist-tags`),按**当前版本所属通道**(rc / alpha / stable)取该通道最高版来对比——`@deepseek-ai/dsh-web-app` 的 `latest` 标签长期停在 `0.0.1-rc.1`,按通道比较才不会误报。结果缓存 6 小时,写在 `$DSH_HOME/dsh-token-use/versions.json`。
|
|
111
115
|
|
|
@@ -125,6 +129,8 @@ curl http://127.0.0.1:3080/dsh-token-use/config
|
|
|
125
129
|
curl http://127.0.0.1:3080/dsh-token-use/health
|
|
126
130
|
# 重启服务(配置信息 tab 的按钮;2 秒后断开并原样拉起)
|
|
127
131
|
curl -X POST http://127.0.0.1:3080/dsh-token-use/restart
|
|
132
|
+
# 关机(配置信息 tab 的按钮;停掉且不再拉起)
|
|
133
|
+
curl -X POST http://127.0.0.1:3080/dsh-token-use/shutdown
|
|
128
134
|
```
|
|
129
135
|
|
|
130
136
|
响应里每个桶都带 `cost`(估算金额),`pricing` 是当前价目表、抓取时间与下次刷新时间,`modelPricing` 是各模型匹配到的单价或未计价原因;`trend` 覆盖最近 366 天(趋势图取尾部 7/30/90 天,热力图用整段)。
|
package/assets/tab-config.jpg
CHANGED
|
Binary file
|
package/assets/tab-models.jpg
CHANGED
|
Binary file
|
package/assets/tab-overview.jpg
CHANGED
|
Binary file
|
package/assets/tab-projects.jpg
CHANGED
|
Binary file
|
package/assets/tab-selfcheck.jpg
CHANGED
|
Binary file
|
package/client/client.js
CHANGED
|
@@ -40,8 +40,31 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
40
40
|
} catch (error) {
|
|
41
41
|
ui = {};
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* `react-dom` is one of the shell's static modules, so the power overlay can be
|
|
45
|
+
* portalled straight into `document.body`. Without it the overlay still renders
|
|
46
|
+
* (position: fixed) — just inside the settings dialog's DOM.
|
|
47
|
+
*/
|
|
48
|
+
var reactDom = null;
|
|
49
|
+
try {
|
|
50
|
+
reactDom = require("react-dom");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
reactDom = null;
|
|
53
|
+
}
|
|
54
|
+
var portalTo = reactDom !== null && typeof reactDom.createPortal === "function"
|
|
55
|
+
? function (node) { return reactDom.createPortal(node, document.body); }
|
|
56
|
+
: null;
|
|
57
|
+
/**
|
|
58
|
+
* React takes more than a plain function as an element type: the shell wraps
|
|
59
|
+
* some atoms (Button) in `memo`/`forwardRef`, which are objects. Gating on
|
|
60
|
+
* "function" quietly dropped those to the fallback, so the gate is "anything
|
|
61
|
+
* React can render".
|
|
62
|
+
*/
|
|
63
|
+
var renderable = function (value) {
|
|
64
|
+
return typeof value === "function" || (typeof value === "object" && value !== null);
|
|
65
|
+
};
|
|
43
66
|
var component = function (name, fallback) {
|
|
44
|
-
return
|
|
67
|
+
return renderable(ui[name]) ? ui[name] : fallback;
|
|
45
68
|
};
|
|
46
69
|
/**
|
|
47
70
|
* Tooltip is the right layer for the ⓘ: it positions in viewport coordinates,
|
|
@@ -53,19 +76,25 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
53
76
|
var stateDot = component("StateDot", null);
|
|
54
77
|
var menu = component("Menu", null);
|
|
55
78
|
var modal = component("Modal", null);
|
|
79
|
+
/**
|
|
80
|
+
* Icon atoms are named `Icon<Name>Outline<Weight>` — `Regular` is the 1px
|
|
81
|
+
* stroke weight, `Medium` the 1.3px one. Each artwork already defaults to the
|
|
82
|
+
* box size its context wants (16px for controls, 14px for inline marks), so no
|
|
83
|
+
* size prop is passed.
|
|
84
|
+
*/
|
|
56
85
|
var icon = function (name) {
|
|
57
|
-
return
|
|
86
|
+
return renderable(ui[name]) ? ui[name] : null;
|
|
58
87
|
};
|
|
59
|
-
var iconQuestion = icon("
|
|
60
|
-
var iconChevron = icon("
|
|
61
|
-
var iconRefresh = icon("
|
|
62
|
-
var iconGauge = icon("
|
|
63
|
-
var iconTrend = icon("
|
|
64
|
-
var iconCheck = icon("
|
|
65
|
-
var iconWarn = icon("
|
|
66
|
-
var iconClock = icon("
|
|
67
|
-
var iconLoading = icon("
|
|
68
|
-
var iconApi = icon("
|
|
88
|
+
var iconQuestion = icon("IconQuestionOutlineRegular");
|
|
89
|
+
var iconChevron = icon("IconChevronDownOutlineRegular");
|
|
90
|
+
var iconRefresh = icon("IconRefreshOutlineRegular");
|
|
91
|
+
var iconGauge = icon("IconGaugeOutlineRegular");
|
|
92
|
+
var iconTrend = icon("IconDataOutlineRegular");
|
|
93
|
+
var iconCheck = icon("IconCheckOutlineRegular");
|
|
94
|
+
var iconWarn = icon("IconWarningOutlineRegular");
|
|
95
|
+
var iconClock = icon("IconClockOutlineRegular");
|
|
96
|
+
var iconLoading = icon("IconLoadingOutlineRegular");
|
|
97
|
+
var iconApi = icon("IconApiOutlineRegular");
|
|
69
98
|
/** Chip and Button degrade into styled native controls on an older shell. */
|
|
70
99
|
var chip = component("Pill", function (props) {
|
|
71
100
|
return h("button", { type: "button", className: "dshtu_seg", "data-on": props.active === true, onClick: props.onClick, disabled: props.disabled === true }, props.children);
|
|
@@ -164,6 +193,25 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
164
193
|
versionChecked: "检查于 {time}",
|
|
165
194
|
versionRefresh: "重新检查",
|
|
166
195
|
versionRefreshing: "检查中…",
|
|
196
|
+
versionFailed: "本次检查未成功:{names}(显示的是上次结果)",
|
|
197
|
+
versionFailedTitle: "失败原因:{reason}",
|
|
198
|
+
shutdown: "关机",
|
|
199
|
+
shutdownTitle: "关闭 dsh web 服务?",
|
|
200
|
+
shutdownBody: "将停止正在提供本页面的 dsh web 进程(PID {pid}),且不会自动重启。关闭后本页面将无法使用,需要时请在终端重新运行 `dsh web`。",
|
|
201
|
+
shutdownConfirm: "确认关机",
|
|
202
|
+
shutdownFailed: "关机失败:{error}",
|
|
203
|
+
cancel: "取消",
|
|
204
|
+
powerRestartTitle: "正在重启 DeepSeek Harness…",
|
|
205
|
+
powerShutdownTitle: "正在关机 DeepSeek Harness…",
|
|
206
|
+
powerPrepareRestart: "正在准备重启…",
|
|
207
|
+
powerPrepareShutdown: "正在准备关机…",
|
|
208
|
+
powerStopping: "正在结束进程,即将断开连接",
|
|
209
|
+
powerRecovering: "正在恢复…",
|
|
210
|
+
powerReloaded: "新实例已就绪,正在刷新页面",
|
|
211
|
+
powerStopped: "服务已关闭",
|
|
212
|
+
powerStoppedNote: "可以关闭此页面了;需要时请在终端重新运行 `dsh web` 启动服务。",
|
|
213
|
+
powerTimeout: "重启超时,请手动刷新页面",
|
|
214
|
+
powerTimeoutNote: "服务未在预期时间内恢复,可能需要手动启动。日志:{log}",
|
|
167
215
|
healthCheck: "健康检查",
|
|
168
216
|
healthRunning: "检查中…",
|
|
169
217
|
healthOk: "接口正常",
|
|
@@ -324,6 +372,25 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
324
372
|
versionChecked: "Checked {time}",
|
|
325
373
|
versionRefresh: "Check again",
|
|
326
374
|
versionRefreshing: "Checking…",
|
|
375
|
+
versionFailed: "This check failed for {names} — showing the last successful result",
|
|
376
|
+
versionFailedTitle: "Reason: {reason}",
|
|
377
|
+
shutdown: "Shut down",
|
|
378
|
+
shutdownTitle: "Shut down the dsh web service?",
|
|
379
|
+
shutdownBody: "This stops the dsh web process serving this page (PID {pid}) and will not start it again. The page stops working until you run `dsh web` in a terminal.",
|
|
380
|
+
shutdownConfirm: "Shut down",
|
|
381
|
+
shutdownFailed: "Shutdown failed: {error}",
|
|
382
|
+
cancel: "Cancel",
|
|
383
|
+
powerRestartTitle: "Restarting DeepSeek Harness…",
|
|
384
|
+
powerShutdownTitle: "Shutting DeepSeek Harness down…",
|
|
385
|
+
powerPrepareRestart: "Preparing to restart…",
|
|
386
|
+
powerPrepareShutdown: "Preparing to shut down…",
|
|
387
|
+
powerStopping: "Stopping the process — the connection is about to drop",
|
|
388
|
+
powerRecovering: "Coming back…",
|
|
389
|
+
powerReloaded: "The new instance is up — reloading",
|
|
390
|
+
powerStopped: "The service is down",
|
|
391
|
+
powerStoppedNote: "You can close this page; run `dsh web` in a terminal when you need it again.",
|
|
392
|
+
powerTimeout: "The restart timed out — reload the page manually",
|
|
393
|
+
powerTimeoutNote: "The service did not come back in time and may need to be started by hand. Log: {log}",
|
|
327
394
|
healthCheck: "Health check",
|
|
328
395
|
healthRunning: "Checking…",
|
|
329
396
|
healthOk: "Endpoint healthy",
|
|
@@ -401,7 +468,7 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
401
468
|
hintServiceVersion: "a service bundle older or newer than the running CLI is a common cause of failed plugin startups"
|
|
402
469
|
};
|
|
403
470
|
|
|
404
|
-
var css = ".dshtu_wrap{max-width:860px;display:flex;flex-direction:column;gap:14px;padding:2px 2px 4px;color:var(--dsw-alias-label-primary);font-size:13px}.dshtu_card{background:var(--dsw-alias-bg-layer-3);border-radius:14px;box-shadow:var(--dsw-elevation-stroke);padding:14px 16px}.dshtu_bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dshtu_bar label{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_bar .dshtu_select,.dshtu_bar .dshtu_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:4px 8px;font-size:12.5px}.dshtu_bar .dshtu_select{width:168px;text-overflow:ellipsis;white-space:nowrap}.dshtu_chips{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.dshtu_subBar{margin-top:12px}.dshtu_pick{display:inline-flex;align-items:center;gap:4px;max-width:230px}.dshtu_pickLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dshtu_metaIcon{display:inline-flex;align-items:center;margin-right:5px;vertical-align:-2px;color:var(--dsw-alias-label-tertiary)}.dshtu_cardTitle{display:inline-flex;align-items:center}.dshtu_foot{display:flex;align-items:center;gap:6px}.dshtu_statLabel{display:inline-flex;align-items:center;gap:3px;white-space:nowrap}.dshtu_field{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.dshtu_seg{display:inline-flex;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;overflow:hidden}.dshtu_seg button{border:0;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;padding:4px 10px;font-size:12.5px}.dshtu_seg button[data-on=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dshtu_totalsMeta{margin-top:12px}.dshtu_grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:8px}.dshtu_stat{background:var(--dsw-alias-bg-module-platform);border-radius:10px;padding:10px 12px}.dshtu_stat b{display:block;font-size:16px;line-height:24px;font-variant-numeric:tabular-nums}.dshtu_stat span{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_stat[data-main=true]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,var(--dsw-alias-bg-module-platform))}.dshtu_stat[data-main=true] b{color:var(--dsw-alias-state-business-primary);font-size:20px}.dshtu_table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.dshtu_table th,.dshtu_table td{text-align:right;padding:6px 8px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:400}.dshtu_table th:first-child,.dshtu_table td:first-child{text-align:left}.dshtu_table th{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dshtu_table td:first-child{overflow-wrap:anywhere;max-width:300px;color:var(--dsw-alias-label-secondary)}.dshtu_meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.dshtu_err{color:var(--dsw-alias-state-error-primary);display:flex;align-items:center;gap:10px}.dshtu_chart{width:100%;height:260px}.dshtu_info{display:inline-flex;align-items:center;justify-content:center;margin-left:6px;color:var(--dsw-alias-label-tertiary);cursor:help}.dshtu_info:hover{color:var(--dsw-alias-state-business-primary)}.dshtu_pop{display:block;max-width:420px;font-size:12px;font-weight:400;line-height:18px;text-align:left;white-space:normal}.dshtu_pop .dshtu_sep{margin:0 5px;opacity:.55}[role=tooltip]:has(.dshtu_pop){background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:10px 12px;border:0;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16)}.dshtu_nd{color:var(--dsw-alias-label-tertiary)}.dshtu_range{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.dshtu_range input{width:132px}.dshtu_range em{font-style:normal;color:var(--dsw-alias-label-tertiary)}.dshtu_tabRow{display:flex;align-items:center;gap:10px}.dshtu_tabs{display:flex;align-items:center;gap:2px;flex:1;flex-wrap:wrap}.dshtu_tabs button{border:0;background:0 0;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:5px 10px 6px;border-bottom:2px solid transparent;font-size:13px}.dshtu_tabs button:hover{color:var(--dsw-alias-label-primary)}.dshtu_tabs button[data-on=true]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:500}.dshtu_heatCard{position:relative;display:flex;flex-direction:column;gap:10px}.dshtu_heatHead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dshtu_heatScroll{overflow-x:auto;padding-bottom:4px}.dshtu_heatBody{display:flex;gap:3px}.dshtu_heatDays{display:flex;flex-direction:column;gap:3px;width:17px}.dshtu_heatDay{height:11px;font-size:10px;line-height:11px;color:var(--dsw-alias-label-tertiary)}.dshtu_heat{display:flex;gap:3px}.dshtu_heatCol{display:flex;flex-direction:column;gap:3px}.dshtu_heatColHead{position:relative;height:13px}.dshtu_heatColLabel{position:absolute;left:0;top:0;font-size:10px;line-height:13px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dshtu_heatCell{width:11px;height:11px;border-radius:2px;background:var(--dsw-alias-bg-module-platform)}.dshtu_heatCell[data-future=true]{background:0 0}.dshtu_heatCell[data-day]:hover{outline:1.5px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.dshtu_heatTip{position:absolute;z-index:6;transform:translate(-50%,-100%);pointer-events:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.18);padding:6px 9px;font-size:11.5px;line-height:17px;white-space:nowrap;display:flex;flex-direction:column}.dshtu_heatTip b{font-weight:500;margin-bottom:1px}.dshtu_heatTipRow{color:var(--dsw-alias-label-secondary)}.dshtu_heatLegend{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dshtu_heatLegend i{width:11px;height:11px;border-radius:2px;display:inline-block}.dshtu_kv{display:grid;grid-template-columns:minmax(96px,auto) 1fr;gap:6px 14px;margin-top:8px;font-size:12.5px}.dshtu_kv dt{color:var(--dsw-alias-label-tertiary)}.dshtu_kv dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.dshtu_acts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}.dshtu_health{display:grid;grid-template-columns:minmax(96px,auto) 1fr;gap:6px 14px;margin-top:12px;padding-top:10px;border-top:.5px solid var(--dsw-alias-border-l2);font-size:12.5px}.dshtu_health dt{color:var(--dsw-alias-label-tertiary)}.dshtu_health dd{margin:0;font-variant-numeric:tabular-nums}.dshtu_stack{display:flex;flex-direction:column;gap:14px}.dshtu_tblBar{margin-top:12px}.dshtu_checkList{margin:8px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}.dshtu_head{display:flex;align-items:center;gap:6px;margin-bottom:8px}.dshtu_head h3{margin:0;font-size:13px;font-weight:500}.dshtu_slim .dshtu_bar{margin:0}.dshtu_state{display:inline-flex;align-items:center;gap:6px;font-size:12.5px}.dshtu_state[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}.dshtu_state[data-tone=warn]{color:var(--dsw-alias-state-warning-primary)}.dshtu_state[data-tone=bad]{color:var(--dsw-alias-state-error-primary)}.dshtu_state[data-tone=idle]{color:var(--dsw-alias-label-tertiary)}.dshtu_modalBody{font-size:12.5px;line-height:20px;color:var(--dsw-alias-label-secondary);max-width:420px}.dshtu_modalFoot{display:flex;justify-content:flex-end;gap:8px}.dshtu_verCell{font-variant-numeric:tabular-nums;white-space:nowrap}";
|
|
471
|
+
var css = ".dshtu_wrap{max-width:860px;display:flex;flex-direction:column;gap:14px;padding:2px 2px 4px;color:var(--dsw-alias-label-primary);font-size:13px}.dshtu_card{background:var(--dsw-alias-bg-layer-3);border-radius:14px;box-shadow:var(--dsw-elevation-stroke);padding:14px 16px}.dshtu_bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dshtu_bar label{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_bar .dshtu_select,.dshtu_bar .dshtu_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:4px 8px;font-size:12.5px}.dshtu_bar .dshtu_select{width:168px;text-overflow:ellipsis;white-space:nowrap}.dshtu_chips{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.dshtu_subBar{margin-top:12px}.dshtu_pick{display:inline-flex;align-items:center;gap:4px;max-width:230px}.dshtu_pickLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dshtu_metaIcon{display:inline-flex;align-items:center;margin-right:5px;vertical-align:-2px;color:var(--dsw-alias-label-tertiary)}.dshtu_cardTitle{display:inline-flex;align-items:center}.dshtu_foot{display:flex;align-items:center;gap:6px}.dshtu_statLabel{display:inline-flex;align-items:center;gap:3px;white-space:nowrap}.dshtu_field{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.dshtu_seg{display:inline-flex;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;overflow:hidden}.dshtu_seg button{border:0;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;padding:4px 10px;font-size:12.5px}.dshtu_seg button[data-on=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dshtu_totalsMeta{margin-top:12px}.dshtu_grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:8px}.dshtu_stat{background:var(--dsw-alias-bg-module-platform);border-radius:10px;padding:10px 12px}.dshtu_stat b{display:block;font-size:16px;line-height:24px;font-variant-numeric:tabular-nums}.dshtu_stat span{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_stat[data-main=true]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,var(--dsw-alias-bg-module-platform))}.dshtu_stat[data-main=true] b{color:var(--dsw-alias-state-business-primary);font-size:20px}.dshtu_table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.dshtu_table th,.dshtu_table td{text-align:right;padding:6px 8px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:400}.dshtu_table th:first-child,.dshtu_table td:first-child{text-align:left}.dshtu_table th{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dshtu_table td:first-child{overflow-wrap:anywhere;max-width:300px;color:var(--dsw-alias-label-secondary)}.dshtu_meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.dshtu_err{color:var(--dsw-alias-state-error-primary);display:flex;align-items:center;gap:10px}.dshtu_chart{width:100%;height:260px}.dshtu_info{display:inline-flex;align-items:center;justify-content:center;margin-left:6px;color:var(--dsw-alias-label-tertiary);cursor:help}.dshtu_info:hover{color:var(--dsw-alias-state-business-primary)}.dshtu_pop{display:block;max-width:420px;font-size:12px;font-weight:400;line-height:18px;text-align:left;white-space:normal}.dshtu_pop .dshtu_sep{margin:0 5px;opacity:.55}[role=tooltip]:has(.dshtu_pop){background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:10px 12px;border:0;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16)}.dshtu_nd{color:var(--dsw-alias-label-tertiary)}.dshtu_range{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.dshtu_range input{width:132px}.dshtu_range em{font-style:normal;color:var(--dsw-alias-label-tertiary)}.dshtu_tabRow{display:flex;align-items:center;gap:10px}.dshtu_tabs{display:flex;align-items:center;gap:2px;flex:1;flex-wrap:wrap}.dshtu_tabs button{border:0;background:0 0;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:5px 10px 6px;border-bottom:2px solid transparent;font-size:13px}.dshtu_tabs button:hover{color:var(--dsw-alias-label-primary)}.dshtu_tabs button[data-on=true]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:500}.dshtu_heatCard{position:relative;display:flex;flex-direction:column;gap:10px}.dshtu_heatHead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dshtu_heatScroll{overflow-x:auto;padding-bottom:4px}.dshtu_heatBody{display:flex;gap:3px}.dshtu_heatDays{display:flex;flex-direction:column;gap:3px;width:17px}.dshtu_heatDay{height:11px;font-size:10px;line-height:11px;color:var(--dsw-alias-label-tertiary)}.dshtu_heat{display:flex;gap:3px}.dshtu_heatCol{display:flex;flex-direction:column;gap:3px}.dshtu_heatColHead{position:relative;height:13px}.dshtu_heatColLabel{position:absolute;left:0;top:0;font-size:10px;line-height:13px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}.dshtu_heatCell{width:11px;height:11px;border-radius:2px;background:var(--dsw-alias-bg-module-platform)}.dshtu_heatCell[data-future=true]{background:0 0}.dshtu_heatCell[data-day]:hover{outline:1.5px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.dshtu_heatTip{position:absolute;z-index:6;transform:translate(-50%,-100%);pointer-events:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.18);padding:6px 9px;font-size:11.5px;line-height:17px;white-space:nowrap;display:flex;flex-direction:column}.dshtu_heatTip b{font-weight:500;margin-bottom:1px}.dshtu_heatTipRow{color:var(--dsw-alias-label-secondary)}.dshtu_heatLegend{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dshtu_heatLegend i{width:11px;height:11px;border-radius:2px;display:inline-block}.dshtu_kv{display:grid;grid-template-columns:minmax(96px,auto) 1fr;gap:6px 14px;margin-top:8px;font-size:12.5px}.dshtu_kv dt{color:var(--dsw-alias-label-tertiary)}.dshtu_kv dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.dshtu_acts{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}.dshtu_health{display:grid;grid-template-columns:minmax(96px,auto) 1fr;gap:6px 14px;margin-top:12px;padding-top:10px;border-top:.5px solid var(--dsw-alias-border-l2);font-size:12.5px}.dshtu_health dt{color:var(--dsw-alias-label-tertiary)}.dshtu_health dd{margin:0;font-variant-numeric:tabular-nums}.dshtu_stack{display:flex;flex-direction:column;gap:14px}.dshtu_tblBar{margin-top:12px}.dshtu_checkList{margin:8px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}.dshtu_head{display:flex;align-items:center;gap:6px;margin-bottom:8px}.dshtu_head h3{margin:0;font-size:13px;font-weight:500}.dshtu_slim .dshtu_bar{margin:0}.dshtu_state{display:inline-flex;align-items:center;gap:6px;font-size:12.5px}.dshtu_state[data-tone=ok]{color:var(--dsw-alias-state-success-primary)}.dshtu_state[data-tone=warn]{color:var(--dsw-alias-state-warn-primary)}.dshtu_state[data-tone=bad]{color:var(--dsw-alias-state-error-primary)}.dshtu_state[data-tone=idle]{color:var(--dsw-alias-label-tertiary)}.dshtu_modalBody{font-size:12.5px;line-height:20px;color:var(--dsw-alias-label-secondary);max-width:420px}.dshtu_modalFoot{display:flex;justify-content:flex-end;gap:8px}.dshtu_verCell{font-variant-numeric:tabular-nums;white-space:nowrap}.dshtu_acts{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:12px}.dshtu_acts button{flex:none}.dshtu_danger{color:var(--dsw-alias-state-error-primary)}.dshtu_danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}.dshtu_dangerFill{background:var(--dsw-alias-state-error-primary)!important;color:#fff!important;border-color:transparent!important}.dshtu_dangerFill:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 86%,#000)!important}.dshtu_warnBtn{color:var(--dsw-alias-state-warn-primary)}.dshtu_warnBtn:hover{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}.dshtu_head .dshtu_grow{flex:1}.dshtu_power{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 92%,#000);backdrop-filter:blur(7px);animation:dshtu-power-in .3s ease both}.dshtu_powerBox{display:flex;flex-direction:column;align-items:center;gap:14px;padding:36px 44px;text-align:center;max-width:min(560px,80vw)}.dshtu_ring{position:relative;width:120px;height:120px}.dshtu_ring svg{position:absolute;inset:0;width:100%;height:100%}.dshtu_ringTrack{fill:none;stroke:color-mix(in srgb,var(--dsw-alias-label-primary) 14%,transparent);stroke-width:5}.dshtu_ringArc{fill:none;stroke:var(--dsw-alias-state-business-primary);stroke-width:5;stroke-linecap:round;stroke-dasharray:74 260;transform-origin:60px 60px;animation:dshtu-ring-spin 1.15s linear infinite}.dshtu_glyph{fill:none;stroke:var(--dsw-alias-label-primary);stroke-width:6;stroke-linecap:round;opacity:.9;animation:dshtu-glyph-pulse 1.8s ease-in-out infinite}.dshtu_power[data-mode=shutdown] .dshtu_ringArc{stroke:var(--dsw-alias-state-error-primary)}.dshtu_power[data-mode=shutdown] .dshtu_glyph{stroke:var(--dsw-alias-state-error-primary)}.dshtu_powerTitle{font-size:17px;font-weight:600;letter-spacing:.2px}.dshtu_powerPhase{font-size:13px;color:var(--dsw-alias-label-secondary);min-height:20px}.dshtu_powerBar{width:240px;height:3px;border-radius:2px;overflow:hidden;background:color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent)}.dshtu_powerBar i{display:block;width:38%;height:100%;border-radius:2px;background:var(--dsw-alias-state-business-primary);animation:dshtu-bar-slide 1.35s ease-in-out infinite}.dshtu_power[data-mode=shutdown] .dshtu_powerBar i{background:var(--dsw-alias-state-error-primary)}.dshtu_powerNote{font-size:12.5px;line-height:20px;color:var(--dsw-alias-label-secondary);max-width:420px}.dshtu_powerNote code{background:var(--dsw-alias-bg-module-platform);border-radius:5px;padding:2px 6px;font-size:12px}.dshtu_power[data-phase=stopped] .dshtu_ringArc,.dshtu_power[data-phase=timeout] .dshtu_ringArc{animation:none;stroke-dasharray:none;opacity:.45}.dshtu_power[data-phase=stopped] .dshtu_powerBar,.dshtu_power[data-phase=timeout] .dshtu_powerBar,.dshtu_power[data-phase=reloaded] .dshtu_powerBar{visibility:hidden}.dshtu_power[data-phase=stopped] .dshtu_glyph,.dshtu_power[data-phase=timeout] .dshtu_glyph{animation:none}@keyframes dshtu-power-in{from{opacity:0}to{opacity:1}}@keyframes dshtu-ring-spin{to{transform:rotate(360deg)}}@keyframes dshtu-glyph-pulse{0%,100%{opacity:.9}50%{opacity:.45}}@keyframes dshtu-bar-slide{0%{transform:translateX(-110%)}100%{transform:translateX(370%)}}";
|
|
405
472
|
|
|
406
473
|
function trimNum(v, decimals) {
|
|
407
474
|
var f = v.toFixed(decimals);
|
|
@@ -1202,6 +1269,60 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1202
1269
|
}).reduce(function (flat, pair) { return flat.concat(pair); }, []))));
|
|
1203
1270
|
}
|
|
1204
1271
|
|
|
1272
|
+
/** A power symbol for the shutdown button — the shell ships no power icon. */
|
|
1273
|
+
function powerGlyph(size) {
|
|
1274
|
+
return h("svg", {
|
|
1275
|
+
width: size || 16,
|
|
1276
|
+
height: size || 16,
|
|
1277
|
+
viewBox: "0 0 24 24",
|
|
1278
|
+
fill: "none",
|
|
1279
|
+
stroke: "currentColor",
|
|
1280
|
+
strokeWidth: 2,
|
|
1281
|
+
strokeLinecap: "round"
|
|
1282
|
+
},
|
|
1283
|
+
h("path", { d: "M12 3v9" }),
|
|
1284
|
+
h("path", { d: "M6.6 6.8a8 8 0 1 0 10.8 0" }));
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Fullscreen transition for restart and shutdown: a power ring that keeps
|
|
1289
|
+
* turning while the old process winds down, the phase it is in, and — once it
|
|
1290
|
+
* is over — what to do next. Nothing here is a fake progress bar: the phases
|
|
1291
|
+
* follow what the browser can actually observe (the request landed, the old
|
|
1292
|
+
* process stopped answering, a *new* instance answered).
|
|
1293
|
+
*/
|
|
1294
|
+
function PowerOverlay(props) {
|
|
1295
|
+
var t = props.t;
|
|
1296
|
+
var power = props.power;
|
|
1297
|
+
if (power === null || power === undefined) return null;
|
|
1298
|
+
var mode = power.mode === "shutdown" ? "shutdown" : "restart";
|
|
1299
|
+
var phase = power.phase;
|
|
1300
|
+
var phaseText = phase === "prepare"
|
|
1301
|
+
? (mode === "restart" ? t("powerPrepareRestart") : t("powerPrepareShutdown"))
|
|
1302
|
+
: phase === "stopping" ? t("powerStopping")
|
|
1303
|
+
: phase === "recovering" ? t("powerRecovering")
|
|
1304
|
+
: phase === "reloaded" ? t("powerReloaded")
|
|
1305
|
+
: phase === "stopped" ? t("powerStopped")
|
|
1306
|
+
: t("powerTimeout");
|
|
1307
|
+
var note = phase === "stopped" ? t("powerStoppedNote")
|
|
1308
|
+
: phase === "timeout" ? t("powerTimeoutNote").replace("{log}", power.log === null || power.log === undefined ? "—" : power.log)
|
|
1309
|
+
: null;
|
|
1310
|
+
var node = h("div", { className: "dshtu_power", "data-mode": mode, "data-phase": phase, role: "status", "aria-live": "polite" },
|
|
1311
|
+
h("div", { className: "dshtu_powerBox" },
|
|
1312
|
+
h("div", { className: "dshtu_ring" },
|
|
1313
|
+
h("svg", { viewBox: "0 0 120 120" },
|
|
1314
|
+
h("circle", { className: "dshtu_ringTrack", cx: 60, cy: 60, r: 52 }),
|
|
1315
|
+
h("circle", { className: "dshtu_ringArc", cx: 60, cy: 60, r: 52 })),
|
|
1316
|
+
h("svg", { viewBox: "0 0 120 120" },
|
|
1317
|
+
h("path", { className: "dshtu_glyph", d: "M60 30 V58" }),
|
|
1318
|
+
h("path", { className: "dshtu_glyph", d: "M41 41a27 27 0 1 0 38 0" }))),
|
|
1319
|
+
h("div", { className: "dshtu_powerTitle" }, mode === "restart" ? t("powerRestartTitle") : t("powerShutdownTitle")),
|
|
1320
|
+
h("div", { className: "dshtu_powerPhase" }, phaseText),
|
|
1321
|
+
h("div", { className: "dshtu_powerBar" }, h("i", null)),
|
|
1322
|
+
note === null ? null : h("div", { className: "dshtu_powerNote" }, note)));
|
|
1323
|
+
return portalTo === null ? node : portalTo(node);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1205
1326
|
/** `已安装 / 最新 / 状态` for one tracked package. */
|
|
1206
1327
|
function versionRow(t, entry) {
|
|
1207
1328
|
var tone = entry.status === "current" ? "ok" : (entry.status === "outdated" ? "warn" : "idle");
|
|
@@ -1217,10 +1338,11 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1217
1338
|
h("td", { className: "dshtu_verCell" }, entry.installed === null ? "—" : entry.installed),
|
|
1218
1339
|
h("td", { className: "dshtu_verCell" }, entry.latest === null ? "—" : entry.latest),
|
|
1219
1340
|
// The status stays a real table cell so its column lines up with the
|
|
1220
|
-
// header; the inline-flex badge lives inside it.
|
|
1341
|
+
// header; the inline-flex badge lives inside it. A failed lookup never
|
|
1342
|
+
// mixes into this label — it is reported under the table instead.
|
|
1221
1343
|
h("td", null, h("span", { className: "dshtu_state", "data-tone": tone },
|
|
1222
1344
|
glyph === null ? null : h(glyph, {}),
|
|
1223
|
-
|
|
1345
|
+
label)));
|
|
1224
1346
|
}
|
|
1225
1347
|
|
|
1226
1348
|
/** Uptime as `2d 3h` / `45m`, whichever reads better at this scale. */
|
|
@@ -1242,6 +1364,11 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1242
1364
|
return h("div", { className: "dshtu_card dshtu_meta" }, props.error === null ? t("loading") : t("error") + ": " + props.error);
|
|
1243
1365
|
}
|
|
1244
1366
|
var runtime = config.platform || {};
|
|
1367
|
+
// Packages whose last registry lookup failed: their row keeps the previous
|
|
1368
|
+
// answer, and the reason is reported once, under the table.
|
|
1369
|
+
var failed = (config.packages || []).filter(function (entry) { return entry.error !== null && entry.error !== undefined; });
|
|
1370
|
+
// Cap what reaches the tooltip: a cached error string can be arbitrary.
|
|
1371
|
+
var reasonOf = function (entry) { return String(entry.error).replace(/\s+/g, " ").slice(0, 140); };
|
|
1245
1372
|
var health = props.health;
|
|
1246
1373
|
var rows = [];
|
|
1247
1374
|
if (health && health.state === "ok") {
|
|
@@ -1262,7 +1389,17 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1262
1389
|
var healthLabel = health === null ? null : health.state === "ok" ? t("healthOk") : health.state === "error" ? t("healthFail") + ":" + health.error : t("healthRunning");
|
|
1263
1390
|
return h("div", { className: "dshtu_stack" },
|
|
1264
1391
|
h("div", { className: "dshtu_card" },
|
|
1265
|
-
h("div", { className: "dshtu_head" },
|
|
1392
|
+
h("div", { className: "dshtu_head" },
|
|
1393
|
+
iconApi === null ? null : h(iconApi, {}),
|
|
1394
|
+
h("h3", null, t("configVersions")),
|
|
1395
|
+
h("span", { className: "dshtu_grow" }),
|
|
1396
|
+
h(button, {
|
|
1397
|
+
variant: "ghost",
|
|
1398
|
+
size: "sm",
|
|
1399
|
+
disabled: props.refreshing === true,
|
|
1400
|
+
icon: iconRefresh === null ? null : h(iconRefresh, {}),
|
|
1401
|
+
onClick: props.onRefresh
|
|
1402
|
+
}, props.refreshing === true ? t("versionRefreshing") : t("versionRefresh"))),
|
|
1266
1403
|
h("table", { className: "dshtu_table" },
|
|
1267
1404
|
h("thead", null, h("tr", null,
|
|
1268
1405
|
h("th", null, t("versionPackage")),
|
|
@@ -1270,16 +1407,18 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1270
1407
|
h("th", null, t("versionLatest")),
|
|
1271
1408
|
h("th", null, t("versionState")))),
|
|
1272
1409
|
h("tbody", null, (config.packages || []).map(function (entry) { return versionRow(t, entry); }))),
|
|
1273
|
-
h("div", { className: "
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1410
|
+
h("div", { className: "dshtu_meta", style: { marginTop: "10px" } },
|
|
1411
|
+
config.checkedAt > 0 ? t("versionChecked").replace("{time}", fmtTime(config.checkedAt)) : ""),
|
|
1412
|
+
failed.length === 0 ? null : h("div", {
|
|
1413
|
+
className: "dshtu_state",
|
|
1414
|
+
"data-tone": "warn",
|
|
1415
|
+
style: { marginTop: "8px" },
|
|
1416
|
+
// The reason rides the title, not the label: a lookup failure must not
|
|
1417
|
+
// be mistaken for a package's version verdict.
|
|
1418
|
+
title: t("versionFailedTitle").replace("{reason}", failed.map(function (entry) { return entry.label + ": " + reasonOf(entry); }).join(" / "))
|
|
1419
|
+
},
|
|
1420
|
+
iconWarn === null ? null : h(iconWarn, {}),
|
|
1421
|
+
h("span", null, t("versionFailed").replace("{names}", failed.map(function (entry) { return entry.label; }).join("、"))))),
|
|
1283
1422
|
h("div", { className: "dshtu_card" },
|
|
1284
1423
|
h("div", { className: "dshtu_head" }, iconGauge === null ? null : h(iconGauge, {}), h("h3", null, t("configRuntime"))),
|
|
1285
1424
|
h("dl", { className: "dshtu_kv" }, [
|
|
@@ -1303,19 +1442,30 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1303
1442
|
config.restartable === false
|
|
1304
1443
|
? h("span", { className: "dshtu_meta" }, t("restartBlocked"))
|
|
1305
1444
|
: h(button, {
|
|
1306
|
-
variant: "
|
|
1445
|
+
variant: "outline",
|
|
1307
1446
|
size: "sm",
|
|
1447
|
+
className: "dshtu_warnBtn",
|
|
1308
1448
|
icon: iconRefresh === null ? null : h(iconRefresh, {}),
|
|
1309
|
-
disabled: props.
|
|
1449
|
+
disabled: props.busy === true,
|
|
1310
1450
|
onClick: props.onRestart
|
|
1311
1451
|
}, t("restart")),
|
|
1452
|
+
config.shutdownable === false
|
|
1453
|
+
? null
|
|
1454
|
+
: h(button, {
|
|
1455
|
+
variant: "outline",
|
|
1456
|
+
size: "sm",
|
|
1457
|
+
className: "dshtu_danger",
|
|
1458
|
+
icon: powerGlyph(16),
|
|
1459
|
+
disabled: props.busy === true,
|
|
1460
|
+
onClick: props.onShutdown
|
|
1461
|
+
}, t("shutdown")),
|
|
1312
1462
|
healthLabel === null ? null : h("span", { className: "dshtu_state", "data-tone": tone },
|
|
1313
1463
|
health.state === "running" && iconLoading !== null ? h(iconLoading, {}) : null,
|
|
1314
1464
|
healthLabel)),
|
|
1315
|
-
props.
|
|
1316
|
-
props.
|
|
1317
|
-
|
|
1318
|
-
: t("restartFailed").replace("{error}", props.
|
|
1465
|
+
props.powerError === null || props.powerError === undefined ? null : h("div", { className: "dshtu_meta", style: { marginTop: "10px" } },
|
|
1466
|
+
props.powerError.kind === "shutdown"
|
|
1467
|
+
? t("shutdownFailed").replace("{error}", props.powerError.error)
|
|
1468
|
+
: t("restartFailed").replace("{error}", props.powerError.error))),
|
|
1319
1469
|
h(HealthPanel, {
|
|
1320
1470
|
key: "health",
|
|
1321
1471
|
t: t,
|
|
@@ -1361,8 +1511,11 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1361
1511
|
var _p = useState(null), configError = _p[0], setConfigError = _p[1];
|
|
1362
1512
|
var _q = useState(0), configTick = _q[0], setConfigTick = _q[1];
|
|
1363
1513
|
var _r = useState(null), health = _r[0], setHealth = _r[1];
|
|
1364
|
-
var _s = useState(null),
|
|
1514
|
+
var _s = useState(null), powerError = _s[0], setPowerError = _s[1];
|
|
1365
1515
|
var _t = useState(false), confirming = _t[0], setConfirming = _t[1];
|
|
1516
|
+
// The fullscreen power transition: `{ mode, phase, log }` or null when idle.
|
|
1517
|
+
var _x = useState(null), power = _x[0], setPower = _x[1];
|
|
1518
|
+
var _y = useState(false), confirmingShutdown = _y[0], setConfirmingShutdown = _y[1];
|
|
1366
1519
|
var _u = useState(null), harness = _u[0], setHarness = _u[1];
|
|
1367
1520
|
var _v = useState(null), bundles = _v[0], setBundles = _v[1];
|
|
1368
1521
|
var _w = useState(false), refreshing = _w[0], setRefreshing = _w[1];
|
|
@@ -1541,9 +1694,17 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1541
1694
|
* had time to go down (the script waits two seconds), so the first success
|
|
1542
1695
|
* cannot be the old process answering.
|
|
1543
1696
|
*/
|
|
1697
|
+
/**
|
|
1698
|
+
* Restart, watched from the browser. Once the old process is gone it cannot
|
|
1699
|
+
* report anything, so the phases are what the page can actually observe: the
|
|
1700
|
+
* request landed, the socket stopped answering, and finally a *different*
|
|
1701
|
+
* instance answered (a new `startedAt`) — only then is the page reloaded.
|
|
1702
|
+
*/
|
|
1544
1703
|
var doRestart = function () {
|
|
1545
1704
|
setConfirming(false);
|
|
1546
|
-
|
|
1705
|
+
setPowerError(null);
|
|
1706
|
+
var previousStartedAt = data === null || data === undefined ? null : data.startedAt;
|
|
1707
|
+
setPower({ mode: "restart", phase: "prepare", log: null });
|
|
1547
1708
|
fetch("/dsh-token-use/restart", { method: "POST", headers: { accept: "application/json" } })
|
|
1548
1709
|
.then(function (res) {
|
|
1549
1710
|
return res.json().then(function (json) {
|
|
@@ -1551,25 +1712,80 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1551
1712
|
return json;
|
|
1552
1713
|
});
|
|
1553
1714
|
})
|
|
1554
|
-
.then(function () {
|
|
1555
|
-
|
|
1715
|
+
.then(function (json) {
|
|
1716
|
+
setPower({ mode: "restart", phase: "stopping", log: json.logFile || null });
|
|
1556
1717
|
setTimeout(function () {
|
|
1718
|
+
setPower({ mode: "restart", phase: "recovering", log: json.logFile || null });
|
|
1719
|
+
var deadline = Date.now() + 90000;
|
|
1557
1720
|
var timer = setInterval(function () {
|
|
1558
|
-
|
|
1721
|
+
if (Date.now() > deadline) {
|
|
1722
|
+
clearInterval(timer);
|
|
1723
|
+
setPower({ mode: "restart", phase: "timeout", log: json.logFile || null });
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1559
1726
|
fetch("/dsh-token-use", { cache: "no-store" })
|
|
1560
|
-
.then(function (res) {
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1727
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
1728
|
+
.then(function (next) {
|
|
1729
|
+
if (next === null) return;
|
|
1730
|
+
if (previousStartedAt === null || next.startedAt !== previousStartedAt) {
|
|
1731
|
+
clearInterval(timer);
|
|
1732
|
+
setPower({ mode: "restart", phase: "reloaded", log: null });
|
|
1733
|
+
setTimeout(function () { location.reload(); }, 900);
|
|
1734
|
+
}
|
|
1565
1735
|
})
|
|
1566
1736
|
.catch(function () {});
|
|
1567
|
-
|
|
1568
|
-
|
|
1737
|
+
}, 1500);
|
|
1738
|
+
}, 6000);
|
|
1739
|
+
})
|
|
1740
|
+
.catch(function (err) {
|
|
1741
|
+
setPower(null);
|
|
1742
|
+
setPowerError({ kind: "restart", error: String(err) });
|
|
1743
|
+
});
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Shutdown: the same transition, but it ends with the service staying down.
|
|
1748
|
+
* A refused request is the success signal here, so the first failure (or the
|
|
1749
|
+
* deadline) settles the overlay on 「服务已关闭」.
|
|
1750
|
+
*/
|
|
1751
|
+
var doShutdown = function () {
|
|
1752
|
+
setConfirmingShutdown(false);
|
|
1753
|
+
setPowerError(null);
|
|
1754
|
+
setPower({ mode: "shutdown", phase: "prepare", log: null });
|
|
1755
|
+
fetch("/dsh-token-use/shutdown", { method: "POST", headers: { accept: "application/json" } })
|
|
1756
|
+
.then(function (res) {
|
|
1757
|
+
return res.json().then(function (json) {
|
|
1758
|
+
if (!res.ok) throw new Error(json && json.error ? json.error : "http " + res.status);
|
|
1759
|
+
return json;
|
|
1760
|
+
});
|
|
1761
|
+
})
|
|
1762
|
+
.then(function (json) {
|
|
1763
|
+
setPower({ mode: "shutdown", phase: "stopping", log: json.logFile || null });
|
|
1764
|
+
setTimeout(function () {
|
|
1765
|
+
setPower({ mode: "shutdown", phase: "recovering", log: json.logFile || null });
|
|
1766
|
+
var deadline = Date.now() + 45000;
|
|
1767
|
+
var timer = setInterval(function () {
|
|
1768
|
+
fetch("/dsh-token-use", { cache: "no-store" })
|
|
1769
|
+
.then(function (res) {
|
|
1770
|
+
if (!res.ok) throw new Error("down");
|
|
1771
|
+
return res.json();
|
|
1772
|
+
})
|
|
1773
|
+
.then(function () {
|
|
1774
|
+
if (Date.now() > deadline) {
|
|
1775
|
+
clearInterval(timer);
|
|
1776
|
+
setPower({ mode: "shutdown", phase: "stopped", log: null });
|
|
1777
|
+
}
|
|
1778
|
+
})
|
|
1779
|
+
.catch(function () {
|
|
1780
|
+
clearInterval(timer);
|
|
1781
|
+
setPower({ mode: "shutdown", phase: "stopped", log: null });
|
|
1782
|
+
});
|
|
1783
|
+
}, 1500);
|
|
1569
1784
|
}, 6000);
|
|
1570
1785
|
})
|
|
1571
1786
|
.catch(function (err) {
|
|
1572
|
-
|
|
1787
|
+
setPower(null);
|
|
1788
|
+
setPowerError({ kind: "shutdown", error: String(err) });
|
|
1573
1789
|
});
|
|
1574
1790
|
};
|
|
1575
1791
|
|
|
@@ -1620,6 +1836,11 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1620
1836
|
}
|
|
1621
1837
|
metaParts.push(t("updated").replace("{time}", fmtTime(data.updatedAt)));
|
|
1622
1838
|
var scanState = data.scan && data.scan.error ? "error" : (data.scan && data.scan.done ? "done" : "ongoing");
|
|
1839
|
+
/** The pid both confirmation dialogs name, when the config tab has loaded it. */
|
|
1840
|
+
var pidText = function () {
|
|
1841
|
+
return config === null || config === undefined || config.platform === undefined ? "?"
|
|
1842
|
+
: String(config.platform.pid);
|
|
1843
|
+
};
|
|
1623
1844
|
var rangeItems = [
|
|
1624
1845
|
{ id: "day", label: t("filterDay") },
|
|
1625
1846
|
{ id: "month", label: t("filterMonth") },
|
|
@@ -1719,11 +1940,13 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1719
1940
|
config: config,
|
|
1720
1941
|
error: configError,
|
|
1721
1942
|
health: health,
|
|
1722
|
-
restart: restart,
|
|
1723
1943
|
harness: harness,
|
|
1724
1944
|
bundles: bundles,
|
|
1725
1945
|
onHealth: runHealth,
|
|
1726
1946
|
onRestart: function () { setConfirming(true); },
|
|
1947
|
+
onShutdown: function () { setConfirmingShutdown(true); },
|
|
1948
|
+
busy: power !== null,
|
|
1949
|
+
powerError: powerError,
|
|
1727
1950
|
refreshing: refreshing,
|
|
1728
1951
|
onRefresh: refreshConfig
|
|
1729
1952
|
})];
|
|
@@ -1736,10 +1959,21 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1736
1959
|
title: t("restartTitle"),
|
|
1737
1960
|
closeLabel: t("close"),
|
|
1738
1961
|
footer: h("div", { className: "dshtu_modalFoot" },
|
|
1739
|
-
h(button, { variant: "ghost", size: "sm", onClick: function () { setConfirming(false); } }, t("
|
|
1962
|
+
h(button, { variant: "ghost", size: "sm", onClick: function () { setConfirming(false); } }, t("cancel")),
|
|
1740
1963
|
h(button, { variant: "primary", size: "sm", onClick: doRestart }, t("restartConfirm")))
|
|
1741
1964
|
}, h("div", { className: "dshtu_modalBody" },
|
|
1742
|
-
t("restartBody").replace("{pid}",
|
|
1965
|
+
t("restartBody").replace("{pid}", pidText()))),
|
|
1966
|
+
modal === null ? null : h(modal, {
|
|
1967
|
+
open: confirmingShutdown,
|
|
1968
|
+
onClose: function () { setConfirmingShutdown(false); },
|
|
1969
|
+
title: t("shutdownTitle"),
|
|
1970
|
+
closeLabel: t("close"),
|
|
1971
|
+
footer: h("div", { className: "dshtu_modalFoot" },
|
|
1972
|
+
h(button, { variant: "ghost", size: "sm", onClick: function () { setConfirmingShutdown(false); } }, t("cancel")),
|
|
1973
|
+
h(button, { variant: "primary", size: "sm", className: "dshtu_dangerFill", onClick: doShutdown }, t("shutdownConfirm")))
|
|
1974
|
+
}, h("div", { className: "dshtu_modalBody" },
|
|
1975
|
+
t("shutdownBody").replace("{pid}", pidText()))),
|
|
1976
|
+
h(PowerOverlay, { key: "power", t: t, power: power }),
|
|
1743
1977
|
h("div", { className: "dshtu_foot" },
|
|
1744
1978
|
stateDot === null ? null : h(stateDot, { state: scanState, size: 8 }),
|
|
1745
1979
|
h("span", { className: "dshtu_meta" }, metaParts.join(" · "))));
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,8 @@ import { join } from 'node:path'
|
|
|
24
24
|
import { zstdDecompressSync } from 'node:zlib'
|
|
25
25
|
import { scheduler } from 'node:timers/promises'
|
|
26
26
|
import { PricingStore } from './pricing.js'
|
|
27
|
-
import { VersionStore, installedVersion
|
|
27
|
+
import { VersionStore, installedVersion } from './versions.js'
|
|
28
|
+
import { powerCapability, scheduleRestart, scheduleShutdown } from './power.js'
|
|
28
29
|
import { checkHarness } from './health.js'
|
|
29
30
|
|
|
30
31
|
export const name = 'dsh-token-use'
|
|
@@ -790,14 +791,13 @@ export class UsageTracker {
|
|
|
790
791
|
// `?refresh=1` is the panel's re-check button: it skips the six-hour cache.
|
|
791
792
|
const force = new URL(req.url, 'http://localhost').searchParams.get('refresh') === '1'
|
|
792
793
|
const report = await versions.describe({ port, dshHome: this.dshHome, force })
|
|
793
|
-
const planned = restartScript({ pid: process.pid, argv: process.argv, cwd: process.cwd(), logFile: 'probe' })
|
|
794
794
|
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
|
|
795
795
|
res.end(JSON.stringify({
|
|
796
796
|
ok: true,
|
|
797
797
|
name,
|
|
798
798
|
version: VERSION,
|
|
799
799
|
endpoint: this.endpoint,
|
|
800
|
-
|
|
800
|
+
...powerCapability(),
|
|
801
801
|
...report,
|
|
802
802
|
}))
|
|
803
803
|
}
|
|
@@ -863,6 +863,33 @@ export class UsageTracker {
|
|
|
863
863
|
res.end(JSON.stringify({ ok: true, delaySec: 2, logFile: scheduled.logFile, script: scheduled.script }))
|
|
864
864
|
}
|
|
865
865
|
|
|
866
|
+
/** Shutdown: stop the service for good, after the panel's confirmation. */
|
|
867
|
+
handleShutdown(req, res) {
|
|
868
|
+
res.setHeader('cache-control', 'no-store')
|
|
869
|
+
if (req.method !== 'POST') {
|
|
870
|
+
res.writeHead(405, { 'content-type': 'application/json; charset=utf-8' })
|
|
871
|
+
res.end(JSON.stringify({ ok: false, error: 'method not allowed' }))
|
|
872
|
+
return
|
|
873
|
+
}
|
|
874
|
+
if (!this.trusted(req)) {
|
|
875
|
+
this.deny(res)
|
|
876
|
+
return
|
|
877
|
+
}
|
|
878
|
+
const scheduled = scheduleShutdown(this.dshHome)
|
|
879
|
+
if (scheduled === undefined) {
|
|
880
|
+
res.writeHead(409, { 'content-type': 'application/json; charset=utf-8' })
|
|
881
|
+
res.end(JSON.stringify({ ok: false, error: 'this process cannot be stopped from here' }))
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
if (scheduled.error !== undefined) {
|
|
885
|
+
res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
|
|
886
|
+
res.end(JSON.stringify({ ok: false, error: scheduled.error }))
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
|
|
890
|
+
res.end(JSON.stringify({ ok: true, graceSec: scheduled.graceSeconds, logFile: scheduled.logFile }))
|
|
891
|
+
}
|
|
892
|
+
|
|
866
893
|
handle(req, res) {
|
|
867
894
|
res.setHeader('cache-control', 'no-store')
|
|
868
895
|
if (req.method !== 'GET') {
|
|
@@ -959,6 +986,7 @@ export function apply(ctx, config = {}) {
|
|
|
959
986
|
{ path: `${endpoint}/config`, handler: (req, res) => tracker.handleConfig(req, res, versions) },
|
|
960
987
|
{ path: `${endpoint}/health`, handler: (req, res) => tracker.handleHealth(req, res) },
|
|
961
988
|
{ path: `${endpoint}/restart`, handler: (req, res) => tracker.handleRestart(req, res) },
|
|
989
|
+
{ path: `${endpoint}/shutdown`, handler: (req, res) => tracker.handleShutdown(req, res) },
|
|
962
990
|
]
|
|
963
991
|
const disposers = routes.map((route) => host.webServer.register({ kind: 'exact', ...route }))
|
|
964
992
|
return () => {
|
package/lib/power.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process lifecycle for the configuration tab: restart and shutdown.
|
|
3
|
+
*
|
|
4
|
+
* Both actions change the page under the user's feet, so the work happens in a
|
|
5
|
+
* detached shell script rather than inline: the HTTP response has to reach the
|
|
6
|
+
* browser first, and the script must outlive the process it is about to stop.
|
|
7
|
+
*
|
|
8
|
+
* Relaunching replays the running command line (`process.execPath` + `argv` +
|
|
9
|
+
* cwd) instead of guessing a CLI invocation, so custom ports, flags and working
|
|
10
|
+
* directories survive. Shutdown simply stops and never starts again — the user
|
|
11
|
+
* brings the service back by hand, which the panel says in as many words.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-token-use/power
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from 'node:child_process'
|
|
16
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
|
|
19
|
+
/** How long the script waits before touching the process (ms, in whole seconds). */
|
|
20
|
+
const GRACE_SECONDS = 2
|
|
21
|
+
/** How long the script waits for a graceful exit before escalating. */
|
|
22
|
+
const STOP_TIMEOUT_SECONDS = 30
|
|
23
|
+
|
|
24
|
+
/** Single-quote a shell word so paths with spaces survive. */
|
|
25
|
+
function shellQuote(value) {
|
|
26
|
+
return `'${String(value).replaceAll("'", `'\\''`)}'`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The stop half both scripts share: TERM, wait, then KILL. */
|
|
30
|
+
function stopCommands(pid) {
|
|
31
|
+
return [
|
|
32
|
+
`kill -TERM ${pid} 2>/dev/null`,
|
|
33
|
+
'i=0',
|
|
34
|
+
`while kill -0 ${pid} 2>/dev/null && [ "$i" -lt ${STOP_TIMEOUT_SECONDS} ]; do sleep 1; i=$((i+1)); done`,
|
|
35
|
+
`kill -9 ${pid} 2>/dev/null`,
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The detached restart script: wait for the response to land, stop the process
|
|
41
|
+
* that serves this request, then start the very same command again from the
|
|
42
|
+
* same directory.
|
|
43
|
+
*
|
|
44
|
+
* @returns the script text, or undefined when the running command is unusable.
|
|
45
|
+
*/
|
|
46
|
+
export function restartScript({ pid, argv, cwd: workingDir, logFile }) {
|
|
47
|
+
const entry = argv[1]
|
|
48
|
+
const rest = argv.slice(2)
|
|
49
|
+
if (typeof entry !== 'string' || entry.length === 0 || typeof pid !== 'number') return undefined
|
|
50
|
+
const command = [process.execPath, entry, ...rest].map(shellQuote).join(' ')
|
|
51
|
+
return [
|
|
52
|
+
'#!/bin/sh',
|
|
53
|
+
'# Written by dsh-token-use: restart the dsh web service on demand.',
|
|
54
|
+
`sleep ${GRACE_SECONDS}`,
|
|
55
|
+
...stopCommands(pid),
|
|
56
|
+
'sleep 1',
|
|
57
|
+
`cd ${shellQuote(workingDir)} || exit 1`,
|
|
58
|
+
`nohup ${command} >>${shellQuote(logFile)} 2>&1 </dev/null &`,
|
|
59
|
+
'',
|
|
60
|
+
].join('\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The detached shutdown script: stop the process and leave it stopped. There is
|
|
65
|
+
* no relaunch, because "关闭" has to mean what it says.
|
|
66
|
+
*
|
|
67
|
+
* @returns the script text, or undefined when the running pid is unusable.
|
|
68
|
+
*/
|
|
69
|
+
export function shutdownScript({ pid, logFile }) {
|
|
70
|
+
if (typeof pid !== 'number' || Number.isNaN(pid)) return undefined
|
|
71
|
+
return [
|
|
72
|
+
'#!/bin/sh',
|
|
73
|
+
'# Written by dsh-token-use: stop the dsh web service and stay down.',
|
|
74
|
+
`sleep ${GRACE_SECONDS}`,
|
|
75
|
+
...stopCommands(pid),
|
|
76
|
+
`echo "dsh-token-use: stopped pid ${pid} at $(date)" >>${shellQuote(logFile)}`,
|
|
77
|
+
'',
|
|
78
|
+
].join('\n')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Write a script and run it detached, so it survives the process it stops. */
|
|
82
|
+
function fire(dshHome, name, script) {
|
|
83
|
+
if (script === undefined) return undefined
|
|
84
|
+
const dir = join(dshHome, 'dsh-token-use')
|
|
85
|
+
const scriptFile = join(dir, `${name}.sh`)
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dir, { recursive: true })
|
|
88
|
+
writeFileSync(scriptFile, script, { mode: 0o700 })
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return { error: String(error?.message ?? error) }
|
|
91
|
+
}
|
|
92
|
+
const child = spawn('sh', [scriptFile], { detached: true, stdio: 'ignore' })
|
|
93
|
+
child.unref()
|
|
94
|
+
return { script: scriptFile, logFile: join(dir, `${name}.log`), graceSeconds: GRACE_SECONDS }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Restart the service.
|
|
99
|
+
* @returns `{ script, logFile, graceSeconds }`, or undefined when the current
|
|
100
|
+
* command line cannot be replayed, or `{ error }` when the script cannot be written.
|
|
101
|
+
*/
|
|
102
|
+
export function scheduleRestart(dshHome) {
|
|
103
|
+
const logFile = join(dshHome, 'dsh-token-use', 'restart.log')
|
|
104
|
+
return fire(dshHome, 'restart', restartScript({
|
|
105
|
+
pid: process.pid,
|
|
106
|
+
argv: process.argv,
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
logFile,
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Shut the service down. `restartable` in the config report tells the panel
|
|
114
|
+
* whether this is offered at all.
|
|
115
|
+
*/
|
|
116
|
+
export function scheduleShutdown(dshHome) {
|
|
117
|
+
const logFile = join(dshHome, 'dsh-token-use', 'shutdown.log')
|
|
118
|
+
return fire(dshHome, 'shutdown', shutdownScript({ pid: process.pid, logFile }))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether this launch can be restarted / shut down at all. */
|
|
122
|
+
export function powerCapability() {
|
|
123
|
+
return {
|
|
124
|
+
restartable: restartScript({ pid: process.pid, argv: process.argv, cwd: process.cwd(), logFile: 'probe' }) !== undefined,
|
|
125
|
+
shutdownable: shutdownScript({ pid: process.pid, logFile: 'probe' }) !== undefined,
|
|
126
|
+
}
|
|
127
|
+
}
|
package/lib/versions.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module dsh-token-use/versions
|
|
15
15
|
*/
|
|
16
|
-
import { execFileSync
|
|
16
|
+
import { execFileSync } from 'node:child_process'
|
|
17
17
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { createRequire } from 'node:module'
|
|
19
19
|
import { arch, platform as platformName, release, type as osType } from 'node:os'
|
|
@@ -224,17 +224,39 @@ export class VersionStore {
|
|
|
224
224
|
/**
|
|
225
225
|
* The registry's install metadata (versions + dist-tags) — the abbreviated
|
|
226
226
|
* packument, which is a fraction of the full document's size.
|
|
227
|
+
*
|
|
228
|
+
* `accept-encoding: identity` is deliberate: a proxy in front of a local
|
|
229
|
+
* machine can hand back a compressed body that never gets decoded (seen in the
|
|
230
|
+
* wild as brotli bytes), and these documents are small enough that asking for
|
|
231
|
+
* them uncompressed costs nothing. The body is read as text and validated
|
|
232
|
+
* before parsing, so a bad response produces a short diagnostic instead of a
|
|
233
|
+
* JSON error quoting raw binary.
|
|
234
|
+
*
|
|
235
|
+
* @param name - package name.
|
|
236
|
+
* @param variant - `abbreviated` (default) or `full` for the retry.
|
|
227
237
|
*/
|
|
228
|
-
async fetchPackument(name) {
|
|
238
|
+
async fetchPackument(name, variant = 'abbreviated') {
|
|
229
239
|
const url = `${this.registry}/${name.replace('/', '%2f')}`
|
|
230
240
|
const response = await fetch(url, {
|
|
231
|
-
headers: {
|
|
241
|
+
headers: {
|
|
242
|
+
accept: variant === 'full' ? 'application/json' : 'application/vnd.npm.install-v1+json',
|
|
243
|
+
'accept-encoding': 'identity',
|
|
244
|
+
'user-agent': USER_AGENT,
|
|
245
|
+
},
|
|
232
246
|
signal: AbortSignal.timeout(this.timeoutMs),
|
|
233
247
|
})
|
|
234
248
|
if (!response.ok) throw new Error(`http ${response.status}`)
|
|
235
|
-
const
|
|
249
|
+
const type = response.headers.get('content-type') ?? ''
|
|
250
|
+
const text = await response.text()
|
|
251
|
+
if (!type.includes('json')) throw new Error(`content-type ${type.length > 0 ? type : '(none)'}, ${text.length} bytes`)
|
|
252
|
+
let body
|
|
253
|
+
try {
|
|
254
|
+
body = JSON.parse(text)
|
|
255
|
+
} catch {
|
|
256
|
+
throw new Error(`${text.length} bytes that are not JSON`)
|
|
257
|
+
}
|
|
236
258
|
const versions = Object.keys(body?.versions ?? {})
|
|
237
|
-
if (versions.length === 0) throw new Error('no versions in
|
|
259
|
+
if (versions.length === 0) throw new Error('no versions in the response')
|
|
238
260
|
return { versions, tags: body['dist-tags'] ?? {} }
|
|
239
261
|
}
|
|
240
262
|
|
|
@@ -254,18 +276,9 @@ export class VersionStore {
|
|
|
254
276
|
const packages = { ...this.cache.packages }
|
|
255
277
|
const errors = {}
|
|
256
278
|
await Promise.all(names.map(async (name) => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
packages[name] = {
|
|
261
|
-
versions: versions.slice(-40),
|
|
262
|
-
tags,
|
|
263
|
-
next: next ?? null,
|
|
264
|
-
latest: highestInChannel(versions, channelOf(next ?? '')) ?? highestInChannel(versions, 'stable') ?? versions[versions.length - 1],
|
|
265
|
-
}
|
|
266
|
-
} catch (error) {
|
|
267
|
-
errors[name] = String(error?.message ?? error)
|
|
268
|
-
}
|
|
279
|
+
const record = await this.lookup(name)
|
|
280
|
+
if (typeof record === 'string') errors[name] = record
|
|
281
|
+
else packages[name] = record
|
|
269
282
|
}))
|
|
270
283
|
this.cache = { checkedAt: Date.now(), packages, errors }
|
|
271
284
|
this.persist()
|
|
@@ -277,6 +290,30 @@ export class VersionStore {
|
|
|
277
290
|
}
|
|
278
291
|
}
|
|
279
292
|
|
|
293
|
+
/**
|
|
294
|
+
* One package's registry record, or a short reason string when the lookup
|
|
295
|
+
* failed. A second attempt with the full document rides out the transient
|
|
296
|
+
* failures a proxy can introduce.
|
|
297
|
+
*/
|
|
298
|
+
async lookup(name) {
|
|
299
|
+
let first
|
|
300
|
+
for (const variant of ['abbreviated', 'full']) {
|
|
301
|
+
try {
|
|
302
|
+
const { versions, tags } = await this.fetchPackument(name, variant)
|
|
303
|
+
const next = typeof tags.next === 'string' ? tags.next : undefined
|
|
304
|
+
return {
|
|
305
|
+
versions: versions.slice(-40),
|
|
306
|
+
tags,
|
|
307
|
+
next: next ?? null,
|
|
308
|
+
latest: highestInChannel(versions, channelOf(next ?? '')) ?? highestInChannel(versions, 'stable') ?? versions[versions.length - 1],
|
|
309
|
+
}
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (first === undefined) first = cleanMessage(error?.message ?? error)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return first
|
|
315
|
+
}
|
|
316
|
+
|
|
280
317
|
persist() {
|
|
281
318
|
try {
|
|
282
319
|
mkdirSync(dirname(this.file), { recursive: true })
|
|
@@ -333,64 +370,12 @@ export class VersionStore {
|
|
|
333
370
|
}
|
|
334
371
|
}
|
|
335
372
|
|
|
336
|
-
/** Single-quote a shell word so paths with spaces survive. */
|
|
337
|
-
function shellQuote(value) {
|
|
338
|
-
return `'${String(value).replaceAll("'", `'\\''`)}'`
|
|
339
|
-
}
|
|
340
|
-
|
|
341
373
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* same directory. Relaunching by `argv` (not by a guessed CLI invocation)
|
|
345
|
-
* keeps custom flags, ports and working directories intact.
|
|
346
|
-
*
|
|
347
|
-
* @returns the script text, or undefined when the running command is unusable.
|
|
374
|
+
* Keep a diagnostic to one printable short line. Error text can quote a
|
|
375
|
+
* response body, and a panel has no business rendering raw bytes.
|
|
348
376
|
*/
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const command = [process.execPath, entry, ...rest].map(shellQuote).join(' ')
|
|
354
|
-
return [
|
|
355
|
-
'#!/bin/sh',
|
|
356
|
-
'# Written by dsh-token-use: restart the dsh web service on demand.',
|
|
357
|
-
'sleep 2',
|
|
358
|
-
`kill -TERM ${pid} 2>/dev/null`,
|
|
359
|
-
'i=0',
|
|
360
|
-
`while kill -0 ${pid} 2>/dev/null && [ "$i" -lt 30 ]; do sleep 1; i=$((i+1)); done`,
|
|
361
|
-
`kill -9 ${pid} 2>/dev/null`,
|
|
362
|
-
'sleep 1',
|
|
363
|
-
`cd ${shellQuote(workingDir)} || exit 1`,
|
|
364
|
-
`nohup ${command} >>${shellQuote(logFile)} 2>&1 </dev/null &`,
|
|
365
|
-
'',
|
|
366
|
-
].join('\n')
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Write and fire the restart script, fully detached so it outlives the server
|
|
371
|
-
* it is about to stop.
|
|
372
|
-
*
|
|
373
|
-
* @returns `{ script, logFile }` for the response, or undefined when the
|
|
374
|
-
* current command line cannot be relaunched safely.
|
|
375
|
-
*/
|
|
376
|
-
export function scheduleRestart(dshHome) {
|
|
377
|
-
const dir = join(dshHome, 'dsh-token-use')
|
|
378
|
-
const logFile = join(dir, 'restart.log')
|
|
379
|
-
const scriptFile = join(dir, 'restart.sh')
|
|
380
|
-
const script = restartScript({
|
|
381
|
-
pid: process.pid,
|
|
382
|
-
argv: process.argv,
|
|
383
|
-
cwd: process.cwd(),
|
|
384
|
-
logFile,
|
|
385
|
-
})
|
|
386
|
-
if (script === undefined) return undefined
|
|
387
|
-
try {
|
|
388
|
-
mkdirSync(dir, { recursive: true })
|
|
389
|
-
writeFileSync(scriptFile, script, { mode: 0o700 })
|
|
390
|
-
} catch (error) {
|
|
391
|
-
return { error: String(error?.message ?? error) }
|
|
392
|
-
}
|
|
393
|
-
const child = spawn('sh', [scriptFile], { detached: true, stdio: 'ignore' })
|
|
394
|
-
child.unref()
|
|
395
|
-
return { script: scriptFile, logFile }
|
|
377
|
+
function cleanMessage(message) {
|
|
378
|
+
const text = String(message).replace(/[^\t\n\r\x20-\x7e]/g, '').replace(/\s+/g, ' ').trim()
|
|
379
|
+
if (text.length === 0) return 'unknown error'
|
|
380
|
+
return text.length > 140 ? `${text.slice(0, 137)}...` : text
|
|
396
381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-token-use",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Real-time token usage and estimated spend for DeepSeek Harness, in a four-tab settings panel: an overview with cost/totals cards, a trend chart with a daily-average line and a 12-month usage heatmap; by-model and by-project tables with day/month/year/date-range queries; and versions, runtime facts, a harness self-check and a one-click restart. Prices come from DeepSeek's official page, refreshed daily, peak/off-peak aware. · 实时 Token 用量与估算金额面板,四个 tab:概览(金额/总量卡片、带日均线的趋势图、近 12 个月使用热力图)、模型统计与项目统计(按天/月/年/时间段/全部查询)、配置信息(版本对比、运行环境、Harness 自检、一键重启)。定价取 DeepSeek 官网、每日自动更新,区分高峰/空闲。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|