@huaqiu/dsh-tool-plugin-update 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +516 -13
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +86 -18
- package/package.json +4 -4
- package/src/client/api.ts +156 -1
- package/src/client/copy.ts +65 -0
- package/src/client/index.ts +63 -0
- package/src/client/update-view.ts +513 -8
- package/src/routes.ts +110 -12
package/lib/client.js
CHANGED
|
@@ -255,6 +255,22 @@ window.__ModuleLoader__.load({
|
|
|
255
255
|
* @module
|
|
256
256
|
*/
|
|
257
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
|
+
}
|
|
258
274
|
async function request(path, init) {
|
|
259
275
|
const res = await fetch(`${UPDATE_BASE}${path}`, {
|
|
260
276
|
cache: "no-store",
|
|
@@ -294,6 +310,13 @@ window.__ModuleLoader__.load({
|
|
|
294
310
|
body: JSON.stringify({ setVersion })
|
|
295
311
|
});
|
|
296
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
|
+
}
|
|
297
320
|
/** POST unignore for the set version (reverses an ignore). */
|
|
298
321
|
function unignoreOffer(setVersion) {
|
|
299
322
|
return request("/unignore", {
|
|
@@ -301,6 +324,37 @@ window.__ModuleLoader__.load({
|
|
|
301
324
|
body: JSON.stringify({ setVersion })
|
|
302
325
|
});
|
|
303
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
|
+
}
|
|
304
358
|
const COPY = {
|
|
305
359
|
zh: {
|
|
306
360
|
entryLabel: "插件更新",
|
|
@@ -348,11 +402,39 @@ window.__ModuleLoader__.load({
|
|
|
348
402
|
source: "更新源",
|
|
349
403
|
unknown: "版本未知",
|
|
350
404
|
notConfigured: "未配置更新源",
|
|
351
|
-
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"
|
|
352
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: "未配置",
|
|
353
433
|
lastError: "上次检查失败",
|
|
354
434
|
installError: "安装失败",
|
|
355
|
-
installSuccess: "更新已安装,正在重启 DSH…"
|
|
435
|
+
installSuccess: "更新已安装,正在重启 DSH…",
|
|
436
|
+
confirmInstall: "确认安装",
|
|
437
|
+
cancel: "取消"
|
|
356
438
|
},
|
|
357
439
|
en: {
|
|
358
440
|
entryLabel: "Plugin Updates",
|
|
@@ -400,11 +482,39 @@ window.__ModuleLoader__.load({
|
|
|
400
482
|
source: "Update source",
|
|
401
483
|
unknown: "Version unknown",
|
|
402
484
|
notConfigured: "No update source configured",
|
|
403
|
-
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"
|
|
404
509
|
},
|
|
510
|
+
originOverride: "Custom",
|
|
511
|
+
originConfig: "Configured",
|
|
512
|
+
originUnset: "Not configured",
|
|
405
513
|
lastError: "Last check failed",
|
|
406
514
|
installError: "Install failed",
|
|
407
|
-
installSuccess: "Update installed — restarting DSH…"
|
|
515
|
+
installSuccess: "Update installed — restarting DSH…",
|
|
516
|
+
confirmInstall: "Confirm install",
|
|
517
|
+
cancel: "Cancel"
|
|
408
518
|
}
|
|
409
519
|
};
|
|
410
520
|
//#endregion
|
|
@@ -645,8 +755,154 @@ window.__ModuleLoader__.load({
|
|
|
645
755
|
section.setAttribute("aria-label", copy.loading);
|
|
646
756
|
return section;
|
|
647
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
|
+
}
|
|
648
904
|
/** The independent HQ Edge runtime report, as ruled fact rows. */
|
|
649
|
-
function runtimeGroup(report, copy) {
|
|
905
|
+
function runtimeGroup(report, copy, actions) {
|
|
650
906
|
const section = el("section", "hqpu-group");
|
|
651
907
|
section.dataset.group = "runtime";
|
|
652
908
|
const head = el("div", "hqpu-group-head");
|
|
@@ -654,6 +910,7 @@ window.__ModuleLoader__.load({
|
|
|
654
910
|
section.append(head);
|
|
655
911
|
const facts = el("dl", "hqpu-facts");
|
|
656
912
|
const runtime = report.runtime;
|
|
913
|
+
const latestVersion = runtimeLatestVersion(runtime);
|
|
657
914
|
const row = (label, value, code = false) => {
|
|
658
915
|
const wrap = el("div");
|
|
659
916
|
wrap.append(el("dt", "", label));
|
|
@@ -665,15 +922,51 @@ window.__ModuleLoader__.load({
|
|
|
665
922
|
wrap.append(dd);
|
|
666
923
|
facts.append(wrap);
|
|
667
924
|
};
|
|
668
|
-
|
|
669
|
-
|
|
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 {
|
|
670
930
|
row(copy.runtime.current, runtime.currentVersion ?? copy.runtime.unknown);
|
|
671
|
-
if (runtime.updateAvailable &&
|
|
931
|
+
if (runtime.updateAvailable && latestVersion !== null) row(copy.runtime.latest, latestVersion);
|
|
672
932
|
if (report.manifestVersion !== null) row(copy.runtime.set, report.manifestVersion, true);
|
|
673
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);
|
|
674
935
|
}
|
|
675
936
|
section.append(facts);
|
|
676
|
-
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));
|
|
677
970
|
if (typeof runtime.lastError === "string" && runtime.lastError.length > 0) {
|
|
678
971
|
const line = el("p", "hqpu-status hqpu-status--dot");
|
|
679
972
|
line.setAttribute("role", "status");
|
|
@@ -706,10 +999,17 @@ window.__ModuleLoader__.load({
|
|
|
706
999
|
if (node !== null) section.append(node);
|
|
707
1000
|
}
|
|
708
1001
|
}
|
|
709
|
-
function renderRuntime(section, report, copy) {
|
|
1002
|
+
function renderRuntime(section, report, copy, actions) {
|
|
1003
|
+
section.textContent = "";
|
|
1004
|
+
if (report === null) return;
|
|
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) {
|
|
710
1009
|
section.textContent = "";
|
|
711
1010
|
if (report === null) return;
|
|
712
|
-
|
|
1011
|
+
const node = pluginSourceGroup(report, copy, actions);
|
|
1012
|
+
if (node !== null) section.append(node);
|
|
713
1013
|
}
|
|
714
1014
|
/** Run the install transaction, then keep polling until the state settles. */
|
|
715
1015
|
async function runInstall(store, copy) {
|
|
@@ -852,10 +1152,11 @@ window.__ModuleLoader__.load({
|
|
|
852
1152
|
panel.append(head);
|
|
853
1153
|
const scroll = el("div", "hqpu-scroll");
|
|
854
1154
|
const column = el("div", "hqpu-column");
|
|
1155
|
+
const pluginSourceSection = el("div", "hqpu-stack");
|
|
855
1156
|
const listSection = el("div", "hqpu-stack");
|
|
856
1157
|
const runtimeSection = el("div", "hqpu-stack");
|
|
857
1158
|
const toastSection = el("div", "hqpu-toast-host");
|
|
858
|
-
column.append(listSection, runtimeSection, toastSection);
|
|
1159
|
+
column.append(pluginSourceSection, listSection, runtimeSection, toastSection);
|
|
859
1160
|
scroll.append(column);
|
|
860
1161
|
panel.append(scroll);
|
|
861
1162
|
const footer = el("div", "hqpu-footer");
|
|
@@ -867,6 +1168,144 @@ window.__ModuleLoader__.load({
|
|
|
867
1168
|
toastHost = toastSection;
|
|
868
1169
|
panel.focus();
|
|
869
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
|
+
}
|
|
870
1309
|
const render = () => {
|
|
871
1310
|
const report = store.report();
|
|
872
1311
|
const installing = store.installing();
|
|
@@ -876,7 +1315,8 @@ window.__ModuleLoader__.load({
|
|
|
876
1315
|
listSection.textContent = "";
|
|
877
1316
|
listSection.append(skeleton(copy));
|
|
878
1317
|
} else renderList(listSection, report, copy, installing, store);
|
|
879
|
-
|
|
1318
|
+
renderPluginSource(pluginSourceSection, report, copy, pluginActions);
|
|
1319
|
+
renderRuntime(runtimeSection, report, copy, runtimeActions);
|
|
880
1320
|
renderActions(actionBar, report, copy, store, installing);
|
|
881
1321
|
};
|
|
882
1322
|
const unsubscribe = store.subscribe(render);
|
|
@@ -1585,6 +2025,69 @@ window.__ModuleLoader__.load({
|
|
|
1585
2025
|
gap: 8px;
|
|
1586
2026
|
}
|
|
1587
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
|
+
|
|
1588
2091
|
/* ── Footer action row (ui-primitives Modal .footer) ───────────────────── */
|
|
1589
2092
|
.hqpu-footer {
|
|
1590
2093
|
display: flex;
|