@ran-sh/dsh-crew 1.0.2 → 1.1.0

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.
Files changed (40) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -0
  3. package/codex/AGENTS.md +101 -92
  4. package/docs/ui-surfaces.md +48 -21
  5. package/lib/client.js +235 -34
  6. package/official-web-bridge/lib/client.js +368 -4532
  7. package/official-web-bridge/package.json +1 -2
  8. package/package.json +130 -67
  9. package/scripts/build-client.mjs +30 -15
  10. package/scripts/remove-legacy-official-bridge.ps1 +89 -0
  11. package/scripts/setup.mjs +152 -140
  12. package/scripts/verify-npm-install.mjs +311 -310
  13. package/scripts/verify-official-bridge-e2e.mjs +12 -1
  14. package/src/client/host-readiness.mjs +62 -59
  15. package/src/client/index.tsx +117 -24
  16. package/src/client/quick-entry.tsx +10 -0
  17. package/src/client/quick-panel.tsx +295 -0
  18. package/src/client/surface-detection.mjs +43 -30
  19. package/src/credential-reference.mjs +38 -0
  20. package/src/dsh-cli-runtime.mjs +500 -40
  21. package/src/dsh-cohort.mjs +20 -0
  22. package/src/hub/index.mjs +1602 -1016
  23. package/src/install/npx-lifecycle.mjs +1983 -468
  24. package/src/install/official-web.mjs +36 -73
  25. package/src/jobs.mjs +20 -24
  26. package/src/model-catalog.mjs +8 -1
  27. package/src/official-web-bridge.mjs +329 -249
  28. package/src/provider-delete-adapters.mjs +165 -17
  29. package/src/provider-inventory.mjs +17 -3
  30. package/src/provider-layer-migration-adapters.mjs +759 -0
  31. package/src/provider-layer-migration.mjs +198 -0
  32. package/src/provider-lifecycle-state.mjs +4 -1
  33. package/src/provider-profile-store.mjs +193 -4
  34. package/src/provider-settings-store.mjs +82 -6
  35. package/src/provider-store-lock.mjs +67 -0
  36. package/src/runtime-identity.mjs +24 -1
  37. package/src/supervisor/restart-request.mjs +202 -0
  38. package/windows/start-dsh-crew.ps1 +783 -370
  39. package/worker.cordis.yml +32 -46
  40. package/zcode/AGENTS.md +35 -26
package/lib/client.js CHANGED
@@ -195,7 +195,7 @@ function runtimeState(runtime, readinessSnapshot) {
195
195
  }
196
196
  function bridgeState(surface) {
197
197
  if (surface === "official-bridge") return READINESS_STATES.READY;
198
- if (surface === "native-crew-harness") return READINESS_STATES.UNAVAILABLE;
198
+ if (surface === "native-crew-harness") return READINESS_STATES.UNKNOWN;
199
199
  return READINESS_STATES.UNKNOWN;
200
200
  }
201
201
  /** Project only structured, non-secret installer/runtime evidence. */
@@ -265,12 +265,31 @@ function classifyCrewSurface({ bridgeStatus, runtime } = {}) {
265
265
  if (runtime?.ok === true && runtime.service === "dsh-crew-hub" && (runtime.surface === CREW_UI_SURFACES.NATIVE || runtime.surface === void 0)) return CREW_UI_SURFACES.NATIVE;
266
266
  return CREW_UI_SURFACES.UNKNOWN;
267
267
  }
