dsh-update-status 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -53,6 +53,12 @@ window.__ModuleLoader__.load({
53
53
  getStatus: "dsh-update-status.get-status",
54
54
  checkUpdate: "dsh-update-status.check-update"
55
55
  };
56
+ /** Restart RPC remains on the same authenticated Connection `/api` channel. */
57
+ const RESTART_ENDPOINTS = {
58
+ status: "dsh-update-status.restart-status",
59
+ check: "dsh-update-status.restart-check",
60
+ request: "dsh-update-status.restart"
61
+ };
56
62
  //#endregion
57
63
  //#region src/client/i18n.ts
58
64
  const DICTIONARY = {
@@ -177,12 +183,88 @@ window.__ModuleLoader__.load({
177
183
  en: "Clipboard unavailable; the command is selected for long-press or copy."
178
184
  },
179
185
  "panel.commandNote": {
180
- zh: "仅复制,不会执行。请在运行 DSH 的那台电脑的终端执行;完成后由你自行重启 DSH。",
181
- en: "Copy only — nothing runs here. Execute it in a terminal on the computer running DSH, then restart DSH yourself."
186
+ zh: "仅复制,不会执行。请在运行 DSH 的那台电脑的终端执行;完成后仅在已安装用户级监督服务时使用本面板的重启,否则由你自行重启 DSH。",
187
+ en: "Copy only — nothing runs here. Execute it in a terminal on the computer running DSH, then use this panel’s restart only when user-level supervision is installed; otherwise restart DSH yourself."
182
188
  },
183
189
  "panel.readOnly": {
184
- zh: "阶段 1 仅提示:本插件不会安装、重启、回滚或替换任何文件。",
185
- en: "Phase 1 is advisory only: this plugin never installs, restarts, rolls back, or replaces files."
190
+ zh: "插件不会安装、升级、回滚或替换 DSH 文件;只有当前 DSH 已由用户显式安装的后台服务监督时,才允许安全重启。",
191
+ en: "The plugin never installs, upgrades, rolls back, or replaces DSH files. Safe restart is available only when the current DSH is supervised by a user-installed service."
192
+ },
193
+ "restart.title": {
194
+ zh: "安全重启 DSH Web",
195
+ en: "Safely restart DSH Web"
196
+ },
197
+ "restart.button": {
198
+ zh: "重启",
199
+ en: "Restart"
200
+ },
201
+ "restart.loading": {
202
+ zh: "读取重启状态…",
203
+ en: "Reading restart status…"
204
+ },
205
+ "restart.checking": {
206
+ zh: "检查活动任务…",
207
+ en: "Checking active work…"
208
+ },
209
+ "restart.confirm": {
210
+ zh: "确认重启",
211
+ en: "Confirm restart"
212
+ },
213
+ "restart.force": {
214
+ zh: "强制重启",
215
+ en: "Force restart"
216
+ },
217
+ "restart.cancel": {
218
+ zh: "取消",
219
+ en: "Cancel"
220
+ },
221
+ "restart.retry": {
222
+ zh: "重新检查",
223
+ en: "Check again"
224
+ },
225
+ "restart.confirmHint": {
226
+ zh: "再次点击“确认重启”后,页面会短暂断开;后台服务恢复新实例后将自动刷新。",
227
+ en: "Click Confirm restart again. The page disconnects briefly and refreshes once the supervised service starts a new instance."
228
+ },
229
+ "restart.activeWarning": {
230
+ zh: "检测到 {count} 个正在运行的任务。强制重启会中断它们。",
231
+ en: "{count} active work item(s) are running. A forced restart interrupts them."
232
+ },
233
+ "restart.waitingShort": {
234
+ zh: "重启中…",
235
+ en: "Restarting…"
236
+ },
237
+ "restart.waiting": {
238
+ zh: "正在等待后台服务恢复 DSH(已等待 {seconds} 秒)…",
239
+ en: "Waiting for the user service to restore DSH ({seconds}s elapsed)…"
240
+ },
241
+ "restart.timeout": {
242
+ zh: "60 秒内未检测到新 DSH 实例。请检查用户服务状态和日志;重新加载本页不会再次触发重启。",
243
+ en: "No new DSH instance was detected within 60 seconds. Check the user-service status and logs; reloading this page will not trigger another restart."
244
+ },
245
+ "restart.unavailable": {
246
+ zh: "当前 DSH 不是由已确认的用户级后台服务启动。为避免页面重启后无法恢复,按钮已禁用;请先按服务管理文档安装并接管 DSH。",
247
+ en: "This DSH is not running under a verified user-level service. Restart is disabled so the page cannot strand itself; install and activate supervision first."
248
+ },
249
+ "restart.unavailableDesktop": {
250
+ zh: "DSH Desktop 管理当前 Host;请退出并重新打开桌面应用。",
251
+ en: "DSH Desktop manages this Host. Quit and reopen the desktop app instead."
252
+ },
253
+ "restart.unavailableMismatch": {
254
+ zh: "检测到监督标记,但当前进程不符合对应平台服务的身份。为安全起见,重启已禁用。",
255
+ en: "A supervision marker exists, but this process does not match the platform service identity. Restart is disabled for safety."
256
+ },
257
+ "restart.unavailableActivity": {
258
+ zh: "无法完整读取正在运行的任务。为避免中断未能列出的工作,重启已禁用。",
259
+ en: "Active work could not be inspected completely. Restart is disabled to avoid interrupting work that could not be listed."
260
+ },
261
+ "restart.unavailableTimer": {
262
+ zh: "当前 Host 没有可用的安全退出调度器。为确保响应先送达浏览器,重启已禁用。",
263
+ en: "This Host has no safe exit scheduler. Restart is disabled so its response can reach the browser first."
264
+ },
265
+ "restart.unavailableStale": {
266
+ zh: "此页面连接的是过期 DSH 实例。请等待恢复或重新加载后再重试。",
267
+ en: "This page is connected to a stale DSH instance. Wait for recovery or reload before trying again."
186
268
  },
187
269
  "panel.error": {
188
270
  zh: "检查提示",
@@ -412,6 +494,85 @@ window.__ModuleLoader__.load({
412
494
  })]
413
495
  });
414
496
  }
