@huaqiu/dsh-tool-plugin-update 0.6.6 → 0.6.8

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
@@ -375,6 +375,108 @@ window.__ModuleLoader__.load({
375
375
  }
376
376
  return null;
377
377
  }
378
+ /** Phases after which a job never runs again — polling stops and a result view
379
+ * replaces the active-job view. */
380
+ const TERMINAL_PHASES = /* @__PURE__ */ new Set([
381
+ "done",
382
+ "failed",
383
+ "rolled-back",
384
+ "abandoned"
385
+ ]);
386
+ const PHASE_VALUES = /* @__PURE__ */ new Set([
387
+ "queued",
388
+ "resolving",
389
+ "downloading",
390
+ "verifying",
391
+ "staging",
392
+ "activating",
393
+ "awaiting-host",
394
+ "restarting",
395
+ "verifying-after-restart",
396
+ "done",
397
+ "failed",
398
+ "rolled-back",
399
+ "abandoned"
400
+ ]);
401
+ function asFiniteNumber(value) {
402
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
403
+ }
404
+ /** Defensively normalize one job payload from the wire into {@link UpdateJob}.
405
+ * Unknown/older servers may omit fields; a malformed `phase` falls back to
406
+ * `queued` rather than crashing the view. */
407
+ function normalizeJob(raw) {
408
+ if (raw === null || typeof raw !== "object") return null;
409
+ const r = raw;
410
+ if (typeof r.id !== "string" || r.id.length === 0) return null;
411
+ const phase = typeof r.phase === "string" && PHASE_VALUES.has(r.phase) ? r.phase : "queued";
412
+ const progressRaw = r.progress ?? {};
413
+ const percentRaw = asFiniteNumber(progressRaw.percent);
414
+ const progress = { percent: percentRaw === void 0 ? null : Math.min(100, Math.max(0, percentRaw)) };
415
+ const bytesDone = asFiniteNumber(progressRaw.bytesDone);
416
+ const bytesTotal = asFiniteNumber(progressRaw.bytesTotal);
417
+ const bytesPerSecond = asFiniteNumber(progressRaw.bytesPerSecond);
418
+ const etaSeconds = asFiniteNumber(progressRaw.etaSeconds);
419
+ if (bytesDone !== void 0) progress.bytesDone = bytesDone;
420
+ if (bytesTotal !== void 0) progress.bytesTotal = bytesTotal;
421
+ if (bytesPerSecond !== void 0) progress.bytesPerSecond = bytesPerSecond;
422
+ if (etaSeconds !== void 0) progress.etaSeconds = etaSeconds;
423
+ const job = {
424
+ id: r.id,
425
+ kind: r.kind === "runtime" ? "runtime" : "plugin-set",
426
+ targetVersion: typeof r.targetVersion === "string" ? r.targetVersion : "",
427
+ previousVersion: typeof r.previousVersion === "string" ? r.previousVersion : null,
428
+ startedAt: typeof r.startedAt === "string" ? r.startedAt : "",
429
+ updatedAt: typeof r.updatedAt === "string" ? r.updatedAt : "",
430
+ phase,
431
+ progress
432
+ };
433
+ if (typeof r.message === "string" && r.message.length > 0) job.message = r.message;
434
+ if (typeof r.logHint === "string" && r.logHint.length > 0) job.logHint = r.logHint;
435
+ if (Array.isArray(r.steps)) {
436
+ const steps = [];
437
+ for (const entry of r.steps) {
438
+ if (entry === null || typeof entry !== "object") continue;
439
+ const s = entry;
440
+ if (typeof s.name !== "string") continue;
441
+ const state = s.state === "running" || s.state === "installed" || s.state === "skipped" || s.state === "failed" ? s.state : "pending";
442
+ const step = {
443
+ name: s.name,
444
+ state
445
+ };
446
+ const pct = asFiniteNumber(s.percent);
447
+ if (pct !== void 0) step.percent = pct;
448
+ if (typeof s.message === "string" && s.message.length > 0) step.message = s.message;
449
+ steps.push(step);
450
+ }
451
+ if (steps.length > 0) job.steps = steps;
452
+ }
453
+ if (r.result !== null && typeof r.result === "object") {
454
+ const res = r.result;
455
+ const result = {
456
+ ok: res.ok === true,
457
+ installedVersion: typeof res.installedVersion === "string" ? res.installedVersion : null,
458
+ rolledBack: res.rolledBack === true,
459
+ finishedAt: typeof res.finishedAt === "string" ? res.finishedAt : ""
460
+ };
461
+ if (typeof res.errorCode === "string" && res.errorCode.length > 0) result.errorCode = res.errorCode;
462
+ if (typeof res.message === "string" && res.message.length > 0) result.message = res.message;
463
+ job.result = result;
464
+ }
465
+ return job;
466
+ }
467
+ /** Normalize a `GET /status` payload; an empty/absent body degrades to "no
468
+ * active job" rather than throwing. */
469
+ function normalizeJobStatus(raw) {
470
+ if (raw === null || typeof raw !== "object") return {
471
+ activeJob: null,
472
+ recentJobs: []
473
+ };
474
+ const r = raw;
475
+ return {
476
+ activeJob: normalizeJob(r.activeJob),
477
+ recentJobs: Array.isArray(r.recentJobs) ? r.recentJobs.map(normalizeJob).filter((j) => j !== null) : []
478
+ };
479
+ }
378
480
  async function request(path, init) {
379
481
  const res = await fetch(`${UPDATE_BASE}${path}`, {
380
482
  cache: "no-store",
@@ -459,87 +561,221 @@ window.__ModuleLoader__.load({
459
561
  body: JSON.stringify({ url })
460
562
  });
461
563
  }
462
- const COPY = {
463
- zh: {
464
- entryLabel: "插件更新",
465
- entryTooltip: "查看可用的插件更新",
466
- viewTitle: "插件更新",
467
- viewIntro: "检查并安装可用的插件更新",
468
- viewInfo: "在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。",
469
- refresh: "刷新",
470
- close: "关闭",
471
- loading: "正在读取插件更新…",
472
- empty: "没有可用的插件更新",
473
- checkFailed: "检查更新失败,请重试",
474
- installAll: (count) => `安装 ${count} 项更新`,
475
- installOne: "安装",
476
- installing: "正在安装…(DSH 将自动重启)",
477
- nothingToInstall: "暂无可安装的更新",
478
- ignore: "忽略此版本",
479
- unignore: "恢复提醒",
480
- ignoredOffer: "已忽略当前插件集更新",
481
- groupCount: (count) => `${count} 个插件`,
482
- group: {
483
- actionable: "待处理",
484
- blocked: "无法安装",
485
- ignored: "已忽略",
486
- current: "已是最新"
487
- },
488
- state: {
489
- current: "当前",
490
- update: "可更新",
491
- install: "待安装",
492
- ignored: "已忽略",
493
- blocked: "被阻止"
494
- },
495
- blockedReason: {
496
- host: "不适用于当前 EDA 主机",
497
- "hq-edge-version": "当前 HQ Edge 版本不兼容",
498
- "runtime-unknown": "HQ Edge 版本未知,无法校验"
499
- },
500
- runtime: {
501
- title: "HQ Edge 运行时",
502
- current: "当前版本",
503
- latest: "最新版本",
504
- set: "发布插件集",
505
- checkedAt: "上次检查",
506
- source: "更新源",
507
- unknown: "版本未知",
508
- notConfigured: "未配置更新源",
509
- note: "运行时更新由 EDA 安装器处理",
510
- update: "安装运行时更新",
511
- updateConfirm: (version) => `安装 HQ Edge 运行时 ${version}?HQ Edge 会先关闭并完成替换,然后重新启动。`,
512
- updating: "正在下载并校验运行时包,可能需要一分钟…",
513
- updateAccepted: "运行时更新已受理,HQ Edge 正在重启…",
514
- sourceHint: "运行时更新清单的下载地址,保存后立即生效",
515
- sourceEdit: "修改",
516
- sourceSave: "保存",
517
- sourceReset: "恢复默认",
518
- saved: "更新源已保存,正在重新检查更新…",
519
- saveFailed: "保存更新源失败",
520
- sourceInputLabel: "运行时更新清单地址",
521
- sourcePlaceholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json"
564
+ /**
565
+ * GET the durable job status: the currently active job (if any) plus a bounded
566
+ * list of recent jobs. This is what the panel reads BEFORE running discovery
567
+ * on open, so an in-flight install/runtime update is resumed instead of
568
+ * forgotten. An older hq-edge without the status route 404s — callers catch
569
+ * that and degrade to "no active job".
570
+ */
571
+ function fetchJobStatus() {
572
+ return request("/status").then(normalizeJobStatus);
573
+ }
574
+ //#endregion
575
+ //#region src/client/copy.ts
576
+ /**
577
+ * Localized copy for the plugin-update UI (browser half).
578
+ *
579
+ * zh is the fallback, matching the rest of the Huaqiu plugin family
580
+ * (`<html lang>` drives the resolution via the shared sidebar-entry locale
581
+ * helpers). The wording follows the shell's own plugin manager
582
+ * (`ui-plugin-manager/src/client/locales.ts`) so both read as one product.
583
+ *
584
+ * @module
585
+ */
586
+ const zh = {
587
+ entryLabel: "插件更新",
588
+ entryTooltip: "查看可用的插件更新",
589
+ viewTitle: "插件更新",
590
+ viewIntro: "检查并安装可用的插件更新",
591
+ viewInfo: "在这里查看插件集的更新。标注为「被阻止」的插件因主机或 HQ Edge 版本不兼容而无法安装;已是最新的插件无需操作。",
592
+ refresh: "刷新",
593
+ close: "关闭",
594
+ loading: "正在读取插件更新…",
595
+ empty: "没有可用的插件更新",
596
+ checkFailed: "检查更新失败,请重试",
597
+ installAll: (count) => `安装整套更新(${count} 项)`,
598
+ installOne: "安装整套",
599
+ installOneTip: "将安装整套 N 项更新并自动重启 DSH,而不仅是此插件",
600
+ installing: "正在安装…(DSH 将自动重启)",
601
+ retry: "重试",
602
+ checkedAt: (when) => `上次检查:${when}`,
603
+ checkFailedBanner: (message) => `检查更新失败:${message}`,
604
+ staleNote: "以下为上次成功检查的结果,可能已过期",
605
+ nothingToInstall: "暂无可安装的更新",
606
+ ignore: "忽略此版本",
607
+ unignore: "恢复提醒",
608
+ ignoredOffer: "已忽略当前插件集更新",
609
+ groupCount: (count) => `${count} 个插件`,
610
+ group: {
611
+ actionable: "待处理",
612
+ blocked: "无法安装",
613
+ ignored: "已忽略",
614
+ current: "已是最新"
615
+ },
616
+ state: {
617
+ current: "当前",
618
+ update: "可更新",
619
+ install: "待安装",
620
+ ignored: "已忽略",
621
+ blocked: "被阻止"
622
+ },
623
+ blockedReason: {
624
+ host: "不适用于当前 EDA 主机",
625
+ "hq-edge-version": "当前 HQ Edge 版本不兼容",
626
+ "runtime-unknown": "HQ Edge 版本未知,无法校验"
627
+ },
628
+ runtime: {
629
+ title: "HQ Edge 运行时",
630
+ current: "当前版本",
631
+ latest: "最新版本",
632
+ set: "发布插件集",
633
+ checkedAt: "上次检查",
634
+ source: "更新源",
635
+ unknown: "版本未知",
636
+ notConfigured: "未配置更新源",
637
+ note: "运行时更新由 EDA 安装器处理",
638
+ sizeLabel: "包大小",
639
+ update: "安装运行时更新",
640
+ updateConfirm: (version) => `安装 HQ Edge 运行时 ${version}?HQ Edge 会先关闭并完成替换,然后重新启动。`,
641
+ updating: "正在下载并校验运行时包…",
642
+ durationNote: "耗时取决于下载速度与包体大小;运行时包较大,替换后 HQ Edge 会自动重启。",
643
+ updateAccepted: "运行时更新已受理,HQ Edge 正在重启…",
644
+ size: (bytes) => `约 ${formatMb(bytes)}`,
645
+ lastResult: {
646
+ title: "上次运行时更新结果",
647
+ succeeded: "运行时更新成功",
648
+ failed: "运行时更新失败",
649
+ rolledBack: "运行时更新已回滚(已恢复旧版本)",
650
+ elevation: "系统级安装可能弹出 UAC 提权窗口;拒绝提权会导致替换失败并自动回滚到旧版本。"
522
651
  },
523
- pluginSource: {
524
- title: "插件更新源",
525
- hint: "插件更新清单的下载地址,保存后立即生效",
526
- edit: "修改",
527
- save: "保存",
528
- reset: "恢复默认",
529
- saved: "更新源已保存,正在重新检查更新…",
530
- saveFailed: "保存更新源失败",
531
- inputLabel: "插件更新清单地址",
532
- placeholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json"
652
+ sourceHint: "运行时更新清单的下载地址,保存后立即生效",
653
+ sourceEdit: "修改",
654
+ sourceSave: "保存",
655
+ sourceReset: "恢复默认",
656
+ saved: "更新源已保存,正在重新检查更新…",
657
+ saveFailed: "保存更新源失败",
658
+ sourceInputLabel: "运行时更新清单地址",
659
+ sourcePlaceholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json"
660
+ },
661
+ pluginSource: {
662
+ title: "插件更新源",
663
+ hint: "插件更新清单的下载地址,保存后立即生效",
664
+ edit: "修改",
665
+ save: "保存",
666
+ reset: "恢复默认",
667
+ saved: "更新源已保存,正在重新检查更新…",
668
+ saveFailed: "保存更新源失败",
669
+ inputLabel: "插件更新清单地址",
670
+ placeholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json"
671
+ },
672
+ originOverride: "自定义",
673
+ originConfig: "默认配置",
674
+ originUnset: "未配置",
675
+ lastError: "上次检查失败",
676
+ installError: "安装失败",
677
+ installSuccess: "更新已安装,正在重启 DSH…",
678
+ confirmInstall: "确认安装",
679
+ cancel: "取消",
680
+ job: {
681
+ kindPluginSet: "插件集更新",
682
+ kindRuntime: "HQ Edge 运行时更新",
683
+ phase: {
684
+ queued: "排队中",
685
+ resolving: "解析版本",
686
+ downloading: "下载中",
687
+ verifying: "校验完整性",
688
+ staging: "暂存文件",
689
+ activating: "启用插件集",
690
+ "awaiting-host": "等待主机替换运行时",
691
+ restarting: "正在重启",
692
+ "verifying-after-restart": "重启后校验",
693
+ done: "已完成",
694
+ failed: "失败",
695
+ "rolled-back": "已回滚",
696
+ abandoned: "已中断"
533
697
  },
534
- originOverride: "自定义",
535
- originConfig: "默认配置",
536
- originUnset: "未配置",
537
- lastError: "上次检查失败",
538
- installError: "安装失败",
539
- installSuccess: "更新已安装,正在重启 DSH…",
540
- confirmInstall: "确认安装",
541
- cancel: "取消"
698
+ willRestartDsh: "完成后 DSH 将自动重启,更新本身会在后台继续。",
699
+ willSwapRuntime: "HQ Edge 会下载完整运行时包并退出,由 EDA 主机替换后重启;系统级安装可能弹出 UAC 提权窗口。",
700
+ closeNote: "关闭此面板不会取消更新。",
701
+ bytes: (done, total) => `${done} / ${total}`,
702
+ speed: (v) => `${v}/s`,
703
+ eta: (s) => `剩余约 ${s}`,
704
+ nextDone: "正在完成,请稍候…",
705
+ resultDone: "更新已完成",
706
+ resultFailed: "更新失败",
707
+ resultRolledBack: "更新已回滚",
708
+ resultAbandoned: "更新未完成",
709
+ retry: "重新尝试(发起新的更新)",
710
+ recentTitle: "最近更新",
711
+ stepsTitle: "各组件进度",
712
+ logHint: (hint) => `日志标识:${hint}`
713
+ },
714
+ release: {
715
+ summaryTitle: "版本说明",
716
+ notesLink: "查看发布说明",
717
+ breakingTitle: "破坏性变更",
718
+ changesTitle: "更新内容",
719
+ advanced: "高级:更新源",
720
+ publishedAt: (when) => `发布于 ${when}`
542
721
  },
722
+ ui: {
723
+ title: "更新中心",
724
+ gearTooltip: "高级设置",
725
+ settingsTitle: "更新源配置 (高级)",
726
+ originDefault: "默认源",
727
+ originCustom: "自定义源",
728
+ pluginUrlLabel: "插件 Manifest URL",
729
+ runtimeUrlLabel: "运行时 Manifest URL",
730
+ saveAndRefresh: "保存并刷新",
731
+ checkedAgo: (ago) => `上次检查: ${ago}`,
732
+ checkedUnknown: "尚未检查更新",
733
+ agoJust: "刚刚",
734
+ agoMinutes: (n) => `${n} 分钟前`,
735
+ agoHours: (n) => `${n} 小时前`,
736
+ agoDays: (n) => `${n} 天前`,
737
+ jobPluginTitle: (v) => `正在升级全套插件 (v${v})`,
738
+ jobRuntimeTitle: (v) => `正在升级 HQ Edge 运行时 (v${v})`,
739
+ jobSteps: [
740
+ "下载",
741
+ "校验",
742
+ "替换",
743
+ "重启"
744
+ ],
745
+ rollbackTitle: "上次 HQ Edge 运行时更新已自动回滚",
746
+ failedTitle: "上次 HQ Edge 运行时更新失败",
747
+ rollbackBody: (oldVer, code) => `系统已安全恢复至旧版本 (v${oldVer})。代码: ${code}`,
748
+ failedBody: (oldVer, code) => `未能完成运行时替换 (旧版本 v${oldVer})。代码: ${code}`,
749
+ dismiss: "忽略",
750
+ successInline: "上次运行时更新成功",
751
+ runtimeEngine: "HQ Edge 核心运行时",
752
+ currentChip: (v) => `v${v} (当前)`,
753
+ newVersion: (v) => `新版本 v${v} 可用`,
754
+ breakingTag: "关键修复",
755
+ sizeLine: (size, summary) => `体积: ${size}${summary ? ` · ${summary}` : ""}`,
756
+ upgradeCore: "升级核心",
757
+ runtimeRestartNote: "升级运行时需自动重启 KiCad 进程服务",
758
+ pendingTitle: "待更新插件",
759
+ allChangelog: "展开全部更新日志",
760
+ changelogBtn: "日志",
761
+ changelogTitle: "更新内容:",
762
+ blockedBadge: "无法安装",
763
+ footerCount: (n) => `准备安装 ${n} 项兼容插件`,
764
+ footerSet: (v) => `版本集: v${v}`,
765
+ installAllSet: (v) => `一键更新全套插件 (v${v})`,
766
+ ignoreBtn: "忽略",
767
+ footnote: "更新将自动应用并重启 DSH 视图,不影响 EDA 图纸数据"
768
+ }
769
+ };
770
+ /** Compact a byte count for display: "约 12.3 MB" / "约 480 MB". */
771
+ function formatMb(bytes) {
772
+ if (!Number.isFinite(bytes) || bytes <= 0) return "";
773
+ const mb = bytes / 1048576;
774
+ if (mb >= 100) return `${Math.round(mb)} MB`;
775
+ return `${mb.toFixed(1)} MB`;
776
+ }
777
+ const COPY = {
778
+ zh,
543
779
  en: {
544
780
  entryLabel: "Plugin Updates",
545
781
  entryTooltip: "Review available plugin updates",
@@ -551,9 +787,14 @@ window.__ModuleLoader__.load({
551
787
  loading: "Reading plugin updates…",
552
788
  empty: "No plugin updates available",
553
789
  checkFailed: "Failed to check for updates — please retry",
554
- installAll: (count) => `Install ${count} update${count === 1 ? "" : "s"}`,
555
- installOne: "Install",
790
+ installAll: (count) => `Install whole set (${count})`,
791
+ installOne: "Install set",
792
+ installOneTip: "Installs the whole set of N updates and restarts DSH — not just this plugin",
556
793
  installing: "Installing… (DSH will restart)",
794
+ retry: "Retry",
795
+ checkedAt: (when) => `Last checked: ${when}`,
796
+ checkFailedBanner: (message) => `Update check failed: ${message}`,
797
+ staleNote: "Showing the last successful check — it may be stale",
557
798
  nothingToInstall: "Nothing to install right now",
558
799
  ignore: "Ignore this release",
559
800
  unignore: "Show again",
@@ -587,10 +828,20 @@ window.__ModuleLoader__.load({
587
828
  unknown: "Version unknown",
588
829
  notConfigured: "No update source configured",
589
830
  note: "Runtime updates are handled by the EDA installer",
831
+ sizeLabel: "Package size",
590
832
  update: "Install runtime update",
591
833
  updateConfirm: (version) => `Install HQ Edge runtime ${version}? HQ Edge shuts down to swap the runtime, then restarts.`,
592
- updating: "Downloading and verifying the runtime bundle — this can take a minute…",
834
+ updating: "Downloading and verifying the runtime bundle…",
835
+ durationNote: "Duration depends on download speed and package size; the runtime bundle is large, and HQ Edge restarts after the swap.",
593
836
  updateAccepted: "Runtime update accepted — HQ Edge is restarting…",
837
+ size: (bytes) => `~ ${formatMb(bytes)}`,
838
+ lastResult: {
839
+ title: "Last runtime update result",
840
+ succeeded: "Runtime update succeeded",
841
+ failed: "Runtime update failed",
842
+ rolledBack: "Runtime update rolled back (previous version restored)",
843
+ elevation: "A system-wide install may show a UAC elevation prompt; declining it fails the swap and rolls back to the previous version."
844
+ },
594
845
  sourceHint: "Where the runtime update manifest is fetched from; saved changes take effect immediately",
595
846
  sourceEdit: "Edit",
596
847
  sourceSave: "Save",
@@ -618,7 +869,96 @@ window.__ModuleLoader__.load({
618
869
  installError: "Install failed",
619
870
  installSuccess: "Update installed — restarting DSH…",
620
871
  confirmInstall: "Confirm install",
621
- cancel: "Cancel"
872
+ cancel: "Cancel",
873
+ job: {
874
+ kindPluginSet: "Plugin-set update",
875
+ kindRuntime: "HQ Edge runtime update",
876
+ phase: {
877
+ queued: "Queued",
878
+ resolving: "Resolving version",
879
+ downloading: "Downloading",
880
+ verifying: "Verifying integrity",
881
+ staging: "Staging files",
882
+ activating: "Activating plugin set",
883
+ "awaiting-host": "Waiting for host to swap runtime",
884
+ restarting: "Restarting",
885
+ "verifying-after-restart": "Verifying after restart",
886
+ done: "Done",
887
+ failed: "Failed",
888
+ "rolled-back": "Rolled back",
889
+ abandoned: "Abandoned"
890
+ },
891
+ willRestartDsh: "DSH restarts automatically once this finishes; the update continues in the background.",
892
+ willSwapRuntime: "HQ Edge downloads the full runtime bundle and exits; the EDA host swaps and relaunches it. A system-wide install may show a UAC prompt.",
893
+ closeNote: "Closing this panel does not cancel the update.",
894
+ bytes: (done, total) => `${done} / ${total}`,
895
+ speed: (v) => `${v}/s`,
896
+ eta: (s) => `~${s} left`,
897
+ nextDone: "Finishing — please wait…",
898
+ resultDone: "Update completed",
899
+ resultFailed: "Update failed",
900
+ resultRolledBack: "Update rolled back",
901
+ resultAbandoned: "Update did not complete",
902
+ retry: "Retry (start a new update)",
903
+ recentTitle: "Recent updates",
904
+ stepsTitle: "Per-component progress",
905
+ logHint: (hint) => `Log id: ${hint}`
906
+ },
907
+ release: {
908
+ summaryTitle: "Release notes",
909
+ notesLink: "View release notes",
910
+ breakingTitle: "Breaking changes",
911
+ changesTitle: "What changed",
912
+ advanced: "Advanced: update sources",
913
+ publishedAt: (when) => `Published ${when}`
914
+ },
915
+ ui: {
916
+ title: "Update Center",
917
+ gearTooltip: "Advanced settings",
918
+ settingsTitle: "Update source (advanced)",
919
+ originDefault: "Default source",
920
+ originCustom: "Custom source",
921
+ pluginUrlLabel: "Plugin manifest URL",
922
+ runtimeUrlLabel: "Runtime manifest URL",
923
+ saveAndRefresh: "Save & refresh",
924
+ checkedAgo: (ago) => `Last checked: ${ago}`,
925
+ checkedUnknown: "No check yet",
926
+ agoJust: "just now",
927
+ agoMinutes: (n) => `${n} min ago`,
928
+ agoHours: (n) => `${n} h ago`,
929
+ agoDays: (n) => `${n} d ago`,
930
+ jobPluginTitle: (v) => `Updating plugin set (v${v})`,
931
+ jobRuntimeTitle: (v) => `Upgrading HQ Edge runtime (v${v})`,
932
+ jobSteps: [
933
+ "Download",
934
+ "Verify",
935
+ "Swap",
936
+ "Restart"
937
+ ],
938
+ rollbackTitle: "Last HQ Edge runtime update rolled back automatically",
939
+ failedTitle: "Last HQ Edge runtime update failed",
940
+ rollbackBody: (oldVer, code) => `Safely restored to the previous version (v${oldVer}). Code: ${code}`,
941
+ failedBody: (oldVer, code) => `Runtime swap did not complete (old v${oldVer}). Code: ${code}`,
942
+ dismiss: "Dismiss",
943
+ successInline: "Last runtime update succeeded",
944
+ runtimeEngine: "HQ Core Runtime",
945
+ currentChip: (v) => `v${v} (current)`,
946
+ newVersion: (v) => `New v${v} available`,
947
+ breakingTag: "Critical fix",
948
+ sizeLine: (size, summary) => `Size: ${size}${summary ? ` · ${summary}` : ""}`,
949
+ upgradeCore: "Upgrade core",
950
+ runtimeRestartNote: "Upgrading the runtime restarts the KiCad process service",
951
+ pendingTitle: "Plugins to update",
952
+ allChangelog: "Expand all changelogs",
953
+ changelogBtn: "Log",
954
+ changelogTitle: "What changed:",
955
+ blockedBadge: "Cannot install",
956
+ footerCount: (n) => `Ready to install ${n} compatible plugin${n === 1 ? "" : "s"}`,
957
+ footerSet: (v) => `Set: v${v}`,
958
+ installAllSet: (v) => `Update all plugins (v${v})`,
959
+ ignoreBtn: "Ignore",
960
+ footnote: "Updates apply automatically and restart the DSH view — your EDA schematic data is untouched"
961
+ }
622
962
  }
623
963
  };
624
964
  //#endregion
@@ -672,18 +1012,6 @@ window.__ModuleLoader__.load({
672
1012
  ]);
673
1013
  /** The settled-state glyph (`IconCheckCircleFillRegular`). */
674
1014
  const CHECK_CIRCLE_ICON = (size = 16) => fillGlyph(size, ["M28.1936 14.6936L19.8066 23.0806C19.2373 23.65 18.7159 24.1742 18.24 24.5571C17.7389 24.9602 17.1365 25.3359 16.3657 25.458C15.9581 25.5225 15.5428 25.5225 15.1353 25.458C14.3645 25.3359 13.7621 24.9602 13.261 24.5571C12.7851 24.1742 12.2637 23.65 11.6943 23.0806L7.80737 19.1936L10.1936 16.8074L14.0806 20.6943C14.7033 21.317 15.0763 21.6873 15.377 21.9292C15.6523 22.1507 15.7109 22.1325 15.6626 22.1248C15.7208 22.1339 15.7802 22.1339 15.8384 22.1248C15.7901 22.1325 15.8486 22.1507 16.124 21.9292C16.4247 21.6873 16.7977 21.317 17.4204 20.6943L25.8074 12.3074L28.1936 14.6936Z", "M32.8496 18.0005C32.8496 9.79906 26.2019 3.15137 18.0005 3.15137C9.79906 3.15137 3.15137 9.79906 3.15137 18.0005C3.15137 26.2019 9.79906 32.8496 18.0005 32.8496C26.2019 32.8496 32.8496 26.2019 32.8496 18.0005ZM35.7764 18.0005C35.7764 27.8173 27.8173 35.7764 18.0005 35.7764C8.18363 35.7764 0.224609 27.8173 0.224609 18.0005C0.224609 8.18363 8.18363 0.224609 18.0005 0.224609C27.8173 0.224609 35.7764 8.18363 35.7764 18.0005Z"], "0 0 36 36");
675
- /** The install action (`IconDownloadOutlineRegular`). */
676
- const DOWNLOAD_ICON = (size = 16) => glyph(size, REGULAR, [
677
- "M8 1.95317V10.0469",
678
- "M4.25 6.29688L8 10.0469L11.75 6.29688",
679
- "M1.5 10.0469V13.158C1.5 13.3937 1.60536 13.6198 1.79289 13.7865C1.98043 13.9532 2.23478 14.0469 2.5 14.0469H13.5C13.7652 14.0469 14.0196 13.9532 14.2071 13.7865C14.3946 13.6198 14.5 13.3937 14.5 13.158V10.0469"
680
- ]);
681
- /** The head's info affordance (`IconInfoOutlineRegular`, fill-only artwork). */
682
- const INFO_ICON = (size = 11) => fillGlyph(size, [
683
- "M12.5757 7.00012C12.5757 3.92085 10.0794 1.42463 7.00012 1.42456C3.9208 1.42456 1.42456 3.9208 1.42456 7.00012C1.42463 10.0794 3.92085 12.5757 7.00012 12.5757C10.0793 12.5756 12.5756 10.0793 12.5757 7.00012ZM13.8002 7.00012C13.8001 10.7559 10.7559 13.8001 7.00012 13.8002C3.2443 13.8002 0.199291 10.7559 0.199219 7.00012C0.199219 3.24426 3.24426 0.199219 7.00012 0.199219C10.7559 0.199291 13.8002 3.2443 13.8002 7.00012Z",
684
- "M7.6127 3.18921V4.55986H6.38735V3.18921H7.6127Z",
685
- "M7.6127 5.68921V10.8109H6.38735V5.68921H7.6127Z"
686
- ], "0 0 14 14");
687
1015
  //#endregion
688
1016
  //#region src/client/update-view.ts
689
1017
  /**
@@ -737,33 +1065,6 @@ window.__ModuleLoader__.load({
737
1065
  if (Array.isArray(by)) return by.filter((entry) => typeof entry === "string");
738
1066
  return typeof by === "string" && by.length > 0 ? [by] : [];
739
1067
  }
740
- /** The tag tone a state reads as, matching the shell's Tag palette. */
741
- function stateTone(item) {
742
- switch (item.state) {
743
- case "update": return "info";
744
- case "install": return "warning";
745
- case "blocked": return "danger";
746
- case "ignored": return "neutral";
747
- case "current": return "success";
748
- }
749
- }
750
- function stateLabel(item, copy) {
751
- switch (item.state) {
752
- case "current": return copy.state.current;
753
- case "update": return copy.state.update;
754
- case "install": return copy.state.install;
755
- case "ignored": return copy.state.ignored;
756
- case "blocked": return copy.state.blocked;
757
- }
758
- }
759
- /** The one-line version transition under a card's title. */
760
- function versionLine(item) {
761
- switch (item.state) {
762
- case "current": return item.currentVersion ?? item.availableVersion;
763
- case "install": return item.availableVersion;
764
- default: return item.currentVersion === null ? item.availableVersion : `${item.currentVersion} → ${item.availableVersion}`;
765
- }
766
- }
767
1068
  function blockedReason(item, copy) {
768
1069
  const by = blockedByList(item);
769
1070
  if (by.includes("host")) return copy.blockedReason.host;
@@ -771,55 +1072,93 @@ window.__ModuleLoader__.load({
771
1072
  if (by.includes("runtime-unknown")) return copy.blockedReason["runtime-unknown"];
772
1073
  return by.length > 0 ? by.join(", ") : void 0;
773
1074
  }
774
- /** One package card: glyph, name with its state tag, version line, end actions. */
775
- function packageCard(item, copy, installing, store) {
776
- const card = el("li", "hqpu-card");
1075
+ /** Two-letter avatar initials for a card (CJK-safe). */
1076
+ function avatarInitials(item) {
1077
+ const src = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName.trim() : shortName(item.name);
1078
+ if (/[⺀-鿿]/.test(src)) return src.slice(0, 2);
1079
+ const words = src.split(/[\s_\-./@]+/).filter(Boolean);
1080
+ if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase();
1081
+ return src.replace(/^@/, "").slice(0, 2).toUpperCase();
1082
+ }
1083
+ /** Shared per-card changelog toggle drawer (the per-plugin changes[] list). */
1084
+ function changelogDrawer(item, copy, open) {
1085
+ const drawer = el("div", "hqpu-changelog-drawer");
1086
+ drawer.style.display = open ? "" : "none";
1087
+ drawer.append(el("div", "hqpu-changelog-drawer-title", copy.ui.changelogTitle));
1088
+ const ul = el("ul");
1089
+ for (const change of item.changes ?? []) if (typeof change === "string" && change.length > 0) ul.append(el("li", "", change));
1090
+ drawer.append(ul);
1091
+ return drawer;
1092
+ }
1093
+ /** One package card per the mockup: avatar initials, display name + package
1094
+ * subtitle, old→new version row (new in emerald), a 日志 toggle expanding the
1095
+ * inline changelog. Blocked/incompatible cards are dimmed with a rose
1096
+ * 无法安装 badge and a reason line, and carry NO install button. */
1097
+ function packageCard(item, copy, open, onToggle) {
1098
+ const blocked = item.state === "blocked";
1099
+ const card = el("li", blocked ? "hqpu-card hqpu-card--blocked" : "hqpu-card");
777
1100
  card.dataset.pluginName = item.name;
778
1101
  card.dataset.state = item.state;
779
- card.title = `${item.name} · ${versionLine(item)}`;
780
1102
  const head = el("div", "hqpu-card-head");
781
- const glyph = el("span", "hqpu-card-icon");
782
- glyph.setAttribute("aria-hidden", "true");
783
- glyph.innerHTML = PLUGIN_ICON(24);
1103
+ const left = el("div");
1104
+ const avatar = el("span", "hqpu-avatar", avatarInitials(item));
1105
+ avatar.setAttribute("aria-hidden", "true");
784
1106
  const main = el("div", "hqpu-card-main");
785
- const titleRow = el("div", "hqpu-title-row");
786
- const title = el("span", "hqpu-card-title", shortName(item.name));
787
- const tag = el("span", "hqpu-tag");
788
- tag.dataset.tone = stateTone(item);
789
- tag.textContent = stateLabel(item, copy);
790
- titleRow.append(title, tag);
791
- const desc = el("span", "hqpu-card-desc", versionLine(item));
792
- main.append(titleRow, desc);
793
- head.append(glyph, main);
794
- const end = el("div", "hqpu-card-end");
795
- if (item.state === "update" || item.state === "install") {
796
- const button = el("button", "hqpu-btn hqpu-btn--sm hqpu-btn--primary");
797
- button.type = "button";
798
- button.disabled = installing;
799
- button.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installOne}</span>`;
800
- button.addEventListener("click", () => {
801
- runInstall(store, copy);
802
- });
803
- end.append(button);
1107
+ const titleText = typeof item.displayName === "string" && item.displayName.trim().length > 0 ? item.displayName.trim() : shortName(item.name);
1108
+ main.append(el("span", "hqpu-card-title", titleText));
1109
+ main.append(el("span", "hqpu-card-sub", shortName(item.name)));
1110
+ const ver = el("div", "hqpu-version-row");
1111
+ if (item.currentVersion !== null && item.currentVersion !== void 0) {
1112
+ ver.append(el("span", "hqpu-ver-old", item.currentVersion));
1113
+ ver.append(el("span", "hqpu-ver-arrow", "→"));
804
1114
  }
805
- head.append(end);
1115
+ const size = formatMb(item.sizeBytes ?? 0);
1116
+ ver.append(el("span", "hqpu-ver-new", size.length > 0 ? `${item.availableVersion} (${size})` : item.availableVersion));
1117
+ main.append(ver);
1118
+ left.append(avatar, main);
1119
+ const right = el("div", "hqpu-card-end");
1120
+ if (blocked) right.append(el("span", "hqpu-blocked-badge", copy.ui.blockedBadge));
1121
+ else if (Array.isArray(item.changes) && item.changes.length > 0) {
1122
+ const toggle = el("button", "hqpu-log-toggle");
1123
+ toggle.type = "button";
1124
+ toggle.dataset.open = open ? "true" : "false";
1125
+ toggle.innerHTML = `${copy.ui.changelogBtn}<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6"/></svg>`;
1126
+ toggle.addEventListener("click", onToggle);
1127
+ right.append(toggle);
1128
+ }
1129
+ head.append(left, right);
806
1130
  card.append(head);
807
- if (item.state === "blocked") {
1131
+ if (!blocked && Array.isArray(item.changes) && item.changes.length > 0) card.append(changelogDrawer(item, copy, open));
1132
+ if (blocked) {
808
1133
  const reason = blockedReason(item, copy);
809
- if (reason !== void 0) card.append(el("p", "hqpu-card-reason", reason));
1134
+ const entries = [...reason !== void 0 ? [reason] : [], ...[...item.reasons ?? [], ...item.notes ?? []].filter((e) => typeof e === "string" && e.length > 0)];
1135
+ for (const entry of entries) {
1136
+ const line = el("p", "hqpu-reason-line");
1137
+ line.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5V8.5M8 11.25h.01M14.5 8a6.5 6.5 0 11-13 0 6.5 6.5 0 0113 0Z"/></svg>`;
1138
+ line.append(document.createTextNode(entry));
1139
+ card.append(line);
1140
+ }
810
1141
  }
811
1142
  return card;
812
1143
  }
813
- /** A section: group head with its count, then the cards. */
814
- function group(heading, items, copy, installing, store) {
1144
+ /** A section: head (title + count pill + expand-all toggle) then its cards. */
1145
+ function group(heading, items, copy, changelogOpen, allOpen, onToggle, onToggleAll) {
815
1146
  if (items.length === 0) return null;
816
1147
  const section = el("section", "hqpu-group");
817
1148
  section.dataset.group = heading;
818
- const head = el("div", "hqpu-group-head");
819
- head.append(el("h3", "hqpu-group-title", heading));
820
- head.append(el("span", "hqpu-group-count", copy.groupCount(items.length)));
1149
+ const head = el("div", "hqpu-section-head");
1150
+ const titleWrap = el("div", "hqpu-section-title");
1151
+ titleWrap.append(el("span", "", heading));
1152
+ titleWrap.append(el("span", "hqpu-count-pill", String(items.length)));
1153
+ head.append(titleWrap);
1154
+ if (items.some((i) => i.state !== "blocked" && Array.isArray(i.changes) && i.changes.length > 0)) {
1155
+ const all = el("button", "hqpu-all-changelog", copy.ui.allChangelog);
1156
+ all.type = "button";
1157
+ all.addEventListener("click", onToggleAll);
1158
+ head.append(all);
1159
+ }
821
1160
  const list = el("ul", "hqpu-cards");
822
- for (const item of items) list.append(packageCard(item, copy, installing, store));
1161
+ for (const item of items) list.append(packageCard(item, copy, allOpen || changelogOpen.has(item.name), () => onToggle(item.name)));
823
1162
  section.append(head, list);
824
1163
  return section;
825
1164
  }
@@ -859,6 +1198,234 @@ window.__ModuleLoader__.load({
859
1198
  section.setAttribute("aria-label", copy.loading);
860
1199
  return section;
861
1200
  }
1201
+ /** Format a byte count for the progress line, or null when unknown. */
1202
+ function formatBytes(n) {
1203
+ if (n === void 0 || !Number.isFinite(n) || n < 0) return null;
1204
+ if (n >= 1024 ** 3) return `${(n / 1024 ** 3).toFixed(1)} GB`;
1205
+ if (n >= 1024 ** 2) return `${(n / 1024 ** 2).toFixed(1)} MB`;
1206
+ if (n >= 1024) return `${(n / 1024).toFixed(0)} KB`;
1207
+ return `${n} B`;
1208
+ }
1209
+ /** "上次检查: N 分钟前" — relative time from an ISO timestamp. */
1210
+ function relativeAgo(iso, copy) {
1211
+ if (typeof iso !== "string" || iso.length === 0) return null;
1212
+ const then = new Date(iso).getTime();
1213
+ if (!Number.isFinite(then)) return null;
1214
+ const diffMin = Math.round((Date.now() - then) / 6e4);
1215
+ if (diffMin < 1) return copy.ui.agoJust;
1216
+ if (diffMin < 60) return copy.ui.agoMinutes(diffMin);
1217
+ const hours = Math.round(diffMin / 60);
1218
+ if (hours < 24) return copy.ui.agoHours(hours);
1219
+ return copy.ui.agoDays(Math.round(hours / 24));
1220
+ }
1221
+ function jobKindLabel(job, copy) {
1222
+ return job.kind === "runtime" ? copy.job.kindRuntime : copy.job.kindPluginSet;
1223
+ }
1224
+ /** The phase in human words, falling back to the raw code. */
1225
+ function phaseLabel(job, copy) {
1226
+ return copy.job.phase[job.phase] ?? job.phase;
1227
+ }
1228
+ /** One per-step line for plugin-set installs (and any job that reports steps). */
1229
+ function stepsList(job, copy) {
1230
+ if (!Array.isArray(job.steps) || job.steps.length === 0) return null;
1231
+ const wrap = el("div", "hqpu-job-steps");
1232
+ wrap.append(el("p", "hqpu-group-count", copy.job.stepsTitle));
1233
+ const ol = el("ol", "hqpu-steps");
1234
+ for (const step of job.steps) {
1235
+ const li = el("li", `hqpu-step hqpu-step--${step.state}`);
1236
+ const dot = el("span", "hqpu-dot");
1237
+ dot.dataset.state = step.state === "installed" ? "done" : step.state === "failed" ? "error" : step.state === "running" ? "warning" : "idle";
1238
+ li.append(dot);
1239
+ const text = step.message && step.message.length > 0 ? `${step.name} — ${step.message}` : step.name;
1240
+ li.append(el("span", "", text));
1241
+ ol.append(li);
1242
+ }
1243
+ wrap.append(ol);
1244
+ return wrap;
1245
+ }
1246
+ /** Map a job phase onto the 4-step indicator index (下载/校验/替换/重启). */
1247
+ function stepIndex(phase) {
1248
+ switch (phase) {
1249
+ case "downloading": return 0;
1250
+ case "verifying":
1251
+ case "resolving": return 1;
1252
+ case "staging":
1253
+ case "activating":
1254
+ case "awaiting-host": return 2;
1255
+ case "restarting":
1256
+ case "verifying-after-restart": return 3;
1257
+ case "queued": return -1;
1258
+ default: return -1;
1259
+ }
1260
+ }
1261
+ /**
1262
+ * The ACTIVE-JOB banner per the mockup: pulsing brand dot + job title with the
1263
+ * target version, mono percentage, a gradient bar only when `percent != null`,
1264
+ * phase + speed line ("3.1 MB/s (286MB / 450MB)"), and a 4-step indicator row
1265
+ * (done=emerald, active=pulsing brand, upcoming=muted).
1266
+ */
1267
+ function activeJobView(job, copy) {
1268
+ const section = el("section", "hqpu-group");
1269
+ section.dataset.group = "active-job";
1270
+ const card = el("div", "hqpu-active");
1271
+ const top = el("div", "hqpu-active-top");
1272
+ const titleWrap = el("div", "hqpu-active-title");
1273
+ const pulse = el("span", "hqpu-pulse-dot");
1274
+ pulse.append(el("span", "hqpu-pulse-ring"));
1275
+ const title = job.kind === "runtime" ? copy.ui.jobRuntimeTitle(job.targetVersion) : copy.ui.jobPluginTitle(job.targetVersion);
1276
+ titleWrap.append(pulse, el("span", "", title));
1277
+ const pct = job.progress.percent;
1278
+ if (pct !== null) top.append(el("span", "hqpu-active-percent", `${Math.round(pct)}%`));
1279
+ card.append(top);
1280
+ if (pct !== null) {
1281
+ const bar = el("div", "hqpu-progress");
1282
+ const fill = el("div", "hqpu-progress-fill");
1283
+ fill.style.width = `${Math.min(100, Math.max(0, pct))}%`;
1284
+ bar.append(fill);
1285
+ card.append(bar);
1286
+ }
1287
+ const meta = el("div", "hqpu-active-meta");
1288
+ const phaseText = typeof job.message === "string" && job.message.length > 0 ? job.message : phaseLabel(job, copy);
1289
+ meta.append(el("span", "", phaseText));
1290
+ const speed = formatBytes(job.progress.bytesPerSecond);
1291
+ const done = formatBytes(job.progress.bytesDone);
1292
+ const total = formatBytes(job.progress.bytesTotal);
1293
+ const telemetry = speed !== null && done !== null && total !== null ? `${speed}/s (${done} / ${total})` : speed !== null ? `${speed}/s` : done !== null && total !== null ? copy.job.bytes(done, total) : "";
1294
+ if (telemetry.length > 0) meta.append(el("span", "", telemetry));
1295
+ card.append(meta);
1296
+ const transition = el("div", "hqpu-active-meta");
1297
+ transition.append(el("span", "", job.previousVersion !== null && job.previousVersion.length > 0 ? `${job.previousVersion} → ${job.targetVersion}` : job.targetVersion));
1298
+ transition.append(el("span", "", job.kind === "runtime" ? copy.job.willSwapRuntime : copy.job.willRestartDsh));
1299
+ card.append(transition);
1300
+ card.append(el("p", "hqpu-job-meta", copy.job.closeNote));
1301
+ const current = stepIndex(job.phase);
1302
+ const stepsRow = el("div", "hqpu-job-steps4");
1303
+ copy.ui.jobSteps.forEach((label, index) => {
1304
+ const cls = current === -1 ? "hqpu-job-step--todo" : index < current ? "hqpu-job-step--done" : index === current ? "hqpu-job-step--active" : "hqpu-job-step--todo";
1305
+ stepsRow.append(el("div", cls, label));
1306
+ });
1307
+ card.append(stepsRow);
1308
+ section.append(card);
1309
+ return section;
1310
+ }
1311
+ /**
1312
+ * The RESULT view for a terminal job: success / failed / rolled-back /
1313
+ * abandoned, previous→installed, per-package steps, errorCode + message, a log
1314
+ * correlation hint, and retry guidance (a retry starts a NEW job).
1315
+ */
1316
+ function resultView(job, copy, onRetry) {
1317
+ const section = el("section", "hqpu-group");
1318
+ section.dataset.group = "job-result";
1319
+ const tone = job.phase === "done" ? "success" : job.phase === "failed" ? "danger" : "warning";
1320
+ const titleText = job.phase === "done" ? copy.job.resultDone : job.phase === "failed" ? copy.job.resultFailed : job.phase === "rolled-back" ? copy.job.resultRolledBack : copy.job.resultAbandoned;
1321
+ const head = el("div", "hqpu-group-head");
1322
+ const tag = el("span", "hqpu-tag");
1323
+ tag.dataset.tone = tone;
1324
+ tag.textContent = titleText;
1325
+ head.append(el("h3", "hqpu-group-title", `${jobKindLabel(job, copy)} ${job.targetVersion}`), tag);
1326
+ section.append(head);
1327
+ const card = el("div", "hqpu-job");
1328
+ const res = job.result;
1329
+ const from = job.previousVersion ?? res?.installedVersion ?? "—";
1330
+ const to = res?.installedVersion ?? job.targetVersion;
1331
+ const finished = res !== void 0 && res.finishedAt.length > 0 ? ` · ${new Date(res.finishedAt).toLocaleString()}` : "";
1332
+ card.append(el("p", "hqpu-status", `${from} → ${to}${finished}`));
1333
+ if (res !== void 0 && res.message !== void 0 && res.message.length > 0) card.append(el("p", "hqpu-status", res.message));
1334
+ if (res !== void 0 && res.errorCode !== void 0 && res.errorCode.length > 0) card.append(el("p", "hqpu-status", res.errorCode));
1335
+ if (typeof job.logHint === "string" && job.logHint.length > 0) card.append(el("p", "hqpu-status hqpu-job-meta", copy.job.logHint(job.logHint)));
1336
+ const steps = stepsList(job, copy);
1337
+ if (steps !== null) card.append(steps);
1338
+ if (job.phase === "failed" || job.phase === "abandoned") {
1339
+ const retry = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.job.retry);
1340
+ retry.type = "button";
1341
+ retry.addEventListener("click", onRetry);
1342
+ card.append(retry);
1343
+ }
1344
+ section.append(card);
1345
+ return section;
1346
+ }
1347
+ /** A one-line row for older recent jobs (newest already shown as the card). */
1348
+ function recentJobsList(jobs, copy) {
1349
+ const older = jobs.slice(1, 5);
1350
+ if (older.length === 0) return null;
1351
+ const wrap = el("div", "hqpu-group");
1352
+ wrap.dataset.group = "recent-jobs";
1353
+ wrap.append(el("h3", "hqpu-group-count", copy.job.recentTitle));
1354
+ const ul = el("ul", "hqpu-cards");
1355
+ for (const job of older) {
1356
+ const li = el("li", "hqpu-status");
1357
+ const when = job.updatedAt.length > 0 ? ` · ${new Date(job.updatedAt).toLocaleString()}` : "";
1358
+ li.textContent = `${jobKindLabel(job, copy)} ${job.targetVersion} · ${phaseLabel(job, copy)}${when}`;
1359
+ ul.append(li);
1360
+ }
1361
+ wrap.append(ul);
1362
+ return wrap;
1363
+ }
1364
+ /**
1365
+ * Check-status banner: a FAILED check shows a clear error row + Retry (never
1366
+ * the silent "no updates" empty state); a successful check shows the time;
1367
+ * when a check failed but a prior report exists it is labelled stale.
1368
+ */
1369
+ function checkBanner(report, copy, onRetry) {
1370
+ const failed = typeof report.lastError === "string" && report.lastError.length > 0;
1371
+ const checked = typeof report.checkedAt === "string" && report.checkedAt.length > 0;
1372
+ if (!failed && !checked) return null;
1373
+ const wrap = el("section", "hqpu-group");
1374
+ wrap.dataset.group = "check-status";
1375
+ if (failed) {
1376
+ const banner = el("div", "hqpu-banner");
1377
+ banner.dataset.tone = "danger";
1378
+ banner.innerHTML = WARNING_ICON(14);
1379
+ banner.append(el("span", "", copy.checkFailedBanner(report.lastError)));
1380
+ wrap.append(banner);
1381
+ if (report.items.length > 0) wrap.append(el("p", "hqpu-status", copy.staleNote));
1382
+ const row = el("div", "hqpu-actions");
1383
+ const retry = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm", copy.retry);
1384
+ retry.type = "button";
1385
+ retry.addEventListener("click", onRetry);
1386
+ row.append(retry);
1387
+ wrap.append(row);
1388
+ return wrap;
1389
+ }
1390
+ wrap.append(el("p", "hqpu-status", copy.checkedAt(new Date(report.checkedAt).toLocaleString())));
1391
+ return wrap;
1392
+ }
1393
+ /** Set-level release metadata (v2 manifest): summary, notes, breaking, link. */
1394
+ function releaseHeader(release, copy) {
1395
+ if (release === null || release === void 0) return null;
1396
+ const notes = Array.isArray(release.notes) ? release.notes.filter((n) => typeof n === "string" && n.length > 0) : [];
1397
+ const breaking = Array.isArray(release.breaking) ? release.breaking.filter((n) => typeof n === "string" && n.length > 0) : [];
1398
+ if (!(typeof release.summary === "string" && release.summary.length > 0 || notes.length > 0 || breaking.length > 0 || typeof release.notesUrl === "string" || typeof release.publishedAt === "string")) return null;
1399
+ const section = el("section", "hqpu-group");
1400
+ section.dataset.group = "release";
1401
+ section.append(el("h3", "hqpu-group-title", copy.release.summaryTitle));
1402
+ if (typeof release.summary === "string" && release.summary.length > 0) section.append(el("p", "hqpu-status", release.summary));
1403
+ if (typeof release.publishedAt === "string" && release.publishedAt.length > 0) section.append(el("p", "hqpu-status", copy.release.publishedAt(new Date(release.publishedAt).toLocaleString())));
1404
+ if (release.requiresRestart === true) section.append(el("p", "hqpu-status", copy.job.willRestartDsh));
1405
+ if (notes.length > 0) {
1406
+ const ul = el("ul", "hqpu-release-notes");
1407
+ for (const note of notes) ul.append(el("li", "", note));
1408
+ section.append(ul);
1409
+ }
1410
+ if (breaking.length > 0) {
1411
+ const banner = el("div", "hqpu-banner");
1412
+ banner.dataset.tone = "danger";
1413
+ banner.innerHTML = WARNING_ICON(14);
1414
+ const inner = el("span");
1415
+ inner.append(el("strong", "", `${copy.release.breakingTitle}: `));
1416
+ inner.append(document.createTextNode(breaking.join(" · ")));
1417
+ banner.append(inner);
1418
+ section.append(banner);
1419
+ }
1420
+ if (typeof release.notesUrl === "string" && release.notesUrl.length > 0) {
1421
+ const link = el("a", "hqpu-status hqpu-release-link", copy.release.notesLink);
1422
+ link.href = release.notesUrl;
1423
+ link.target = "_blank";
1424
+ link.rel = "noreferrer";
1425
+ section.append(link);
1426
+ }
1427
+ return section;
1428
+ }
862
1429
  /**
863
1430
  * Normalize the report's `runtime.source` to the full shape.
864
1431
  *
@@ -877,29 +1444,6 @@ window.__ModuleLoader__.load({
877
1444
  override: candidate.override === true
878
1445
  };
879
1446
  }
880
- /** The tag naming where the URL in effect came from. */
881
- function originLabel(origin, copy) {
882
- switch (origin) {
883
- case "override": return copy.originOverride;
884
- case "config": return copy.originConfig;
885
- default: return copy.originUnset;
886
- }
887
- }
888
- /** The 更新源 fact row: the URL in effect plus an origin tag. */
889
- function sourceFactRow(facts, source, copy) {
890
- const wrap = el("div");
891
- wrap.append(el("dt", "", copy.runtime.source));
892
- const dd = el("dd");
893
- const line = el("div", "hqpu-source-line");
894
- const code = el("code", "hqpu-source-url", source.url.length > 0 ? source.url : copy.runtime.notConfigured);
895
- const tag = el("span", "hqpu-tag");
896
- tag.dataset.tone = source.override ? "info" : source.origin === "config" ? "success" : "neutral";
897
- tag.textContent = originLabel(source.origin, copy);
898
- line.append(code, tag);
899
- dd.append(line);
900
- wrap.append(dd);
901
- facts.append(wrap);
902
- }
903
1447
  /** Pick the labels for one kind out of the copy table. */
904
1448
  function sourceLabels(copy, kind) {
905
1449
  return kind === "runtime" ? {
@@ -932,6 +1476,9 @@ window.__ModuleLoader__.load({
932
1476
  const labels = sourceLabels(copy, kind);
933
1477
  const block = el("div", `hqpu-source-editor hqpu-source-editor--${kind}`);
934
1478
  if (!actions.sourceEditing) {
1479
+ const urlLine = el("p", "hqpu-source-current-url", source.url);
1480
+ urlLine.style.cssText = "margin:2px 0 6px;font-size:9px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:#a1a1aa;word-break:break-all;";
1481
+ block.append(urlLine);
935
1482
  block.append(el("p", "hqpu-status hqpu-source-hint", labels.hint));
936
1483
  const edit = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", labels.edit);
937
1484
  edit.type = "button";
@@ -986,109 +1533,119 @@ window.__ModuleLoader__.load({
986
1533
  return block;
987
1534
  }
988
1535
  /**
989
- * The plugin release-manifest source, as its own group above the plugin
990
- * groups: it says where the whole list comes from, so it belongs above the
991
- * list rather than inside it. Absent on an older hq-edge (no `pluginSource`
992
- * in the report) — then nothing is rendered at all.
1536
+ * The host's own report of the last runtime swap. Rolled-back / failed renders
1537
+ * as an AMBER alert card (title, old→new body, errorCode chip, 忽略 dismiss);
1538
+ * succeeded renders as a subtle green inline line — not a loud banner. The
1539
+ * plugin has NO log-open affordance, so "查看诊断日志" is omitted (never faked).
993
1540
  */
994
- function pluginSourceGroup(report, copy, actions) {
995
- const source = normalizeSource(report.pluginSource);
996
- if (source === null) return null;
1541
+ function runtimeResultAlert(report, copy, dismissed, onDismiss) {
1542
+ const lastResult = report.runtime.lastResult;
1543
+ if (lastResult === null || lastResult === void 0) return null;
1544
+ if (lastResult.state === "succeeded") {
1545
+ const line = el("p", "hqpu-alert-green");
1546
+ line.dataset.role = "runtime-success";
1547
+ line.textContent = copy.ui.successInline;
1548
+ return line;
1549
+ }
1550
+ if (dismissed) return null;
997
1551
  const section = el("section", "hqpu-group");
998
- section.dataset.group = "plugin-source";
999
- const head = el("div", "hqpu-group-head");
1000
- head.append(el("h3", "hqpu-group-title", copy.pluginSource.title));
1001
- section.append(head);
1002
- const facts = el("dl", "hqpu-facts");
1003
- sourceFactRow(facts, source, copy);
1004
- section.append(facts);
1005
- section.append(sourceEditor("plugin", source, copy, actions));
1552
+ section.dataset.group = "runtime-alert";
1553
+ const card = el("div", "hqpu-alert hqpu-alert--amber");
1554
+ card.innerHTML = WARNING_ICON(14);
1555
+ const body = el("div");
1556
+ const title = el("div", "hqpu-alert-title");
1557
+ title.textContent = lastResult.state === "failed" ? copy.ui.failedTitle : copy.ui.rollbackTitle;
1558
+ body.append(title);
1559
+ const oldVer = lastResult.installedVersion ?? lastResult.targetVersion ?? "—";
1560
+ const code = typeof lastResult.errorCode === "string" && lastResult.errorCode.length > 0 ? lastResult.errorCode : "—";
1561
+ lastResult.state === "failed" ? copy.ui.failedBody(oldVer, code) : copy.ui.rollbackBody(oldVer, code);
1562
+ const bodyP = el("p", "hqpu-alert-body");
1563
+ const textOnly = lastResult.state === "failed" ? copy.ui.failedBody(oldVer, "") : copy.ui.rollbackBody(oldVer, "");
1564
+ bodyP.append(document.createTextNode(textOnly.replace(/\s*$/, "")));
1565
+ if (typeof lastResult.errorCode === "string" && lastResult.errorCode.length > 0) {
1566
+ bodyP.append(document.createTextNode(" "));
1567
+ const chip = el("code", "", lastResult.errorCode);
1568
+ bodyP.append(chip);
1569
+ }
1570
+ if (typeof lastResult.message === "string" && lastResult.message.length > 0) bodyP.append(el("br"), document.createTextNode(lastResult.message));
1571
+ body.append(bodyP);
1572
+ const actions = el("div", "hqpu-alert-actions");
1573
+ const dismiss = el("button", "hqpu-alert-dismiss", copy.ui.dismiss);
1574
+ dismiss.type = "button";
1575
+ dismiss.addEventListener("click", onDismiss);
1576
+ actions.append(dismiss);
1577
+ body.append(actions);
1578
+ card.append(body);
1579
+ section.append(card);
1006
1580
  return section;
1007
1581
  }
1008
- /** The independent HQ Edge runtime report, as ruled fact rows. */
1009
- function runtimeGroup(report, copy, actions) {
1010
- const section = el("section", "hqpu-group");
1011
- section.dataset.group = "runtime";
1012
- const head = el("div", "hqpu-group-head");
1013
- head.append(el("h3", "hqpu-group-title", copy.runtime.title));
1014
- section.append(head);
1015
- const facts = el("dl", "hqpu-facts");
1582
+ /** The HQ Edge runtime engine card per the mockup: current chip, "新版本 vY
1583
+ * 可用" with a 关键修复 tag when breaking/notes exist, size+summary line, a
1584
+ * 升级核心 button (existing arm→confirm flow), and the amber restart note. */
1585
+ function runtimeEngineCard(report, copy, actions) {
1016
1586
  const runtime = report.runtime;
1017
1587
  const latestVersion = runtimeLatestVersion(runtime);
1018
- const row = (label, value, code = false) => {
1019
- const wrap = el("div");
1020
- wrap.append(el("dt", "", label));
1021
- const dd = el("dd");
1022
- if (code) {
1023
- const node = el("code", "", value);
1024
- dd.append(node);
1025
- } else dd.textContent = value;
1026
- wrap.append(dd);
1027
- facts.append(wrap);
1028
- };
1029
- const source = normalizeSource(runtime.source);
1030
- if (runtime.configured === false) {
1031
- if (source !== null) sourceFactRow(facts, source, copy);
1032
- else row(copy.runtime.source, copy.runtime.notConfigured);
1033
- } else {
1034
- row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown);
1035
- if (runtime.updateAvailable && latestVersion !== null) row(copy.runtime.latest, latestVersion);
1036
- if (report.manifestVersion !== null) row(copy.runtime.set, report.manifestVersion, true);
1037
- if (typeof runtime.checkedAt === "string" && runtime.checkedAt.length > 0) row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString());
1038
- if (source !== null) sourceFactRow(facts, source, copy);
1039
- }
1040
- section.append(facts);
1041
- if (source !== null) section.append(sourceEditor("runtime", source, copy, actions));
1588
+ const card = el("section", "hqpu-engine");
1589
+ card.dataset.group = "runtime";
1590
+ const head = el("div", "hqpu-engine-head");
1591
+ const titleWrap = el("div", "hqpu-engine-title");
1592
+ titleWrap.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.33333 2L4.66667 8H8L6.66667 14L11.33333 8H8L9.33333 2Z"/></svg>`;
1593
+ titleWrap.append(el("span", "", copy.ui.runtimeEngine));
1594
+ head.append(titleWrap);
1595
+ if (typeof runtime.currentVersion === "string" && runtime.currentVersion.length > 0) head.append(el("span", "hqpu-chip", copy.ui.currentChip(runtime.currentVersion)));
1596
+ card.append(head);
1042
1597
  if (runtime.updateAvailable && latestVersion !== null) {
1043
- const row = el("div", "hqpu-actions hqpu-runtime-actions");
1044
- if (actions.accepted) {
1045
- const line = el("span", "hqpu-status hqpu-status--dot");
1046
- line.setAttribute("role", "status");
1047
- line.append(el("span", "hqpu-spinner"), el("span", "", copy.runtime.updateAccepted));
1048
- row.append(line);
1049
- } else if (actions.busy) {
1050
- const line = el("span", "hqpu-status hqpu-status--dot");
1051
- line.setAttribute("role", "status");
1052
- line.append(el("span", "hqpu-spinner"), el("span", "", copy.runtime.updating));
1053
- row.append(line);
1054
- } else if (actions.armed) {
1055
- row.append(el("p", "hqpu-status hqpu-runtime-confirm", copy.runtime.updateConfirm(latestVersion)));
1056
- const yes = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm", copy.confirmInstall);
1598
+ const row = el("div", "hqpu-engine-update-row");
1599
+ const left = el("div");
1600
+ const newLine = el("div", "hqpu-engine-new-line");
1601
+ newLine.append(el("span", "", copy.ui.newVersion(latestVersion)));
1602
+ const latestEntry = runtime.latest;
1603
+ if (Array.isArray(latestEntry?.breaking) && (latestEntry?.breaking?.length ?? 0) > 0 || Array.isArray(latestEntry?.notes) && (latestEntry?.notes?.length ?? 0) > 0) newLine.append(el("span", "hqpu-tag--brand", copy.ui.breakingTag));
1604
+ left.append(newLine);
1605
+ const size = typeof latestEntry?.sizeBytes === "number" ? formatMb(latestEntry.sizeBytes) : "";
1606
+ const summary = typeof latestEntry?.summary === "string" ? latestEntry.summary : "";
1607
+ if (size.length > 0 || summary.length > 0) left.append(el("p", "hqpu-engine-size", copy.ui.sizeLine(size, summary)));
1608
+ row.append(left);
1609
+ if (actions.accepted) row.append(el("span", "hqpu-status", copy.runtime.updateAccepted));
1610
+ else if (actions.busy) row.append(el("span", "hqpu-status", copy.runtime.updating));
1611
+ else if (actions.armed) {
1612
+ const confirmWrap = el("div");
1613
+ confirmWrap.append(el("p", "hqpu-status", copy.runtime.updateConfirm(latestVersion)));
1614
+ const yes = el("button", "hqpu-btn hqpu-btn--engine", copy.confirmInstall);
1057
1615
  yes.type = "button";
1058
1616
  yes.dataset.runtimeConfirm = "";
1059
1617
  yes.addEventListener("click", actions.onConfirm);
1060
- const no = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.cancel);
1618
+ const no = el("button", "hqpu-btn hqpu-btn--engine", copy.cancel);
1061
1619
  no.type = "button";
1062
1620
  no.addEventListener("click", actions.onCancel);
1063
- row.append(yes, no);
1621
+ confirmWrap.append(yes, no);
1622
+ row.append(confirmWrap);
1064
1623
  } else {
1065
- const start = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm");
1624
+ const start = el("button", "hqpu-btn hqpu-btn--engine", copy.ui.upgradeCore);
1066
1625
  start.type = "button";
1067
1626
  start.dataset.runtimeUpdate = "";
1068
- start.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.runtime.update}</span>`;
1069
1627
  start.addEventListener("click", actions.onArm);
1070
1628
  row.append(start);
1071
1629
  }
1072
- section.append(row);
1073
- } else section.append(el("p", "hqpu-status", copy.runtime.note));
1074
- if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) {
1075
- const line = el("p", "hqpu-status hqpu-status--dot");
1076
- line.setAttribute("role", "status");
1077
- const dot = el("span", "hqpu-dot");
1078
- dot.dataset.state = "error";
1079
- line.append(dot, el("span", "", `${copy.lastError}: ${runtime.lastError}`));
1080
- section.append(line);
1081
- }
1082
- return section;
1630
+ card.append(row);
1631
+ const note = el("p", "hqpu-engine-note");
1632
+ note.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2.5L14 13H2L8 2.5Z"/></svg><span>${copy.ui.runtimeRestartNote}</span>`;
1633
+ card.append(note);
1634
+ } else if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) card.append(el("p", "hqpu-status", `${copy.lastError}: ${runtime.lastError}`));
1635
+ return card;
1083
1636
  }
1084
- function renderList(section, report, copy, installing, store) {
1637
+ function renderList(section, report, copy, changelogOpen, allOpen, onToggleCard, onToggleAll, onRetryCheck) {
1085
1638
  section.textContent = "";
1086
1639
  if (report === null) {
1087
1640
  section.append(skeleton(copy));
1088
1641
  return;
1089
1642
  }
1643
+ const banner = checkBanner(report, copy, onRetryCheck);
1644
+ if (banner !== null) section.append(banner);
1645
+ const release = releaseHeader(report.release, copy);
1646
+ if (release !== null) section.append(release);
1090
1647
  if (report.items.length === 0) {
1091
- section.append(el("div", "hqpu-empty", copy.empty));
1648
+ if (typeof report.lastError !== "string" || report.lastError.length === 0) section.append(el("div", "hqpu-empty", copy.empty));
1092
1649
  return;
1093
1650
  }
1094
1651
  const byState = (state) => report.items.filter((item) => item.state === state);
@@ -1099,23 +1656,19 @@ window.__ModuleLoader__.load({
1099
1656
  [copy.group.current, byState("current")]
1100
1657
  ];
1101
1658
  for (const [heading, items] of order) {
1102
- const node = group(heading, items, copy, installing, store);
1659
+ const node = group(heading, items, copy, changelogOpen, allOpen, onToggleCard, onToggleAll);
1103
1660
  if (node !== null) section.append(node);
1104
1661
  }
1105
1662
  }
1106
- function renderRuntime(section, report, copy, actions) {
1107
- section.textContent = "";
1108
- if (report === null) return;
1109
- section.append(runtimeGroup(report, copy, actions));
1110
- }
1111
- /** The plugin-source group, above the plugin groups. No-op without the field. */
1112
- function renderPluginSource(section, report, copy, actions) {
1663
+ function renderRuntime(section, report, copy, actions, dismissedAlert, onDismissAlert) {
1113
1664
  section.textContent = "";
1114
1665
  if (report === null) return;
1115
- const node = pluginSourceGroup(report, copy, actions);
1116
- if (node !== null) section.append(node);
1666
+ const alert = runtimeResultAlert(report, copy, dismissedAlert, onDismissAlert);
1667
+ if (alert !== null) section.append(alert);
1668
+ const engine = runtimeEngineCard(report, copy, actions);
1669
+ if (engine !== null) section.append(engine);
1117
1670
  }
1118
- /** Run the install transaction, then keep polling until the state settles. */
1671
+ /** Run the install transaction (a new durable job), then let the store poll it. */
1119
1672
  async function runInstall(store, copy) {
1120
1673
  const report = store.report();
1121
1674
  if (report === null) return;
@@ -1123,15 +1676,18 @@ window.__ModuleLoader__.load({
1123
1676
  showToast(copy.checkFailed, "error");
1124
1677
  return;
1125
1678
  }
1679
+ store.setInstalling(true);
1126
1680
  try {
1127
1681
  const result = await installSet(report.manifestVersion);
1128
1682
  if (result.error !== void 0) {
1683
+ store.setInstalling(false);
1129
1684
  showToast(`${copy.installError}: ${result.error}`, "error");
1130
1685
  return;
1131
1686
  }
1132
1687
  showToast(copy.installSuccess, "success");
1133
- pollUntilSettled(store);
1688
+ store.fetchStatus();
1134
1689
  } catch (err) {
1690
+ store.setInstalling(false);
1135
1691
  showToast(`${copy.installError}: ${err instanceof Error ? err.message : String(err)}`, "error");
1136
1692
  }
1137
1693
  }
@@ -1154,61 +1710,65 @@ window.__ModuleLoader__.load({
1154
1710
  toastTimer = null;
1155
1711
  }, TOAST_HOLD_MS);
1156
1712
  }
1157
- function renderActions(bar, report, copy, store, installing) {
1158
- bar.textContent = "";
1159
- if (report === null) return;
1160
- if (installing) {
1161
- const line = el("span", "hqpu-status hqpu-status--dot");
1162
- const spinner = el("span", "hqpu-spinner");
1163
- line.append(spinner, el("span", "", copy.installing));
1164
- bar.append(line);
1713
+ /**
1714
+ * The collapsible "更新源配置 (高级)" panel (gear-toggled). It hosts the
1715
+ * EXISTING plugin-source and runtime-source editors (POST /plugin-source,
1716
+ * POST /runtime-source, save→check→refresh already wired). No new endpoints:
1717
+ * if a manifest source has no data, that field is omitted rather than faked.
1718
+ */
1719
+ function renderSettingsPanel(panel, report, copy, open, pluginActions, runtimeActions) {
1720
+ panel.textContent = "";
1721
+ panel.style.display = open ? "" : "none";
1722
+ if (!open || report === null) return;
1723
+ const row = el("div", "hqpu-settings-row");
1724
+ row.append(el("div", "hqpu-settings-title", copy.ui.settingsTitle));
1725
+ const pluginSource = normalizeSource(report.pluginSource);
1726
+ const runtimeSource = normalizeSource(report.runtime.source);
1727
+ const overridden = (pluginSource?.override ?? false) || (runtimeSource?.override ?? false);
1728
+ const badge = el("span", "hqpu-origin-badge", overridden ? copy.ui.originCustom : copy.ui.originDefault);
1729
+ badge.dataset.origin = overridden ? "override" : "config";
1730
+ row.append(badge);
1731
+ panel.append(row);
1732
+ if (pluginSource !== null) {
1733
+ panel.append(el("label", "", copy.ui.pluginUrlLabel));
1734
+ panel.append(sourceEditor("plugin", pluginSource, copy, pluginActions));
1735
+ }
1736
+ if (runtimeSource !== null) {
1737
+ panel.append(el("label", "", copy.ui.runtimeUrlLabel));
1738
+ panel.append(sourceEditor("runtime", runtimeSource, copy, runtimeActions));
1739
+ }
1740
+ }
1741
+ /**
1742
+ * The sticky footer: left meta ("准备安装 N 项兼容插件" / "版本集: vX"), a
1743
+ * full-width primary "一键更新全套插件 (vX)" (install = one durable job),
1744
+ * a secondary 忽略 wired to the existing ignore/unignore affordance, and the
1745
+ * footnote. The count EXCLUDES blocked / ignored / already-current entries.
1746
+ */
1747
+ function renderFooter(footer, installBtn, ignoreBtn, countEl, setEl, report, copy, installing, store) {
1748
+ if (report === null) {
1749
+ countEl.textContent = "";
1750
+ setEl.textContent = "";
1751
+ installBtn.disabled = true;
1752
+ ignoreBtn.style.display = "none";
1165
1753
  return;
1166
1754
  }
1167
1755
  const actionable = report.items.filter((item) => item.state === "update" || item.state === "install").length;
1168
1756
  const hasSet = report.manifestVersion !== null;
1169
- if (actionable === 0) bar.append(el("span", "hqpu-status", copy.nothingToInstall));
1170
- else {
1171
- const installBtn = el("button", "hqpu-btn hqpu-btn--primary");
1172
- installBtn.type = "button";
1173
- installBtn.disabled = !hasSet || report.ignoredSet;
1174
- installBtn.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.installAll(actionable)}</span>`;
1175
- installBtn.addEventListener("click", () => {
1176
- runInstall(store, copy);
1177
- });
1178
- bar.append(installBtn);
1179
- }
1180
- if (!hasSet) return;
1181
- if (report.ignoredSet) {
1182
- const unignore = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.unignore);
1183
- unignore.type = "button";
1184
- unignore.addEventListener("click", () => {
1185
- unignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
1186
- });
1187
- bar.append(unignore);
1188
- } else if (actionable > 0) {
1189
- const ignore = el("button", "hqpu-btn hqpu-btn--ghost hqpu-btn--sm", copy.ignore);
1190
- ignore.type = "button";
1191
- ignore.addEventListener("click", () => {
1192
- ignoreOffer(report.manifestVersion).then(() => store.refresh()).catch(() => void 0);
1193
- });
1194
- bar.append(ignore);
1195
- }
1196
- }
1197
- /** After an install, poll the backend until the update report settles
1198
- * (installing flag clears or DSH restarts under us — the next load re-reads
1199
- * the fresh state). */
1200
- function pollUntilSettled(store) {
1201
- let attempts = 0;
1202
- const tick = () => {
1203
- store.refresh().then((report) => {
1204
- if (store.installing()) return;
1205
- if (report !== null && report.ignoredSet === false && attempts >= 2) return;
1206
- }).catch(() => void 0).finally(() => {
1207
- attempts += 1;
1208
- if (attempts < 10) window.setTimeout(tick, 2e3);
1209
- });
1757
+ const setVersion = report.manifestVersion ?? "";
1758
+ countEl.textContent = copy.ui.footerCount(actionable);
1759
+ setEl.textContent = hasSet ? copy.ui.footerSet(setVersion) : "";
1760
+ installBtn.textContent = hasSet ? copy.ui.installAllSet(setVersion) : copy.ui.installAllSet("—");
1761
+ installBtn.disabled = installing || actionable === 0 || !hasSet || report.ignoredSet;
1762
+ installBtn.dataset.busy = installing ? "true" : "false";
1763
+ installBtn.onclick = () => {
1764
+ runInstall(store, copy);
1765
+ };
1766
+ ignoreBtn.style.display = hasSet ? "" : "none";
1767
+ ignoreBtn.textContent = report.ignoredSet ? copy.unignore : copy.ui.ignoreBtn;
1768
+ ignoreBtn.onclick = () => {
1769
+ const version = report.manifestVersion;
1770
+ (report.ignoredSet ? unignoreOffer(version) : ignoreOffer(version)).then(() => store.refresh()).catch(() => void 0);
1210
1771
  };
1211
- tick();
1212
1772
  }
1213
1773
  /**
1214
1774
  * Open (or focus) the update panel. Re-renders from the store; when the store
@@ -1233,39 +1793,53 @@ window.__ModuleLoader__.load({
1233
1793
  panel.tabIndex = -1;
1234
1794
  const head = el("div", "hqpu-head");
1235
1795
  const headMain = el("div", "hqpu-head-main");
1236
- headMain.append(el("h1", "hqpu-title", copy.viewTitle));
1237
- const intro = el("p", "hqpu-intro");
1238
- intro.append(el("span", "", copy.viewIntro));
1239
- const info = el("button", "hqpu-info");
1240
- info.type = "button";
1241
- info.setAttribute("aria-label", copy.viewInfo);
1242
- info.title = copy.viewInfo;
1243
- info.innerHTML = INFO_ICON(11);
1244
- intro.append(info);
1245
- headMain.append(intro);
1796
+ const brandIcon = el("span", "hqpu-brand-icon");
1797
+ brandIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5.6 2.6a6 6 0 015.6 1.6M12.4 13.4a6 6 0 01-5.6-1.6M10.6 2V5.4H7.2M5.4 14V10.6H8.8"/></svg>`;
1798
+ const titleBlock = el("div", "hqpu-title-block");
1799
+ titleBlock.append(el("h1", "hqpu-title", copy.ui.title));
1800
+ const sub = el("p", "hqpu-sub");
1801
+ titleBlock.append(sub);
1802
+ headMain.append(brandIcon, titleBlock);
1246
1803
  const toolbar = el("div", "hqpu-toolbar");
1247
1804
  const refreshBtn = iconButton("hqpu-icon-btn", REFRESH_ICON(16), copy.refresh, () => {
1248
- store.refresh();
1805
+ store.check();
1249
1806
  });
1250
1807
  toolbar.append(refreshBtn);
1808
+ const gearBtn = iconButton("hqpu-icon-btn", `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="2.2"/><path d="M13.5 9.2v-2.4l-1.8-.6a4.4 4.4 0 00-.5-1.2l.9-1.7-1.7-1.7-1.7.9a4.4 4.4 0 00-1.2-.5L7.2 2.5H4.8l-.6 1.8a4.4 4.4 0 00-1.2.5l-1.7-.9-1.7 1.7.9 1.7a4.4 4.4 0 00-.5 1.2L2.5 7.2v2.4l1.8.6a4.4 4.4 0 00.5 1.2l-.9 1.7 1.7 1.7 1.7-.9a4.4 4.4 0 001.2.5l.6 1.8h2.4l.6-1.8a4.4 4.4 0 001.2-.5l1.7.9 1.7-1.7-.9-1.7a4.4 4.4 0 00.5-1.2l1.8-.6z"/></svg>`, copy.ui.gearTooltip, () => {
1809
+ settingsOpen = !settingsOpen;
1810
+ render();
1811
+ });
1812
+ toolbar.append(gearBtn);
1251
1813
  const closeBtn = iconButton("hqpu-icon-btn", CLOSE_ICON(16), copy.close, () => {
1252
1814
  onClose();
1253
1815
  });
1254
1816
  toolbar.append(closeBtn);
1255
1817
  head.append(headMain, toolbar);
1256
1818
  panel.append(head);
1819
+ const settingsPanel = el("div", "hqpu-settings");
1820
+ panel.append(settingsPanel);
1257
1821
  const scroll = el("div", "hqpu-scroll");
1258
1822
  const column = el("div", "hqpu-column");
1259
- const pluginSourceSection = el("div", "hqpu-stack");
1823
+ const jobSection = el("div", "hqpu-stack");
1260
1824
  const listSection = el("div", "hqpu-stack");
1261
1825
  const runtimeSection = el("div", "hqpu-stack");
1262
1826
  const toastSection = el("div", "hqpu-toast-host");
1263
- column.append(pluginSourceSection, listSection, runtimeSection, toastSection);
1827
+ column.append(jobSection, listSection, runtimeSection, toastSection);
1264
1828
  scroll.append(column);
1265
1829
  panel.append(scroll);
1266
1830
  const footer = el("div", "hqpu-footer");
1267
- const actionBar = el("div", "hqpu-actions");
1268
- footer.append(actionBar);
1831
+ const footMeta = el("div", "hqpu-foot-meta");
1832
+ const footCount = el("span", "");
1833
+ const footSet = el("span", "hqpu-foot-set");
1834
+ footMeta.append(footCount, footSet);
1835
+ const footActions = el("div", "hqpu-foot-actions");
1836
+ const installAllBtn = el("button", "hqpu-btn hqpu-btn--block");
1837
+ installAllBtn.type = "button";
1838
+ const ignoreBtn = el("button", "hqpu-btn--ignore", copy.ui.ignoreBtn);
1839
+ ignoreBtn.type = "button";
1840
+ footActions.append(installAllBtn, ignoreBtn);
1841
+ const footnote = el("p", "hqpu-footnote", copy.ui.footnote);
1842
+ footer.append(footMeta, footActions, footnote);
1269
1843
  panel.append(footer);
1270
1844
  overlay.append(scrim, panel);
1271
1845
  document.body.appendChild(overlay);
@@ -1275,6 +1849,10 @@ window.__ModuleLoader__.load({
1275
1849
  let runtimeArmed = false;
1276
1850
  let runtimeBusy = false;
1277
1851
  let runtimeAccepted = false;
1852
+ const changelogOpen = /* @__PURE__ */ new Set();
1853
+ let allOpen = false;
1854
+ let settingsOpen = false;
1855
+ let dismissedAlert = false;
1278
1856
  const runtimeSourceState = {
1279
1857
  editing: false,
1280
1858
  saving: false,
@@ -1403,6 +1981,7 @@ window.__ModuleLoader__.load({
1403
1981
  }
1404
1982
  runtimeAccepted = true;
1405
1983
  showToast(copy.runtime.updateAccepted, "success");
1984
+ store.fetchStatus();
1406
1985
  } catch (err) {
1407
1986
  showToast(`${copy.installError}: ${err instanceof Error ? err.message : String(err)}`, "error");
1408
1987
  } finally {
@@ -1410,18 +1989,59 @@ window.__ModuleLoader__.load({
1410
1989
  render();
1411
1990
  }
1412
1991
  }
1992
+ /** Retry a failed/abandoned job: plugin-set → a fresh install; runtime →
1993
+ * re-arm the runtime confirm flow. Always a NEW job, never re-clicking the
1994
+ * settled job id. */
1995
+ const retryJob = (kind) => {
1996
+ if (kind === "runtime") {
1997
+ runtimeArmed = true;
1998
+ render();
1999
+ } else runInstall(store, copy);
2000
+ };
2001
+ const renderJobSection = () => {
2002
+ jobSection.textContent = "";
2003
+ const active = store.activeJob();
2004
+ if (active !== null && !TERMINAL_PHASES.has(active.phase)) {
2005
+ jobSection.append(activeJobView(active, copy));
2006
+ return;
2007
+ }
2008
+ const recent = store.recentJobs();
2009
+ const latest = recent[0];
2010
+ if (latest !== void 0 && TERMINAL_PHASES.has(latest.phase)) {
2011
+ jobSection.append(resultView(latest, copy, () => retryJob(latest.kind)));
2012
+ const older = recentJobsList(recent, copy);
2013
+ if (older !== null) jobSection.append(older);
2014
+ }
2015
+ };
1413
2016
  const render = () => {
1414
2017
  const report = store.report();
1415
- const installing = store.installing();
2018
+ const jobActive = store.activeJob() !== null && !TERMINAL_PHASES.has(store.activeJob().phase);
2019
+ const installing = store.installing() || jobActive;
1416
2020
  const stale = report === null;
1417
2021
  refreshBtn.disabled = stale;
2022
+ refreshBtn.title = copy.refresh;
2023
+ const ago = relativeAgo(report?.checkedAt, copy);
2024
+ sub.textContent = ago !== null ? copy.ui.checkedAgo(ago) : copy.ui.checkedUnknown;
2025
+ renderSettingsPanel(settingsPanel, report, copy, settingsOpen, pluginActions, runtimeSourceActions);
2026
+ renderJobSection();
1418
2027
  if (stale) {
1419
2028
  listSection.textContent = "";
1420
2029
  listSection.append(skeleton(copy));
1421
- } else renderList(listSection, report, copy, installing, store);
1422
- renderPluginSource(pluginSourceSection, report, copy, pluginActions);
1423
- renderRuntime(runtimeSection, report, copy, runtimeActions);
1424
- renderActions(actionBar, report, copy, store, installing);
2030
+ } else renderList(listSection, report, copy, changelogOpen, allOpen, (name) => {
2031
+ if (changelogOpen.has(name)) changelogOpen.delete(name);
2032
+ else changelogOpen.add(name);
2033
+ render();
2034
+ }, () => {
2035
+ allOpen = !allOpen;
2036
+ render();
2037
+ }, () => {
2038
+ store.check();
2039
+ });
2040
+ renderRuntime(runtimeSection, report, copy, runtimeActions, dismissedAlert, () => {
2041
+ dismissedAlert = true;
2042
+ render();
2043
+ });
2044
+ renderFooter(footer, installAllBtn, ignoreBtn, footCount, footSet, report, copy, installing, store);
1425
2045
  };
1426
2046
  const unsubscribe = store.subscribe(render);
1427
2047
  disposers.push(unsubscribe);
@@ -2202,6 +2822,97 @@ window.__ModuleLoader__.load({
2202
2822
  padding: 0 24px 20px;
2203
2823
  }
2204
2824
 
2825
+ /* ── Durable job (active / result) ────────────────────────────────────── */
2826
+ .hqpu-job {
2827
+ display: flex;
2828
+ flex-direction: column;
2829
+ gap: 6px;
2830
+ padding: 12px;
2831
+ border: 0.5px solid var(--dsw-alias-border-l2);
2832
+ border-radius: var(--dsw-radius-lg);
2833
+ background: var(--dsw-alias-bg-layer-3);
2834
+ }
2835
+ .hqpu-job-meta {
2836
+ font-variant-numeric: tabular-nums;
2837
+ }
2838
+ .hqpu-progress {
2839
+ height: 6px;
2840
+ border-radius: 999px;
2841
+ background: var(--dsw-alias-bg-module-platform);
2842
+ overflow: hidden;
2843
+ }
2844
+ .hqpu-progress-fill {
2845
+ height: 100%;
2846
+ border-radius: 999px;
2847
+ background: var(--dsw-alias-state-business-primary);
2848
+ transition: width var(--ds-transition-duration, 0.2s) ease;
2849
+ }
2850
+ .hqpu-job-steps {
2851
+ display: flex;
2852
+ flex-direction: column;
2853
+ gap: 4px;
2854
+ }
2855
+ .hqpu-steps {
2856
+ margin: 0;
2857
+ padding: 0;
2858
+ list-style: none;
2859
+ display: flex;
2860
+ flex-direction: column;
2861
+ gap: 4px;
2862
+ }
2863
+ .hqpu-step {
2864
+ display: flex;
2865
+ align-items: center;
2866
+ gap: 6px;
2867
+ font-size: 12.5px;
2868
+ line-height: 18px;
2869
+ color: var(--dsw-alias-label-secondary);
2870
+ }
2871
+ .hqpu-step--skipped,
2872
+ .hqpu-step--pending { opacity: 0.6; }
2873
+
2874
+ /* ── Per-plugin change list + release notes ────────────────────────────── */
2875
+ .hqpu-changes {
2876
+ padding: 0 8px 8px 70px;
2877
+ }
2878
+ .hqpu-changes-list,
2879
+ .hqpu-release-notes {
2880
+ margin: 4px 0 0;
2881
+ padding-left: 18px;
2882
+ font-size: 12.5px;
2883
+ line-height: 19px;
2884
+ color: var(--dsw-alias-label-secondary);
2885
+ }
2886
+ .hqpu-release-link {
2887
+ align-self: flex-start;
2888
+ }
2889
+
2890
+ /* ── Advanced disclosure (source editors) ───────────────────────────────── */
2891
+ .hqpu-advanced > summary {
2892
+ cursor: pointer;
2893
+ font-size: 12.5px;
2894
+ line-height: 20px;
2895
+ color: var(--dsw-alias-label-tertiary);
2896
+ list-style: none;
2897
+ padding: 2px 0;
2898
+ }
2899
+ .hqpu-advanced > summary::-webkit-details-marker { display: none; }
2900
+ .hqpu-advanced > summary::before {
2901
+ content: '▸ ';
2902
+ }
2903
+ .hqpu-advanced[open] > summary::before {
2904
+ content: '▾ ';
2905
+ }
2906
+
2907
+ /* Danger-toned banner (check failure, breaking changes) */
2908
+ .hqpu-banner[data-tone='danger'] {
2909
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent);
2910
+ color: var(--dsw-alias-label-secondary);
2911
+ }
2912
+ .hqpu-banner[data-tone='danger'] > svg {
2913
+ color: var(--dsw-alias-state-error-primary);
2914
+ }
2915
+
2205
2916
  .hqpu-visually-hidden {
2206
2917
  position: absolute;
2207
2918
  width: 1px;
@@ -2220,58 +2931,697 @@ window.__ModuleLoader__.load({
2220
2931
  .hqpu-panel,
2221
2932
  .hqpu-skeleton-fill,
2222
2933
  .hqpu-spinner,
2223
- .hqpu-toast {
2934
+ .hqpu-toast,
2935
+ .hqpu-pulse-ring,
2936
+ .hqpu-job-step--active {
2224
2937
  animation: none;
2225
2938
  }
2226
2939
  }
2227
- `;
2228
- const STYLE_TAG_ID = "data-plugin-css=\"huaqiu-plugin-update\"";
2229
- /**
2230
- * Design tokens the bundled DSH shell may not publish.
2231
- *
2232
- * hq-edge ships an older `ui-theme` build. A live probe of the running shell
2233
- * (both themes) shows 13 of the tokens this stylesheet reads are absent:
2234
- * `--dsw-radius-{xs,sm,md,lg,xl,panel}`, `--dsw-focus-ring-width`,
2235
- * `--dsw-focus-ring-color`, `--dsw-alias-toast-label`,
2236
- * `--dsw-alias-state-idle-primary`, `--dsw-font-mono`,
2237
- * `--dsh-frame-overlay-top` and `--dsh-frame-chrome-top`.
2238
- *
2239
- * An unresolved `var()` is not an error — it silently degrades the property to
2240
- * its initial value. That is how this panel once rendered with
2241
- * `border-radius: 0px` and a 3px `auto` focus ring while every colour was
2242
- * already correct.
2243
- *
2244
- * The last three (`--dsw-font-mono`, `--dsh-frame-*`) are deliberately NOT
2245
- * backfilled: the shell's own primitives rely on the inline `var(…, fallback)`
2246
- * form for those (`Modal.module.css`, `PluginManagerPage.module.css`), so this
2247
- * sheet does too. Everything else is backfilled below with the canonical value
2248
- * from `ui-theme/src/styles/{base,focus,design-platform}.css`.
2249
- *
2250
- * Values are feature-detected at runtime and scoped to this plugin's own roots
2251
- * rather than `:root`, so a newer shell always wins and filling a gap can never
2252
- * restyle the host.
2253
- */
2254
- /** The two roots this plugin styles: the view overlay (built in
2255
- * update-view.ts) and the sidebar row ({@link ENTRY_CSS}.entry). */
2256
- const STYLE_ROOTS = ".hqpu-overlay, .hqpu-sidebar-entry";
2257
- /** The shell's dark-theme carrier — audited live as `body[data-ds-dark-theme]`. */
2258
- const DARK_THEME_SELECTOR = "body[data-ds-dark-theme]";
2259
- /** Same roots, one rule per theme, for tokens whose official value flips. */
2260
- const DARK_STYLE_ROOTS = STYLE_ROOTS.split(",").map((root) => `${DARK_THEME_SELECTOR} ${root.trim()}`).join(",");
2261
- const DESIGN_TOKEN_FALLBACKS = [
2262
- {
2263
- token: "--dsw-radius-xs",
2264
- value: "4px"
2265
- },
2266
- {
2267
- token: "--dsw-radius-sm",
2268
- value: "8px"
2269
- },
2270
- {
2271
- token: "--dsw-radius-md",
2272
- value: "12px"
2273
- },
2274
- {
2940
+
2941
+ /* ════════════════════════════════════════════════════════════════════════
2942
+ * Refactored dock (interactive_refactored_ui_mockup.html) — a narrow dark-zinc
2943
+ * update-center column. These rules cascade over the shell-token base: the
2944
+ * panel becomes ~340px wide, the zinc palette hard-coded per mockup, with a
2945
+ * sticky header / scroll body / sticky footer. No Tailwind, no dependencies.
2946
+ * ════════════════════════════════════════════════════════════════════════ */
2947
+ .hqpu-panel,
2948
+ .hqpu-panel * {
2949
+ --hq-bg: #18181b;
2950
+ --hq-card: #27272a;
2951
+ --hq-card-soft: #1c1c1f;
2952
+ --hq-border: #3f3f46;
2953
+ --hq-hover: #323236;
2954
+ --hq-text: #f4f4f5;
2955
+ --hq-muted: #a1a1aa;
2956
+ --hq-faint: #71717a;
2957
+ --hq-brand: #dc2626;
2958
+ --hq-brand-soft: #ef4444;
2959
+ --hq-emerald: #34d399;
2960
+ --hq-amber: #f59e0b;
2961
+ }
2962
+ .hqpu-panel {
2963
+ width: min(360px, calc(100vw - 32px));
2964
+ max-height: min(86vh, 720px);
2965
+ background: var(--hq-bg);
2966
+ color: var(--hq-text);
2967
+ border: 0.5px solid var(--hq-border);
2968
+ border-radius: 12px;
2969
+ gap: 0;
2970
+ overflow: hidden;
2971
+ }
2972
+ .hqpu-scrim {
2973
+ background: rgba(0, 0, 0, 0.55);
2974
+ }
2975
+
2976
+ /* Sticky header */
2977
+ .hqpu-head {
2978
+ flex: none;
2979
+ display: flex;
2980
+ align-items: center;
2981
+ justify-content: space-between;
2982
+ gap: 8px;
2983
+ padding: 10px 12px;
2984
+ border-bottom: 0.5px solid var(--hq-border);
2985
+ background: color-mix(in srgb, var(--hq-bg) 95%, transparent);
2986
+ position: sticky;
2987
+ top: 0;
2988
+ z-index: 2;
2989
+ }
2990
+ .hqpu-head-main {
2991
+ display: flex;
2992
+ align-items: center;
2993
+ gap: 8px;
2994
+ min-width: 0;
2995
+ margin: 0;
2996
+ }
2997
+ .hqpu-brand-icon {
2998
+ display: flex;
2999
+ align-items: center;
3000
+ justify-content: center;
3001
+ flex: none;
3002
+ width: 24px;
3003
+ height: 24px;
3004
+ border-radius: 6px;
3005
+ background: color-mix(in srgb, var(--hq-brand) 20%, transparent);
3006
+ color: var(--hq-brand-soft);
3007
+ }
3008
+ .hqpu-brand-icon svg { width: 14px; height: 14px; display: block; }
3009
+ .hqpu-title-block { min-width: 0; }
3010
+ .hqpu-title {
3011
+ margin: 0;
3012
+ font-size: 13px;
3013
+ font-weight: 600;
3014
+ line-height: 16px;
3015
+ color: var(--hq-text);
3016
+ }
3017
+ .hqpu-sub {
3018
+ margin: 2px 0 0;
3019
+ font-size: 10px;
3020
+ line-height: 14px;
3021
+ color: var(--hq-muted);
3022
+ white-space: nowrap;
3023
+ overflow: hidden;
3024
+ text-overflow: ellipsis;
3025
+ }
3026
+ .hqpu-toolbar { gap: 2px; }
3027
+ .hqpu-icon-btn {
3028
+ width: 26px;
3029
+ height: 26px;
3030
+ border-radius: 6px;
3031
+ color: var(--hq-muted);
3032
+ }
3033
+ .hqpu-icon-btn:hover:not(:disabled) {
3034
+ background: var(--hq-hover);
3035
+ color: var(--hq-text);
3036
+ }
3037
+ .hqpu-icon-btn svg { width: 14px; height: 14px; }
3038
+
3039
+ /* Collapsible settings panel */
3040
+ .hqpu-settings {
3041
+ flex: none;
3042
+ padding: 10px 12px;
3043
+ border-bottom: 0.5px solid var(--hq-border);
3044
+ background: #131316;
3045
+ }
3046
+ .hqpu-settings-row {
3047
+ display: flex;
3048
+ align-items: center;
3049
+ justify-content: space-between;
3050
+ gap: 8px;
3051
+ margin-bottom: 8px;
3052
+ }
3053
+ .hqpu-settings-title {
3054
+ font-size: 11px;
3055
+ font-weight: 500;
3056
+ color: #d4d4d8;
3057
+ }
3058
+ .hqpu-origin-badge {
3059
+ font-size: 9px;
3060
+ padding: 1px 6px;
3061
+ border-radius: 4px;
3062
+ border: 0.5px solid;
3063
+ }
3064
+ .hqpu-origin-badge[data-origin='config'] {
3065
+ background: rgba(6, 78, 59, 0.4);
3066
+ color: var(--hq-emerald);
3067
+ border-color: rgba(6, 95, 70, 0.6);
3068
+ }
3069
+ .hqpu-origin-badge[data-origin='override'] {
3070
+ background: rgba(120, 53, 15, 0.4);
3071
+ color: var(--hq-amber);
3072
+ border-color: rgba(180, 83, 9, 0.5);
3073
+ }
3074
+ .hqpu-settings label {
3075
+ display: block;
3076
+ font-size: 10px;
3077
+ color: var(--hq-muted);
3078
+ margin: 6px 0 3px;
3079
+ }
3080
+ .hqpu-source-input {
3081
+ width: 100%;
3082
+ height: 26px;
3083
+ font-size: 10px;
3084
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3085
+ background: var(--hq-bg);
3086
+ border: 0.5px solid var(--hq-border);
3087
+ border-radius: 5px;
3088
+ color: #d4d4d8;
3089
+ padding: 0 8px;
3090
+ box-sizing: border-box;
3091
+ }
3092
+ .hqpu-source-input:focus-visible {
3093
+ outline: none;
3094
+ border-color: var(--hq-brand-soft);
3095
+ }
3096
+ .hqpu-settings-actions {
3097
+ display: flex;
3098
+ justify-content: flex-end;
3099
+ gap: 6px;
3100
+ margin-top: 8px;
3101
+ }
3102
+
3103
+ /* Scroll body */
3104
+ .hqpu-scroll {
3105
+ flex: 1;
3106
+ padding: 12px;
3107
+ background: var(--hq-bg);
3108
+ --dsh-scrollbar-thumb: #3f3f46;
3109
+ --dsh-scrollbar-thumb-hover: #52525b;
3110
+ }
3111
+ .hqpu-column {
3112
+ max-width: none;
3113
+ margin: 0;
3114
+ gap: 10px;
3115
+ }
3116
+ .hqpu-stack { gap: 10px; }
3117
+
3118
+ /* ── Active-job banner ─────────────────────────────────────────────────── */
3119
+ .hqpu-active {
3120
+ position: relative;
3121
+ padding: 12px;
3122
+ border-radius: 8px;
3123
+ border: 0.5px solid color-mix(in srgb, var(--hq-brand-soft) 30%, transparent);
3124
+ background: #18181b;
3125
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
3126
+ }
3127
+ .hqpu-active-top {
3128
+ display: flex;
3129
+ align-items: center;
3130
+ justify-content: space-between;
3131
+ gap: 8px;
3132
+ }
3133
+ .hqpu-active-title {
3134
+ display: flex;
3135
+ align-items: center;
3136
+ gap: 6px;
3137
+ min-width: 0;
3138
+ font-size: 11px;
3139
+ font-weight: 600;
3140
+ color: #e4e4e7;
3141
+ }
3142
+ .hqpu-pulse-dot {
3143
+ position: relative;
3144
+ flex: none;
3145
+ width: 8px;
3146
+ height: 8px;
3147
+ }
3148
+ .hqpu-pulse-dot::before {
3149
+ content: '';
3150
+ position: absolute;
3151
+ inset: 0;
3152
+ border-radius: 50%;
3153
+ background: var(--hq-brand-soft);
3154
+ }
3155
+ .hqpu-pulse-ring {
3156
+ position: absolute;
3157
+ inset: 0;
3158
+ border-radius: 50%;
3159
+ background: var(--hq-brand-soft);
3160
+ opacity: 0.6;
3161
+ animation: hqpu-ping 1.4s cubic-bezier(0, 0, 0.2, 1) infinite;
3162
+ }
3163
+ @keyframes hqpu-ping {
3164
+ 75%, 100% { transform: scale(2.2); opacity: 0; }
3165
+ }
3166
+ .hqpu-active-percent {
3167
+ flex: none;
3168
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3169
+ font-size: 11px;
3170
+ font-weight: 500;
3171
+ color: var(--hq-brand-soft);
3172
+ }
3173
+ .hqpu-progress {
3174
+ height: 6px;
3175
+ margin-top: 10px;
3176
+ border-radius: 999px;
3177
+ background: #27272a;
3178
+ overflow: hidden;
3179
+ }
3180
+ .hqpu-progress-fill {
3181
+ background: linear-gradient(to right, var(--hq-brand), var(--hq-brand-soft));
3182
+ transition: width 0.3s ease;
3183
+ }
3184
+ .hqpu-active-meta {
3185
+ display: flex;
3186
+ justify-content: space-between;
3187
+ gap: 8px;
3188
+ margin-top: 4px;
3189
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3190
+ font-size: 10px;
3191
+ color: var(--hq-muted);
3192
+ }
3193
+ /* 4-step indicator row */
3194
+ .hqpu-job-steps4 {
3195
+ display: grid;
3196
+ grid-template-columns: repeat(4, 1fr);
3197
+ gap: 4px;
3198
+ margin-top: 10px;
3199
+ padding-top: 8px;
3200
+ border-top: 0.5px solid #27272a;
3201
+ text-align: center;
3202
+ font-size: 9px;
3203
+ font-weight: 500;
3204
+ }
3205
+ .hqpu-job-step--done { color: var(--hq-emerald); }
3206
+ .hqpu-job-step--active {
3207
+ color: var(--hq-brand-soft);
3208
+ animation: hqpu-step-pulse 1.2s ease-in-out infinite;
3209
+ }
3210
+ .hqpu-job-step--todo { color: var(--hq-muted); }
3211
+ @keyframes hqpu-step-pulse {
3212
+ 0%, 100% { opacity: 1; }
3213
+ 50% { opacity: 0.45; }
3214
+ }
3215
+
3216
+ /* ── Result / rollback alert (amber) vs green success ──────────────────── */
3217
+ .hqpu-alert {
3218
+ display: flex;
3219
+ align-items: flex-start;
3220
+ gap: 8px;
3221
+ padding: 10px;
3222
+ border-radius: 8px;
3223
+ }
3224
+ .hqpu-alert--amber {
3225
+ background: rgba(69, 26, 3, 0.35);
3226
+ border: 0.5px solid rgba(180, 83, 9, 0.4);
3227
+ }
3228
+ .hqpu-alert--amber svg { color: var(--hq-amber); flex: none; margin-top: 1px; }
3229
+ .hqpu-alert-green {
3230
+ background: rgba(6, 78, 59, 0.3);
3231
+ border: 0.5px solid rgba(6, 95, 70, 0.4);
3232
+ color: #a7f3d0;
3233
+ font-size: 11px;
3234
+ }
3235
+ .hqpu-alert-title {
3236
+ font-size: 11px;
3237
+ font-weight: 500;
3238
+ color: #fcd34d;
3239
+ }
3240
+ .hqpu-alert-body {
3241
+ margin: 2px 0 0;
3242
+ font-size: 10px;
3243
+ line-height: 15px;
3244
+ color: rgba(252, 211, 77, 0.8);
3245
+ }
3246
+ .hqpu-alert-body code {
3247
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3248
+ background: rgba(120, 53, 15, 0.5);
3249
+ padding: 1px 4px;
3250
+ border-radius: 3px;
3251
+ }
3252
+ .hqpu-alert-actions {
3253
+ display: flex;
3254
+ gap: 8px;
3255
+ margin-top: 6px;
3256
+ }
3257
+ .hqpu-alert-dismiss {
3258
+ background: none;
3259
+ border: 0;
3260
+ padding: 0;
3261
+ font-size: 10px;
3262
+ color: #fbbf24;
3263
+ cursor: pointer;
3264
+ text-decoration: underline;
3265
+ }
3266
+
3267
+ /* ── Runtime engine card ────────────────────────────────────────────────── */
3268
+ .hqpu-engine {
3269
+ padding: 12px;
3270
+ border-radius: 8px;
3271
+ border: 0.5px solid var(--hq-border);
3272
+ background: var(--hq-card);
3273
+ }
3274
+ .hqpu-engine-head {
3275
+ display: flex;
3276
+ align-items: center;
3277
+ justify-content: space-between;
3278
+ gap: 8px;
3279
+ }
3280
+ .hqpu-engine-title {
3281
+ display: flex;
3282
+ align-items: center;
3283
+ gap: 6px;
3284
+ font-size: 12px;
3285
+ font-weight: 600;
3286
+ color: #e4e4e7;
3287
+ }
3288
+ .hqpu-engine-title svg { width: 14px; height: 14px; color: #a1a1aa; }
3289
+ .hqpu-chip {
3290
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3291
+ font-size: 9px;
3292
+ padding: 2px 6px;
3293
+ border-radius: 4px;
3294
+ background: #27272a;
3295
+ border: 0.5px solid #52525b;
3296
+ color: #d4d4d8;
3297
+ }
3298
+ .hqpu-engine-update-row {
3299
+ display: flex;
3300
+ align-items: center;
3301
+ justify-content: space-between;
3302
+ gap: 8px;
3303
+ margin-top: 10px;
3304
+ padding: 8px;
3305
+ border-radius: 6px;
3306
+ border: 0.5px solid rgba(63, 63, 70, 0.7);
3307
+ background: rgba(24, 24, 27, 0.6);
3308
+ }
3309
+ .hqpu-engine-new-line {
3310
+ display: flex;
3311
+ align-items: center;
3312
+ gap: 4px;
3313
+ font-size: 11px;
3314
+ font-weight: 500;
3315
+ color: #d4d4d8;
3316
+ }
3317
+ .hqpu-tag--brand {
3318
+ font-size: 9px;
3319
+ padding: 1px 4px;
3320
+ border-radius: 3px;
3321
+ color: var(--hq-brand-soft);
3322
+ background: rgba(69, 10, 10, 0.6);
3323
+ border: 0.5px solid rgba(153, 27, 27, 0.5);
3324
+ }
3325
+ .hqpu-engine-size {
3326
+ margin: 3px 0 0;
3327
+ font-size: 10px;
3328
+ color: var(--hq-muted);
3329
+ overflow: hidden;
3330
+ text-overflow: ellipsis;
3331
+ white-space: nowrap;
3332
+ }
3333
+ .hqpu-btn--engine {
3334
+ flex: none;
3335
+ height: auto;
3336
+ padding: 6px 10px;
3337
+ font-size: 10px;
3338
+ background: #27272a;
3339
+ border: 0.5px solid #52525b;
3340
+ color: #e4e4e7;
3341
+ border-radius: 5px;
3342
+ }
3343
+ .hqpu-btn--engine:hover:not(:disabled) { background: #3f3f46; }
3344
+ .hqpu-engine-note {
3345
+ display: flex;
3346
+ align-items: center;
3347
+ gap: 4px;
3348
+ margin: 8px 0 0;
3349
+ font-size: 9px;
3350
+ color: var(--hq-muted);
3351
+ }
3352
+ .hqpu-engine-note svg { width: 10px; height: 10px; color: var(--hq-amber); flex: none; }
3353
+
3354
+ /* ── Plugin cards ───────────────────────────────────────────────────────── */
3355
+ .hqpu-section-head {
3356
+ display: flex;
3357
+ align-items: center;
3358
+ justify-content: space-between;
3359
+ gap: 8px;
3360
+ padding: 0 2px;
3361
+ }
3362
+ .hqpu-section-title {
3363
+ display: flex;
3364
+ align-items: center;
3365
+ gap: 6px;
3366
+ font-size: 12px;
3367
+ font-weight: 600;
3368
+ color: #e4e4e7;
3369
+ }
3370
+ .hqpu-count-pill {
3371
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3372
+ font-size: 10px;
3373
+ font-weight: 700;
3374
+ padding: 1px 6px;
3375
+ border-radius: 999px;
3376
+ background: color-mix(in srgb, var(--hq-brand) 20%, transparent);
3377
+ color: var(--hq-brand-soft);
3378
+ }
3379
+ .hqpu-all-changelog {
3380
+ background: none;
3381
+ border: 0;
3382
+ padding: 0;
3383
+ font-size: 10px;
3384
+ color: var(--hq-muted);
3385
+ cursor: pointer;
3386
+ }
3387
+ .hqpu-all-changelog:hover { color: var(--hq-text); }
3388
+
3389
+ .hqpu-cards { gap: 8px; }
3390
+ .hqpu-card {
3391
+ margin: 0;
3392
+ padding: 10px;
3393
+ border-radius: 8px;
3394
+ border: 0.5px solid var(--hq-border);
3395
+ background: var(--hq-card);
3396
+ transition: border-color 0.15s ease;
3397
+ }
3398
+ .hqpu-card:hover { border-color: #52525b; }
3399
+ .hqpu-card-head {
3400
+ display: flex;
3401
+ align-items: flex-start;
3402
+ justify-content: space-between;
3403
+ gap: 8px;
3404
+ padding: 0;
3405
+ }
3406
+ .hqpu-card-head > * { display: flex; align-items: center; gap: 8px; min-width: 0; }
3407
+ .hqpu-avatar {
3408
+ display: flex;
3409
+ align-items: center;
3410
+ justify-content: center;
3411
+ flex: none;
3412
+ width: 28px;
3413
+ height: 28px;
3414
+ border-radius: 6px;
3415
+ border: 0.5px solid #52525b;
3416
+ background: #27272a;
3417
+ color: #d4d4d8;
3418
+ font-size: 10px;
3419
+ font-weight: 700;
3420
+ }
3421
+ .hqpu-card-title {
3422
+ font-size: 12px;
3423
+ font-weight: 600;
3424
+ color: #e4e4e7;
3425
+ }
3426
+ .hqpu-card-sub {
3427
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3428
+ font-size: 9px;
3429
+ color: var(--hq-faint);
3430
+ }
3431
+ .hqpu-version-row {
3432
+ display: flex;
3433
+ align-items: center;
3434
+ gap: 4px;
3435
+ margin-top: 2px;
3436
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3437
+ font-size: 10px;
3438
+ }
3439
+ .hqpu-ver-old { color: var(--hq-muted); }
3440
+ .hqpu-ver-arrow { color: #52525b; }
3441
+ .hqpu-ver-new { color: var(--hq-emerald); font-weight: 700; }
3442
+ .hqpu-log-toggle {
3443
+ display: inline-flex;
3444
+ align-items: center;
3445
+ gap: 2px;
3446
+ background: none;
3447
+ border: 0;
3448
+ padding: 4px;
3449
+ font-size: 10px;
3450
+ color: var(--hq-muted);
3451
+ cursor: pointer;
3452
+ }
3453
+ .hqpu-log-toggle:hover { color: var(--hq-text); }
3454
+ .hqpu-log-toggle svg { width: 12px; height: 12px; transition: transform 0.15s ease; }
3455
+ .hqpu-log-toggle[data-open='true'] svg { transform: rotate(180deg); }
3456
+ .hqpu-changelog-drawer {
3457
+ margin-top: 8px;
3458
+ padding: 8px;
3459
+ border-radius: 6px;
3460
+ border: 0.5px solid #27272a;
3461
+ background: rgba(24, 24, 27, 0.8);
3462
+ font-size: 10px;
3463
+ }
3464
+ .hqpu-changelog-drawer-title {
3465
+ font-size: 9px;
3466
+ font-weight: 500;
3467
+ text-transform: uppercase;
3468
+ letter-spacing: 0.05em;
3469
+ color: #a1a1aa;
3470
+ }
3471
+ .hqpu-changelog-drawer ul {
3472
+ margin: 4px 0 0;
3473
+ padding-left: 16px;
3474
+ color: #d4d4d8;
3475
+ line-height: 16px;
3476
+ }
3477
+
3478
+ /* Blocked / incompatible card: dimmed, rose badge, no install button */
3479
+ .hqpu-card--blocked {
3480
+ opacity: 0.8;
3481
+ background: rgba(39, 39, 42, 0.6);
3482
+ border-color: #27272a;
3483
+ }
3484
+ .hqpu-card--blocked .hqpu-avatar {
3485
+ background: rgba(39, 39, 42, 0.5);
3486
+ border-color: #27272a;
3487
+ color: #71717a;
3488
+ }
3489
+ .hqpu-card--blocked .hqpu-card-title { color: #a1a1aa; }
3490
+ .hqpu-blocked-badge {
3491
+ font-size: 9px;
3492
+ padding: 2px 6px;
3493
+ border-radius: 4px;
3494
+ color: #fb7185;
3495
+ background: rgba(76, 5, 25, 0.5);
3496
+ border: 0.5px solid rgba(136, 19, 55, 0.5);
3497
+ }
3498
+ .hqpu-reason-line {
3499
+ display: flex;
3500
+ align-items: flex-start;
3501
+ gap: 4px;
3502
+ margin: 8px 0 0;
3503
+ padding: 6px;
3504
+ border-radius: 5px;
3505
+ border: 0.5px solid rgba(136, 19, 55, 0.2);
3506
+ background: rgba(76, 5, 25, 0.2);
3507
+ font-size: 10px;
3508
+ color: rgba(251, 113, 133, 0.9);
3509
+ }
3510
+ .hqpu-reason-line svg { width: 12px; height: 12px; flex: none; margin-top: 1px; }
3511
+
3512
+ /* Sticky footer */
3513
+ .hqpu-footer {
3514
+ flex: none;
3515
+ display: flex;
3516
+ flex-direction: column;
3517
+ align-items: stretch;
3518
+ gap: 8px;
3519
+ padding: 10px 12px 12px;
3520
+ border-top: 0.5px solid var(--hq-border);
3521
+ background: color-mix(in srgb, var(--hq-bg) 95%, transparent);
3522
+ }
3523
+ .hqpu-foot-meta {
3524
+ display: flex;
3525
+ align-items: center;
3526
+ justify-content: space-between;
3527
+ gap: 8px;
3528
+ font-size: 10px;
3529
+ color: var(--hq-muted);
3530
+ }
3531
+ .hqpu-foot-set {
3532
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3533
+ }
3534
+ .hqpu-foot-actions {
3535
+ display: flex;
3536
+ gap: 8px;
3537
+ }
3538
+ .hqpu-btn--block {
3539
+ flex: 1;
3540
+ height: 32px;
3541
+ background: var(--hq-brand);
3542
+ color: #fff;
3543
+ font-size: 11px;
3544
+ font-weight: 500;
3545
+ border-radius: 6px;
3546
+ box-shadow: 0 4px 12px rgba(127, 29, 29, 0.4);
3547
+ }
3548
+ .hqpu-btn--block:hover:not(:disabled) { background: var(--hq-brand-soft); }
3549
+ .hqpu-btn--block:disabled { opacity: 0.5; box-shadow: none; }
3550
+ .hqpu-btn--ignore {
3551
+ height: 32px;
3552
+ padding: 0 10px;
3553
+ background: var(--hq-card);
3554
+ border: 0.5px solid var(--hq-border);
3555
+ color: var(--hq-muted);
3556
+ font-size: 10px;
3557
+ border-radius: 6px;
3558
+ }
3559
+ .hqpu-btn--ignore:hover:not(:disabled) { background: var(--hq-hover); color: var(--hq-text); }
3560
+ .hqpu-footnote {
3561
+ margin: 0;
3562
+ font-size: 9px;
3563
+ text-align: center;
3564
+ color: var(--hq-muted);
3565
+ }
3566
+
3567
+ /* Shared bits on the new palette */
3568
+ .hqpu-status { color: var(--hq-muted); font-size: 11px; }
3569
+ .hqpu-empty { color: var(--hq-muted); font-size: 11px; padding: 16px 0; }
3570
+ .hqpu-toast { background: var(--hq-card); border: 0.5px solid var(--hq-border); color: var(--hq-text); font-size: 12px; }
3571
+ .hqpu-skeleton-fill { background: #3f3f46; }
3572
+ .hqpu-job { background: var(--hq-card); border-color: var(--hq-border); }
3573
+ .hqpu-job-meta { color: var(--hq-muted); }
3574
+ .hqpu-release-notes { color: var(--hq-muted); font-size: 11px; }
3575
+ .hqpu-advanced > summary { color: var(--hq-muted); }
3576
+ .hqpu-recent-jobs li { color: var(--hq-muted); font-size: 10px; }
3577
+ `;
3578
+ const STYLE_TAG_ID = "data-plugin-css=\"huaqiu-plugin-update\"";
3579
+ /**
3580
+ * Design tokens the bundled DSH shell may not publish.
3581
+ *
3582
+ * hq-edge ships an older `ui-theme` build. A live probe of the running shell
3583
+ * (both themes) shows 13 of the tokens this stylesheet reads are absent:
3584
+ * `--dsw-radius-{xs,sm,md,lg,xl,panel}`, `--dsw-focus-ring-width`,
3585
+ * `--dsw-focus-ring-color`, `--dsw-alias-toast-label`,
3586
+ * `--dsw-alias-state-idle-primary`, `--dsw-font-mono`,
3587
+ * `--dsh-frame-overlay-top` and `--dsh-frame-chrome-top`.
3588
+ *
3589
+ * An unresolved `var()` is not an error — it silently degrades the property to
3590
+ * its initial value. That is how this panel once rendered with
3591
+ * `border-radius: 0px` and a 3px `auto` focus ring while every colour was
3592
+ * already correct.
3593
+ *
3594
+ * The last three (`--dsw-font-mono`, `--dsh-frame-*`) are deliberately NOT
3595
+ * backfilled: the shell's own primitives rely on the inline `var(…, fallback)`
3596
+ * form for those (`Modal.module.css`, `PluginManagerPage.module.css`), so this
3597
+ * sheet does too. Everything else is backfilled below with the canonical value
3598
+ * from `ui-theme/src/styles/{base,focus,design-platform}.css`.
3599
+ *
3600
+ * Values are feature-detected at runtime and scoped to this plugin's own roots
3601
+ * rather than `:root`, so a newer shell always wins and filling a gap can never
3602
+ * restyle the host.
3603
+ */
3604
+ /** The two roots this plugin styles: the view overlay (built in
3605
+ * update-view.ts) and the sidebar row ({@link ENTRY_CSS}.entry). */
3606
+ const STYLE_ROOTS = ".hqpu-overlay, .hqpu-sidebar-entry";
3607
+ /** The shell's dark-theme carrier — audited live as `body[data-ds-dark-theme]`. */
3608
+ const DARK_THEME_SELECTOR = "body[data-ds-dark-theme]";
3609
+ /** Same roots, one rule per theme, for tokens whose official value flips. */
3610
+ const DARK_STYLE_ROOTS = STYLE_ROOTS.split(",").map((root) => `${DARK_THEME_SELECTOR} ${root.trim()}`).join(",");
3611
+ const DESIGN_TOKEN_FALLBACKS = [
3612
+ {
3613
+ token: "--dsw-radius-xs",
3614
+ value: "4px"
3615
+ },
3616
+ {
3617
+ token: "--dsw-radius-sm",
3618
+ value: "8px"
3619
+ },
3620
+ {
3621
+ token: "--dsw-radius-md",
3622
+ value: "12px"
3623
+ },
3624
+ {
2275
3625
  token: "--dsw-radius-lg",
2276
3626
  value: "16px"
2277
3627
  },
@@ -2346,33 +3696,114 @@ window.__ModuleLoader__.load({
2346
3696
  var UpdateStore = class {
2347
3697
  current = null;
2348
3698
  installingFlag = false;
3699
+ /** The durable job the server currently reports as active (resumed across
3700
+ * panel close and DSH restart); null when idle. */
3701
+ active = null;
3702
+ recent = [];
2349
3703
  listeners = /* @__PURE__ */ new Set();
2350
3704
  refreshTimer = null;
3705
+ pollTimer = null;
3706
+ pollBackoffMs = 1e3;
3707
+ pollInFlight = false;
2351
3708
  report() {
2352
3709
  return this.current;
2353
3710
  }
2354
3711
  installing() {
2355
3712
  return this.installingFlag;
2356
3713
  }
3714
+ activeJob() {
3715
+ return this.active;
3716
+ }
3717
+ recentJobs() {
3718
+ return [...this.recent];
3719
+ }
2357
3720
  subscribe(listener) {
2358
3721
  this.listeners.add(listener);
2359
3722
  return () => {
2360
3723
  this.listeners.delete(listener);
2361
3724
  };
2362
3725
  }
2363
- /** Badge count per spec §6.3: actionable `update` + `install`, excluding the
2364
- * ignored offer (the server already folds ignore into item states; keep the
2365
- * client-side filter as a defensive second pass). */
3726
+ /**
3727
+ * Badge: counts actionable plugin updates AND an available runtime update AND
3728
+ * (while a job runs) an attention pill. A recent failed/rolled-back job with
3729
+ * nothing actionable left also keeps a dot, so the outcome is never silently
3730
+ * forgotten after the panel closes.
3731
+ */
2366
3732
  badgeCount() {
3733
+ const job = this.active;
3734
+ if (job !== null && !TERMINAL_PHASES.has(job.phase)) return 1;
2367
3735
  const report = this.current;
2368
3736
  if (report === null) return 0;
2369
- return report.items.filter((item) => item.state === "update" || item.state === "install").length;
3737
+ let count = report.items.filter((item) => item.state === "update" || item.state === "install").length;
3738
+ if (report.runtime?.updateAvailable) count += 1;
3739
+ if (count === 0) {
3740
+ const latest = this.recent[0];
3741
+ if (latest !== void 0 && (latest.phase === "failed" || latest.phase === "rolled-back")) count = 1;
3742
+ }
3743
+ return count;
2370
3744
  }
2371
3745
  emit() {
2372
3746
  for (const listener of [...this.listeners]) try {
2373
3747
  listener();
2374
3748
  } catch {}
2375
3749
  }
3750
+ /** Adopt a fresh `/status` snapshot and (re)arm the poll loop. */
3751
+ applyStatus(status) {
3752
+ const wasActive = this.active !== null;
3753
+ this.active = status.activeJob;
3754
+ this.recent = status.recentJobs;
3755
+ if (wasActive && this.active === null) this.installingFlag = false;
3756
+ this.emit();
3757
+ this.schedulePoll();
3758
+ }
3759
+ /**
3760
+ * Fetch the durable job status. Called on mount, on panel open (BEFORE
3761
+ * discovery) and by the poll loop. A failure (older hq-edge with no
3762
+ * `/status`, or a transient HTTP error) is swallowed: the last known job is
3763
+ * kept and the poller backs off and retries.
3764
+ */
3765
+ async fetchStatus() {
3766
+ try {
3767
+ const status = await fetchJobStatus();
3768
+ this.pollBackoffMs = 1e3;
3769
+ this.applyStatus(status);
3770
+ } catch {
3771
+ this.schedulePoll();
3772
+ }
3773
+ }
3774
+ /** ~1 s poll while a job is active; stop on terminal; back off on error
3775
+ * without discarding the last known job; never overlap requests. */
3776
+ schedulePoll() {
3777
+ if (this.pollTimer !== null) {
3778
+ window.clearTimeout(this.pollTimer);
3779
+ this.pollTimer = null;
3780
+ }
3781
+ const job = this.active;
3782
+ if (job === null || TERMINAL_PHASES.has(job.phase)) {
3783
+ this.pollBackoffMs = 1e3;
3784
+ return;
3785
+ }
3786
+ this.pollTimer = window.setTimeout(() => {
3787
+ this.pollTick();
3788
+ }, this.pollBackoffMs);
3789
+ }
3790
+ async pollTick() {
3791
+ if (this.pollInFlight) {
3792
+ this.schedulePoll();
3793
+ return;
3794
+ }
3795
+ this.pollInFlight = true;
3796
+ try {
3797
+ const status = await fetchJobStatus();
3798
+ this.pollBackoffMs = 1e3;
3799
+ this.applyStatus(status);
3800
+ } catch {
3801
+ this.pollBackoffMs = Math.min(5e3, Math.max(1e3, this.pollBackoffMs * 2));
3802
+ this.schedulePoll();
3803
+ } finally {
3804
+ this.pollInFlight = false;
3805
+ }
3806
+ }
2376
3807
  async refresh() {
2377
3808
  try {
2378
3809
  const report = await fetchReport();
@@ -2410,6 +3841,10 @@ window.__ModuleLoader__.load({
2410
3841
  window.clearInterval(this.refreshTimer);
2411
3842
  this.refreshTimer = null;
2412
3843
  }
3844
+ if (this.pollTimer !== null) {
3845
+ window.clearTimeout(this.pollTimer);
3846
+ this.pollTimer = null;
3847
+ }
2413
3848
  this.listeners.clear();
2414
3849
  }
2415
3850
  };
@@ -2450,9 +3885,23 @@ window.__ModuleLoader__.load({
2450
3885
  });
2451
3886
  const openView = () => {
2452
3887
  ensureDesignTokens();
2453
- store.check().then((report) => {
2454
- if (report !== null) openUpdateView(store, copyFor());
2455
- });
3888
+ store.fetchStatus();
3889
+ openUpdateView(store, copyFor());
3890
+ store.check();
3891
+ };
3892
+ const statusLabel = () => {
3893
+ const base = entryCopy(locale);
3894
+ const job = store.activeJob();
3895
+ if (job !== null) {
3896
+ const cc = copyFor();
3897
+ const pct = job.progress.percent;
3898
+ const phase = cc.job.phase[job.phase] ?? job.phase;
3899
+ return {
3900
+ label: pct !== null ? `${base.label} · ${Math.round(pct)}%` : `${base.label} · ${phase}`,
3901
+ tooltip: `${base.tooltip} · ${phase}`
3902
+ };
3903
+ }
3904
+ return base;
2456
3905
  };
2457
3906
  const disposeEntry = mountSidebarEntry({
2458
3907
  rowAttribute: UPDATE_ENTRY_ATTRIBUTE,
@@ -2460,8 +3909,8 @@ window.__ModuleLoader__.load({
2460
3909
  plugin: PLUGIN_ID,
2461
3910
  icon: UPDATE_ICON,
2462
3911
  css: ENTRY_CSS,
2463
- label: () => entryCopy(locale).label,
2464
- tooltip: () => entryCopy(locale).tooltip,
3912
+ label: () => statusLabel().label,
3913
+ tooltip: () => statusLabel().tooltip,
2465
3914
  refresh: { subscribe: refreshSubscribe },
2466
3915
  badge: {
2467
3916
  subscribe: (listener) => store.subscribe(listener),
@@ -2471,8 +3920,14 @@ window.__ModuleLoader__.load({
2471
3920
  position: "after",
2472
3921
  familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY
2473
3922
  });
3923
+ store.subscribe(() => {
3924
+ for (const fn of [...localeSubscribers]) try {
3925
+ fn();
3926
+ } catch {}
3927
+ });
2474
3928
  store.startIdleRefresh();
2475
3929
  store.refresh();
3930
+ store.fetchStatus();
2476
3931
  const dispose = () => {
2477
3932
  try {
2478
3933
  disposeEntry();