268
+ /**
269
+ * Surface capability model. The NATIVE 3210 Crew harness is the single full
270
+ * control plane; the OFFICIAL 3080 surface is a narrow quick-controls panel
271
+ * (total switch, flash/pro model priority, vision/imagegen toggles) plus a
272
+ * deep link back to 3210. Unknown surfaces get diagnostics only — never write
273
+ * authority. This replaces the old binary full-vs-minimal assumption.
274
+ */
268
275
  function surfaceResponsibilities(surface) {
269
- const fullControlPlane = surface === CREW_UI_SURFACES.OFFICIAL;
270
- return {
271
- fullControlPlane,
272
- minimalDiagnostics: !fullControlPlane
273
- };
276
+ switch (surface) {
277
+ case CREW_UI_SURFACES.NATIVE: return {
278
+ fullControlPlane: true,
279
+ quickControlPlane: true,
280
+ diagnostics: true
281
+ };
282
+ case CREW_UI_SURFACES.OFFICIAL: return {
283
+ fullControlPlane: false,
284
+ quickControlPlane: true,
285
+ diagnostics: true
286
+ };
287
+ default: return {
288
+ fullControlPlane: false,
289
+ quickControlPlane: false,
290
+ diagnostics: true
291
+ };
292
+ }
274
293
  }
275
294
  //#endregion
276
295
  //#region src/client/task-telemetry.mjs
