@huaqiu/dsh-tool-plugin-update 0.6.16 → 0.6.18

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
@@ -639,6 +639,37 @@ window.__ModuleLoader__.load({
639
639
  function fetchJobStatus() {
640
640
  return request("/status").then(normalizeJobStatus);
641
641
  }
642
+ /**
643
+ * Normalize `GET /activity`. Unknown/missing fields degrade to a usable
644
+ * snapshot: a `bootId` empty string simply never triggers a re-attach, and an
645
+ * unparseable age reads as 0 rather than "stalled".
646
+ */
647
+ function normalizeActivity(raw) {
648
+ const r = raw !== null && typeof raw === "object" ? raw : {};
649
+ const str = (v) => typeof v === "string" ? v : "";
650
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
651
+ return {
652
+ bootId: str(r.bootId),
653
+ startedAt: str(r.startedAt),
654
+ now: str(r.now),
655
+ activeJob: normalizeJob(r.activeJob),
656
+ job: normalizeJob(r.job),
657
+ phaseAgeMs: Math.max(0, num(r.phaseAgeMs)),
658
+ stalled: r.stalled === true,
659
+ setVersion: typeof r.setVersion === "string" && r.setVersion.length > 0 ? r.setVersion : null,
660
+ actionableCount: num(r.actionableCount)
661
+ };
662
+ }
663
+ /**
664
+ * GET the re-attach snapshot, optionally naming the job being watched.
665
+ *
666
+ * Used instead of `/status` whenever the panel is following a job it started:
667
+ * it answers "what happened to MY job" across a DSH restart, and each read lets
668
+ * hq-edge settle a job that its own process could not finish.
669
+ */
670
+ function fetchActivity(jobId) {
671
+ return request(`/activity${jobId ? `?job=${encodeURIComponent(jobId)}` : ""}`).then(normalizeActivity);
672
+ }
642
673
  //#endregion
643
674
  //#region src/client/copy.ts
644
675
  /**
@@ -812,6 +843,7 @@ window.__ModuleLoader__.load({
812
843
  ctaNoSet: "暂无可安装的更新",
813
844
  footerCount: (n) => `准备安装 ${n} 项兼容插件`,
814
845
  footerBusy: "后台处理中,请勿关闭 DSH",
846
+ footerConfirming: "正在确认安装结果…",
815
847
  footerFailed: "无法确认插件版本兼容性",
816
848
  footerAllGood: "所有插件与运行时均正常",
817
849
  footerRuntimeOnly: "运行时更新需在上方单独确认",
@@ -847,6 +879,8 @@ window.__ModuleLoader__.load({
847
879
  "重启"
848
880
  ],
849
881
  activeCloseNote: "关闭此面板不会取消更新,完成后将自动重启。",
882
+ stalledNote: "进度已停止回执,正在向 HQ Edge 确认安装结果…",
883
+ stalledAction: "立即确认",
850
884
  rollbackTitle: "上次 HQ Edge 运行时更新已自动回滚",
851
885
  failedTitle: "上次 HQ Edge 运行时更新失败",
852
886
  rollbackBody: (oldVer, code) => `系统已安全恢复至旧版本 (v${oldVer})。代码: ${code}`,
@@ -1023,6 +1057,7 @@ window.__ModuleLoader__.load({
1023
1057
  ctaNoSet: "Nothing to install yet",
1024
1058
  footerCount: (n) => `Ready to install ${n} compatible plugin${n === 1 ? "" : "s"}`,
1025
1059
  footerBusy: "Running in the background — keep DSH open",
1060
+ footerConfirming: "Confirming the install result…",
1026
1061
  footerFailed: "Cannot confirm plugin compatibility",
1027
1062
  footerAllGood: "All plugins and the runtime are current",
1028
1063
  footerRuntimeOnly: "Confirm the runtime upgrade on its card",
@@ -1058,6 +1093,8 @@ window.__ModuleLoader__.load({
1058
1093
  "Restart"
1059
1094
  ],
1060
1095
  activeCloseNote: "Closing this panel does not cancel the update; DSH restarts when it finishes.",
1096
+ stalledNote: "Progress has stopped reporting — confirming the result with HQ Edge…",
1097
+ stalledAction: "Check now",
1061
1098
  rollbackTitle: "Last HQ Edge runtime update rolled back automatically",
1062
1099
  failedTitle: "Last HQ Edge runtime update failed",
1063
1100
  rollbackBody: (oldVer, code) => `Safely restored to the previous version (v${oldVer}). Code: ${code}`,
@@ -1450,11 +1487,19 @@ window.__ModuleLoader__.load({
1450
1487
  * The active job card: title (with the target version) + percent, a
1451
1488
  * determinate bar when the server publishes one, phase + telemetry, the honest
1452
1489
  * "closing does not cancel" note, and the 4-step pipeline.
1490
+ *
1491
+ * `stalled` marks a job the server has stopped advancing. That is the normal
1492
+ * shape of "the set being installed contains this very plugin": activating it
1493
+ * restarts DSH, so the process that owned the job is gone and the last thing
1494
+ * the page saw was 100 % on the final step. Instead of animating a bar that
1495
+ * will never move, the card says it is confirming the outcome and offers a
1496
+ * retry — and the next `/activity` read settles it from the installed set.
1453
1497
  */
1454
- function activeJobView(job, copy) {
1498
+ function activeJobView(job, copy, opts = {}) {
1455
1499
  const section = el("section", "hqpu-group");
1456
1500
  section.dataset.group = "active-job";
1457
1501
  const card = el("div", "hqpu-active");
1502
+ if (opts.stalled === true) card.dataset.stalled = "1";
1458
1503
  const top = el("div", "hqpu-active-top");
1459
1504
  const titleWrap = el("div", "hqpu-active-title");
1460
1505
  const pulse = el("span", "hqpu-pulse-dot");
@@ -1487,6 +1532,14 @@ window.__ModuleLoader__.load({
1487
1532
  card.append(meta);
1488
1533
  card.append(el("div", "hqpu-active-versions", job.previousVersion !== null && job.previousVersion.length > 0 ? `${job.previousVersion} → ${job.targetVersion}` : job.targetVersion));
1489
1534
  card.append(el("p", "hqpu-active-note", job.kind === "runtime" ? copy.job.willSwapRuntime : copy.ui.activeCloseNote));
1535
+ if (opts.stalled === true) {
1536
+ const stalled = el("div", "hqpu-active-stalled");
1537
+ stalled.append(el("span", "", copy.ui.stalledNote));
1538
+ const recheck = linkButton("hqpu-link", copy.ui.stalledAction, () => opts.onRecheck?.());
1539
+ recheck.setAttribute("data-stalled-recheck", "");
1540
+ stalled.append(recheck);
1541
+ card.append(stalled);
1542
+ }
1490
1543
  const current = stepIndex(job.phase);
1491
1544
  const stepsRow = el("div", "hqpu-job-steps4");
1492
1545
  copy.ui.jobSteps.forEach((label, index) => {
@@ -2024,6 +2077,7 @@ window.__ModuleLoader__.load({
2024
2077
  return;
2025
2078
  }
2026
2079
  runtimeAccepted = true;
2080
+ store.watchJob?.(result.jobId ?? null);
2027
2081
  showToast(copy.runtime.updateAccepted, "success");
2028
2082
  store.fetchStatus();
2029
2083
  } catch (err) {
@@ -2152,7 +2206,12 @@ window.__ModuleLoader__.load({
2152
2206
  function renderJobSection() {
2153
2207
  jobSection.textContent = "";
2154
2208
  const active = store.activeJob();
2155
- if (active !== null && !TERMINAL_PHASES.has(active.phase)) jobSection.append(activeJobView(active, copy));
2209
+ if (active !== null && !TERMINAL_PHASES.has(active.phase)) jobSection.append(activeJobView(active, copy, {
2210
+ stalled: store.stalled?.() === true,
2211
+ onRecheck: () => {
2212
+ store.fetchStatus();
2213
+ }
2214
+ }));
2156
2215
  }
2157
2216
  function renderBanners(report, jobActive) {
2158
2217
  bannerSection.textContent = "";
@@ -2285,7 +2344,7 @@ window.__ModuleLoader__.load({
2285
2344
  if (busy) {
2286
2345
  ctaLabel.textContent = copy.ui.ctaInstalling;
2287
2346
  cta.disabled = true;
2288
- footCount.textContent = copy.ui.footerBusy;
2347
+ footCount.textContent = store.stalled?.() === true ? copy.ui.footerConfirming : copy.ui.footerBusy;
2289
2348
  } else if (report === null) {
2290
2349
  ctaLabel.textContent = copy.ui.ctaNoSet;
2291
2350
  cta.disabled = true;
@@ -2453,6 +2512,7 @@ window.__ModuleLoader__.load({
2453
2512
  showToast(`${copy.installError}: ${result.error}`, "error");
2454
2513
  return;
2455
2514
  }
2515
+ store.watchJob?.(result.jobId ?? null);
2456
2516
  showToast(copy.installSuccess, "success");
2457
2517
  store.fetchStatus();
2458
2518
  } catch (err) {
@@ -3739,6 +3799,45 @@ window.__ModuleLoader__.load({
3739
3799
  line-height: 14px;
3740
3800
  color: var(--hq-faint);
3741
3801
  }
3802
+ /* A stalled job: the card keeps its shape but stops claiming progress. The
3803
+ * pulse stops, the bar dims, and the note line carries the retry. */
3804
+ .hqpu-active[data-stalled='1'] .hqpu-pulse-ring {
3805
+ animation: none;
3806
+ opacity: 0;
3807
+ }
3808
+ .hqpu-active[data-stalled='1'] .hqpu-pulse-dot::before {
3809
+ background: var(--hq-amber);
3810
+ }
3811
+ .hqpu-active[data-stalled='1'] .hqpu-active-percent {
3812
+ color: var(--hq-amber-ink);
3813
+ }
3814
+ .hqpu-active[data-stalled='1'] .hqpu-progress-fill {
3815
+ background: color-mix(in srgb, var(--hq-amber) 55%, var(--hq-border));
3816
+ transition: none;
3817
+ }
3818
+ /* The active step stops blinking too: nothing is advancing. */
3819
+ .hqpu-active[data-stalled='1'] .hqpu-job-step--active {
3820
+ color: var(--hq-amber-ink);
3821
+ animation: none;
3822
+ }
3823
+ .hqpu-active-stalled {
3824
+ display: flex;
3825
+ align-items: baseline;
3826
+ justify-content: space-between;
3827
+ gap: 8px;
3828
+ padding: 6px 8px;
3829
+ border-radius: 6px;
3830
+ background: color-mix(in srgb, var(--hq-amber) 10%, transparent);
3831
+ font-size: var(--hq-fs-micro);
3832
+ line-height: 14px;
3833
+ color: var(--hq-amber-ink);
3834
+ }
3835
+ .hqpu-active-stalled .hqpu-link {
3836
+ flex: none;
3837
+ font-size: var(--hq-fs-micro);
3838
+ color: var(--hq-amber-ink);
3839
+ text-decoration: underline;
3840
+ }
3742
3841
  .hqpu-job-steps4 {
3743
3842
  display: grid;
3744
3843
  grid-template-columns: repeat(4, 1fr);
@@ -4343,7 +4442,13 @@ window.__ModuleLoader__.load({
4343
4442
  tag.textContent = `${ENTRY_CSS_TEXT}\n${VIEW_CSS_TEXT}`;
4344
4443
  document.head.appendChild(tag);
4345
4444
  }
4346
- /** In-memory update-report store shared by the badge and the modal. */
4445
+ /**
4446
+ * In-memory update-report store shared by the badge and the modal.
4447
+ *
4448
+ * Exported for tests: the re-attach logic (watch a job by id, notice that the
4449
+ * server was replaced, stop claiming progress) is the part that cannot be
4450
+ * exercised through the view alone.
4451
+ */
4347
4452
  var UpdateStore = class {
4348
4453
  current = null;
4349
4454
  installingFlag = false;
@@ -4356,6 +4461,12 @@ window.__ModuleLoader__.load({
4356
4461
  pollTimer = null;
4357
4462
  pollBackoffMs = 1e3;
4358
4463
  pollInFlight = false;
4464
+ /** The job this panel started (or adopted) and is following by id. */
4465
+ watchedJobId = null;
4466
+ /** hq-edge process identity from the last `/activity` read. */
4467
+ activityBootId = null;
4468
+ stalledFlag = false;
4469
+ phaseAgeMsValue = 0;
4359
4470
  report() {
4360
4471
  return this.current;
4361
4472
  }
@@ -4368,6 +4479,24 @@ window.__ModuleLoader__.load({
4368
4479
  recentJobs() {
4369
4480
  return [...this.recent];
4370
4481
  }
4482
+ /** True when the server says the active job stopped advancing. */
4483
+ stalled() {
4484
+ return this.stalledFlag;
4485
+ }
4486
+ /** Milliseconds since the active job last advanced (server clock). */
4487
+ phaseAgeMs() {
4488
+ return this.phaseAgeMsValue;
4489
+ }
4490
+ /**
4491
+ * Follow a job by id. The panel calls this with the `jobId` an accept
4492
+ * response handed back, so every later read asks about THAT job — the only
4493
+ * way to learn its outcome when the install replaced this very plugin (the
4494
+ * page comes back to a server that has no "active job" left to report).
4495
+ */
4496
+ watchJob(id) {
4497
+ this.watchedJobId = id;
4498
+ if (id !== null) this.schedulePoll();
4499
+ }
4371
4500
  subscribe(listener) {
4372
4501
  this.listeners.add(listener);
4373
4502
  return () => {
@@ -4394,28 +4523,86 @@ window.__ModuleLoader__.load({
4394
4523
  const wasActive = this.active !== null;
4395
4524
  this.active = status.activeJob;
4396
4525
  this.recent = status.recentJobs;
4397
- if (wasActive && this.active === null) {
4526
+ if (this.active !== null) this.watchedJobId = this.active.id;
4527
+ if (wasActive && this.active === null) this.observeSettled();
4528
+ this.emit();
4529
+ this.schedulePoll();
4530
+ }
4531
+ /**
4532
+ * Adopt a `/activity` snapshot.
4533
+ *
4534
+ * Two things only this read can tell us:
4535
+ *
4536
+ * - `bootId` changed → hq-edge was recreated underneath the page. That is
4537
+ * what an install of this plugin's own package looks like from here: the
4538
+ * DSH process serving this route was replaced. Nothing in memory can be
4539
+ * trusted, so the installing flag is cleared and the report re-read.
4540
+ * - `stalled` → the active job has stopped advancing. The progress bar must
4541
+ * stop pretending; the panel shows a "confirming" note with a retry.
4542
+ */
4543
+ applyActivity(activity) {
4544
+ const rebooted = this.activityBootId !== null && this.activityBootId !== activity.bootId;
4545
+ this.activityBootId = activity.bootId;
4546
+ this.stalledFlag = activity.stalled;
4547
+ this.phaseAgeMsValue = activity.phaseAgeMs;
4548
+ const wasActive = this.active !== null;
4549
+ this.active = activity.activeJob;
4550
+ if (activity.job !== null) this.upsertRecent(activity.job);
4551
+ if (rebooted) {
4398
4552
  this.installingFlag = false;
4553
+ this.stalledFlag = false;
4554
+ this.phaseAgeMsValue = 0;
4399
4555
  this.refresh();
4400
4556
  }
4557
+ if (wasActive && this.active === null) this.observeSettled();
4401
4558
  this.emit();
4402
4559
  this.schedulePoll();
4403
4560
  }
4404
4561
  /**
4405
- * Fetch the durable job status. Called on mount, on panel open (BEFORE
4562
+ * The job we were following reached a terminal phase (or vanished from the
4563
+ * active slot). Clear the transient flags and re-read the report — it still
4564
+ * names the versions that job installed.
4565
+ */
4566
+ observeSettled() {
4567
+ this.installingFlag = false;
4568
+ this.stalledFlag = false;
4569
+ this.phaseAgeMsValue = 0;
4570
+ this.watchedJobId = null;
4571
+ this.refresh();
4572
+ }
4573
+ /** Keep one job fresh in the local history without duplicating it. */
4574
+ upsertRecent(job) {
4575
+ const rest = this.recent.filter((entry) => entry.id !== job.id);
4576
+ this.recent = [job, ...rest].slice(0, 20);
4577
+ }
4578
+ /**
4579
+ * Fetch the durable job state. Called on mount, on panel open (BEFORE
4406
4580
  * discovery) and by the poll loop. A failure (older hq-edge with no
4407
4581
  * `/status`, or a transient HTTP error) is swallowed: the last known job is
4408
4582
  * kept and the poller backs off and retries.
4409
4583
  */
4410
4584
  async fetchStatus() {
4411
4585
  try {
4412
- const status = await fetchJobStatus();
4586
+ await this.pollOnce();
4413
4587
  this.pollBackoffMs = 1e3;
4414
- this.applyStatus(status);
4415
4588
  } catch {
4416
4589
  this.schedulePoll();
4417
4590
  }
4418
4591
  }
4592
+ /**
4593
+ * One read. While a job is being followed, ask `/activity` about THAT job
4594
+ * (survives the restart an update of this plugin causes); otherwise fall back
4595
+ * to the plain `/status` list. An older hq-edge without `/activity` 404s and
4596
+ * the status read still answers.
4597
+ */
4598
+ async pollOnce() {
4599
+ const watched = this.watchedJobId ?? this.active?.id ?? null;
4600
+ if (watched !== null) try {
4601
+ this.applyActivity(await fetchActivity(watched));
4602
+ return;
4603
+ } catch {}
4604
+ this.applyStatus(await fetchJobStatus());
4605
+ }
4419
4606
  /** ~1 s poll while a job is active; stop on terminal; back off on error
4420
4607
  * without discarding the last known job; never overlap requests. */
4421
4608
  schedulePoll() {
@@ -4439,9 +4626,8 @@ window.__ModuleLoader__.load({
4439
4626
  }
4440
4627
  this.pollInFlight = true;
4441
4628
  try {
4442
- const status = await fetchJobStatus();
4629
+ await this.pollOnce();
4443
4630
  this.pollBackoffMs = 1e3;
4444
- this.applyStatus(status);
4445
4631
  } catch {
4446
4632
  this.pollBackoffMs = Math.min(5e3, Math.max(1e3, this.pollBackoffMs * 2));
4447
4633
  this.schedulePoll();
@@ -4587,6 +4773,7 @@ window.__ModuleLoader__.load({
4587
4773
  exports.UPDATE_ENTRY_ATTRIBUTE = UPDATE_ENTRY_ATTRIBUTE;
4588
4774
  exports.UPDATE_ENTRY_SELECTOR = UPDATE_ENTRY_SELECTOR;
4589
4775
  exports.UPDATE_ICON = UPDATE_ICON;
4776
+ exports.UpdateStore = UpdateStore;
4590
4777
  exports.apply = apply;
4591
4778
  exports.ensureDesignTokens = ensureDesignTokens;
4592
4779
  exports.inject = inject;