497
+ function restartUnavailableText(reason) {
498
+ if (reason === "desktop") return t("restart.unavailableDesktop");
499
+ if (reason === "supervisor-mismatch") return t("restart.unavailableMismatch");
500
+ if (reason === "activity-unavailable") return t("restart.unavailableActivity");
501
+ if (reason === "timer-unavailable") return t("restart.unavailableTimer");
502
+ if (reason === "stale-instance") return t("restart.unavailableStale");
503
+ return t("restart.unavailable");
504
+ }
505
+ function RestartControl({ restart }) {
506
+ const snapshot = useObservable(restart);
507
+ React.useEffect(() => {
508
+ restart.refresh();
509
+ }, [restart]);
510
+ const unavailable = snapshot.status !== null && !snapshot.status.available;
511
+ const hasActivity = snapshot.activity.length > 0;
512
+ const waiting = snapshot.phase === "waiting";
513
+ const label = snapshot.phase === "unknown" ? t("restart.loading") : snapshot.phase === "checking" ? t("restart.checking") : snapshot.phase === "armed" ? hasActivity ? t("restart.force") : t("restart.confirm") : waiting ? t("restart.waitingShort") : snapshot.phase === "timeout" ? t("restart.retry") : snapshot.phase === "error" ? t("restart.retry") : t("restart.button");
514
+ const disabled = snapshot.phase === "unknown" || snapshot.phase === "checking" || waiting || unavailable;
515
+ const click = () => {
516
+ if (snapshot.phase === "armed") {
517
+ restart.request(hasActivity);
518
+ return;
519
+ }
520
+ if (snapshot.phase === "timeout" || snapshot.phase === "error") {
521
+ restart.check();
522
+ return;
523
+ }
524
+ restart.check();
525
+ };
526
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
527
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
528
+ className: "dus-action dus-restart",
529
+ type: "button",
530
+ disabled,
531
+ title: unavailable ? restartUnavailableText(snapshot.status?.unavailableReason) : t("restart.title"),
532
+ onClick: click,
533
+ children: label
534
+ }),
535
+ snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
536
+ className: "dus-action",
537
+ type: "button",
538
+ onClick: () => restart.cancel(),
539
+ children: t("restart.cancel")
540
+ }),
541
+ unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
542
+ className: "dus-restart-message",
543
+ role: "status",
544
+ children: restartUnavailableText(snapshot.status?.unavailableReason)
545
+ }),
546
+ snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
547
+ className: "dus-restart-message",
548
+ role: "status",
549
+ children: hasActivity ? t("restart.activeWarning", { count: String(snapshot.activity.length) }) : t("restart.confirmHint")
550
+ }),
551
+ hasActivity && snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
552
+ className: "dus-restart-work",
553
+ children: snapshot.activity.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
554
+ item.label,
555
+ " · ",
556
+ item.status
557
+ ] }, `${item.type}:${item.id}`))
558
+ }),
559
+ waiting && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
560
+ className: "dus-restart-message",
561
+ role: "status",
562
+ children: t("restart.waiting", { seconds: String(Math.floor(snapshot.elapsedMs / 1e3)) })
563
+ }),
564
+ snapshot.phase === "timeout" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
565
+ className: "dus-restart-message",
566
+ role: "alert",
567
+ children: t("restart.timeout")
568
+ }),
569
+ snapshot.phase === "error" && snapshot.error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
570
+ className: "dus-restart-message",
571
+ role: "alert",
572
+ children: snapshot.error
573
+ })
574
+ ] });
575
+ }
415
576
  function statusSummary(status, loading, error) {
416
577
  if (loading && status === null) return {
417
578
  kind: "warning",
@@ -655,21 +816,25 @@ window.__ModuleLoader__.load({
655
816
  }),
656
817
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
657
818
  className: "dus-actions",
658
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
659
- className: "dus-action",
660
- type: "button",
661
- disabled: snapshot.loading,
662
- onClick: () => {
663
- ui.status.refresh(preferences.cacheTtlMinutes);
664
- },
665
- children: snapshot.loading ? t("panel.checking") : t("panel.check")
666
- }), status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
667
- className: "dus-action",
668
- href: status.changelogUrl,
669
- target: "_blank",
670
- rel: "noreferrer",
671
- children: t("panel.releaseNotes")
672
- })]
819
+ children: [
820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
821
+ className: "dus-action",
822
+ type: "button",
823
+ disabled: snapshot.loading,
824
+ onClick: () => {
825
+ ui.status.refresh(preferences.cacheTtlMinutes);
826
+ },
827
+ children: snapshot.loading ? t("panel.checking") : t("panel.check")
828
+ }),
829
+ status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
830
+ className: "dus-action",
831
+ href: status.changelogUrl,
832
+ target: "_blank",
833
+ rel: "noreferrer",
834
+ children: t("panel.releaseNotes")
835
+ }),
836
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartControl, { restart: ui.restart })
837
+ ]
673
838
  }),