@@ -511,6 +530,13 @@ const COPY = {
511
530
  providerJobs: (count) => `${count} 个运行任务`,
512
531
  providerOfficialBuiltin: "· 官方内置(不可删除)",
513
532
  providerSourceUnresolved: "· 来源未解析(安全锁定)",
533
+ providerLayerMigration: "检测到旧版 profile Provider",
534
+ providerLayerMigrationHint: "这些 Provider 仍在 Crew profile/base 层,所以 3210 原生页面不会显示删除。请先按迁移计划提升到 Harness 用户设置层;不会复制密钥。",
535
+ providerLayerMigrationPlan: "迁移计划",
536
+ providerLayerMigrate: "迁移到用户层",
537
+ providerLayerMigrating: "迁移中…",
538
+ providerLayerMigrationConfirm: "确认把这个 Provider 移到 Harness 用户设置层?会先保存可回滚快照,不会复制密钥值。",
539
+ providerLayerRollback: "回滚迁移",
514
540
  providerDeletePlan: "生成删除计划",
515
541
  providerDelete: "确认删除",
516
542
  providerPlanReady: "计划已生成;再次确认后才会修改 3210 配置。",
@@ -537,8 +563,8 @@ const COPY = {
537
563
  credentialPurging: "清理中…",
538
564
  credentialPurgeUnavailable: "凭据清理不可用",
539
565
  credentialPurgeUnverified: "已执行清理,但验证失败;引用仍保留用于恢复检查",
540
- customProviders: "自定义 Provider",
541
- addProvider: "+ 添加 Provider",
566
+ customProviders: "多模态适配器",
567
+ addProvider: "+ 添加多模态适配器",
542
568
  noCustomProviders: "暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。",
543
569
  providerName: "名称",
544
570
  modelsField: "模型列表(逗号分隔)",
@@ -842,6 +868,13 @@ const COPY = {
842
868
  providerJobs: (count) => `${count} running jobs`,
843
869
  providerOfficialBuiltin: "· official built-in (immutable)",
844
870
  providerSourceUnresolved: "· source unresolved (locked)",
871
+ providerLayerMigration: "Legacy profile Providers detected",
872
+ providerLayerMigrationHint: "These Providers still exist in the Crew profile/base layer, so the native 3210 page cannot show Delete. Promote them to Harness user settings with an explicit plan; keys are never copied.",
873
+ providerLayerMigrationPlan: "Migration plan",
874
+ providerLayerMigrate: "Migrate to user layer",
875
+ providerLayerMigrating: "Migrating…",
876
+ providerLayerMigrationConfirm: "Move this Provider into Harness user settings? A rollback snapshot is saved first; credential values are never copied.",
877
+ providerLayerRollback: "Rollback migration",
845
878
  providerDeletePlan: "Plan deletion",
846
879
  providerDelete: "Confirm delete",
847
880
  providerPlanReady: "Plan ready; a second confirmation is required before changing 3210 config.",
@@ -868,8 +901,8 @@ const COPY = {
868
901
  credentialPurging: "Purging…",
869
902
  credentialPurgeUnavailable: "Credential purge unavailable",
870
903
  credentialPurgeUnverified: "Purge ran but verification failed; the reference remains visible for recovery",
871
- customProviders: "Custom providers",
872
- addProvider: "+ Add provider",
904
+ customProviders: "Multimodal adapters",
905
+ addProvider: "+ Add multimodal adapter",
873
906
  noCustomProviders: "None yet. Added providers appear in the vision / image-gen selects above.",
874
907
  providerName: "Name",
875
908
  modelsField: "Models (comma-separated)",
@@ -1577,7 +1610,7 @@ function WorkersPanel({ ctx }) {
1577
1610
  } catch {}
1578
1611
  }, [get]);
1579
1612
  (0, react.useEffect)(() => {
1580
- if (surface !== CREW_UI_SURFACES.OFFICIAL) return void 0;
1613
+ if (!surfaceResponsibilities(surface).fullControlPlane) return void 0;
1581
1614
  refreshAll();
1582
1615
  const timer = setInterval(() => {
1583
1616
  Promise.all([get("/jobs"), get("/provider-health").catch(() => null)]).then(([j, health]) => {
@@ -1661,10 +1694,10 @@ function WorkersPanel({ ctx }) {
1661
1694
  withLang
1662
1695
  ]);
1663
1696
  (0, react.useEffect)(() => {
1664
- if (surface === CREW_UI_SURFACES.OFFICIAL) refreshHarnessModels();
1697
+ if (surfaceResponsibilities(surface).fullControlPlane) refreshHarnessModels();
1665
1698
  }, [surface, refreshHarnessModels]);
1666
1699
  (0, react.useEffect)(() => {
1667
- if (surface === CREW_UI_SURFACES.OFFICIAL) refreshProviderInventory();
1700
+ if (surfaceResponsibilities(surface).fullControlPlane) refreshProviderInventory();
1668
1701
  }, [surface, refreshProviderInventory]);
1669
1702
  const act = (0, react.useCallback)(async (target, confirmName) => {
1670
1703
  if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
@@ -1707,6 +1740,34 @@ function WorkersPanel({ ctx }) {
1707
1740
  get,
1708
1741
  copy
1709
1742
  ]);
1743
+ /** Request a Crew 3210 restart through the durable supervisor channel
1744
+ * (hub writes the request; the Windows watcher executes it). Polls until
1745
+ * VERIFIED or a terminal failure. Same-origin against the 3210 API — the
1746
+ * legacy 3080 supervisor endpoint is retired. */
1747
+ const requestRestart = (0, react.useCallback)(async (reason) => {
1748
+ const created = await post("/runtime/restart-request", {
1749
+ confirm: true,
1750
+ reason: reason ?? "configuration change"
1751
+ });
1752
+ if (!created.ok) throw new Error(created.code ?? created.error ?? copy.providerLifecycleError);
1753
+ const requestId = created.request_id;
1754
+ const deadline = Date.now() + 9e4;
1755
+ for (;;) {
1756
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1757
+ if (Date.now() > deadline) throw new Error("Crew restart timed out");
1758
+ const status = await get(`/runtime/restart-status?id=${encodeURIComponent(requestId)}`);
1759
+ if (!status.ok) continue;
1760
+ if (status.state === "VERIFIED") return {
1761
+ ok: true,
1762
+ ...status
1763
+ };
1764
+ if (status.state !== "RESTART_REQUESTED") throw new Error(status.state ?? "Crew restart failed");
1765
+ }
1766
+ }, [
1767
+ post,
1768
+ get,
1769
+ copy
1770
+ ]);
1710
1771
  /** Selects & checkboxes: apply immediately. */
1711
1772
  const field = (key, value) => {
1712
1773
  setConfig((c) => ({
@@ -2006,13 +2067,7 @@ function WorkersPanel({ ctx }) {
2006
2067
  }), path);
2007
2068
  if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
2008
2069
  if (result.restart_required === true && result.result?.state === "RESTART_PENDING") {
2009
- const restartPath = "http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart";
2010
- const restarted = await readJson(await fetch(restartPath, {
2011
- method: "POST",
2012
- headers: { "content-type": "application/json" },
2013
- body: JSON.stringify({ confirm: true })
2014
- }), restartPath);
2015
- if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
2070
+ await requestRestart("provider delete");
2016
2071
  const verified = await post(`/providers/${encoded}/verify-delete`, {
2017
2072
  transaction_id: planned.plan.plan_id,
2018
2073
  confirm: true
@@ -2030,6 +2085,68 @@ function WorkersPanel({ ctx }) {
2030
2085
  setProviderLifecycleBusy(null);
2031
2086
  }
2032
2087
  };
2088
+ const migrateHarnessProvider = async (entry) => {
2089
+ if (!entry || !["promote-existing-user", "materialize-user"].includes(entry.action)) {
2090
+ setNotice(entry?.blocked_reason ?? copy.providerLayerMigrationHint);
2091
+ return;
2092
+ }
2093
+ if (!window.confirm(copy.providerLayerMigrationConfirm)) return;
2094
+ const id = String(entry.provider_id ?? "");
2095
+ if (!id) return;
2096
+ setProviderLifecycleBusy(`migrate:${id}`);
2097
+ setNotice(copy.providerLayerMigrating);
2098
+ try {
2099
+ const encoded = encodeURIComponent(id);
2100
+ const planned = await post(`/providers/${encoded}/migrate-plan`, {});
2101
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
2102
+ if (!window.confirm(`${copy.providerLayerMigrationConfirm}\n\n${id}`)) return;
2103
+ const result = await post(`/providers/${encoded}/migrate`, {
2104
+ plan_id: planned.plan.plan_id,
2105
+ confirm: true
2106
+ });
2107
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
2108
+ if (result.restart_required === true && result.result?.state === "RESTART_PENDING") {
2109
+ await requestRestart("provider migrate");
2110
+ const verified = await post(`/providers/${encoded}/verify-migration`, {
2111
+ transaction_id: planned.plan.plan_id,
2112
+ confirm: true
2113
+ });
2114
+ if (!verified.ok || verified.state !== "VERIFIED") throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
2115
+ }
2116
+ setNotice(copy.saved);
2117
+ await refreshProviderInventory();
2118
+ } catch (error) {
2119
+ try {
2120
+ await refreshProviderInventory();
2121
+ } catch {}
2122
+ setNotice(String(error?.message ?? error));
2123
+ } finally {
2124
+ setProviderLifecycleBusy(null);
2125
+ }
2126
+ };
2127
+ const rollbackHarnessMigration = async (record, transactionId) => {
2128
+ if (!record?.id || !transactionId || !window.confirm(copy.providerRollbackConfirm)) return;
2129
+ setProviderLifecycleBusy(`rollback-migrate:${record.id}`);
2130
+ setNotice(copy.working);
2131
+ try {
2132
+ const encoded = encodeURIComponent(record.id);
2133
+ const result = await post(`/providers/${encoded}/rollback-migration`, {
2134
+ transaction_id: transactionId,
2135
+ confirm: true
2136
+ });
2137
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
2138
+ if (result.restart_required === true) await requestRestart("provider migrate");
2139
+ setNotice(copy.saved);
2140
+ await refreshProviderInventory();
2141
+ } catch (error) {
2142
+ setNotice(String(error?.message ?? error));
2143
+ try {
2144
+ await refreshProviderInventory();
2145
+ } catch {}
2146
+ } finally {
2147
+ setProviderLifecycleBusy(null);
2148
+ }
2149
+ };
2033
2150
  const rollbackHarnessProvider = async (record, transactionId, transactionState = "VERIFIED") => {
2034
2151
  const continuing = ["ROLLBACK_PENDING", "ROLLBACK_RESTART_PENDING"].includes(transactionState);
2035
2152
  const recoverable = [
@@ -2063,13 +2180,7 @@ function WorkersPanel({ ctx }) {
2063
2180
  });
2064
2181
  if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
2065
2182
  if (result.restart_required === true && result.state === "ROLLBACK_PENDING") {
2066
- const restartPath = "http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart";
2067
- const restarted = await readJson(await fetch(restartPath, {
2068
- method: "POST",
2069
- headers: { "content-type": "application/json" },
2070
- body: JSON.stringify({ confirm: true })
2071
- }), restartPath);
2072
- if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
2183
+ await requestRestart("provider migrate");
2073
2184
  const verified = await post(`/providers/${encoded}/verify-rollback`, {
2074
2185
  transaction_id: transactionId,
2075
2186
  confirm: true
@@ -3642,6 +3753,78 @@ function WorkersPanel({ ctx }) {
3642
3753
  },
3643
3754
  children: providerInventoryError
3644
3755
  }),
3756
+ !providerInventoryError && (providerInventory?.migration?.providers?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3757
+ style: {
3758
+ border: "1px solid rgba(201,135,53,0.45)",
3759
+ borderRadius: 7,
3760
+ padding: "8px 10px",
3761
+ marginBottom: 8
3762
+ },
3763
+ children: [
3764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3765
+ style: {
3766
+ fontWeight: 650,
3767
+ fontSize: 12.5
3768
+ },
3769
+ children: copy.providerLayerMigration
3770
+ }),
3771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3772
+ style: {
3773
+ fontSize: 11.5,
3774
+ opacity: .7,
3775
+ marginTop: 3
3776
+ },
3777
+ children: copy.providerLayerMigrationHint
3778
+ }),
3779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3780
+ style: {
3781
+ fontSize: 11,
3782
+ marginTop: 6,
3783
+ fontFamily: "monospace"
3784
+ },
3785
+ children: (providerInventory.migration.providers ?? []).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3786
+ style: {
3787
+ display: "flex",
3788
+ alignItems: "center",
3789
+ gap: 6,
3790
+ marginTop: 3
3791
+ },
3792
+ children: [
3793
+ entry.provider_id,
3794
+ " · ",
3795
+ entry.action,
3796
+ entry.harness_default ? " · Harness Default" : "",
3797
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
3798
+ ["promote-existing-user", "materialize-user"].includes(entry.action) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3799
+ type: "button",
3800
+ style: {
3801
+ ...S.btn,
3802
+ padding: "2px 7px"
3803
+ },
3804
+ disabled: providerLifecycleBusy !== null,
3805
+ onClick: () => {
3806
+ migrateHarnessProvider(entry);
3807
+ },
3808
+ children: providerLifecycleBusy === `migrate:${entry.provider_id}` ? copy.providerLayerMigrating : copy.providerLayerMigrate
3809
+ })
3810
+ ]
3811
+ }, entry.provider_id))
3812
+ }),
3813
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
3814
+ href: "http://127.0.0.1:3210/",
3815
+ target: "_blank",
3816
+ rel: "noopener noreferrer",
3817
+ style: {
3818
+ ...S.btn,
3819
+ display: "inline-block",
3820
+ textDecoration: "none",
3821
+ marginTop: 7,
3822
+ padding: "3px 8px"
3823
+ },
3824
+ children: [copy.providerLayerMigrationPlan, " → 3210"]
3825
+ })
3826
+ ]
3827
+ }),
3645
3828
  !providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3646
