@huaqiu/dsh-tool-plugin-update 0.5.22 → 0.6.2
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 +548 -23
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +86 -18
- package/package.json +3 -3
- package/src/client/api.ts +156 -1
- package/src/client/copy.ts +65 -0
- package/src/client/index.ts +69 -8
- package/src/client/update-view.ts +513 -8
- package/src/routes.ts +110 -12
package/lib/client.js
CHANGED
|
@@ -64,15 +64,40 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
applyBadge
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Re-insert the entry after the New Session row, in the family's intended
|
|
69
|
+
* order.
|
|
70
|
+
*
|
|
71
|
+
* The `familySelectors` array order is the single source of truth for visual
|
|
72
|
+
* order, so the rendered order is stable regardless of which plugin mounts
|
|
73
|
+
* first or of observer callback order — a re-render can never swap two rows.
|
|
74
|
+
* (Earlier builds used the `position: 'before' | 'after'` convention, which
|
|
75
|
+
* clustered rows by mount order and let a late-mounting `before` row steal the
|
|
76
|
+
* lead. The array order supersedes it; `position` remains only as a fallback
|
|
77
|
+
* for a row whose selector is absent from the family list.)
|
|
78
|
+
*/
|
|
68
79
|
function placeEntry(root, entry, options) {
|
|
69
80
|
const button = newSessionButton(root);
|
|
70
81
|
if (button === void 0) return false;
|
|
71
82
|
if (entry.parentElement !== root) {
|
|
72
83
|
const row = button.closest("[class*=\"logoRow\"]");
|
|
73
84
|
const base = row !== null && row.parentElement === root ? row : button;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
85
|
+
const order = options.familySelectors;
|
|
86
|
+
const present = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(order.join(", "))).map((el) => ({
|
|
87
|
+
el,
|
|
88
|
+
idx: order.findIndex((sel) => el.matches(sel))
|
|
89
|
+
}));
|
|
90
|
+
const selfIndex = order.indexOf(options.rowSelector);
|
|
91
|
+
let anchor;
|
|
92
|
+
if (selfIndex === -1) anchor = options.position === "before" ? present.length > 0 ? present[0].el : base.nextElementSibling : present.length > 0 ? present[present.length - 1].el.nextElementSibling : base.nextElementSibling;
|
|
93
|
+
else {
|
|
94
|
+
const later = present.find((member) => member.idx > selfIndex);
|
|
95
|
+
if (later !== void 0) anchor = later.el;
|
|
96
|
+
else {
|
|
97
|
+
const earlier = [...present].reverse().find((member) => member.idx < selfIndex);
|
|
98
|
+
anchor = earlier !== void 0 ? earlier.el.nextElementSibling : base.nextElementSibling;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
76
101
|
root.insertBefore(entry, anchor ?? null);
|
|
77
102
|
}
|
|
78
103
|
return true;
|
|
@@ -158,10 +183,11 @@ window.__ModuleLoader__.load({
|
|
|
158
183
|
* family row means adding its selector here (and to no plugin-local list).
|
|
159
184
|
*/
|
|
160
185
|
const HUAQIU_SIDEBAR_ENTRY_FAMILY = [
|
|
186
|
+
"[data-hq-plugin-update-entry]",
|
|
161
187
|
"[data-hqcg-footprint-entry]",
|
|
162
188
|
"[data-hqcg-symbol-entry]",
|
|
163
189
|
"[data-hqcg-schematic-entry]",
|
|
164
|
-
"[data-hq-
|
|
190
|
+
"[data-hq-dummy-entry]"
|
|
165
191
|
];
|
|
166
192
|
/**
|
|
167
193
|
* Read the host UI language from `<html lang>`.
|
|
@@ -229,6 +255,22 @@ window.__ModuleLoader__.load({
|
|
|
229
255
|
* @module
|
|
230
256
|
*/
|
|
231
257
|
const UPDATE_BASE = "/api/v1/huaqiu/plugin-update";
|
|
258
|
+
/**
|
|
259
|
+
* The latest runtime version to display, or null when there is none.
|
|
260
|
+
*
|
|
261
|
+
* hq-edge sends the full release entry; an older or future server could send
|
|
262
|
+
* a bare string. Both are accepted here so the facts row can never again fall
|
|
263
|
+
* back to `String(object)` — i.e. "[object Object]" — in the DOM.
|
|
264
|
+
*/
|
|
265
|
+
function runtimeLatestVersion(runtime) {
|
|
266
|
+
const latest = runtime?.latest;
|
|
267
|
+
if (typeof latest === "string") return latest.length > 0 ? latest : null;
|
|
268
|
+
if (latest !== null && typeof latest === "object" && "version" in latest) {
|
|
269
|
+
const version = latest.version;
|
|
270
|
+
if (typeof version === "string" && version.length > 0) return version;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
232
274
|
async function request(path, init) {
|
|
233
275
|
const res = await fetch(`${UPDATE_BASE}${path}`, {
|
|
234
276
|
cache: "no-store",
|
|
@@ -268,6 +310,13 @@ window.__ModuleLoader__.load({
|
|
|
268
310
|
body: JSON.stringify({ setVersion })
|
|
269
311
|
});
|
|
270
312
|
}
|
|
313
|
+
/** POST the user-confirmed HQ Edge runtime update for one target version. */
|
|
314
|
+
function requestRuntimeUpdate(targetVersion) {
|
|
315
|
+
return request("/runtime-request", {
|
|
316
|
+
method: "POST",
|
|
317
|
+
body: JSON.stringify({ targetVersion })
|
|
318
|
+
});
|
|
319
|
+
}
|
|
271
320
|
/** POST unignore for the set version (reverses an ignore). */
|
|
272
321
|
function unignoreOffer(setVersion) {
|
|
273
322
|
return request("/unignore", {
|
|
@@ -275,6 +324,37 @@ window.__ModuleLoader__.load({
|
|
|
275
324
|
body: JSON.stringify({ setVersion })
|
|
276
325
|
});
|
|
277
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* POST a new runtime-update manifest URL, or `""` to clear a user override
|
|
329
|
+
* and fall back to the configured URL.
|
|
330
|
+
*
|
|
331
|
+
* hq-edge validates the URL itself (`https` required except for loopback, no
|
|
332
|
+
* embedded credentials) and answers 422 with a message meant for a person —
|
|
333
|
+
* this client surfaces it verbatim rather than pre-validating, so the rule
|
|
334
|
+
* lives in exactly one place. A successful answer is the NEW source; the
|
|
335
|
+
* caller must then re-run discovery, because the cached report still names
|
|
336
|
+
* the old manifest's offer.
|
|
337
|
+
*/
|
|
338
|
+
function setRuntimeUpdateSource(url) {
|
|
339
|
+
return request("/runtime-source", {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: JSON.stringify({ url })
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* POST a new plugin release-manifest URL, or `""` to clear a user override.
|
|
346
|
+
*
|
|
347
|
+
* hq-edge persists the override per host AND re-runs discovery in the
|
|
348
|
+
* background, so the next report (and the sidebar badge) already reflect the
|
|
349
|
+
* new manifest. This client still re-checks explicitly after a save — the
|
|
350
|
+
* background pass is a safety net for other consumers, not a substitute.
|
|
351
|
+
*/
|
|
352
|
+
function setPluginUpdateSource(url) {
|
|
353
|
+
return request("/plugin-source", {
|
|
354
|
+
method: "POST",
|
|
355
|
+
body: JSON.stringify({ url })
|
|
356
|
+
});
|
|
357
|
+
}
|
|
278
358
|
const COPY = {
|
|
279
359
|
zh: {
|
|
280
360
|
entryLabel: "插件更新",
|
|
@@ -322,11 +402,39 @@ window.__ModuleLoader__.load({
|
|
|
322
402
|
source: "更新源",
|
|
323
403
|
unknown: "版本未知",
|
|
324
404
|
notConfigured: "未配置更新源",
|
|
325
|
-
note: "运行时更新由 EDA 安装器处理"
|
|
405
|
+
note: "运行时更新由 EDA 安装器处理",
|
|
406
|
+
update: "安装运行时更新",
|
|
407
|
+
updateConfirm: (version) => `安装 HQ Edge 运行时 ${version}?HQ Edge 会先关闭并完成替换,然后重新启动。`,
|
|
408
|
+
updating: "正在下载并校验运行时包,可能需要一分钟…",
|
|
409
|
+
updateAccepted: "运行时更新已受理,HQ Edge 正在重启…",
|
|
410
|
+
sourceHint: "运行时更新清单的下载地址,保存后立即生效",
|
|
411
|
+
sourceEdit: "修改",
|
|
412
|
+
sourceSave: "保存",
|
|
413
|
+
sourceReset: "恢复默认",
|
|
414
|
+
saved: "更新源已保存,正在重新检查更新…",
|
|
415
|
+
saveFailed: "保存更新源失败",
|
|
416
|
+
sourceInputLabel: "运行时更新清单地址",
|
|
417
|
+
sourcePlaceholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json"
|
|
326
418
|
},
|
|
419
|
+
pluginSource: {
|
|
420
|
+
title: "插件更新源",
|
|
421
|
+
hint: "插件更新清单的下载地址,保存后立即生效",
|
|
422
|
+
edit: "修改",
|
|
423
|
+
save: "保存",
|
|
424
|
+
reset: "恢复默认",
|
|
425
|
+
saved: "更新源已保存,正在重新检查更新…",
|
|
426
|
+
saveFailed: "保存更新源失败",
|
|
427
|
+
inputLabel: "插件更新清单地址",
|
|
428
|
+
placeholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json"
|
|
429
|
+
},
|
|
430
|
+
originOverride: "自定义",
|
|
431
|
+
originConfig: "默认配置",
|
|
432
|
+
originUnset: "未配置",
|
|
327
433
|
lastError: "上次检查失败",
|
|
328
434
|
installError: "安装失败",
|
|
329
|
-
installSuccess: "更新已安装,正在重启 DSH…"
|
|
435
|
+
installSuccess: "更新已安装,正在重启 DSH…",
|
|
436
|
+
confirmInstall: "确认安装",
|
|
437
|
+
cancel: "取消"
|
|
330
438
|
},
|
|
331
439
|
en: {
|
|
332
440
|
entryLabel: "Plugin Updates",
|
|
@@ -374,11 +482,39 @@ window.__ModuleLoader__.load({
|
|
|
374
482
|
source: "Update source",
|
|
375
483
|
unknown: "Version unknown",
|
|
376
484
|
notConfigured: "No update source configured",
|
|
377
|
-
note: "Runtime updates are handled by the EDA installer"
|
|
485
|
+
note: "Runtime updates are handled by the EDA installer",
|
|
486
|
+
update: "Install runtime update",
|
|
487
|
+
updateConfirm: (version) => `Install HQ Edge runtime ${version}? HQ Edge shuts down to swap the runtime, then restarts.`,
|
|
488
|
+
updating: "Downloading and verifying the runtime bundle — this can take a minute…",
|
|
489
|
+
updateAccepted: "Runtime update accepted — HQ Edge is restarting…",
|
|
490
|
+
sourceHint: "Where the runtime update manifest is fetched from; saved changes take effect immediately",
|
|
491
|
+
sourceEdit: "Edit",
|
|
492
|
+
sourceSave: "Save",
|
|
493
|
+
sourceReset: "Reset to default",
|
|
494
|
+
saved: "Update source saved — re-checking for updates…",
|
|
495
|
+
saveFailed: "Failed to save the update source",
|
|
496
|
+
sourceInputLabel: "Runtime update manifest URL",
|
|
497
|
+
sourcePlaceholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/edge-headless-release-manifest.json"
|
|
498
|
+
},
|
|
499
|
+
pluginSource: {
|
|
500
|
+
title: "Plugin update source",
|
|
501
|
+
hint: "Where the plugin update manifest is fetched from; saved changes take effect immediately",
|
|
502
|
+
edit: "Edit",
|
|
503
|
+
save: "Save",
|
|
504
|
+
reset: "Reset to default",
|
|
505
|
+
saved: "Update source saved — re-checking for updates…",
|
|
506
|
+
saveFailed: "Failed to save the update source",
|
|
507
|
+
inputLabel: "Plugin update manifest URL",
|
|
508
|
+
placeholder: "https://gitee.com/huaqiu-electronics/dsh-pcb-eda/raw/main/plugins-release-manifest.json"
|
|
378
509
|
},
|
|
510
|
+
originOverride: "Custom",
|
|
511
|
+
originConfig: "Configured",
|
|
512
|
+
originUnset: "Not configured",
|
|
379
513
|
lastError: "Last check failed",
|
|
380
514
|
installError: "Install failed",
|
|
381
|
-
installSuccess: "Update installed — restarting DSH…"
|
|
515
|
+
installSuccess: "Update installed — restarting DSH…",
|
|
516
|
+
confirmInstall: "Confirm install",
|
|
517
|
+
cancel: "Cancel"
|
|
382
518
|
}
|
|
383
519
|
};
|
|
384
520
|
//#endregion
|
|
@@ -619,8 +755,154 @@ window.__ModuleLoader__.load({
|
|
|
619
755
|
section.setAttribute("aria-label", copy.loading);
|
|
620
756
|
return section;
|
|
621
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Normalize the report's `runtime.source` to the full shape.
|
|
760
|
+
*
|
|
761
|
+
* An older hq-edge predates the feature and sends no `source` at all — that
|
|
762
|
+
* must degrade to a read-only panel, not crash the render. A future server
|
|
763
|
+
* may add fields; unknown extras are dropped here rather than trusted.
|
|
764
|
+
*/
|
|
765
|
+
function normalizeSource(raw) {
|
|
766
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
767
|
+
const candidate = raw;
|
|
768
|
+
if (typeof candidate.origin !== "string") return null;
|
|
769
|
+
return {
|
|
770
|
+
url: typeof candidate.url === "string" ? candidate.url : "",
|
|
771
|
+
origin: candidate.origin,
|
|
772
|
+
configuredUrl: typeof candidate.configuredUrl === "string" ? candidate.configuredUrl : "",
|
|
773
|
+
override: candidate.override === true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** The tag naming where the URL in effect came from. */
|
|
777
|
+
function originLabel(origin, copy) {
|
|
778
|
+
switch (origin) {
|
|
779
|
+
case "override": return copy.originOverride;
|
|
780
|
+
case "config": return copy.originConfig;
|
|
781
|
+
default: return copy.originUnset;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/** The 更新源 fact row: the URL in effect plus an origin tag. */
|
|
785
|
+
function sourceFactRow(facts, source, copy) {
|
|
786
|
+
const wrap = el("div");
|
|
787
|
+
wrap.append(el("dt", "", copy.runtime.source));
|
|
788
|
+
const dd = el("dd");
|
|
789
|
+
const line = el("div", "hqpu-source-line");
|
|
790
|
+
const code = el("code", "hqpu-source-url", source.url.length > 0 ? source.url : copy.runtime.notConfigured);
|
|
791
|
+
const tag = el("span", "hqpu-tag");
|
|
792
|
+
tag.dataset.tone = source.override ? "info" : source.origin === "config" ? "success" : "neutral";
|
|
793
|
+
tag.textContent = originLabel(source.origin, copy);
|
|
794
|
+
line.append(code, tag);
|
|
795
|
+
dd.append(line);
|
|
796
|
+
wrap.append(dd);
|
|
797
|
+
facts.append(wrap);
|
|
798
|
+
}
|
|
799
|
+
/** Pick the labels for one kind out of the copy table. */
|
|
800
|
+
function sourceLabels(copy, kind) {
|
|
801
|
+
return kind === "runtime" ? {
|
|
802
|
+
hint: copy.runtime.sourceHint,
|
|
803
|
+
edit: copy.runtime.sourceEdit,
|
|
804
|
+
save: copy.runtime.sourceSave,
|
|
805
|
+
reset: copy.runtime.sourceReset,
|
|
806
|
+
inputLabel: copy.runtime.sourceInputLabel,
|
|
807
|
+
placeholder: copy.runtime.sourcePlaceholder
|
|
808
|
+
} : {
|
|
809
|
+
hint: copy.pluginSource.hint,
|
|
810
|
+
edit: copy.pluginSource.edit,
|
|
811
|
+
save: copy.pluginSource.save,
|
|
812
|
+
reset: copy.pluginSource.reset,
|
|
813
|
+
inputLabel: copy.pluginSource.inputLabel,
|
|
814
|
+
placeholder: copy.pluginSource.placeholder
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* One update-source editor, shared by the plugin manifest and the runtime
|
|
819
|
+
* manifest (the only differences are the labels and the data-attribute
|
|
820
|
+
* prefix, so tests and CSS can target each).
|
|
821
|
+
*
|
|
822
|
+
* Read mode is a hint plus a 修改 button; edit mode is the URL field with
|
|
823
|
+
* 保存 / 取消 — plus 恢复默认 while a user override shadows the configured
|
|
824
|
+
* URL. The draft lives in {@link SourceEditorActions} (the panel closure), so
|
|
825
|
+
* a background store refresh re-renders the typed URL instead of discarding it.
|
|
826
|
+
*/
|
|
827
|
+
function sourceEditor(kind, source, copy, actions) {
|
|
828
|
+
const labels = sourceLabels(copy, kind);
|
|
829
|
+
const block = el("div", `hqpu-source-editor hqpu-source-editor--${kind}`);
|
|
830
|
+
if (!actions.sourceEditing) {
|
|
831
|
+
block.append(el("p", "hqpu-status hqpu-source-hint", labels.hint));
|
|
832
|
+
const edit = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", labels.edit);
|
|
833
|
+
edit.type = "button";
|
|
834
|
+
edit.setAttribute(`data-${kind}-source-edit`, "");
|
|
835
|
+
edit.disabled = actions.sourceSaving;
|
|
836
|
+
edit.addEventListener("click", actions.onSourceEdit);
|
|
837
|
+
block.append(edit);
|
|
838
|
+
return block;
|
|
839
|
+
}
|
|
840
|
+
const input = el("input", "hqpu-source-input");
|
|
841
|
+
input.type = "url";
|
|
842
|
+
input.value = actions.sourceDraft;
|
|
843
|
+
input.placeholder = labels.placeholder;
|
|
844
|
+
input.setAttribute("aria-label", labels.inputLabel);
|
|
845
|
+
input.setAttribute(`data-${kind}-source-input`, "");
|
|
846
|
+
input.disabled = actions.sourceSaving;
|
|
847
|
+
input.addEventListener("input", () => {
|
|
848
|
+
actions.onSourceDraft(input.value);
|
|
849
|
+
});
|
|
850
|
+
input.addEventListener("keydown", (ev) => {
|
|
851
|
+
if (ev.key === "Enter") {
|
|
852
|
+
ev.preventDefault();
|
|
853
|
+
actions.onSourceSave();
|
|
854
|
+
} else if (ev.key === "Escape") {
|
|
855
|
+
ev.preventDefault();
|
|
856
|
+
ev.stopPropagation();
|
|
857
|
+
actions.onSourceCancel();
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
const save = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm", labels.save);
|
|
861
|
+
save.type = "button";
|
|
862
|
+
save.setAttribute(`data-${kind}-source-save`, "");
|
|
863
|
+
save.disabled = actions.sourceSaving;
|
|
864
|
+
save.addEventListener("click", actions.onSourceSave);
|
|
865
|
+
const cancel = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.cancel);
|
|
866
|
+
cancel.type = "button";
|
|
867
|
+
cancel.setAttribute(`data-${kind}-source-cancel`, "");
|
|
868
|
+
cancel.disabled = actions.sourceSaving;
|
|
869
|
+
cancel.addEventListener("click", actions.onSourceCancel);
|
|
870
|
+
const controls = el("div", "hqpu-actions hqpu-source-controls");
|
|
871
|
+
controls.append(input, save, cancel);
|
|
872
|
+
if (source.override) {
|
|
873
|
+
const reset = el("button", "hqpu-btn hqpu-btn--ghost hqpu-btn--sm", labels.reset);
|
|
874
|
+
reset.type = "button";
|
|
875
|
+
reset.setAttribute(`data-${kind}-source-reset`, "");
|
|
876
|
+
reset.disabled = actions.sourceSaving;
|
|
877
|
+
reset.addEventListener("click", actions.onSourceReset);
|
|
878
|
+
controls.append(reset);
|
|
879
|
+
}
|
|
880
|
+
block.append(controls);
|
|
881
|
+
input.focus();
|
|
882
|
+
return block;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* The plugin release-manifest source, as its own group above the plugin
|
|
886
|
+
* groups: it says where the whole list comes from, so it belongs above the
|
|
887
|
+
* list rather than inside it. Absent on an older hq-edge (no `pluginSource`
|
|
888
|
+
* in the report) — then nothing is rendered at all.
|
|
889
|
+
*/
|
|
890
|
+
function pluginSourceGroup(report, copy, actions) {
|
|
891
|
+
const source = normalizeSource(report.pluginSource);
|
|
892
|
+
if (source === null) return null;
|
|
893
|
+
const section = el("section", "hqpu-group");
|
|
894
|
+
section.dataset.group = "plugin-source";
|
|
895
|
+
const head = el("div", "hqpu-group-head");
|
|
896
|
+
head.append(el("h3", "hqpu-group-title", copy.pluginSource.title));
|
|
897
|
+
section.append(head);
|
|
898
|
+
const facts = el("dl", "hqpu-facts");
|
|
899
|
+
sourceFactRow(facts, source, copy);
|
|
900
|
+
section.append(facts);
|
|
901
|
+
section.append(sourceEditor("plugin", source, copy, actions));
|
|
902
|
+
return section;
|
|
903
|
+
}
|
|
622
904
|
/** The independent HQ Edge runtime report, as ruled fact rows. */
|
|
623
|
-
function runtimeGroup(report, copy) {
|
|
905
|
+
function runtimeGroup(report, copy, actions) {
|
|
624
906
|
const section = el("section", "hqpu-group");
|
|
625
907
|
section.dataset.group = "runtime";
|
|
626
908
|
const head = el("div", "hqpu-group-head");
|
|
@@ -628,6 +910,7 @@ window.__ModuleLoader__.load({
|
|
|
628
910
|
section.append(head);
|
|
629
911
|
const facts = el("dl", "hqpu-facts");
|
|
630
912
|
const runtime = report.runtime;
|
|
913
|
+
const latestVersion = runtimeLatestVersion(runtime);
|
|
631
914
|
const row = (label, value, code = false) => {
|
|
632
915
|
const wrap = el("div");
|
|
633
916
|
wrap.append(el("dt", "", label));
|
|
@@ -639,15 +922,51 @@ window.__ModuleLoader__.load({
|
|
|
639
922
|
wrap.append(dd);
|
|
640
923
|
facts.append(wrap);
|
|
641
924
|
};
|
|
642
|
-
|
|
643
|
-
|
|
925
|
+
const source = normalizeSource(runtime.source);
|
|
926
|
+
if (runtime.configured === false) {
|
|
927
|
+
if (source !== null) sourceFactRow(facts, source, copy);
|
|
928
|
+
else row(copy.runtime.source, copy.runtime.notConfigured);
|
|
929
|
+
} else {
|
|
644
930
|
row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown);
|
|
645
|
-
if (runtime.updateAvailable &&
|
|
931
|
+
if (runtime.updateAvailable && latestVersion !== null) row(copy.runtime.latest, latestVersion);
|
|
646
932
|
if (report.manifestVersion !== null) row(copy.runtime.set, report.manifestVersion, true);
|
|
647
933
|
if (typeof runtime.checkedAt === "string" && runtime.checkedAt.length > 0) row(copy.runtime.checkedAt, new Date(runtime.checkedAt).toLocaleString());
|
|
934
|
+
if (source !== null) sourceFactRow(facts, source, copy);
|
|
648
935
|
}
|
|
649
936
|
section.append(facts);
|
|
650
|
-
section.append(
|
|
937
|
+
if (source !== null) section.append(sourceEditor("runtime", source, copy, actions));
|
|
938
|
+
if (runtime.updateAvailable && latestVersion !== null) {
|
|
939
|
+
const row = el("div", "hqpu-actions hqpu-runtime-actions");
|
|
940
|
+
if (actions.accepted) {
|
|
941
|
+
const line = el("span", "hqpu-status hqpu-status--dot");
|
|
942
|
+
line.setAttribute("role", "status");
|
|
943
|
+
line.append(el("span", "hqpu-spinner"), el("span", "", copy.runtime.updateAccepted));
|
|
944
|
+
row.append(line);
|
|
945
|
+
} else if (actions.busy) {
|
|
946
|
+
const line = el("span", "hqpu-status hqpu-status--dot");
|
|
947
|
+
line.setAttribute("role", "status");
|
|
948
|
+
line.append(el("span", "hqpu-spinner"), el("span", "", copy.runtime.updating));
|
|
949
|
+
row.append(line);
|
|
950
|
+
} else if (actions.armed) {
|
|
951
|
+
row.append(el("p", "hqpu-status hqpu-runtime-confirm", copy.runtime.updateConfirm(latestVersion)));
|
|
952
|
+
const yes = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm", copy.confirmInstall);
|
|
953
|
+
yes.type = "button";
|
|
954
|
+
yes.dataset.runtimeConfirm = "";
|
|
955
|
+
yes.addEventListener("click", actions.onConfirm);
|
|
956
|
+
const no = el("button", "hqpu-btn hqpu-btn--outline hqpu-btn--sm", copy.cancel);
|
|
957
|
+
no.type = "button";
|
|
958
|
+
no.addEventListener("click", actions.onCancel);
|
|
959
|
+
row.append(yes, no);
|
|
960
|
+
} else {
|
|
961
|
+
const start = el("button", "hqpu-btn hqpu-btn--primary hqpu-btn--sm");
|
|
962
|
+
start.type = "button";
|
|
963
|
+
start.dataset.runtimeUpdate = "";
|
|
964
|
+
start.innerHTML = `${DOWNLOAD_ICON(13)}<span>${copy.runtime.update}</span>`;
|
|
965
|
+
start.addEventListener("click", actions.onArm);
|
|
966
|
+
row.append(start);
|
|
967
|
+
}
|
|
968
|
+
section.append(row);
|
|
969
|
+
} else section.append(el("p", "hqpu-status", copy.runtime.note));
|
|
651
970
|
if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) {
|
|
652
971
|
const line = el("p", "hqpu-status hqpu-status--dot");
|
|
653
972
|
line.setAttribute("role", "status");
|
|
@@ -680,10 +999,17 @@ window.__ModuleLoader__.load({
|
|
|
680
999
|
if (node !== null) section.append(node);
|
|
681
1000
|
}
|
|
682
1001
|
}
|
|
683
|
-
function renderRuntime(section, report, copy) {
|
|
1002
|
+
function renderRuntime(section, report, copy, actions) {
|
|
684
1003
|
section.textContent = "";
|
|
685
1004
|
if (report === null) return;
|
|
686
|
-
section.append(runtimeGroup(report, copy));
|
|
1005
|
+
section.append(runtimeGroup(report, copy, actions));
|
|
1006
|
+
}
|
|
1007
|
+
/** The plugin-source group, above the plugin groups. No-op without the field. */
|
|
1008
|
+
function renderPluginSource(section, report, copy, actions) {
|
|
1009
|
+
section.textContent = "";
|
|
1010
|
+
if (report === null) return;
|
|
1011
|
+
const node = pluginSourceGroup(report, copy, actions);
|
|
1012
|
+
if (node !== null) section.append(node);
|
|
687
1013
|
}
|
|
688
1014
|
/** Run the install transaction, then keep polling until the state settles. */
|
|
689
1015
|
async function runInstall(store, copy) {
|
|
@@ -826,10 +1152,11 @@ window.__ModuleLoader__.load({
|
|
|
826
1152
|
panel.append(head);
|
|
827
1153
|
const scroll = el("div", "hqpu-scroll");
|
|
828
1154
|
const column = el("div", "hqpu-column");
|
|
1155
|
+
const pluginSourceSection = el("div", "hqpu-stack");
|
|
829
1156
|
const listSection = el("div", "hqpu-stack");
|
|
830
1157
|
const runtimeSection = el("div", "hqpu-stack");
|
|
831
1158
|
const toastSection = el("div", "hqpu-toast-host");
|
|
832
|
-
column.append(listSection, runtimeSection, toastSection);
|
|
1159
|
+
column.append(pluginSourceSection, listSection, runtimeSection, toastSection);
|
|
833
1160
|
scroll.append(column);
|
|
834
1161
|
panel.append(scroll);
|
|
835
1162
|
const footer = el("div", "hqpu-footer");
|
|
@@ -841,6 +1168,144 @@ window.__ModuleLoader__.load({
|
|
|
841
1168
|
toastHost = toastSection;
|
|
842
1169
|
panel.focus();
|
|
843
1170
|
const disposers = [];
|
|
1171
|
+
let runtimeArmed = false;
|
|
1172
|
+
let runtimeBusy = false;
|
|
1173
|
+
let runtimeAccepted = false;
|
|
1174
|
+
const runtimeSourceState = {
|
|
1175
|
+
editing: false,
|
|
1176
|
+
saving: false,
|
|
1177
|
+
draft: ""
|
|
1178
|
+
};
|
|
1179
|
+
const pluginSourceState = {
|
|
1180
|
+
editing: false,
|
|
1181
|
+
saving: false,
|
|
1182
|
+
draft: ""
|
|
1183
|
+
};
|
|
1184
|
+
const runtimeSourceActions = sourceActionsFor("runtime", runtimeSourceState);
|
|
1185
|
+
const runtimeActions = {
|
|
1186
|
+
get busy() {
|
|
1187
|
+
return runtimeBusy;
|
|
1188
|
+
},
|
|
1189
|
+
get armed() {
|
|
1190
|
+
return runtimeArmed;
|
|
1191
|
+
},
|
|
1192
|
+
get accepted() {
|
|
1193
|
+
return runtimeAccepted;
|
|
1194
|
+
},
|
|
1195
|
+
onArm: () => {
|
|
1196
|
+
runtimeArmed = true;
|
|
1197
|
+
render();
|
|
1198
|
+
},
|
|
1199
|
+
onCancel: () => {
|
|
1200
|
+
runtimeArmed = false;
|
|
1201
|
+
render();
|
|
1202
|
+
},
|
|
1203
|
+
onConfirm: () => {
|
|
1204
|
+
requestRuntime();
|
|
1205
|
+
},
|
|
1206
|
+
get sourceEditing() {
|
|
1207
|
+
return runtimeSourceActions.sourceEditing;
|
|
1208
|
+
},
|
|
1209
|
+
get sourceSaving() {
|
|
1210
|
+
return runtimeSourceActions.sourceSaving;
|
|
1211
|
+
},
|
|
1212
|
+
get sourceDraft() {
|
|
1213
|
+
return runtimeSourceActions.sourceDraft;
|
|
1214
|
+
},
|
|
1215
|
+
onSourceEdit: () => runtimeSourceActions.onSourceEdit(),
|
|
1216
|
+
onSourceDraft: (value) => runtimeSourceActions.onSourceDraft(value),
|
|
1217
|
+
onSourceSave: () => runtimeSourceActions.onSourceSave(),
|
|
1218
|
+
onSourceCancel: () => runtimeSourceActions.onSourceCancel(),
|
|
1219
|
+
onSourceReset: () => runtimeSourceActions.onSourceReset()
|
|
1220
|
+
};
|
|
1221
|
+
const pluginActions = sourceActionsFor("plugin", pluginSourceState);
|
|
1222
|
+
/** Build the {@link SourceEditorActions} half for one manifest source. */
|
|
1223
|
+
function sourceActionsFor(kind, state) {
|
|
1224
|
+
return {
|
|
1225
|
+
get sourceEditing() {
|
|
1226
|
+
return state.editing;
|
|
1227
|
+
},
|
|
1228
|
+
get sourceSaving() {
|
|
1229
|
+
return state.saving;
|
|
1230
|
+
},
|
|
1231
|
+
get sourceDraft() {
|
|
1232
|
+
return state.draft;
|
|
1233
|
+
},
|
|
1234
|
+
onSourceEdit: () => {
|
|
1235
|
+
const report = store.report();
|
|
1236
|
+
state.draft = (kind === "runtime" ? normalizeSource(report?.runtime.source) : normalizeSource(report?.pluginSource))?.url ?? "";
|
|
1237
|
+
state.editing = true;
|
|
1238
|
+
render();
|
|
1239
|
+
},
|
|
1240
|
+
onSourceDraft: (value) => {
|
|
1241
|
+
state.draft = value;
|
|
1242
|
+
},
|
|
1243
|
+
onSourceSave: () => {
|
|
1244
|
+
submitSource(kind, state);
|
|
1245
|
+
},
|
|
1246
|
+
onSourceCancel: () => {
|
|
1247
|
+
state.editing = false;
|
|
1248
|
+
render();
|
|
1249
|
+
},
|
|
1250
|
+
onSourceReset: () => {
|
|
1251
|
+
submitSource(kind, state, "");
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Save one manifest source — or clear the override with an empty URL —
|
|
1257
|
+
* then re-run discovery.
|
|
1258
|
+
*
|
|
1259
|
+
* The re-check is not optional: the report is cached, so without a fresh
|
|
1260
|
+
* `POST /check` the facts and the update button would keep naming the OLD
|
|
1261
|
+
* manifest's offer after the source changed. hq-edge answers 422 with a
|
|
1262
|
+
* message meant for a person on refusal (bad scheme, credentials, …), which
|
|
1263
|
+
* the toast shows verbatim.
|
|
1264
|
+
*/
|
|
1265
|
+
async function submitSource(kind, state, url = state.draft) {
|
|
1266
|
+
state.saving = true;
|
|
1267
|
+
render();
|
|
1268
|
+
const labels = kind === "runtime" ? copy.runtime : copy.pluginSource;
|
|
1269
|
+
try {
|
|
1270
|
+
if (kind === "runtime") await setRuntimeUpdateSource(url);
|
|
1271
|
+
else await setPluginUpdateSource(url);
|
|
1272
|
+
state.editing = false;
|
|
1273
|
+
showToast(labels.saved, "success");
|
|
1274
|
+
try {
|
|
1275
|
+
await checkUpdates();
|
|
1276
|
+
await store.refresh();
|
|
1277
|
+
} catch {}
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
showToast(`${labels.saveFailed}: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1280
|
+
} finally {
|
|
1281
|
+
state.saving = false;
|
|
1282
|
+
render();
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
async function requestRuntime() {
|
|
1286
|
+
const version = runtimeLatestVersion(store.report()?.runtime);
|
|
1287
|
+
if (version === null) {
|
|
1288
|
+
showToast(copy.checkFailed, "error");
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
runtimeArmed = false;
|
|
1292
|
+
runtimeBusy = true;
|
|
1293
|
+
render();
|
|
1294
|
+
try {
|
|
1295
|
+
const result = await requestRuntimeUpdate(version);
|
|
1296
|
+
if (result?.error !== void 0) {
|
|
1297
|
+
showToast(`${copy.installError}: ${result.error}`, "error");
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
runtimeAccepted = true;
|
|
1301
|
+
showToast(copy.runtime.updateAccepted, "success");
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
showToast(`${copy.installError}: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1304
|
+
} finally {
|
|
1305
|
+
runtimeBusy = false;
|
|
1306
|
+
render();
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
844
1309
|
const render = () => {
|
|
845
1310
|
const report = store.report();
|
|
846
1311
|
const installing = store.installing();
|
|
@@ -850,7 +1315,8 @@ window.__ModuleLoader__.load({
|
|
|
850
1315
|
listSection.textContent = "";
|
|
851
1316
|
listSection.append(skeleton(copy));
|
|
852
1317
|
} else renderList(listSection, report, copy, installing, store);
|
|
853
|
-
|
|
1318
|
+
renderPluginSource(pluginSourceSection, report, copy, pluginActions);
|
|
1319
|
+
renderRuntime(runtimeSection, report, copy, runtimeActions);
|
|
854
1320
|
renderActions(actionBar, report, copy, store, installing);
|
|
855
1321
|
};
|
|
856
1322
|
const unsubscribe = store.subscribe(render);
|
|
@@ -1559,6 +2025,69 @@ window.__ModuleLoader__.load({
|
|
|
1559
2025
|
gap: 8px;
|
|
1560
2026
|
}
|
|
1561
2027
|
|
|
2028
|
+
/* ── Runtime update action row (arm → confirm step) ───────────────────── */
|
|
2029
|
+
.hqpu-runtime-actions {
|
|
2030
|
+
margin-top: 12px;
|
|
2031
|
+
}
|
|
2032
|
+
.hqpu-runtime-confirm {
|
|
2033
|
+
flex: 1 1 100%;
|
|
2034
|
+
margin: 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/* ── Runtime update source (fact row + editable URL field) ────────────── */
|
|
2038
|
+
.hqpu-source-line {
|
|
2039
|
+
display: inline-flex;
|
|
2040
|
+
align-items: center;
|
|
2041
|
+
gap: 8px;
|
|
2042
|
+
min-width: 0;
|
|
2043
|
+
max-width: 100%;
|
|
2044
|
+
}
|
|
2045
|
+
.hqpu-source-url {
|
|
2046
|
+
min-width: 0;
|
|
2047
|
+
overflow: hidden;
|
|
2048
|
+
text-overflow: ellipsis;
|
|
2049
|
+
white-space: nowrap;
|
|
2050
|
+
}
|
|
2051
|
+
.hqpu-source-editor {
|
|
2052
|
+
display: flex;
|
|
2053
|
+
align-items: center;
|
|
2054
|
+
flex-wrap: wrap;
|
|
2055
|
+
gap: 8px;
|
|
2056
|
+
margin-top: 12px;
|
|
2057
|
+
}
|
|
2058
|
+
/* The plugin-source editor sits inside its own group right under the fact
|
|
2059
|
+
* row, so it needs less air than the runtime one under the facts block. */
|
|
2060
|
+
.hqpu-source-editor--plugin {
|
|
2061
|
+
margin-top: 4px;
|
|
2062
|
+
}
|
|
2063
|
+
.hqpu-source-hint {
|
|
2064
|
+
flex: 1 1 100%;
|
|
2065
|
+
margin: 0;
|
|
2066
|
+
}
|
|
2067
|
+
.hqpu-source-controls {
|
|
2068
|
+
flex: 1 1 100%;
|
|
2069
|
+
}
|
|
2070
|
+
.hqpu-source-input {
|
|
2071
|
+
flex: 1 1 240px;
|
|
2072
|
+
min-width: 0;
|
|
2073
|
+
height: 28px;
|
|
2074
|
+
box-sizing: border-box;
|
|
2075
|
+
padding: 0 10px;
|
|
2076
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
2077
|
+
border-radius: var(--dsw-radius-md);
|
|
2078
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
2079
|
+
color: var(--dsw-alias-label-primary);
|
|
2080
|
+
font: inherit;
|
|
2081
|
+
font-size: 12.5px;
|
|
2082
|
+
}
|
|
2083
|
+
.hqpu-source-input:disabled {
|
|
2084
|
+
opacity: 0.5;
|
|
2085
|
+
}
|
|
2086
|
+
.hqpu-source-input:focus-visible {
|
|
2087
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
2088
|
+
outline-offset: 1px;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1562
2091
|
/* ── Footer action row (ui-primitives Modal .footer) ───────────────────── */
|
|
1563
2092
|
.hqpu-footer {
|
|
1564
2093
|
display: flex;
|
|
@@ -1815,10 +2344,6 @@ window.__ModuleLoader__.load({
|
|
|
1815
2344
|
fn();
|
|
1816
2345
|
} catch {}
|
|
1817
2346
|
});
|
|
1818
|
-
const labelState = {
|
|
1819
|
-
label: entryCopy(locale).label,
|
|
1820
|
-
tooltip: entryCopy(locale).tooltip
|
|
1821
|
-
};
|
|
1822
2347
|
const openView = () => {
|
|
1823
2348
|
ensureDesignTokens();
|
|
1824
2349
|
store.check().then((report) => {
|
|
@@ -1831,8 +2356,8 @@ window.__ModuleLoader__.load({
|
|
|
1831
2356
|
plugin: PLUGIN_ID,
|
|
1832
2357
|
icon: UPDATE_ICON,
|
|
1833
2358
|
css: ENTRY_CSS,
|
|
1834
|
-
label: () =>
|
|
1835
|
-
tooltip: () =>
|
|
2359
|
+
label: () => entryCopy(locale).label,
|
|
2360
|
+
tooltip: () => entryCopy(locale).tooltip,
|
|
1836
2361
|
refresh: { subscribe: refreshSubscribe },
|
|
1837
2362
|
badge: {
|
|
1838
2363
|
subscribe: (listener) => store.subscribe(listener),
|