dsh-token-use 0.3.2 → 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 +260 -46
- package/lib/index.js +31 -3
- package/lib/power.js +127 -0
- package/lib/versions.js +1 -63
- 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);
|
|
@@ -166,6 +195,23 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
166
195
|
versionRefreshing: "检查中…",
|
|
167
196
|
versionFailed: "本次检查未成功:{names}(显示的是上次结果)",
|
|
168
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}",
|
|
169
215
|
healthCheck: "健康检查",
|
|
170
216
|
healthRunning: "检查中…",
|
|
171
217
|
healthOk: "接口正常",
|
|
@@ -328,6 +374,23 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
328
374
|
versionRefreshing: "Checking…",
|
|
329
375
|
versionFailed: "This check failed for {names} — showing the last successful result",
|
|
330
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}",
|
|
331
394
|
healthCheck: "Health check",
|
|
332
395
|
healthRunning: "Checking…",
|
|
333
396
|
healthOk: "Endpoint healthy",
|
|
@@ -405,7 +468,7 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
405
468
|
hintServiceVersion: "a service bundle older or newer than the running CLI is a common cause of failed plugin startups"
|
|
406
469
|
};
|
|
407
470
|
|
|
408
|
-
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%)}}";
|
|
409
472
|
|
|
410
473
|
function trimNum(v, decimals) {
|
|
411
474
|
var f = v.toFixed(decimals);
|
|
@@ -1206,6 +1269,60 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1206
1269
|
}).reduce(function (flat, pair) { return flat.concat(pair); }, []))));
|
|
1207
1270
|
}
|
|
1208
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
|
+
|
|
1209
1326
|
/** `已安装 / 最新 / 状态` for one tracked package. */
|
|
1210
1327
|
function versionRow(t, entry) {
|
|
1211
1328
|
var tone = entry.status === "current" ? "ok" : (entry.status === "outdated" ? "warn" : "idle");
|
|
@@ -1272,7 +1389,17 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1272
1389
|
var healthLabel = health === null ? null : health.state === "ok" ? t("healthOk") : health.state === "error" ? t("healthFail") + ":" + health.error : t("healthRunning");
|
|
1273
1390
|
return h("div", { className: "dshtu_stack" },
|
|
1274
1391
|
h("div", { className: "dshtu_card" },
|
|
1275
|
-
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"))),
|
|
1276
1403
|
h("table", { className: "dshtu_table" },
|
|
1277
1404
|
h("thead", null, h("tr", null,
|
|
1278
1405
|
h("th", null, t("versionPackage")),
|
|
@@ -1280,16 +1407,8 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1280
1407
|
h("th", null, t("versionLatest")),
|
|
1281
1408
|
h("th", null, t("versionState")))),
|
|
1282
1409
|
h("tbody", null, (config.packages || []).map(function (entry) { return versionRow(t, entry); }))),
|
|
1283
|
-
h("div", { className: "
|
|
1284
|
-
|
|
1285
|
-
h("span", { style: { flex: "1" } }),
|
|
1286
|
-
h(button, {
|
|
1287
|
-
variant: "outline",
|
|
1288
|
-
size: "sm",
|
|
1289
|
-
disabled: props.refreshing === true,
|
|
1290
|
-
icon: iconRefresh === null ? null : h(iconRefresh, {}),
|
|
1291
|
-
onClick: props.onRefresh
|
|
1292
|
-
}, props.refreshing === true ? t("versionRefreshing") : t("versionRefresh"))),
|
|
1410
|
+
h("div", { className: "dshtu_meta", style: { marginTop: "10px" } },
|
|
1411
|
+
config.checkedAt > 0 ? t("versionChecked").replace("{time}", fmtTime(config.checkedAt)) : ""),
|
|
1293
1412
|
failed.length === 0 ? null : h("div", {
|
|
1294
1413
|
className: "dshtu_state",
|
|
1295
1414
|
"data-tone": "warn",
|
|
@@ -1323,19 +1442,30 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1323
1442
|
config.restartable === false
|
|
1324
1443
|
? h("span", { className: "dshtu_meta" }, t("restartBlocked"))
|
|
1325
1444
|
: h(button, {
|
|
1326
|
-
variant: "
|
|
1445
|
+
variant: "outline",
|
|
1327
1446
|
size: "sm",
|
|
1447
|
+
className: "dshtu_warnBtn",
|
|
1328
1448
|
icon: iconRefresh === null ? null : h(iconRefresh, {}),
|
|
1329
|
-
disabled: props.
|
|
1449
|
+
disabled: props.busy === true,
|
|
1330
1450
|
onClick: props.onRestart
|
|
1331
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")),
|
|
1332
1462
|
healthLabel === null ? null : h("span", { className: "dshtu_state", "data-tone": tone },
|
|
1333
1463
|
health.state === "running" && iconLoading !== null ? h(iconLoading, {}) : null,
|
|
1334
1464
|
healthLabel)),
|
|
1335
|
-
props.
|
|
1336
|
-
props.
|
|
1337
|
-
|
|
1338
|
-
: 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))),
|
|
1339
1469
|
h(HealthPanel, {
|
|
1340
1470
|
key: "health",
|
|
1341
1471
|
t: t,
|
|
@@ -1381,8 +1511,11 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1381
1511
|
var _p = useState(null), configError = _p[0], setConfigError = _p[1];
|
|
1382
1512
|
var _q = useState(0), configTick = _q[0], setConfigTick = _q[1];
|
|
1383
1513
|
var _r = useState(null), health = _r[0], setHealth = _r[1];
|
|
1384
|
-
var _s = useState(null),
|
|
1514
|
+
var _s = useState(null), powerError = _s[0], setPowerError = _s[1];
|
|
1385
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];
|
|
1386
1519
|
var _u = useState(null), harness = _u[0], setHarness = _u[1];
|
|
1387
1520
|
var _v = useState(null), bundles = _v[0], setBundles = _v[1];
|
|
1388
1521
|
var _w = useState(false), refreshing = _w[0], setRefreshing = _w[1];
|
|
@@ -1561,9 +1694,17 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1561
1694
|
* had time to go down (the script waits two seconds), so the first success
|
|
1562
1695
|
* cannot be the old process answering.
|
|
1563
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
|
+
*/
|
|
1564
1703
|
var doRestart = function () {
|
|
1565
1704
|
setConfirming(false);
|
|
1566
|
-
|
|
1705
|
+
setPowerError(null);
|
|
1706
|
+
var previousStartedAt = data === null || data === undefined ? null : data.startedAt;
|
|
1707
|
+
setPower({ mode: "restart", phase: "prepare", log: null });
|
|
1567
1708
|
fetch("/dsh-token-use/restart", { method: "POST", headers: { accept: "application/json" } })
|
|
1568
1709
|
.then(function (res) {
|
|
1569
1710
|
return res.json().then(function (json) {
|
|
@@ -1571,25 +1712,80 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1571
1712
|
return json;
|
|
1572
1713
|
});
|
|
1573
1714
|
})
|
|
1574
|
-
.then(function () {
|
|
1575
|
-
|
|
1715
|
+
.then(function (json) {
|
|
1716
|
+
setPower({ mode: "restart", phase: "stopping", log: json.logFile || null });
|
|
1576
1717
|
setTimeout(function () {
|
|
1718
|
+
setPower({ mode: "restart", phase: "recovering", log: json.logFile || null });
|
|
1719
|
+
var deadline = Date.now() + 90000;
|
|
1577
1720
|
var timer = setInterval(function () {
|
|
1578
|
-
|
|
1721
|
+
if (Date.now() > deadline) {
|
|
1722
|
+
clearInterval(timer);
|
|
1723
|
+
setPower({ mode: "restart", phase: "timeout", log: json.logFile || null });
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1579
1726
|
fetch("/dsh-token-use", { cache: "no-store" })
|
|
1580
|
-
.then(function (res) {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
+
}
|
|
1585
1735
|
})
|
|
1586
1736
|
.catch(function () {});
|
|
1587
|
-
|
|
1588
|
-
|
|
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);
|
|
1589
1784
|
}, 6000);
|
|
1590
1785
|
})
|
|
1591
1786
|
.catch(function (err) {
|
|
1592
|
-
|
|
1787
|
+
setPower(null);
|
|
1788
|
+
setPowerError({ kind: "shutdown", error: String(err) });
|
|
1593
1789
|
});
|
|
1594
1790
|
};
|
|
1595
1791
|
|
|
@@ -1640,6 +1836,11 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1640
1836
|
}
|
|
1641
1837
|
metaParts.push(t("updated").replace("{time}", fmtTime(data.updatedAt)));
|
|
1642
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
|
+
};
|
|
1643
1844
|
var rangeItems = [
|
|
1644
1845
|
{ id: "day", label: t("filterDay") },
|
|
1645
1846
|
{ id: "month", label: t("filterMonth") },
|
|
@@ -1739,11 +1940,13 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1739
1940
|
config: config,
|
|
1740
1941
|
error: configError,
|
|
1741
1942
|
health: health,
|
|
1742
|
-
restart: restart,
|
|
1743
1943
|
harness: harness,
|
|
1744
1944
|
bundles: bundles,
|
|
1745
1945
|
onHealth: runHealth,
|
|
1746
1946
|
onRestart: function () { setConfirming(true); },
|
|
1947
|
+
onShutdown: function () { setConfirmingShutdown(true); },
|
|
1948
|
+
busy: power !== null,
|
|
1949
|
+
powerError: powerError,
|
|
1747
1950
|
refreshing: refreshing,
|
|
1748
1951
|
onRefresh: refreshConfig
|
|
1749
1952
|
})];
|
|
@@ -1756,10 +1959,21 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
|
|
|
1756
1959
|
title: t("restartTitle"),
|
|
1757
1960
|
closeLabel: t("close"),
|
|
1758
1961
|
footer: h("div", { className: "dshtu_modalFoot" },
|
|
1759
|
-
h(button, { variant: "ghost", size: "sm", onClick: function () { setConfirming(false); } }, t("
|
|
1962
|
+
h(button, { variant: "ghost", size: "sm", onClick: function () { setConfirming(false); } }, t("cancel")),
|
|
1760
1963
|
h(button, { variant: "primary", size: "sm", onClick: doRestart }, t("restartConfirm")))
|
|
1761
1964
|
}, h("div", { className: "dshtu_modalBody" },
|
|
1762
|
-
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 }),
|
|
1763
1977
|
h("div", { className: "dshtu_foot" },
|
|
1764
1978
|
stateDot === null ? null : h(stateDot, { state: scanState, size: 8 }),
|
|
1765
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'
|
|
@@ -379,65 +379,3 @@ function cleanMessage(message) {
|
|
|
379
379
|
if (text.length === 0) return 'unknown error'
|
|
380
380
|
return text.length > 140 ? `${text.slice(0, 137)}...` : text
|
|
381
381
|
}
|
|
382
|
-
|
|
383
|
-
/** Single-quote a shell word so paths with spaces survive. */
|
|
384
|
-
function shellQuote(value) {
|
|
385
|
-
return `'${String(value).replaceAll("'", `'\\''`)}'`
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* The detached restart script: wait for the response to land, stop the process
|
|
390
|
-
* that serves this request, then start the very same command again from the
|
|
391
|
-
* same directory. Relaunching by `argv` (not by a guessed CLI invocation)
|
|
392
|
-
* keeps custom flags, ports and working directories intact.
|
|
393
|
-
*
|
|
394
|
-
* @returns the script text, or undefined when the running command is unusable.
|
|
395
|
-
*/
|
|
396
|
-
export function restartScript({ pid, argv, cwd: workingDir, logFile }) {
|
|
397
|
-
const entry = argv[1]
|
|
398
|
-
const rest = argv.slice(2)
|
|
399
|
-
if (typeof entry !== 'string' || entry.length === 0 || typeof pid !== 'number') return undefined
|
|
400
|
-
const command = [process.execPath, entry, ...rest].map(shellQuote).join(' ')
|
|
401
|
-
return [
|
|
402
|
-
'#!/bin/sh',
|
|
403
|
-
'# Written by dsh-token-use: restart the dsh web service on demand.',
|
|
404
|
-
'sleep 2',
|
|
405
|
-
`kill -TERM ${pid} 2>/dev/null`,
|
|
406
|
-
'i=0',
|
|
407
|
-
`while kill -0 ${pid} 2>/dev/null && [ "$i" -lt 30 ]; do sleep 1; i=$((i+1)); done`,
|
|
408
|
-
`kill -9 ${pid} 2>/dev/null`,
|
|
409
|
-
'sleep 1',
|
|
410
|
-
`cd ${shellQuote(workingDir)} || exit 1`,
|
|
411
|
-
`nohup ${command} >>${shellQuote(logFile)} 2>&1 </dev/null &`,
|
|
412
|
-
'',
|
|
413
|
-
].join('\n')
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Write and fire the restart script, fully detached so it outlives the server
|
|
418
|
-
* it is about to stop.
|
|
419
|
-
*
|
|
420
|
-
* @returns `{ script, logFile }` for the response, or undefined when the
|
|
421
|
-
* current command line cannot be relaunched safely.
|
|
422
|
-
*/
|
|
423
|
-
export function scheduleRestart(dshHome) {
|
|
424
|
-
const dir = join(dshHome, 'dsh-token-use')
|
|
425
|
-
const logFile = join(dir, 'restart.log')
|
|
426
|
-
const scriptFile = join(dir, 'restart.sh')
|
|
427
|
-
const script = restartScript({
|
|
428
|
-
pid: process.pid,
|
|
429
|
-
argv: process.argv,
|
|
430
|
-
cwd: process.cwd(),
|
|
431
|
-
logFile,
|
|
432
|
-
})
|
|
433
|
-
if (script === undefined) return undefined
|
|
434
|
-
try {
|
|
435
|
-
mkdirSync(dir, { recursive: true })
|
|
436
|
-
writeFileSync(scriptFile, script, { mode: 0o700 })
|
|
437
|
-
} catch (error) {
|
|
438
|
-
return { error: String(error?.message ?? error) }
|
|
439
|
-
}
|
|
440
|
-
const child = spawn('sh', [scriptFile], { detached: true, stdio: 'ignore' })
|
|
441
|
-
child.unref()
|
|
442
|
-
return { script: scriptFile, logFile }
|
|
443
|
-
}
|
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",
|