3829
  style: {
3647
3830
  fontSize: 12,
@@ -3673,7 +3856,10 @@ function WorkersPanel({ ctx }) {
3673
3856
  "DELETE_RESTART_PENDING",
3674
3857
  "ROLLBACK_RESTART_PENDING",
3675
3858
  "ROLLBACK_RESTORED",
3676
- "ROLLBACK_APPLYING"
3859
+ "ROLLBACK_APPLYING",
3860
+ "PREPARED",
3861
+ "USER_MATERIALIZED",
3862
+ "BASE_REMOVED"
3677
3863
  ].includes(entry.state)).sort((a, b) => String(a.updated_at ?? "").localeCompare(String(b.updated_at ?? ""))).at(-1);
3678
3864
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3679
3865
  style: {
@@ -3741,7 +3927,7 @@ function WorkersPanel({ ctx }) {
3741
3927
  "ROLLBACK_RESTART_PENDING",
3742
3928
  "ROLLBACK_RESTORED",
3743
3929
  "ROLLBACK_APPLYING"
3744
- ].includes(rollbackTransaction?.state)) && rollbackTransaction && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3930
+ ].includes(rollbackTransaction?.state)) && rollbackTransaction && rollbackTransaction.source !== "provider-layer-migration" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3745
3931
  type: "button",