674
839
  hasUpdate && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandBlock, {
675
840
  command,
@@ -762,21 +927,29 @@ window.__ModuleLoader__.load({
762
927
  }),
763
928
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
764
929
  className: "dus-actions",
765
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
766
- className: "dus-action",
767
- type: "button",
768
- disabled: snapshot.loading,
769
- onClick: () => {
770
- ui.status.refresh(preferences.cacheTtlMinutes);
771
- },
772
- children: snapshot.loading ? t("panel.checking") : t("panel.check")
773
- }), status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
774
- className: "dus-action",
775
- href: status.changelogUrl,
776
- target: "_blank",
777
- rel: "noreferrer",
778
- children: t("panel.releaseNotes")
779
- })]
930
+ children: [
931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
932
+ className: "dus-action",
933
+ type: "button",
934
+ disabled: snapshot.loading,
935
+ onClick: () => {
936
+ ui.status.refresh(preferences.cacheTtlMinutes);
937
+ },
938
+ children: snapshot.loading ? t("panel.checking") : t("panel.check")
939
+ }),
940
+ status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
941
+ className: "dus-action",
942
+ href: status.changelogUrl,
943
+ target: "_blank",
944
+ rel: "noreferrer",
945
+ children: t("panel.releaseNotes")
946
+ }),
947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartControl, { restart: ui.restart })
948
+ ]
949
+ }),
950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
951
+ className: "dus-settings-hint",
952
+ children: t("panel.readOnly")
780
953
  })