3746
3932
  style: {
3747
3933
  ...S.btn,
@@ -3752,6 +3938,18 @@ function WorkersPanel({ ctx }) {
3752
3938
  rollbackHarnessProvider(record, rollbackTransaction.transaction_id, rollbackTransaction.state);
3753
3939
  },
3754
3940
  children: providerLifecycleBusy === `rollback:${record.id}` ? copy.working : rollbackTransaction.state === "ROLLBACK_PENDING" ? copy.providerRollbackContinue : copy.providerRollback
3941
+ }),
3942
+ rollbackTransaction?.source === "provider-layer-migration" && rollbackTransaction.transaction_id && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3943
+ type: "button",
3944
+ style: {
3945
+ ...S.btn,
3946
+ padding: "2px 8px"
3947
+ },
3948
+ disabled: providerLifecycleBusy !== null,
3949
+ onClick: () => {
3950
+ rollbackHarnessMigration(record, rollbackTransaction.transaction_id);
3951
+ },
3952
+ children: providerLifecycleBusy === `rollback-migrate:${record.id}` ? copy.working : copy.providerLayerRollback
3755
3953
  })
3756
3954
  ]
3757
3955
  }, record.id);
@@ -4577,7 +4775,7 @@ function WorkersPanel({ ctx }) {
4577
4775
  }
4578
4776
  function apply$1(ctx) {
4579
4777
  let runningCount = 0;
4580
- let officialSurface = null;
4778
+ let badgeSurface = null;
4581
4779
  ctx.slots.inject("settings.section", () => {
4582
4780
  let dispose = register();
4583
4781
  function register() {
@@ -4606,11 +4804,14 @@ function apply$1(ctx) {
4606
4804
  const poll = async () => {
4607
4805
  if (document.visibilityState !== "visible") return;
4608
4806
  try {
4609
- if (officialSurface === null) {
4610
- const response = await fetch(`${API}/bridge-status`, { cache: "no-store" });
4611
- officialSurface = classifyCrewSurface({ bridgeStatus: response.ok ? await response.json() : null }) === CREW_UI_SURFACES.OFFICIAL;
4807
+ if (badgeSurface === null) {
4808
+ const [bridgeStatus, runtime] = await Promise.all([fetch(`${API}/bridge-status`, { cache: "no-store" }).then((r) => r.ok ? r.json() : null).catch(() => null), fetch(`${API}/runtime`, { cache: "no-store" }).then((r) => r.ok ? r.json() : null).catch(() => null)]);
4809
+ badgeSurface = surfaceResponsibilities(classifyCrewSurface({
4810
+ bridgeStatus,
4811
+ runtime
4812
+ })).fullControlPlane;
4612
4813
  }
4613
- if (!officialSurface) return;
4814
+ if (!badgeSurface) return;
4614
4815
  const r = await (await fetch(`${API}/jobs`, { cache: "no-store" })).json();
4615
4816
  const n = r.ok ? (r.jobs ?? []).filter((j) => j.status === "running").length : 0;
4616
4817
  if (n !== runningCount) {