781
954
  ]
782
955
  })
@@ -788,6 +961,71 @@ window.__ModuleLoader__.load({
788
961
  function errorMessage(error) {
789
962
  return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim().slice(0, 220) || "unknown error";
790
963
  }
964
+ function objectOf(value) {
965
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
966
+ }
967
+ function restartStatusOf(value) {
968
+ const record = objectOf(value);
969
+ if (record === void 0 || typeof record.instanceId !== "string" || record.instanceId === "" || typeof record.available !== "boolean") return void 0;
970
+ const supervisor = record.supervisor === null ? null : record.supervisor === "launchd" || record.supervisor === "systemd" || record.supervisor === "task-scheduler" || record.supervisor === "unknown" ? record.supervisor : void 0;
971
+ const unavailableReason = record.unavailableReason === null ? null : record.unavailableReason === "not-supervised" || record.unavailableReason === "desktop" || record.unavailableReason === "supervisor-mismatch" || record.unavailableReason === "activity-unavailable" || record.unavailableReason === "timer-unavailable" || record.unavailableReason === "stale-instance" ? record.unavailableReason : void 0;
972
+ if (supervisor === void 0 || unavailableReason === void 0) return void 0;
973
+ if (record.available && (supervisor === null || unavailableReason !== null)) return void 0;
974
+ if (!record.available && unavailableReason === null) return void 0;
975
+ return {
976
+ instanceId: record.instanceId,
977
+ available: record.available,
978
+ supervisor,
979
+ unavailableReason
980
+ };
981
+ }
982
+ function restartActivityOf(value) {
983
+ const record = objectOf(value);
984
+ if (record === void 0 || typeof record.hasActive !== "boolean" || !Array.isArray(record.items)) return void 0;
985
+ const items = [];
986
+ for (const raw of record.items) {
987
+ const item = objectOf(raw);
988
+ if (item === void 0 || item.type !== "agent" && item.type !== "job" && item.type !== "terminal" || typeof item.id !== "string" || typeof item.label !== "string" || typeof item.status !== "string") return void 0;
989
+ if (item.ownerSession !== void 0 && typeof item.ownerSession !== "string") return void 0;
990
+ items.push({
991
+ type: item.type,
992
+ id: item.id,
993
+ label: item.label,
994
+ status: item.status,
995
+ ...item.ownerSession === void 0 ? {} : { ownerSession: item.ownerSession }
996
+ });
997
+ }
998
+ if (record.hasActive !== items.length > 0) return void 0;
999
+ return {
1000
+ hasActive: record.hasActive,
1001
+ items
1002
+ };
1003
+ }
1004
+ function restartCheckOf(value) {
1005
+ const record = objectOf(value);
1006
+ if (record === void 0 || record.kind !== "ready" && record.kind !== "active-work" && record.kind !== "unavailable") return void 0;
1007
+ const status = restartStatusOf(record.status);
1008
+ if (status === void 0) return void 0;
1009
+ const activity = record.activity === void 0 ? void 0 : restartActivityOf(record.activity);
1010
+ if (record.activity !== void 0 && activity === void 0) return void 0;
1011
+ if (record.kind === "active-work" && (activity === void 0 || !activity.hasActive)) return void 0;
1012
+ if (record.kind !== "active-work" && activity !== void 0) return void 0;
1013
+ return {
1014
+ kind: record.kind,
1015
+ status,
1016
+ ...activity === void 0 ? {} : { activity }
1017
+ };
1018
+ }
1019
+ function restartRequestOf(value) {
1020
+ const record = objectOf(value);
1021
+ if (record === void 0 || record.kind !== "ready" && record.kind !== "active-work" && record.kind !== "unavailable" && record.kind !== "scheduled" && record.kind !== "in-progress") return void 0;
1022
+ if (record.kind === "scheduled" || record.kind === "in-progress") return typeof record.instanceId === "string" && record.instanceId !== "" ? {
1023
+ kind: record.kind,
1024
+ instanceId: record.instanceId
1025
+ } : void 0;
1026
+ const check = restartCheckOf(record);
1027
+ return check === void 0 ? void 0 : check;
1028
+ }
791
1029
  function stringOrNull(value) {
792
1030
  return typeof value === "string" ? value : null;
793
1031
  }
@@ -1064,6 +1302,292 @@ window.__ModuleLoader__.load({
1064
1302
  for (const listener of this.listeners) listener();
1065
1303
  }
1066
1304
  };
1305
+ const INITIAL_RESTART = {
1306
+ phase: "unknown",
1307
+ status: null,
1308
+ activity: [],
1309
+ elapsedMs: 0,
1310
+ error: null
1311
+ };
1312
+ /**
1313
+ * Restart state belongs to the client entry, not a modal component, so closing
1314
+ * the update panel cannot strand a confirmed restart half-way through recovery.
1315
+ * It starts no timer until the user confirms a restart request.
1316
+ */
1317
+ var RestartStore = class {
1318
+ connection;
1319
+ snapshot = INITIAL_RESTART;
1320
+ listeners = /* @__PURE__ */ new Set();
1321
+ timer;
1322
+ generation = 0;
1323
+ stopped = false;
1324
+ schedule;
1325
+ cancelTimer;
1326
+ requestTimeout;
1327
+ cancelRequestTimeout;
1328
+ liveness;
1329
+ reload;
1330
+ pendingAbort;
1331
+ reloaded = false;
1332
+ constructor(connection, options = {}) {
1333
+ this.connection = connection;
1334
+ this.schedule = options.schedule ?? ((callback, delay) => window.setTimeout(callback, delay));
1335
+ this.cancelTimer = options.cancel ?? ((handle) => {
1336
+ window.clearTimeout(handle);
1337
+ });
1338
+ this.requestTimeout = options.requestTimeout ?? ((callback, delay) => window.setTimeout(callback, delay));
1339
+ this.cancelRequestTimeout = options.cancelRequestTimeout ?? ((handle) => {
1340
+ window.clearTimeout(handle);
1341
+ });
1342
+ this.liveness = options.liveness ?? (async () => {
1343
+ try {
1344
+ const response = await fetch(new URL(".", window.location.href), {
1345
+ method: "HEAD",
1346
+ credentials: "same-origin",
1347
+ cache: "no-store"
1348
+ });
1349
+ return response.status === 200 || response.status === 401;
1350
+ } catch {
1351
+ return false;
1352
+ }
1353
+ });
1354
+ this.reload = options.reload ?? (() => window.location.reload());
1355
+ }
1356
+ getSnapshot = () => this.snapshot;
1357
+ subscribe = (listener) => {
1358
+ this.listeners.add(listener);
1359
+ return () => {
1360
+ this.listeners.delete(listener);
1361
+ };
1362
+ };
1363
+ stop() {
1364
+ this.stopped = true;
1365
+ this.generation += 1;
1366
+ if (this.timer !== void 0) this.cancelTimer(this.timer);
1367
+ this.timer = void 0;
1368
+ this.pendingAbort?.abort();
1369
+ this.pendingAbort = void 0;
1370
+ this.listeners.clear();
1371
+ }
1372
+ /** Read capability on a visible surface; this is not a restart and never polls. */
1373
+ async refresh() {
1374
+ if (this.snapshot.phase === "waiting") return;
1375
+ const raw = await this.call(RESTART_ENDPOINTS.status, {});
1376
+ if (raw === void 0) return;
1377
+ const status = restartStatusOf(raw);
1378
+ if (status === void 0) {
1379
+ this.publish({
1380
+ ...this.snapshot,
1381
+ phase: "error",
1382
+ error: "invalid restart-status payload"
1383
+ });
1384
+ return;
1385
+ }
1386
+ this.publish({
1387
+ phase: status.available ? "idle" : "idle",
1388
+ status,
1389
+ activity: [],
1390
+ elapsedMs: 0,
1391
+ error: null
1392
+ });
1393
+ }
1394
+ /** First click checks active work and arms the second, destructive action. */
1395
+ async check() {
1396
+ this.publish({
1397
+ ...this.snapshot,
1398
+ phase: "checking",
1399
+ error: null
1400
+ });
1401
+ const raw = await this.call(RESTART_ENDPOINTS.check, {});
1402
+ if (raw === void 0) return;
1403
+ const checked = restartCheckOf(raw);
1404
+ if (checked === void 0) {
1405
+ this.publish({
1406
+ ...this.snapshot,
1407
+ phase: "error",
1408
+ error: "invalid restart-check payload"
1409
+ });
1410
+ return;
1411
+ }
1412
+ if (checked.kind === "unavailable") {
1413
+ this.publish({
1414
+ phase: "idle",
1415
+ status: checked.status,
1416
+ activity: [],
1417
+ elapsedMs: 0,
1418
+ error: null
1419
+ });
1420
+ return;
1421
+ }
1422
+ this.publish({
1423
+ phase: "armed",
1424
+ status: checked.status,
1425
+ activity: checked.activity?.items ?? [],
1426
+ elapsedMs: 0,
1427
+ error: null
1428
+ });
1429
+ }
1430
+ cancel() {
1431
+ if (this.snapshot.phase !== "armed" && this.snapshot.phase !== "error" && this.snapshot.phase !== "timeout") return;
1432
+ this.publish({
1433
+ ...this.snapshot,
1434
+ phase: this.snapshot.status === null ? "unknown" : "idle",
1435
+ activity: [],
1436
+ elapsedMs: 0,
1437
+ error: null
1438
+ });
1439
+ }
1440
+ /** Second click emits the Host request; `force` is only true after the activity list was shown. */
1441
+ async request(force) {
1442
+ const current = this.snapshot;
1443
+ if (current.phase !== "armed" || current.status === null || !current.status.available) return;
1444
+ this.publish({
1445
+ ...current,
1446
+ phase: "checking",
1447
+ error: null
1448
+ });
1449
+ const raw = await this.call(RESTART_ENDPOINTS.request, {
1450
+ force,
1451
+ expectedInstanceId: current.status.instanceId
1452
+ });
1453
+ if (raw === void 0) return;
1454
+ const result = restartRequestOf(raw);
1455
+ if (result === void 0) {
1456
+ this.publish({
1457
+ ...this.snapshot,
1458
+ phase: "error",
1459
+ error: "invalid restart payload"
1460
+ });
1461
+ return;
1462
+ }
1463
+ if (result.kind === "scheduled" || result.kind === "in-progress") {
1464
+ if (result.instanceId === void 0) {
1465
+ this.publish({
1466
+ ...this.snapshot,
1467
+ phase: "error",
1468
+ error: "restart response omitted instance identity"
1469
+ });
1470
+ return;
1471
+ }
1472
+ this.beginRecovery(result.instanceId);
1473
+ return;
1474
+ }
1475
+ if (result.kind === "active-work" || result.kind === "ready") {
1476
+ this.publish({
1477
+ phase: "armed",
1478
+ status: result.status ?? current.status,
1479
+ activity: result.activity?.items ?? [],
1480
+ elapsedMs: 0,
1481
+ error: null
1482
+ });
1483
+ return;
1484
+ }
1485
+ this.publish({
1486
+ phase: "idle",
1487
+ status: result.status ?? current.status,
1488
+ activity: [],
1489
+ elapsedMs: 0,
1490
+ error: null
1491
+ });
1492
+ }
1493
+ beginRecovery(previousInstanceId) {
1494
+ const generation = ++this.generation;
1495
+ const startedAt = Date.now();
1496
+ this.publish({
1497
+ ...this.snapshot,
1498
+ phase: "waiting",
1499
+ elapsedMs: 0,
1500
+ error: null
1501
+ });
1502
+ const attempt = async (delay) => {
1503
+ if (this.stopped || generation !== this.generation) return;
1504
+ const elapsedMs = Date.now() - startedAt;
1505
+ if (elapsedMs >= 6e4) {
1506
+ this.publish({
1507
+ ...this.snapshot,
1508
+ phase: "timeout",
1509
+ elapsedMs,
1510
+ error: null
1511
+ });
1512
+ return;
1513
+ }
1514
+ this.timer = this.schedule(() => {
1515
+ (async () => {
1516
+ if (this.stopped || generation !== this.generation) return;
1517
+ const raw = await this.call(RESTART_ENDPOINTS.status, {}, false, 4e3);
1518
+ if (this.stopped || generation !== this.generation) return;
1519
+ const status = raw === void 0 ? void 0 : restartStatusOf(raw);
1520
+ const nextElapsed = Date.now() - startedAt;
1521
+ if (status !== void 0 && status.instanceId !== previousInstanceId) {
1522
+ this.publish({
1523
+ phase: "waiting",
1524
+ status,
1525
+ activity: [],
1526
+ elapsedMs: nextElapsed,
1527
+ error: null
1528
+ });
1529
+ if (!this.reloaded) {
1530
+ this.reloaded = true;
1531
+ this.reload();
1532
+ }
1533
+ return;
1534
+ }
1535
+ if (raw === void 0 && await this.liveness()) {
1536
+ if (!this.reloaded) {
1537
+ this.reloaded = true;
1538
+ this.reload();
1539
+ }
1540
+ return;
1541
+ }
1542
+ this.publish({
1543
+ ...this.snapshot,
1544
+ phase: "waiting",
1545
+ elapsedMs: nextElapsed
1546
+ });
1547
+ await attempt(Math.min(delay * 2, 1e4));
1548
+ })();
1549
+ }, delay);
1550
+ };
1551
+ attempt(1e3);
1552
+ }
1553
+ async call(endpoint, payload, surfaceError = true, timeoutMs) {
1554
+ const rpc = this.connection.rpc;
1555
+ if (rpc === void 0 || typeof rpc.call !== "function") {
1556
+ if (surfaceError) this.publish({
1557
+ ...this.snapshot,
1558
+ phase: "error",
1559
+ error: "DSH connection RPC is unavailable"
1560
+ });
1561
+ return;
1562
+ }
1563
+ const controller = timeoutMs === void 0 ? void 0 : new AbortController();
1564
+ if (controller !== void 0) this.pendingAbort = controller;
1565
+ let timeout;
1566
+ if (controller !== void 0 && timeoutMs !== void 0) timeout = this.requestTimeout(() => controller.abort(), timeoutMs);
1567
+ try {
1568
+ const raw = await rpc.call(UPDATE_STATUS_CHANNEL, endpoint, payload, controller?.signal);
1569
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid restart RPC response");
1570
+ const envelope = raw;
1571
+ if (envelope.ok !== true) throw new Error(typeof envelope.error?.message === "string" ? envelope.error.message : "restart RPC failed");
1572
+ return envelope.value;
1573
+ } catch (error) {
1574
+ if (surfaceError) this.publish({
1575
+ ...this.snapshot,
1576
+ phase: "error",
1577
+ error: errorMessage(error)
1578
+ });
1579
+ return;
1580
+ } finally {
1581
+ if (timeout !== void 0) this.cancelRequestTimeout(timeout);
1582
+ if (this.pendingAbort === controller) this.pendingAbort = void 0;
1583
+ }
1584
+ }
1585
+ publish(next) {
1586
+ if (this.stopped) return;
1587
+ this.snapshot = next;
1588
+ for (const listener of this.listeners) listener();
1589
+ }
1590
+ };
1067
1591
  /**
1068
1592
  * The one settings namespace, which on DSH 0.1.7 IS the Loader entry id in
1069
1593
  * `cordis.patch.yml`: the Host half's volatile `Config` fields are projected as
@@ -1543,6 +2067,12 @@ window.__ModuleLoader__.load({
1543
2067
  .dus-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
1544
2068
  .dus-action{background:var(--dsw-alias-button-floating-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:inherit;cursor:pointer;font-size:12px;line-height:30px;min-height:32px;padding:0 10px;touch-action:manipulation}
1545
2069
  .dus-action:disabled{cursor:wait;opacity:.65}
2070
+ .dus-restart{border-color:var(--dsw-alias-state-warn-primary);color:var(--dsw-alias-state-warn-primary)}
2071
+ .dus-restart-message{color:var(--dsw-alias-label-secondary);flex-basis:100%;font-size:11px;line-height:1.45;margin:0}
2072
+ .dus-restart-work{color:var(--dsw-alias-label-secondary);flex-basis:100%;font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding-left:18px}
2073
+ /* A job label is a whole shell command: clamp it so one long command cannot push
2074
+ the panel's buttons off a 390px card. */
2075
+ .dus-restart-work li{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
1546
2076
  .dus-command-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;margin:0 0 6px}
1547
2077
  .dus-command{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;display:block;font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding:9px;tab-size:2;user-select:text;white-space:pre-wrap}
1548
2078
  .dus-command:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
@@ -1586,12 +2116,16 @@ window.__ModuleLoader__.load({
1586
2116
  }
1587
2117
  }
1588
2118
  function apply(ctx) {
1589
- const status = new StatusStore(connectionOf(ctx));
2119
+ const connection = connectionOf(ctx);
2120
+ const status = new StatusStore(connection);
1590
2121
  const preferences = new PreferencesStore();
2122
+ const panel = new PanelStore();
2123
+ const restart = new RestartStore(connection);
1591
2124
  const ui = {
1592
2125
  status,
1593
2126
  preferences,
1594
- panel: new PanelStore()
2127
+ panel,
2128
+ restart
1595
2129
  };
1596
2130
  const disposers = [];
1597
2131
  let stopped = false;
@@ -1644,6 +2178,7 @@ window.__ModuleLoader__.load({
1644
2178
  dispose();
1645
2179
  } catch {}
1646
2180
  channel.dispose();
2181
+ restart.stop();
1647
2182
  status.stop();
1648
2183
  disposeStyles();
1649
2184
  };
@@ -1668,7 +2203,10 @@ window.__ModuleLoader__.load({
1668
2203
  });
1669
2204
  } catch {}
1670
2205
  if (typeof ctx.on === "function") try {
1671
- own(ctx.on("connection/reset", () => channel.reload()));
2206
+ own(ctx.on("connection/reset", () => {
2207
+ channel.reload();
2208
+ restart.refresh();
2209
+ }));
1672
2210
  } catch {}
1673
2211
  ctx.slots.inject("sidebar.brand.name", () => {
1674
2212
  let registration;