@camstack/addon-mqtt-broker 1.2.154 → 1.2.156

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.
@@ -39,7 +39,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
40
40
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
41
41
  //#endregion
42
- //#region ../types/dist/event-category-BmPm98n0.mjs
42
+ //#region ../types/dist/event-category-78tcBemo.mjs
43
43
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
44
44
  EventCategory["SystemBoot"] = "system.boot";
45
45
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -160,6 +160,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
160
160
  */
161
161
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
162
162
  /**
163
+ * An addon is RUNNING although its installed package is below what the
164
+ * running server requires (`camstack.requires.addons` of the server, or the
165
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
166
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
167
+ * operator alert per node and package. The addon is never held back for it.
168
+ *
169
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
170
+ */
171
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
172
+ /**
173
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
174
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
175
+ *
176
+ * Payload: `{ packageName, addonId, nodeId }`.
177
+ */
178
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
179
+ /**
163
180
  * Monitor scheduled the next retry for a failed addon. Transient —
164
181
  * surfaced to the UI for live-updating the "next retry in Ns"
165
182
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -270,7 +287,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
270
287
  EventCategory["ProcessRestarted"] = "process.restarted";
271
288
  /**
272
289
  * The SET of storage locations changed — one was created, edited, enabled,
273
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
290
+ * disabled or deleted through the location instances or the forced-delete action.
274
291
  *
275
292
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
276
293
  * it must also converge on its own periodic path, because a dropped event
@@ -825,7 +842,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
825
842
  return EventCategory;
826
843
  }({});
827
844
  //#endregion
828
- //#region ../types/dist/device-type-D1fH5kaC.mjs
845
+ //#region ../types/dist/device-type-DxCT0krO.mjs
829
846
  /**
830
847
  import { errMsg } from '@camstack/types'
831
848
  * Extract a human-readable message from an unknown error value.
@@ -987,6 +1004,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
987
1004
  * filters that want "all devices with on-motion behaviour".
988
1005
  */
989
1006
  DeviceFeature["MotionTrigger"] = "motion-trigger";
1007
+ /**
1008
+ * Device can be run for a chosen time and then stopped. Mirrors the
1009
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
1010
+ * a driver, so the flag is derived from the device's bindings
1011
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
1012
+ */
1013
+ DeviceFeature["TimedRun"] = "timed-run";
990
1014
  /** Light supports rgb-triplet color via `color` cap. */
991
1015
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
992
1016
  /** Light supports HSV color via `color` cap. */
@@ -6021,7 +6045,7 @@ var ZodIssueCode = {
6021
6045
  var ZodFirstPartyTypeKind;
6022
6046
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6023
6047
  //#endregion
6024
- //#region ../types/dist/sleep-CdKka2ri.mjs
6048
+ //#region ../types/dist/sleep-7El4r5Bl.mjs
6025
6049
  /**
6026
6050
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6027
6051
  * window to float samples (D455).
@@ -6385,6 +6409,59 @@ Object.fromEntries([
6385
6409
  function isValuelessField(field) {
6386
6410
  return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
6387
6411
  }
6412
+ /**
6413
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6414
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6415
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6416
+ * pre-resolving) lets callers both read the effective value AND detect
6417
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6418
+ * malformed input yields an empty map, never throws.
6419
+ */
6420
+ function collectHydratedFieldEntries(schema) {
6421
+ const out = /* @__PURE__ */ new Map();
6422
+ if (typeof schema !== "object" || schema === null) return out;
6423
+ const sections = schema.sections;
6424
+ if (!Array.isArray(sections)) return out;
6425
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6426
+ return out;
6427
+ }
6428
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6429
+ function collectFieldEntriesInto(fields, into) {
6430
+ if (!Array.isArray(fields)) return;
6431
+ for (const field of fields) {
6432
+ if (typeof field !== "object" || field === null) continue;
6433
+ const type = field.type;
6434
+ if (type === "group") {
6435
+ collectFieldEntriesInto(field.fields, into);
6436
+ continue;
6437
+ }
6438
+ if (type === "sub-tabs") {
6439
+ const tabs = field.tabs;
6440
+ if (Array.isArray(tabs)) {
6441
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6442
+ }
6443
+ continue;
6444
+ }
6445
+ const key = field.key;
6446
+ if (typeof key !== "string" || key.length === 0) continue;
6447
+ if (!("value" in field)) continue;
6448
+ into.set(key, {
6449
+ value: field.value,
6450
+ default: field.default
6451
+ });
6452
+ }
6453
+ }
6454
+ /**
6455
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6456
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6457
+ * common "just give me the resolved values" case. Built on
6458
+ * {@link collectHydratedFieldEntries}.
6459
+ */
6460
+ function collectHydratedFieldValues(schema) {
6461
+ const out = /* @__PURE__ */ new Map();
6462
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6463
+ return out;
6464
+ }
6388
6465
  function hydrateSchema(schema, values, options = {}) {
6389
6466
  return {
6390
6467
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -6730,10 +6807,33 @@ function integrationRowsApiOver(api) {
6730
6807
  };
6731
6808
  }
6732
6809
  var PREFIX = "records/";
6810
+ /**
6811
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6812
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6813
+ * instance of the host's classes, not of the addon's. The brand plus the code
6814
+ * make `instanceof` answer by identity of the failure, not of the copy.
6815
+ */
6816
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6817
+ function carriesBrand(value) {
6818
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6819
+ }
6733
6820
  /** The base of every records failure. `detail` is the human part of the message. */
6734
6821
  var RecordsError = class extends Error {
6822
+ /** The code a subclass stands for; the base stands for every code. */
6823
+ static code = void 0;
6824
+ /**
6825
+ * `instanceof` across copies of this module: any branded error is a
6826
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6827
+ * subclass's code. Same-copy answers are unchanged.
6828
+ */
6829
+ static [Symbol.hasInstance](value) {
6830
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6831
+ if (!carriesBrand(value)) return false;
6832
+ return this.code === void 0 || value.code === this.code;
6833
+ }
6735
6834
  code;
6736
6835
  detail;
6836
+ [RECORDS_ERROR_BRAND] = true;
6737
6837
  constructor(code, detail) {
6738
6838
  super(`${PREFIX}${code}: ${detail}`);
6739
6839
  this.name = "RecordsError";
@@ -6742,22 +6842,25 @@ var RecordsError = class extends Error {
6742
6842
  }
6743
6843
  };
6744
6844
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6745
- var RecordSecretUnavailableError = class extends RecordsError {
6845
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6846
+ static code = "secret-unavailable";
6746
6847
  constructor(detail) {
6747
- super("secret-unavailable", detail);
6848
+ super(RecordSecretUnavailableError.code, detail);
6748
6849
  this.name = "RecordSecretUnavailableError";
6749
6850
  }
6750
6851
  };
6751
- var InvalidRecordError = class extends RecordsError {
6852
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6853
+ static code = "invalid-record";
6752
6854
  constructor(detail) {
6753
- super("invalid-record", detail);
6855
+ super(InvalidRecordError.code, detail);
6754
6856
  this.name = "InvalidRecordError";
6755
6857
  }
6756
6858
  };
6757
6859
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6758
- var DeclarationRefusedError = class extends RecordsError {
6860
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6861
+ static code = "declaration-refused";
6759
6862
  constructor(detail) {
6760
- super("declaration-refused", detail);
6863
+ super(DeclarationRefusedError.code, detail);
6761
6864
  this.name = "DeclarationRefusedError";
6762
6865
  }
6763
6866
  };
@@ -7235,17 +7338,35 @@ var RecordIntegrationInstances = class {
7235
7338
  ...kind !== void 0 ? { kind } : {}
7236
7339
  });
7237
7340
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7341
+ const initial = this.hooks.initialBody(name, kind);
7342
+ const patch = this.patchOf(values ?? {});
7343
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7344
+ kind,
7345
+ body: {
7346
+ ...initial,
7347
+ ...patch
7348
+ }
7349
+ }));
7238
7350
  await this.handle.put({
7239
7351
  id: minted,
7240
- data: this.hooks.initialBody(name, kind)
7352
+ data: initial
7241
7353
  }, { ifRev: 0 });
7242
- const patch = this.patchOf(values ?? {});
7243
7354
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7244
7355
  await this.hooks.onChanged?.(minted, "created");
7245
7356
  return { id: minted };
7246
7357
  };
7247
7358
  updateInstance = async ({ id, name, values }) => {
7248
7359
  const patch = this.patchOf(values);
7360
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7361
+ const stored = await this.handle.get(id);
7362
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7363
+ kind: void 0,
7364
+ body: {
7365
+ ...stored.data,
7366
+ ...patch
7367
+ }
7368
+ }));
7369
+ }
7249
7370
  if (name !== void 0) patch[this.nameField] = name;
7250
7371
  await this.handle.patch(id, patch);
7251
7372
  await this.hooks.onChanged?.(id, "updated");
@@ -7556,6 +7677,21 @@ var SCOPED_SETTINGS_SCOPES = [
7556
7677
  "device-defaults",
7557
7678
  "node"
7558
7679
  ];
7680
+ /** The one refusal of a key a scope did not declare. */
7681
+ function assertDeclaredKeys(keys, declared) {
7682
+ const undeclared = keys.filter((key) => !declared.has(key));
7683
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7684
+ }
7685
+ /** Build the handle over an addon's scoped read, write and unset. */
7686
+ function createScopedSettings(deps) {
7687
+ return {
7688
+ async get() {
7689
+ return deps.schema.parse(await deps.read());
7690
+ },
7691
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7692
+ unset: (keys) => deps.unset(keys)
7693
+ };
7694
+ }
7559
7695
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7560
7696
  var KeychainWriteRefusedError = class extends Error {
7561
7697
  fieldPath;
@@ -8131,7 +8267,16 @@ function createHandle(raw, opts) {
8131
8267
  return blob;
8132
8268
  }
8133
8269
  },
8134
- writeAddonStore: writeAddon
8270
+ writeAddonStore: writeAddon,
8271
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8272
+ const stored = await readStoredAddonBlob(raw);
8273
+ index.seen("addon", stored);
8274
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8275
+ if (removable.length === 0) return;
8276
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8277
+ await raw.writeAddonStore(patch);
8278
+ index.wrote("addon", patch);
8279
+ })
8135
8280
  };
8136
8281
  }
8137
8282
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8261,7 +8406,7 @@ function boundOf(list) {
8261
8406
  * Base class for CamStack addons. Eliminates settings boilerplate:
8262
8407
  *
8263
8408
  * - Typed `config` property with automatic resolution from store + defaults
8264
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8409
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8265
8410
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8266
8411
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8267
8412
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8271,7 +8416,7 @@ function boundOf(list) {
8271
8416
  * Subclasses override:
8272
8417
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8273
8418
  * - `onShutdown()` — cleanup
8274
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8419
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8275
8420
  *
8276
8421
  * @example
8277
8422
  * ```ts
@@ -8285,7 +8430,7 @@ function boundOf(list) {
8285
8430
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8286
8431
  * }
8287
8432
  *
8288
- * protected globalSettingsSchema() {
8433
+ * protected clusterSettingsSchema() {
8289
8434
  * return this.schema({
8290
8435
  * sections: [{
8291
8436
  * id: 'main', title: 'Settings',
@@ -8328,7 +8473,7 @@ var BaseAddon = class {
8328
8473
  defaults;
8329
8474
  /**
8330
8475
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8331
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8476
+ * itself (`state()`, the scoped writers) that no form field declares.
8332
8477
  * Stored in the keychain exactly like a `password` field. Syntax:
8333
8478
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8334
8479
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8338,7 +8483,7 @@ var BaseAddon = class {
8338
8483
  /**
8339
8484
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8340
8485
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8341
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8486
+ * see them. Empty until loaded.
8342
8487
  */
8343
8488
  _scopedByCap = /* @__PURE__ */ new Map();
8344
8489
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8524,7 +8669,7 @@ var BaseAddon = class {
8524
8669
  /** Addon-specific cleanup. Override if needed. */
8525
8670
  async onShutdown() {}
8526
8671
  /**
8527
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8672
+ * Called after config is resolved during a settings write.
8528
8673
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8529
8674
  * Not called during initialize() — use onInitialize() for initial setup.
8530
8675
  */
@@ -8553,82 +8698,12 @@ var BaseAddon = class {
8553
8698
  scopedSettingsCapabilities() {
8554
8699
  return [];
8555
8700
  }
8556
- /** Override to provide global-level settings UI schema. */
8557
- globalSettingsSchema(_cap) {
8558
- return null;
8559
- }
8560
8701
  /** Override to provide device-level settings UI schema. */
8561
8702
  deviceSettingsSchema() {
8562
8703
  return null;
8563
8704
  }
8564
8705
  /**
8565
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8566
- * ARE the configuration of its integration.
8567
- *
8568
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8569
- * operator should find on the addon's integration page (System →
8570
- * Integrations → <name>) rather than only in the cluster-wide list of every
8571
- * addon. Empty (the default) means the addon has no integration-level
8572
- * settings and no such surface is offered — this is opt-in, because whether
8573
- * an addon's configuration IS its integration's configuration depends on the
8574
- * nature of the integration.
8575
- *
8576
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8577
- * the ONE global schema, in the ONE addon store, written by the ONE
8578
- * `updateGlobalSettings` path. There is deliberately no
8579
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8580
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8581
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8582
- *
8583
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8584
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8585
- * removed with the reason recorded at
8586
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8587
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8588
- * marker sprinkled across sections also has to borrow a field that already
8589
- * means something else; borrowing `section.tab` put the literal word
8590
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8591
- * GROUP this visually" and cannot also mean "where this lives" (D269
8592
- * supersedes D268). One declaration, in one place, next to the schema whose
8593
- * ids it names.
8594
- */
8595
- integrationSettingSections() {
8596
- return [];
8597
- }
8598
- async getGlobalSettings(overlay, cap, nodeId) {
8599
- await this.loadScopedSchemas(cap);
8600
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8601
- if (!schema) return { sections: [] };
8602
- const projected = await this.readRawProjectedStore(nodeId, cap);
8603
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8604
- }
8605
- /**
8606
- * The integration-level view of this addon's settings: exactly the sections
8607
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8608
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8609
- *
8610
- * Returns `null` when the addon declared nothing — an addon that opts out has
8611
- * no integration settings surface at all, rather than an empty one that reads
8612
- * as a failed load.
8613
- *
8614
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8615
- * and not in whichever UI happens to render this:
8616
- *
8617
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8618
- * shown here is the same field, with the same bare key, that the addon's
8619
- * own page shows. There is no integration-specific writer — callers save
8620
- * through `updateGlobalSettings` — so a second store key is unreachable,
8621
- * not merely discouraged.
8622
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8623
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8624
- * such a field silently picked would be a wrong answer for the operator
8625
- * who opened the page (D266).
8626
- * 3. **No silent typo.** A declared id that names no section throws. The
8627
- * alternative — skip it — turns a rename into a surface that quietly
8628
- * empties, which looks exactly like an addon with nothing to configure.
8629
- */
8630
- /**
8631
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8706
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8632
8707
  *
8633
8708
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8634
8709
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8676,37 +8751,178 @@ var BaseAddon = class {
8676
8751
  }
8677
8752
  }
8678
8753
  /**
8754
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8755
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8756
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8757
+ */
8758
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8759
+ return null;
8760
+ }
8761
+ /**
8762
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8763
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8764
+ */
8765
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8766
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8767
+ clusterSettings(cap, schema) {
8768
+ return this.scopedHandle("cluster", cap, schema);
8769
+ }
8770
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8771
+ deviceDefaults(cap, schema) {
8772
+ return this.scopedHandle("device-defaults", cap, schema);
8773
+ }
8774
+ /**
8775
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8776
+ * with no row yields the schema defaults — never a bare-key fallback.
8777
+ */
8778
+ nodeSettings(cap, schema, nodeId) {
8779
+ return this.scopedHandle("node", cap, schema, nodeId);
8780
+ }
8781
+ /**
8782
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8783
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8784
+ * central delete. `nodeId` omitted ⇒ the local node.
8785
+ */
8786
+ nodeState(key, schema, fallback, nodeId) {
8787
+ this._declaredStateKeys.add(key);
8788
+ return createDurableState({
8789
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8790
+ schema,
8791
+ fallback,
8792
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8793
+ write: async (patch) => {
8794
+ await this._ctx?.settings?.writeAddonStore(patch);
8795
+ },
8796
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8797
+ addonId: this._ctx?.id,
8798
+ key: k,
8799
+ error: String(e)
8800
+ } })
8801
+ });
8802
+ }
8803
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8804
+ async declaredScopedSchema(scope, cap) {
8805
+ await this.loadScopedSchemas(cap);
8806
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8807
+ }
8808
+ async requireScopedSchema(scope, cap) {
8809
+ const schema = await this.declaredScopedSchema(scope, cap);
8810
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8811
+ return schema;
8812
+ }
8813
+ scopedKeys(schema) {
8814
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8815
+ }
8816
+ /** `store` narrowed to the keys `schema` declares. */
8817
+ narrowToScope(store, schema) {
8818
+ const keys = this.scopedKeys(schema);
8819
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8820
+ }
8821
+ scopedHandle(scope, cap, schema, nodeId) {
8822
+ return createScopedSettings({
8823
+ schema,
8824
+ read: async () => {
8825
+ const declared = await this.requireScopedSchema(scope, cap);
8826
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8827
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8828
+ return {
8829
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8830
+ ...stored
8831
+ };
8832
+ },
8833
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8834
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8835
+ });
8836
+ }
8837
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8838
+ async writeScoped(scope, cap, patch, nodeId) {
8839
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8840
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8841
+ }
8842
+ /**
8843
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8844
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8845
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8846
+ */
8847
+ async unsetScoped(scope, cap, keys, nodeId) {
8848
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8849
+ const target = scope === "node" ? nodeId : void 0;
8850
+ const tree = this.addonSecretTree();
8851
+ const secretKeys = keys.filter((key) => tree.has(key));
8852
+ const plainKeys = keys.filter((key) => !tree.has(key));
8853
+ let changed = false;
8854
+ if (plainKeys.length > 0) {
8855
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8856
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8857
+ }
8858
+ if (secretKeys.length > 0) {
8859
+ await this.clearSecretKeys(secretKeys, target, cap);
8860
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8861
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8862
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8863
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8864
+ changed = true;
8865
+ }
8866
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8867
+ }
8868
+ /**
8869
+ * The hydrated form of one scope of one capability's settings (any
8870
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8871
+ */
8872
+ async getScopedSettings(scope, cap, nodeId) {
8873
+ const schema = await this.declaredScopedSchema(scope, cap);
8874
+ if (schema === null) return null;
8875
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8876
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8877
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8878
+ }
8879
+ /**
8880
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8881
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8882
+ * nothing.
8883
+ */
8884
+ async listScopedSettings() {
8885
+ await this.loadScopedSchemas();
8886
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8887
+ cap,
8888
+ scope: entry.scope,
8889
+ sectionIds: entry.schema.sections.map((section) => section.id)
8890
+ })));
8891
+ }
8892
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8893
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8894
+ this._operatorWriteDepth++;
8895
+ try {
8896
+ await this.writeScoped(scope, cap, patch, nodeId);
8897
+ } finally {
8898
+ this._operatorWriteDepth--;
8899
+ }
8900
+ }
8901
+ /**
8902
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8903
+ * its value. `null` when the capability declares nothing for the scope.
8904
+ */
8905
+ async readScopedValues(scope, cap, nodeId) {
8906
+ const schema = await this.declaredScopedSchema(scope, cap);
8907
+ if (schema === null) return null;
8908
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8909
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8910
+ }
8911
+ /**
8679
8912
  * What the create wizard and the integration edit page render for one instance: the addon's
8680
8913
  * per-instance schema when it declares one, else its global schema (what they always showed).
8681
8914
  */
8682
8915
  async getIntegrationFormSchema(instance, nodeId) {
8683
8916
  const scoped = await this.integrationFormSchema(instance);
8684
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8917
+ if (scoped === null) return this.scopedForm(nodeId);
8685
8918
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8686
8919
  }
8687
- async getIntegrationSettings(nodeId) {
8688
- const declared = this.integrationSettingSections();
8689
- if (declared.length === 0) return null;
8690
- const schema = this.globalSettingsSchema();
8691
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8692
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8693
- const sections = [];
8694
- for (const id of declared) {
8695
- const section = byId.get(id);
8696
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8697
- const fields = dropPerNodeFields(section.fields);
8698
- if (fields.length === 0) continue;
8699
- sections.push({
8700
- ...section,
8701
- fields
8702
- });
8703
- }
8704
- if (sections.length === 0) return null;
8705
- const projected = await this.readRawProjectedStore(nodeId);
8706
- return hydrateSchema({
8707
- ...schema,
8708
- sections
8709
- }, projected, { inlineSecretsAsViews: true });
8920
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8921
+ async scopedForm(nodeId) {
8922
+ await this.loadScopedSchemas();
8923
+ const schema = this.scopedFormSchema();
8924
+ if (!schema) return { sections: [] };
8925
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8710
8926
  }
8711
8927
  /**
8712
8928
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8715,8 +8931,8 @@ var BaseAddon = class {
8715
8931
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8716
8932
  * A no-op passthrough when the schema declares no `perNode` field.
8717
8933
  *
8718
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8719
- * the store for custom option logic (option narrowing, value snapping) to
8934
+ * This is the sanctioned way for an addon that needs
8935
+ * the store for custom logic (option narrowing, value snapping) to
8720
8936
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8721
8937
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8722
8938
  *
@@ -8768,7 +8984,7 @@ var BaseAddon = class {
8768
8984
  }
8769
8985
  /**
8770
8986
  * Whether ONE global key of this addon's store was written, read three-way
8771
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
8987
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8772
8988
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8773
8989
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8774
8990
  * not supported here: its stored name is scoped.
@@ -8802,7 +9018,7 @@ var BaseAddon = class {
8802
9018
  }
8803
9019
  /**
8804
9020
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8805
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9021
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8806
9022
  * the operator sends is stored as sent even when it equals a pre-keychain
8807
9023
  * inline value: the operator re-entering a password is a change, an addon
8808
9024
  * writing back what it read is not (D787).
@@ -8815,9 +9031,6 @@ var BaseAddon = class {
8815
9031
  this._operatorWriteDepth--;
8816
9032
  }
8817
9033
  }
8818
- async updateGlobalSettings(patch, nodeId) {
8819
- await this.applyGlobalPatch(patch, nodeId);
8820
- }
8821
9034
  /**
8822
9035
  * REMOVE stored secrets — their keychain values and their markers (D787).
8823
9036
  *
@@ -8827,6 +9040,9 @@ var BaseAddon = class {
8827
9040
  * a keychain (an older kernel) it writes `''`, as such code always did.
8828
9041
  */
8829
9042
  async clearSecrets(keys, nodeId) {
9043
+ await this.clearSecretKeys(keys, nodeId);
9044
+ }
9045
+ async clearSecretKeys(keys, nodeId, cap) {
8830
9046
  const tree = this.addonSecretTree();
8831
9047
  const keychain = this._ctx?.secrets !== void 0;
8832
9048
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8839,25 +9055,26 @@ var BaseAddon = class {
8839
9055
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8840
9056
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8841
9057
  }
8842
- await this.applyGlobalPatch(patch, nodeId);
9058
+ await this.applyGlobalPatch(patch, nodeId, cap);
8843
9059
  }
8844
- async applyGlobalPatch(barePatch, nodeId) {
8845
- await this.loadScopedSchemas();
8846
- const keys = this.perNodeKeys();
9060
+ /** Resolves `true` when the write changed stored values. */
9061
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9062
+ await this.loadScopedSchemas(cap);
9063
+ const keys = this.perNodeKeys(cap);
8847
9064
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8848
9065
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8849
9066
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8850
9067
  const handle = this._secretSettings;
8851
9068
  if (handle !== null) {
8852
9069
  const { changed } = await handle.writeAddonStore(scoped);
8853
- if (!changed) return;
9070
+ if (!changed) return false;
8854
9071
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8855
- if (target !== localNode) return;
9072
+ if (target !== localNode) return true;
8856
9073
  await this.resolveConfig();
8857
9074
  await this.onConfigChanged();
8858
9075
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8859
- const legacy = this.globalSettingsSchema();
8860
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9076
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9077
+ return true;
8861
9078
  }
8862
9079
  /**
8863
9080
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8894,10 +9111,7 @@ var BaseAddon = class {
8894
9111
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8895
9112
  this._addonSecretTree = null;
8896
9113
  }
8897
- /**
8898
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8899
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8900
- */
9114
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8901
9115
  scopedFormSchema(cap) {
8902
9116
  const byId = /* @__PURE__ */ new Map();
8903
9117
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8909,8 +9123,7 @@ var BaseAddon = class {
8909
9123
  return [...this._scopedByCap.values()].flat();
8910
9124
  }
8911
9125
  /**
8912
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8913
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9126
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8914
9127
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8915
9128
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8916
9129
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8919,8 +9132,7 @@ var BaseAddon = class {
8919
9132
  const cacheKey = cap ?? "";
8920
9133
  const cached = this._nodeKeysByCap.get(cacheKey);
8921
9134
  if (cached) return cached;
8922
- const legacy = this.globalSettingsSchema(cap);
8923
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9135
+ const keys = /* @__PURE__ */ new Set();
8924
9136
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8925
9137
  if (scope !== "node") continue;
8926
9138
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9138,8 +9350,6 @@ var BaseAddon = class {
9138
9350
  }
9139
9351
  declaredKeySet() {
9140
9352
  const keys = new Set(Object.keys(this.defaults));
9141
- const schema = this.globalSettingsSchema();
9142
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9143
9353
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9144
9354
  for (const key of this._declaredStateKeys) keys.add(key);
9145
9355
  for (const path of this.secretStatePaths) {
@@ -9237,7 +9447,7 @@ var BaseAddon = class {
9237
9447
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9238
9448
  addonSecretTree() {
9239
9449
  if (this._addonSecretTree === null) {
9240
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9450
+ const tree = /* @__PURE__ */ new Map();
9241
9451
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9242
9452
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9243
9453
  }
@@ -9449,47 +9659,6 @@ function classifyStoreError(err) {
9449
9659
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9450
9660
  return "permanent";
9451
9661
  }
9452
- /**
9453
- * Collect the keys of every field marked `perNode: true`, recursing into
9454
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9455
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9456
- * don't declare `perNode` and are excluded by the `in` narrowing.
9457
- */
9458
- /**
9459
- * The same fields with every `perNode: true` one removed, recursing into layout
9460
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9461
- * with no child is dropped rather than rendered empty.
9462
- *
9463
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9464
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9465
- */
9466
- function dropPerNodeFields(fields) {
9467
- const kept = [];
9468
- for (const field of fields) {
9469
- if (field.type === "group") {
9470
- const inner = dropPerNodeFields(field.fields);
9471
- if (inner.length > 0) kept.push({
9472
- ...field,
9473
- fields: inner
9474
- });
9475
- continue;
9476
- }
9477
- if (field.type === "sub-tabs") {
9478
- const tabs = field.tabs.map((tab) => ({
9479
- ...tab,
9480
- fields: dropPerNodeFields(tab.fields)
9481
- })).filter((tab) => tab.fields.length > 0);
9482
- if (tabs.length > 0) kept.push({
9483
- ...field,
9484
- tabs
9485
- });
9486
- continue;
9487
- }
9488
- if ("perNode" in field && field.perNode === true) continue;
9489
- kept.push(field);
9490
- }
9491
- return kept;
9492
- }
9493
9662
  /** The keys of the value-holding fields, recursing into layout containers. */
9494
9663
  function collectValueFieldKeys(fields) {
9495
9664
  const collected = [];
@@ -9522,21 +9691,6 @@ function collectFieldKeys(fields) {
9522
9691
  }
9523
9692
  return collected;
9524
9693
  }
9525
- function collectPerNodeFieldKeys(fields) {
9526
- const collected = [];
9527
- for (const field of fields) {
9528
- if (field.type === "group") {
9529
- collected.push(...collectPerNodeFieldKeys(field.fields));
9530
- continue;
9531
- }
9532
- if (field.type === "sub-tabs") {
9533
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9534
- continue;
9535
- }
9536
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9537
- }
9538
- return collected;
9539
- }
9540
9694
  /**
9541
9695
  * Normalize an `ICamstackAddon.initialize()` return value into the
9542
9696
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10587,7 +10741,6 @@ var CAP_NAMES = Object.freeze([
10587
10741
  "notifier",
10588
10742
  "numeric-sensor",
10589
10743
  "oauth-integration",
10590
- "oidc-connections",
10591
10744
  "osd",
10592
10745
  "osd-manager",
10593
10746
  "pet-feeder",
@@ -12294,13 +12447,25 @@ Object.freeze({
12294
12447
  "path": "temperature",
12295
12448
  "valueType": "number"
12296
12449
  }],
12297
- "timed-run": [{
12298
- "path": "activeUntil",
12299
- "valueType": "number"
12300
- }, {
12301
- "path": "requestedBy",
12302
- "valueType": "string"
12303
- }],
12450
+ "timed-run": [
12451
+ {
12452
+ "path": "activeUntil",
12453
+ "valueType": "number"
12454
+ },
12455
+ {
12456
+ "path": "requestedBy",
12457
+ "valueType": "string"
12458
+ },
12459
+ {
12460
+ "path": "quickRun.duration",
12461
+ "valueType": "number"
12462
+ },
12463
+ {
12464
+ "path": "quickRun.unit",
12465
+ "valueType": "enum",
12466
+ "enumValues": ["s", "min"]
12467
+ }
12468
+ ],
12304
12469
  "update": [
12305
12470
  {
12306
12471
  "path": "currentVersion",
@@ -12479,8 +12644,7 @@ Object.freeze({
12479
12644
  "device-provider": { "reloadDevice": 18e4 },
12480
12645
  "llm": {
12481
12646
  "generate": 6e5,
12482
- "generateVision": 6e5,
12483
- "testProfile": 6e5
12647
+ "generateVision": 6e5
12484
12648
  },
12485
12649
  "llm-runtime": {
12486
12650
  "complete": 6e5,
@@ -12525,7 +12689,7 @@ object({
12525
12689
  params: record(string(), union([string(), number()])).optional()
12526
12690
  });
12527
12691
  //#endregion
12528
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12692
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12529
12693
  /**
12530
12694
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12531
12695
  *
@@ -13687,6 +13851,387 @@ var NodeColorIdSchema = _enum([
13687
13851
  "node-h"
13688
13852
  ]);
13689
13853
  //#endregion
13854
+ //#region ../types/dist/convert-BAzH15HU.mjs
13855
+ /**
13856
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13857
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13858
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13859
+ * decimals). K derives from `°C = K − 273.15`.
13860
+ */
13861
+ var UNIT_TABLE = {
13862
+ "°C": {
13863
+ dimension: "temperature",
13864
+ factor: 1,
13865
+ offset: 0
13866
+ },
13867
+ "°F": {
13868
+ dimension: "temperature",
13869
+ factor: 5 / 9,
13870
+ offset: -160 / 9
13871
+ },
13872
+ K: {
13873
+ dimension: "temperature",
13874
+ factor: 1,
13875
+ offset: -273.15
13876
+ },
13877
+ hPa: {
13878
+ dimension: "pressure",
13879
+ factor: 1,
13880
+ offset: 0
13881
+ },
13882
+ kPa: {
13883
+ dimension: "pressure",
13884
+ factor: 10,
13885
+ offset: 0
13886
+ },
13887
+ Pa: {
13888
+ dimension: "pressure",
13889
+ factor: .01,
13890
+ offset: 0
13891
+ },
13892
+ mbar: {
13893
+ dimension: "pressure",
13894
+ factor: 1,
13895
+ offset: 0
13896
+ },
13897
+ bar: {
13898
+ dimension: "pressure",
13899
+ factor: 1e3,
13900
+ offset: 0
13901
+ },
13902
+ inHg: {
13903
+ dimension: "pressure",
13904
+ factor: 33.8639,
13905
+ offset: 0
13906
+ },
13907
+ mmHg: {
13908
+ dimension: "pressure",
13909
+ factor: 1.33322,
13910
+ offset: 0
13911
+ },
13912
+ psi: {
13913
+ dimension: "pressure",
13914
+ factor: 68.9476,
13915
+ offset: 0
13916
+ },
13917
+ "m/s": {
13918
+ dimension: "speed",
13919
+ factor: 1,
13920
+ offset: 0
13921
+ },
13922
+ "km/h": {
13923
+ dimension: "speed",
13924
+ factor: 1 / 3.6,
13925
+ offset: 0
13926
+ },
13927
+ mph: {
13928
+ dimension: "speed",
13929
+ factor: .44704,
13930
+ offset: 0
13931
+ },
13932
+ kn: {
13933
+ dimension: "speed",
13934
+ factor: .514444,
13935
+ offset: 0
13936
+ },
13937
+ "mm/h": {
13938
+ dimension: "precipitation-rate",
13939
+ factor: 1,
13940
+ offset: 0
13941
+ },
13942
+ "in/h": {
13943
+ dimension: "precipitation-rate",
13944
+ factor: 25.4,
13945
+ offset: 0
13946
+ },
13947
+ m: {
13948
+ dimension: "length",
13949
+ factor: 1,
13950
+ offset: 0
13951
+ },
13952
+ mm: {
13953
+ dimension: "length",
13954
+ factor: .001,
13955
+ offset: 0
13956
+ },
13957
+ cm: {
13958
+ dimension: "length",
13959
+ factor: .01,
13960
+ offset: 0
13961
+ },
13962
+ km: {
13963
+ dimension: "length",
13964
+ factor: 1e3,
13965
+ offset: 0
13966
+ },
13967
+ in: {
13968
+ dimension: "length",
13969
+ factor: .0254,
13970
+ offset: 0
13971
+ },
13972
+ ft: {
13973
+ dimension: "length",
13974
+ factor: .3048,
13975
+ offset: 0
13976
+ },
13977
+ mi: {
13978
+ dimension: "length",
13979
+ factor: 1609.344,
13980
+ offset: 0
13981
+ },
13982
+ "m²": {
13983
+ dimension: "area",
13984
+ factor: 1,
13985
+ offset: 0
13986
+ },
13987
+ lx: {
13988
+ dimension: "illuminance",
13989
+ factor: 1,
13990
+ offset: 0
13991
+ },
13992
+ "W/m²": {
13993
+ dimension: "irradiance",
13994
+ factor: 1,
13995
+ offset: 0
13996
+ },
13997
+ W: {
13998
+ dimension: "power",
13999
+ factor: 1,
14000
+ offset: 0
14001
+ },
14002
+ kW: {
14003
+ dimension: "power",
14004
+ factor: 1e3,
14005
+ offset: 0
14006
+ },
14007
+ VA: {
14008
+ dimension: "apparent-power",
14009
+ factor: 1,
14010
+ offset: 0
14011
+ },
14012
+ Wh: {
14013
+ dimension: "energy",
14014
+ factor: 1,
14015
+ offset: 0
14016
+ },
14017
+ kWh: {
14018
+ dimension: "energy",
14019
+ factor: 1e3,
14020
+ offset: 0
14021
+ },
14022
+ MWh: {
14023
+ dimension: "energy",
14024
+ factor: 1e6,
14025
+ offset: 0
14026
+ },
14027
+ V: {
14028
+ dimension: "voltage",
14029
+ factor: 1,
14030
+ offset: 0
14031
+ },
14032
+ mV: {
14033
+ dimension: "voltage",
14034
+ factor: .001,
14035
+ offset: 0
14036
+ },
14037
+ A: {
14038
+ dimension: "current",
14039
+ factor: 1,
14040
+ offset: 0
14041
+ },
14042
+ mA: {
14043
+ dimension: "current",
14044
+ factor: .001,
14045
+ offset: 0
14046
+ },
14047
+ ppm: {
14048
+ dimension: "concentration-volume",
14049
+ factor: 1,
14050
+ offset: 0
14051
+ },
14052
+ ppb: {
14053
+ dimension: "concentration-volume",
14054
+ factor: .001,
14055
+ offset: 0
14056
+ },
14057
+ "µg/m³": {
14058
+ dimension: "concentration-mass",
14059
+ factor: 1,
14060
+ offset: 0
14061
+ },
14062
+ "mg/m³": {
14063
+ dimension: "concentration-mass",
14064
+ factor: 1e3,
14065
+ offset: 0
14066
+ },
14067
+ kg: {
14068
+ dimension: "mass",
14069
+ factor: 1,
14070
+ offset: 0
14071
+ },
14072
+ g: {
14073
+ dimension: "mass",
14074
+ factor: .001,
14075
+ offset: 0
14076
+ },
14077
+ lb: {
14078
+ dimension: "mass",
14079
+ factor: .453592,
14080
+ offset: 0
14081
+ },
14082
+ oz: {
14083
+ dimension: "mass",
14084
+ factor: .0283495,
14085
+ offset: 0
14086
+ },
14087
+ "%": {
14088
+ dimension: "percentage",
14089
+ factor: 1,
14090
+ offset: 0
14091
+ },
14092
+ L: {
14093
+ dimension: "volume",
14094
+ factor: 1,
14095
+ offset: 0
14096
+ },
14097
+ mL: {
14098
+ dimension: "volume",
14099
+ factor: .001,
14100
+ offset: 0
14101
+ },
14102
+ gal: {
14103
+ dimension: "volume",
14104
+ factor: 3.785411784,
14105
+ offset: 0
14106
+ },
14107
+ s: {
14108
+ dimension: "duration",
14109
+ factor: 1,
14110
+ offset: 0
14111
+ },
14112
+ ms: {
14113
+ dimension: "duration",
14114
+ factor: .001,
14115
+ offset: 0
14116
+ },
14117
+ min: {
14118
+ dimension: "duration",
14119
+ factor: 60,
14120
+ offset: 0
14121
+ },
14122
+ h: {
14123
+ dimension: "duration",
14124
+ factor: 3600,
14125
+ offset: 0
14126
+ },
14127
+ d: {
14128
+ dimension: "duration",
14129
+ factor: 86400,
14130
+ offset: 0
14131
+ },
14132
+ B: {
14133
+ dimension: "information",
14134
+ factor: 1,
14135
+ offset: 0
14136
+ },
14137
+ KiB: {
14138
+ dimension: "information",
14139
+ factor: 1024,
14140
+ offset: 0
14141
+ },
14142
+ MiB: {
14143
+ dimension: "information",
14144
+ factor: 1024 ** 2,
14145
+ offset: 0
14146
+ },
14147
+ GiB: {
14148
+ dimension: "information",
14149
+ factor: 1024 ** 3,
14150
+ offset: 0
14151
+ },
14152
+ TiB: {
14153
+ dimension: "information",
14154
+ factor: 1024 ** 4,
14155
+ offset: 0
14156
+ },
14157
+ kB: {
14158
+ dimension: "information",
14159
+ factor: 1e3,
14160
+ offset: 0
14161
+ },
14162
+ MB: {
14163
+ dimension: "information",
14164
+ factor: 1e6,
14165
+ offset: 0
14166
+ },
14167
+ GB: {
14168
+ dimension: "information",
14169
+ factor: 1e9,
14170
+ offset: 0
14171
+ },
14172
+ "°": {
14173
+ dimension: "angle",
14174
+ factor: 1,
14175
+ offset: 0
14176
+ },
14177
+ dBFS: {
14178
+ dimension: "sound-level",
14179
+ factor: 1,
14180
+ offset: 0
14181
+ },
14182
+ dBm: {
14183
+ dimension: "signal-level",
14184
+ factor: 1,
14185
+ offset: 0
14186
+ },
14187
+ mired: {
14188
+ dimension: "colour-temperature",
14189
+ factor: 1,
14190
+ offset: 0
14191
+ },
14192
+ kbps: {
14193
+ dimension: "bitrate",
14194
+ factor: 1,
14195
+ offset: 0
14196
+ },
14197
+ Mbps: {
14198
+ dimension: "bitrate",
14199
+ factor: 1e3,
14200
+ offset: 0
14201
+ },
14202
+ fps: {
14203
+ dimension: "rate",
14204
+ factor: 1,
14205
+ offset: 0
14206
+ }
14207
+ };
14208
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14209
+ function specFor(unit) {
14210
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14211
+ }
14212
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14213
+ function toCanonical(value, spec) {
14214
+ return value * spec.factor + spec.offset;
14215
+ }
14216
+ /** Canonical-unit value → the unit described by `spec`. */
14217
+ function fromCanonical(canonical, spec) {
14218
+ return (canonical - spec.offset) / spec.factor;
14219
+ }
14220
+ /**
14221
+ * Graceful variant for the render path: returns the converted value, or
14222
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14223
+ * Never throws — a render caller falls back to the raw value + source unit.
14224
+ */
14225
+ function tryConvertUnit(value, from, to) {
14226
+ if (from === to) return value;
14227
+ const fromSpec = specFor(from);
14228
+ if (!fromSpec) return void 0;
14229
+ const toSpec = specFor(to);
14230
+ if (!toSpec) return void 0;
14231
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14232
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14233
+ }
14234
+ //#endregion
13690
14235
  //#region ../types/dist/index.mjs
13691
14236
  var SMTP_INBOUND_DEFAULT_PORT = 2525;
13692
14237
  var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
@@ -13883,17 +14428,18 @@ var IntegrationInstanceRefSchema = object({
13883
14428
  * message to what the kind supports — callers never special-case a service.
13884
14429
  *
13885
14430
  * DESIGN DECISIONS (locked):
13886
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13887
- * `setTargetEnabled`), each provider persisting via the `settings-store`
13888
- * cap. Rationale: the admin UI needs one uniform surface across the
13889
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13890
- * alternative would fork the UI per addon and cannot host the
13891
- * discovery→adopt flow.
13892
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13893
- * the generated cap-mount auto-`concatCollection`-fans them across every
13894
- * registered provider (notifiers addon + HA addon) so one catalog is
13895
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13896
- * `addonId` the generated collection router extracts from the call input.
14431
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14432
+ * providers serve their targets as `integration-instances` records, one
14433
+ * record kind per target kind, and the admin lists, adds, edits, switches
14434
+ * and deletes them through that cap. This cap keeps what an instance cannot
14435
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14436
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14437
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14438
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14439
+ * cap-mount auto-`concatCollection`-fans them across every registered
14440
+ * provider so one catalog is routable. `send` / `testTarget` /
14441
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14442
+ * collection router extracts from the call input.
13897
14443
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13898
14444
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13899
14445
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14138,7 +14684,7 @@ var TargetKindCapsSchema = object({
14138
14684
  * the union is large and not meant for runtime validation here; the exported
14139
14685
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14140
14686
  */
14141
- var ConfigSchemaPassthrough$4 = unknown();
14687
+ var ConfigSchemaPassthrough$3 = unknown();
14142
14688
  var TargetKindSchema = object({
14143
14689
  kind: string(),
14144
14690
  label: string(),
@@ -14175,7 +14721,7 @@ var TargetKindSchema = object({
14175
14721
  * its raster path, which is the safe default for an unknown image.
14176
14722
  */
14177
14723
  iconMediaType: string().optional(),
14178
- configSchema: ConfigSchemaPassthrough$4,
14724
+ configSchema: ConfigSchemaPassthrough$3,
14179
14725
  /**
14180
14726
  * The config keys that decide WHERE this kind's secrets are sent (server,
14181
14727
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14197,9 +14743,11 @@ var TargetKindSchema = object({
14197
14743
  caps: TargetKindCapsSchema
14198
14744
  });
14199
14745
  /**
14200
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14201
- * (return a presence marker only) when serving `listTargets` — never
14202
- * round-trip a stored secret to the UI.
14746
+ * A target as a provider's own code sees it (the notification-center bridge, the
14747
+ * send path): the flat record body split into the name, the switch and the
14748
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14749
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14750
+ * secret to the UI.
14203
14751
  */
14204
14752
  var TargetSchema = object({
14205
14753
  id: string(),
@@ -14242,13 +14790,13 @@ var ProbeTargetConfigResultSchema = object({
14242
14790
  stateInstance: string().optional(),
14243
14791
  error: string().optional()
14244
14792
  });
14245
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14793
+ method(object({}), array(TargetKindSchema)), method(object({
14246
14794
  kind: string(),
14247
14795
  config: record(string(), unknown()).optional(),
14248
14796
  /**
14249
14797
  * When editing / duplicating, the blank password field never round-trips
14250
14798
  * the stored token. Pass the source target id so the provider can restore
14251
- * secrets server-side before dialling (same merge as upsert).
14799
+ * secrets server-side before dialling.
14252
14800
  */
14253
14801
  targetId: string().optional()
14254
14802
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14271,27 +14819,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14271
14819
  }), TestResultSchema, {
14272
14820
  kind: "mutation",
14273
14821
  auth: "admin"
14274
- }), method(object({
14275
- target: TargetSchema,
14276
- /**
14277
- * Duplicate flow: create a NEW target id while copying secrets from an
14278
- * existing row. The UI never holds the stored token; blank / redacted
14279
- * password fields are restored from this source (D634 still applies if
14280
- * destinations diverge without a re-typed secret).
14281
- */
14282
- cloneSecretsFrom: string().optional()
14283
- }), TargetSchema, {
14284
- kind: "mutation",
14285
- auth: "admin"
14286
- }), method(object({ targetId: string() }), _void(), {
14287
- kind: "mutation",
14288
- auth: "admin"
14289
- }), method(object({
14290
- targetId: string(),
14291
- enabled: boolean()
14292
- }), _void(), {
14293
- kind: "mutation",
14294
- auth: "admin"
14295
14822
  });
14296
14823
  looseObject({
14297
14824
  name: string().min(1),
@@ -22559,7 +23086,6 @@ var SYSTEM_CAP_VOCABULARY = {
22559
23086
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22560
23087
  "notification-rules": system("notification rules — authored on their own page"),
22561
23088
  "oauth-integration": system(IDENTITY),
22562
- "oidc-connections": system(IDENTITY),
22563
23089
  "osd-manager": system(PIPELINE),
22564
23090
  "pipeline-executor": system(PIPELINE),
22565
23091
  "pipeline-orchestrator": system(PIPELINE),
@@ -42580,6 +43106,7 @@ var ExpressionBindingSourceSchema = union([
42580
43106
  object({
42581
43107
  kind: literal("global"),
42582
43108
  sourceStableId: string(),
43109
+ addonId: string().optional(),
42583
43110
  cap: string(),
42584
43111
  fieldPath: string()
42585
43112
  })
@@ -42646,385 +43173,6 @@ var SourceInfoSchema = object({
42646
43173
  raw: record(string(), unknown()).optional()
42647
43174
  });
42648
43175
  /**
42649
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42650
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42651
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42652
- * decimals). K derives from `°C = K − 273.15`.
42653
- */
42654
- var UNIT_TABLE = {
42655
- "°C": {
42656
- dimension: "temperature",
42657
- factor: 1,
42658
- offset: 0
42659
- },
42660
- "°F": {
42661
- dimension: "temperature",
42662
- factor: 5 / 9,
42663
- offset: -160 / 9
42664
- },
42665
- K: {
42666
- dimension: "temperature",
42667
- factor: 1,
42668
- offset: -273.15
42669
- },
42670
- hPa: {
42671
- dimension: "pressure",
42672
- factor: 1,
42673
- offset: 0
42674
- },
42675
- kPa: {
42676
- dimension: "pressure",
42677
- factor: 10,
42678
- offset: 0
42679
- },
42680
- Pa: {
42681
- dimension: "pressure",
42682
- factor: .01,
42683
- offset: 0
42684
- },
42685
- mbar: {
42686
- dimension: "pressure",
42687
- factor: 1,
42688
- offset: 0
42689
- },
42690
- bar: {
42691
- dimension: "pressure",
42692
- factor: 1e3,
42693
- offset: 0
42694
- },
42695
- inHg: {
42696
- dimension: "pressure",
42697
- factor: 33.8639,
42698
- offset: 0
42699
- },
42700
- mmHg: {
42701
- dimension: "pressure",
42702
- factor: 1.33322,
42703
- offset: 0
42704
- },
42705
- psi: {
42706
- dimension: "pressure",
42707
- factor: 68.9476,
42708
- offset: 0
42709
- },
42710
- "m/s": {
42711
- dimension: "speed",
42712
- factor: 1,
42713
- offset: 0
42714
- },
42715
- "km/h": {
42716
- dimension: "speed",
42717
- factor: 1 / 3.6,
42718
- offset: 0
42719
- },
42720
- mph: {
42721
- dimension: "speed",
42722
- factor: .44704,
42723
- offset: 0
42724
- },
42725
- kn: {
42726
- dimension: "speed",
42727
- factor: .514444,
42728
- offset: 0
42729
- },
42730
- "mm/h": {
42731
- dimension: "precipitation-rate",
42732
- factor: 1,
42733
- offset: 0
42734
- },
42735
- "in/h": {
42736
- dimension: "precipitation-rate",
42737
- factor: 25.4,
42738
- offset: 0
42739
- },
42740
- m: {
42741
- dimension: "length",
42742
- factor: 1,
42743
- offset: 0
42744
- },
42745
- mm: {
42746
- dimension: "length",
42747
- factor: .001,
42748
- offset: 0
42749
- },
42750
- cm: {
42751
- dimension: "length",
42752
- factor: .01,
42753
- offset: 0
42754
- },
42755
- km: {
42756
- dimension: "length",
42757
- factor: 1e3,
42758
- offset: 0
42759
- },
42760
- in: {
42761
- dimension: "length",
42762
- factor: .0254,
42763
- offset: 0
42764
- },
42765
- ft: {
42766
- dimension: "length",
42767
- factor: .3048,
42768
- offset: 0
42769
- },
42770
- mi: {
42771
- dimension: "length",
42772
- factor: 1609.344,
42773
- offset: 0
42774
- },
42775
- "m²": {
42776
- dimension: "area",
42777
- factor: 1,
42778
- offset: 0
42779
- },
42780
- lx: {
42781
- dimension: "illuminance",
42782
- factor: 1,
42783
- offset: 0
42784
- },
42785
- "W/m²": {
42786
- dimension: "irradiance",
42787
- factor: 1,
42788
- offset: 0
42789
- },
42790
- W: {
42791
- dimension: "power",
42792
- factor: 1,
42793
- offset: 0
42794
- },
42795
- kW: {
42796
- dimension: "power",
42797
- factor: 1e3,
42798
- offset: 0
42799
- },
42800
- VA: {
42801
- dimension: "apparent-power",
42802
- factor: 1,
42803
- offset: 0
42804
- },
42805
- Wh: {
42806
- dimension: "energy",
42807
- factor: 1,
42808
- offset: 0
42809
- },
42810
- kWh: {
42811
- dimension: "energy",
42812
- factor: 1e3,
42813
- offset: 0
42814
- },
42815
- MWh: {
42816
- dimension: "energy",
42817
- factor: 1e6,
42818
- offset: 0
42819
- },
42820
- V: {
42821
- dimension: "voltage",
42822
- factor: 1,
42823
- offset: 0
42824
- },
42825
- mV: {
42826
- dimension: "voltage",
42827
- factor: .001,
42828
- offset: 0
42829
- },
42830
- A: {
42831
- dimension: "current",
42832
- factor: 1,
42833
- offset: 0
42834
- },
42835
- mA: {
42836
- dimension: "current",
42837
- factor: .001,
42838
- offset: 0
42839
- },
42840
- ppm: {
42841
- dimension: "concentration-volume",
42842
- factor: 1,
42843
- offset: 0
42844
- },
42845
- ppb: {
42846
- dimension: "concentration-volume",
42847
- factor: .001,
42848
- offset: 0
42849
- },
42850
- "µg/m³": {
42851
- dimension: "concentration-mass",
42852
- factor: 1,
42853
- offset: 0
42854
- },
42855
- "mg/m³": {
42856
- dimension: "concentration-mass",
42857
- factor: 1e3,
42858
- offset: 0
42859
- },
42860
- kg: {
42861
- dimension: "mass",
42862
- factor: 1,
42863
- offset: 0
42864
- },
42865
- g: {
42866
- dimension: "mass",
42867
- factor: .001,
42868
- offset: 0
42869
- },
42870
- lb: {
42871
- dimension: "mass",
42872
- factor: .453592,
42873
- offset: 0
42874
- },
42875
- oz: {
42876
- dimension: "mass",
42877
- factor: .0283495,
42878
- offset: 0
42879
- },
42880
- "%": {
42881
- dimension: "percentage",
42882
- factor: 1,
42883
- offset: 0
42884
- },
42885
- L: {
42886
- dimension: "volume",
42887
- factor: 1,
42888
- offset: 0
42889
- },
42890
- mL: {
42891
- dimension: "volume",
42892
- factor: .001,
42893
- offset: 0
42894
- },
42895
- gal: {
42896
- dimension: "volume",
42897
- factor: 3.785411784,
42898
- offset: 0
42899
- },
42900
- s: {
42901
- dimension: "duration",
42902
- factor: 1,
42903
- offset: 0
42904
- },
42905
- ms: {
42906
- dimension: "duration",
42907
- factor: .001,
42908
- offset: 0
42909
- },
42910
- min: {
42911
- dimension: "duration",
42912
- factor: 60,
42913
- offset: 0
42914
- },
42915
- h: {
42916
- dimension: "duration",
42917
- factor: 3600,
42918
- offset: 0
42919
- },
42920
- d: {
42921
- dimension: "duration",
42922
- factor: 86400,
42923
- offset: 0
42924
- },
42925
- B: {
42926
- dimension: "information",
42927
- factor: 1,
42928
- offset: 0
42929
- },
42930
- KiB: {
42931
- dimension: "information",
42932
- factor: 1024,
42933
- offset: 0
42934
- },
42935
- MiB: {
42936
- dimension: "information",
42937
- factor: 1024 ** 2,
42938
- offset: 0
42939
- },
42940
- GiB: {
42941
- dimension: "information",
42942
- factor: 1024 ** 3,
42943
- offset: 0
42944
- },
42945
- TiB: {
42946
- dimension: "information",
42947
- factor: 1024 ** 4,
42948
- offset: 0
42949
- },
42950
- kB: {
42951
- dimension: "information",
42952
- factor: 1e3,
42953
- offset: 0
42954
- },
42955
- MB: {
42956
- dimension: "information",
42957
- factor: 1e6,
42958
- offset: 0
42959
- },
42960
- GB: {
42961
- dimension: "information",
42962
- factor: 1e9,
42963
- offset: 0
42964
- },
42965
- "°": {
42966
- dimension: "angle",
42967
- factor: 1,
42968
- offset: 0
42969
- },
42970
- dBFS: {
42971
- dimension: "sound-level",
42972
- factor: 1,
42973
- offset: 0
42974
- },
42975
- dBm: {
42976
- dimension: "signal-level",
42977
- factor: 1,
42978
- offset: 0
42979
- },
42980
- mired: {
42981
- dimension: "colour-temperature",
42982
- factor: 1,
42983
- offset: 0
42984
- },
42985
- kbps: {
42986
- dimension: "bitrate",
42987
- factor: 1,
42988
- offset: 0
42989
- },
42990
- Mbps: {
42991
- dimension: "bitrate",
42992
- factor: 1e3,
42993
- offset: 0
42994
- },
42995
- fps: {
42996
- dimension: "rate",
42997
- factor: 1,
42998
- offset: 0
42999
- }
43000
- };
43001
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43002
- function specFor(unit) {
43003
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43004
- }
43005
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43006
- function toCanonical(value, spec) {
43007
- return value * spec.factor + spec.offset;
43008
- }
43009
- /** Canonical-unit value → the unit described by `spec`. */
43010
- function fromCanonical(canonical, spec) {
43011
- return (canonical - spec.offset) / spec.factor;
43012
- }
43013
- /**
43014
- * Graceful variant for the render path: returns the converted value, or
43015
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43016
- * Never throws — a render caller falls back to the raw value + source unit.
43017
- */
43018
- function tryConvertUnit(value, from, to) {
43019
- if (from === to) return value;
43020
- const fromSpec = specFor(from);
43021
- if (!fromSpec) return void 0;
43022
- const toSpec = specFor(to);
43023
- if (!toSpec) return void 0;
43024
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43025
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43026
- }
43027
- /**
43028
43176
  * A number a person types for a physical quantity — the input side of the
43029
43177
  * value + unit contract (D972).
43030
43178
  *
@@ -44701,15 +44849,28 @@ var SettingsSchemaWithValuesSchema = object({
44701
44849
  });
44702
44850
  /** Patch object — keys are field names, values are the new field values. */
44703
44851
  var SettingsPatchSchema = record(string(), unknown());
44852
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
44853
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
44854
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
44855
+ var ScopedSettingsDeclarationSchema = object({
44856
+ cap: string(),
44857
+ scope: ScopedSettingsScopeSchema,
44858
+ sectionIds: array(string())
44859
+ });
44704
44860
  /** Standard success response for update operations. */
44705
44861
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44706
44862
  method(object({
44707
44863
  addonId: string(),
44708
- nodeId: string().optional(),
44709
- overlay: record(string(), unknown()).optional(),
44710
- cap: string().optional()
44864
+ cap: string(),
44865
+ scope: ScopedSettingsScopeSchema,
44866
+ nodeId: string().optional()
44711
44867
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44712
44868
  addonId: string(),
44869
+ nodeId: string().optional()
44870
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
44871
+ addonId: string(),
44872
+ cap: string(),
44873
+ scope: ScopedSettingsScopeSchema,
44713
44874
  nodeId: string().optional(),
44714
44875
  patch: SettingsPatchSchema
44715
44876
  }), SettingsUpdateResultSchema, {
@@ -44717,8 +44878,10 @@ method(object({
44717
44878
  auth: "admin"
44718
44879
  }), method(object({
44719
44880
  addonId: string(),
44881
+ cap: string(),
44882
+ scope: ScopedSettingsScopeSchema,
44720
44883
  nodeId: string().optional()
44721
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44884
+ }), record(string(), unknown()).nullable()), method(object({
44722
44885
  addonId: string(),
44723
44886
  nodeId: string().optional(),
44724
44887
  instance: IntegrationInstanceRefSchema
@@ -46058,7 +46221,7 @@ var BackupDestinationInfoSchema = object({
46058
46221
  id: string(),
46059
46222
  /**
46060
46223
  * Manifest id of the owning addon. The Settings button on the
46061
- * destination card opens that addon's `globalSettingsSchema` panel.
46224
+ * destination card opens that addon's scoped settings panel.
46062
46225
  */
46063
46226
  addonId: string(),
46064
46227
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -50528,6 +50691,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
50528
50691
  kind: "mutation",
50529
50692
  auth: "admin"
50530
50693
  });
50694
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
50531
50695
  /**
50532
50696
  * A SITE ZONE — a named part of the property that several locations belong to.
50533
50697
  *
@@ -50921,6 +51085,12 @@ var DeviceInfoSchema = object({
50921
51085
  addonId: string(),
50922
51086
  type: _enum(DeviceType),
50923
51087
  name: string(),
51088
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
51089
+ * entity_id style. Generated once from the name at first registration, unique
51090
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
51091
+ * Optional: a record produced by an older build omits it until the boot
51092
+ * backfill has run. */
51093
+ alias: DeviceAliasSchema.optional(),
50924
51094
  /** Operator-organisational location label. `null` when unset. */
50925
51095
  location: string().nullable(),
50926
51096
  /** Soft-disabled flag. */
@@ -51240,6 +51410,25 @@ method(object({
51240
51410
  }), _void(), {
51241
51411
  kind: "mutation",
51242
51412
  auth: "admin"
51413
+ }), method(object({
51414
+ deviceId: number(),
51415
+ alias: string()
51416
+ }), discriminatedUnion("ok", [object({
51417
+ ok: literal(true),
51418
+ alias: DeviceAliasSchema
51419
+ }), object({
51420
+ ok: literal(false),
51421
+ reason: _enum([
51422
+ "empty",
51423
+ "too-long",
51424
+ "grammar",
51425
+ "reserved",
51426
+ "taken"
51427
+ ]),
51428
+ takenBy: number().optional()
51429
+ })]), {
51430
+ kind: "mutation",
51431
+ auth: "admin"
51243
51432
  }), method(object({
51244
51433
  deviceId: number(),
51245
51434
  location: string().nullable()
@@ -54265,7 +54454,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54265
54454
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54266
54455
  * clears it.
54267
54456
  */
54268
- var ConfigSchemaPassthrough$3 = custom();
54457
+ var ConfigSchemaPassthrough$2 = custom();
54269
54458
  var IntegrationInstanceIdSchema = string().min(1);
54270
54459
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54271
54460
  /**
@@ -54311,7 +54500,16 @@ var IntegrationInstanceKindSchema = object({
54311
54500
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54312
54501
  connectPath: string().min(1).optional(),
54313
54502
  /** Relative hub path to disconnect that account. */
54314
- disconnectPath: string().min(1).optional()
54503
+ disconnectPath: string().min(1).optional(),
54504
+ /**
54505
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54506
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54507
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54508
+ */
54509
+ widgets: array(object({
54510
+ id: string().min(1),
54511
+ placement: _enum(["connection", "model"])
54512
+ })).optional()
54315
54513
  });
54316
54514
  /**
54317
54515
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54416,7 +54614,7 @@ var integrationInstancesCapability = {
54416
54614
  id: IntegrationInstanceIdSchema.nullable(),
54417
54615
  /** The kind of the create form (`id: null`) of a provider with several. */
54418
54616
  kind: string().min(1).optional()
54419
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54617
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54420
54618
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54421
54619
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54422
54620
  /**
@@ -55367,12 +55565,12 @@ var LlmModelInfoSchema = object({
55367
55565
  });
55368
55566
  /**
55369
55567
  * The provider-specific part of a profile, validated by the provider that owns
55370
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55568
+ * the kind (saving an instance refuses what it does not accept). A field its
55371
55569
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55372
55570
  * keychain, never in the row, and reads back as the redaction marker.
55373
55571
  */
55374
55572
  var LlmProviderConfigSchema = record(string(), unknown());
55375
- var LlmProfileSchema = object({
55573
+ object({
55376
55574
  id: string(),
55377
55575
  name: string(),
55378
55576
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55466,8 +55664,8 @@ var LlmProviderWidgetSchema = object({
55466
55664
  /** ConfigUISchema tree passed through untyped on the wire (the
55467
55665
  * notification-output `ConfigSchemaPassthrough` precedent at
55468
55666
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55469
- var ConfigSchemaPassthrough$2 = unknown();
55470
- var LlmProfileKindDescriptorSchema = object({
55667
+ var ConfigSchemaPassthrough$1 = unknown();
55668
+ object({
55471
55669
  kind: LlmProfileKindSchema,
55472
55670
  label: string(),
55473
55671
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55475,9 +55673,9 @@ var LlmProfileKindDescriptorSchema = object({
55475
55673
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55476
55674
  addonId: string(),
55477
55675
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55478
- connectionSchema: ConfigSchemaPassthrough$2,
55676
+ connectionSchema: ConfigSchemaPassthrough$1,
55479
55677
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55480
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55678
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55481
55679
  /** The kind's defaults; a model from `listModels` may override any of them. */
55482
55680
  features: LlmModelFeaturesSchema,
55483
55681
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55606,20 +55804,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55606
55804
  }), method(object({
55607
55805
  addonId: string().optional(),
55608
55806
  requestId: string()
55609
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55610
- kind: "mutation",
55611
- auth: "admin"
55612
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55613
- kind: "mutation",
55614
- auth: "admin"
55615
- }), method(ProfileRefInputSchema, _void(), {
55616
- kind: "mutation",
55617
- auth: "admin"
55618
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55619
- kind: "mutation",
55620
- auth: "admin",
55621
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55622
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55807
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55623
55808
  kind: "mutation",
55624
55809
  auth: "admin"
55625
55810
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56614,20 +56799,7 @@ method(_void(), MeshStatusSchema), method(object({
56614
56799
  /** Optional hostname override the host should advertise once joined. */
56615
56800
  hostname: string().optional() }), object({
56616
56801
  /** Authentication URL the operator should open in a browser. */
56617
- loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() })), method(object({
56618
- /** Optional auth key — when provided, probes the key validity
56619
- * against the provider's API. Omit when already joined to
56620
- * just ping the daemon. */
56621
- authKey: string().optional() }), object({
56622
- ok: boolean(),
56623
- /** Provider-side identifier resolved by the probe (tailnet
56624
- * name for Tailscale, network id for ZeroTier, etc.). */
56625
- tenant: string().optional(),
56626
- /** Daemon binary version, when reachable. */
56627
- daemonVersion: string().optional(),
56628
- /** Human-readable error when `ok: false`. */
56629
- error: string().optional()
56630
- }), { kind: "mutation" });
56802
+ loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() }));
56631
56803
  object({
56632
56804
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56633
56805
  lastFetchedAt: number()
@@ -64006,6 +64178,11 @@ var NcScheduleSchema = object({
64006
64178
  * as a STATE SUMMARY at the end of each window of `schedule`.
64007
64179
  */
64008
64180
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
64181
+ _enum([
64182
+ "activation",
64183
+ "deactivation",
64184
+ "schedule"
64185
+ ]);
64009
64186
  /**
64010
64187
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
64011
64188
  * every section; `section` = one notification per section with something to
@@ -64022,11 +64199,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
64022
64199
  * midnight crossing); the send time is each window's END. Refused on the
64023
64200
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
64024
64201
  */
64025
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
64026
- mode: literal("summary"),
64027
- schedule: NcScheduleSchema,
64028
- split: NcStateSummarySplitSchema.optional()
64029
- })]);
64202
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
64203
+ object({ mode: literal("instant") }),
64204
+ object({
64205
+ mode: literal("summary"),
64206
+ schedule: NcScheduleSchema,
64207
+ split: NcStateSummarySplitSchema.optional()
64208
+ }),
64209
+ object({
64210
+ mode: literal("modes"),
64211
+ /** The bad edge, and every kind that has a single edge. */
64212
+ onActivation: boolean(),
64213
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
64214
+ onDeactivation: boolean(),
64215
+ /** A state summary at the end of each window; absent = no summary. */
64216
+ onSchedule: object({
64217
+ schedule: NcScheduleSchema,
64218
+ grouping: NcStateSummarySplitSchema
64219
+ }).optional()
64220
+ })
64221
+ ]);
64030
64222
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
64031
64223
  var NcPlateMatcherSchema = object({
64032
64224
  /**
@@ -66303,61 +66495,6 @@ var OauthIntegrationDescriptorSchema = object({
66303
66495
  });
66304
66496
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66305
66497
  /**
66306
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66307
- *
66308
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66309
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66310
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66311
- * immutable after create.
66312
- */
66313
- var ConfigSchemaPassthrough$1 = unknown();
66314
- var OidcConnectionSchema = object({
66315
- id: string().min(1),
66316
- displayName: string(),
66317
- icon: string(),
66318
- issuerUrl: string(),
66319
- clientId: string(),
66320
- /** Client id + secret present — never the secret value. */
66321
- configured: boolean(),
66322
- /**
66323
- * Values for the connection form schema. `clientSecret` is a keychain
66324
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66325
- */
66326
- formValues: record(string(), unknown()).optional()
66327
- });
66328
- /**
66329
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66330
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66331
- */
66332
- var OidcConnectionUpdateInputSchema = object({
66333
- id: string().min(1),
66334
- displayName: string().min(1),
66335
- icon: string().optional(),
66336
- issuerUrl: string().optional(),
66337
- clientId: string().optional(),
66338
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66339
- redirectUri: string().optional(),
66340
- scopes: string().optional(),
66341
- usernameClaim: _enum([
66342
- "preferred_username",
66343
- "email",
66344
- "sub"
66345
- ]).optional()
66346
- });
66347
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66348
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66349
- displayName: string().min(1)
66350
- }), object({ id: string() }), {
66351
- kind: "mutation",
66352
- auth: "admin"
66353
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66354
- kind: "mutation",
66355
- auth: "admin"
66356
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66357
- kind: "mutation",
66358
- auth: "admin"
66359
- });
66360
- /**
66361
66498
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66362
66499
  * a firmware-defined list of overlays (channel name, timestamp,
66363
66500
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -72530,21 +72667,6 @@ method(object({
72530
72667
  */
72531
72668
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72532
72669
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72533
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72534
- providerId: string().min(1),
72535
- displayName: string().min(1),
72536
- configSchema: unknown(),
72537
- nodeLocal: boolean(),
72538
- shouldSaveDiskSpace: literal(true),
72539
- minFreePercent: number().min(0).max(100)
72540
- }), object({
72541
- providerId: string().min(1),
72542
- displayName: string().min(1),
72543
- configSchema: unknown(),
72544
- nodeLocal: boolean(),
72545
- shouldSaveDiskSpace: literal(false),
72546
- minFreePercent: literal(null)
72547
- })]);
72548
72670
  method(object({
72549
72671
  location: StorageLocationRefSchema,
72550
72672
  relativePath: string()
@@ -72606,42 +72728,7 @@ method(object({
72606
72728
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72607
72729
  kind: "mutation",
72608
72730
  auth: "admin"
72609
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72610
- createdAt: true,
72611
- updatedAt: true
72612
- }), StorageLocationSchema, {
72613
- kind: "mutation",
72614
- auth: "admin"
72615
- }), method(object({
72616
- id: string(),
72617
- force: boolean().optional()
72618
- }), _void(), {
72619
- kind: "mutation",
72620
- auth: "admin"
72621
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72622
- ok: boolean(),
72623
- error: string().optional()
72624
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72625
- providerId: string(),
72626
- /**
72627
- * The location this config is an UNSAVED edit of, when there is one.
72628
- *
72629
- * `listLocations` replaces every declared secret with the redaction
72630
- * sentinel, so the edit modal's form state holds the sentinel for any
72631
- * credential the operator did not retype — and posting that here
72632
- * without a way to resolve it makes the provider try to authenticate
72633
- * as `__camstack_redacted__` and report the operator's own working
72634
- * password as wrong. Given this id, the orchestrator restores each
72635
- * sentinel from the stored config (same rule as `upsertLocation`)
72636
- * before dispatching. Omitted by the "Add location" wizard, where
72637
- * every value was typed just now and nothing is stored yet.
72638
- */
72639
- locationId: string().optional(),
72640
- config: record(string(), unknown())
72641
- }), object({
72642
- ok: boolean(),
72643
- error: string().optional()
72644
- }), { auth: "admin" });
72731
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72645
72732
  /** One table of the settings database. */
72646
72733
  var StorageTableSchema = object({
72647
72734
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -73872,7 +73959,7 @@ var TerminalInstanceInfoSchema = object({
73872
73959
  /**
73873
73960
  * False while the node the instance is bound to is not in the cluster
73874
73961
  * topology (forgotten, renamed, re-paired). The instance is kept; the
73875
- * operator re-points it with `updateInstance({ targetNodeId })`.
73962
+ * operator re-points it on the Terminal page.
73876
73963
  */
73877
73964
  nodeInCluster: boolean(),
73878
73965
  profileId: string(),
@@ -73910,41 +73997,7 @@ var TerminalOutputBatchSchema = object({
73910
73997
  snapshot: string().optional(),
73911
73998
  events: array(TerminalOutputEventSchema).readonly()
73912
73999
  });
73913
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
73914
- targetNodeId: string().min(1),
73915
- profileId: string().min(1),
73916
- name: string().trim().min(1).max(160).optional(),
73917
- executable: string().max(1024).optional(),
73918
- args: array(string().max(2048)).max(64).optional(),
73919
- cwd: string().max(1024).optional(),
73920
- environment: array(string().max(4096)).max(64).optional(),
73921
- profileSettings: ProfileSettingsBagSchema.optional()
73922
- }), TerminalInstanceInfoSchema, {
73923
- kind: "mutation",
73924
- auth: "admin"
73925
- }), method(object({
73926
- instanceId: string().min(1),
73927
- /** Re-point the instance to another node; the profile must exist there. */
73928
- targetNodeId: string().min(1).max(256).optional(),
73929
- name: string().trim().min(1).max(160).optional(),
73930
- executable: string().max(1024).optional(),
73931
- args: array(string().max(2048)).max(64).optional(),
73932
- cwd: string().max(1024).optional(),
73933
- environment: array(string().max(4096)).max(64).optional(),
73934
- profileSettings: ProfileSettingsBagSchema.optional()
73935
- }), TerminalInstanceInfoSchema, {
73936
- kind: "mutation",
73937
- auth: "admin"
73938
- }), method(object({ instanceId: string().min(1) }), _void(), {
73939
- kind: "mutation",
73940
- auth: "admin"
73941
- }), method(object({
73942
- instanceId: string().min(1),
73943
- enabled: boolean()
73944
- }), TerminalInstanceInfoSchema, {
73945
- kind: "mutation",
73946
- auth: "admin"
73947
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74000
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
73948
74001
  stableId: string().min(1),
73949
74002
  name: string().trim().min(1).max(160).optional()
73950
74003
  }), TerminalInstanceInfoSchema, {
@@ -74013,6 +74066,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
74013
74066
  _enum(["switch", "pet-fountain"]);
74014
74067
  DeviceType.Heater, DeviceType.PetFountain;
74015
74068
  DeviceType.Heater, DeviceType.PetFountain;
74069
+ /** One duration, as the one-tap run asks for it. */
74070
+ var TimedRunQuickRunSchema = object({
74071
+ duration: number().positive().finite(),
74072
+ unit: TimedRunUnitSchema
74073
+ });
74016
74074
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
74017
74075
  var TimedRunRefusalSchema = _enum([
74018
74076
  "unsupported-device",
@@ -74033,9 +74091,30 @@ object({
74033
74091
  activeUntil: number().nullable(),
74034
74092
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
74035
74093
  requestedBy: string().nullable(),
74094
+ /**
74095
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
74096
+ * the device is gone or its type has no timed run; a status from before this field
74097
+ * existed has none, and a surface falls back to the type's default.
74098
+ */
74099
+ quickRun: TimedRunQuickRunSchema.nullable(),
74036
74100
  lastFetchedAt: number()
74037
74101
  });
74038
- method(object({
74102
+ /** What the settings form needs: the unit, the bounds and the type's default. */
74103
+ var TimedRunOptionsSchema = object({
74104
+ unit: TimedRunUnitSchema,
74105
+ /** The longest run, in `unit`. */
74106
+ maxDuration: number(),
74107
+ defaultDuration: number()
74108
+ });
74109
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
74110
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
74111
+ method(object({ deviceId: number().int() }), TimedRunOptionsSchema), method(object({
74112
+ deviceId: number().int(),
74113
+ settings: TimedRunSettingsPatchSchema
74114
+ }), _void(), {
74115
+ kind: "mutation",
74116
+ auth: "admin"
74117
+ }), method(object({
74039
74118
  deviceId: number().int(),
74040
74119
  duration: number().positive().finite(),
74041
74120
  unit: TimedRunUnitSchema
@@ -76741,21 +76820,28 @@ Object.freeze({
76741
76820
  access: "view",
76742
76821
  role: "admin"
76743
76822
  },
76744
- "addonSettings.getGlobalSettings": {
76823
+ "addonSettings.getIntegrationFormSchema": {
76824
+ capName: "addon-settings",
76825
+ capScope: "system",
76826
+ addonId: null,
76827
+ access: "view",
76828
+ role: "admin"
76829
+ },
76830
+ "addonSettings.getScopedSettings": {
76745
76831
  capName: "addon-settings",
76746
76832
  capScope: "system",
76747
76833
  addonId: null,
76748
76834
  access: "view",
76749
76835
  role: "admin"
76750
76836
  },
76751
- "addonSettings.getIntegrationFormSchema": {
76837
+ "addonSettings.listScopedSettings": {
76752
76838
  capName: "addon-settings",
76753
76839
  capScope: "system",
76754
76840
  addonId: null,
76755
76841
  access: "view",
76756
76842
  role: "admin"
76757
76843
  },
76758
- "addonSettings.getIntegrationSettings": {
76844
+ "addonSettings.readScopedValues": {
76759
76845
  capName: "addon-settings",
76760
76846
  capScope: "system",
76761
76847
  addonId: null,
@@ -76776,7 +76862,7 @@ Object.freeze({
76776
76862
  access: "create",
76777
76863
  role: "admin"
76778
76864
  },
76779
- "addonSettings.updateGlobalSettings": {
76865
+ "addonSettings.updateScopedSettings": {
76780
76866
  capName: "addon-settings",
76781
76867
  capScope: "system",
76782
76868
  addonId: null,
@@ -78946,6 +79032,13 @@ Object.freeze({
78946
79032
  access: "create",
78947
79033
  role: "admin"
78948
79034
  },
79035
+ "deviceManager.setAlias": {
79036
+ capName: "device-manager",
79037
+ capScope: "system",
79038
+ addonId: null,
79039
+ access: "create",
79040
+ role: "admin"
79041
+ },
78949
79042
  "deviceManager.setChildLayout": {
78950
79043
  capName: "device-manager",
78951
79044
  capScope: "system",
@@ -80234,13 +80327,6 @@ Object.freeze({
80234
80327
  access: "delete",
80235
80328
  role: "admin"
80236
80329
  },
80237
- "llm.deleteProfile": {
80238
- capName: "llm",
80239
- capScope: "system",
80240
- addonId: null,
80241
- access: "delete",
80242
- role: "admin"
80243
- },
80244
80330
  "llm.generate": {
80245
80331
  capName: "llm",
80246
80332
  capScope: "system",
@@ -80304,20 +80390,6 @@ Object.freeze({
80304
80390
  access: "view",
80305
80391
  role: "admin"
80306
80392
  },
80307
- "llm.listProfileKinds": {
80308
- capName: "llm",
80309
- capScope: "system",
80310
- addonId: null,
80311
- access: "view",
80312
- role: "admin"
80313
- },
80314
- "llm.listProfiles": {
80315
- capName: "llm",
80316
- capScope: "system",
80317
- addonId: null,
80318
- access: "view",
80319
- role: "admin"
80320
- },
80321
80393
  "llm.listRuntimeNodes": {
80322
80394
  capName: "llm",
80323
80395
  capScope: "system",
@@ -80325,13 +80397,6 @@ Object.freeze({
80325
80397
  access: "view",
80326
80398
  role: "admin"
80327
80399
  },
80328
- "llm.migrateRawSecrets": {
80329
- capName: "llm",
80330
- capScope: "system",
80331
- addonId: null,
80332
- access: "create",
80333
- role: "admin"
80334
- },
80335
80400
  "llm.resolveModelRef": {
80336
80401
  capName: "llm",
80337
80402
  capScope: "system",
@@ -80360,20 +80425,6 @@ Object.freeze({
80360
80425
  access: "create",
80361
80426
  role: "admin"
80362
80427
  },
80363
- "llm.testProfile": {
80364
- capName: "llm",
80365
- capScope: "system",
80366
- addonId: null,
80367
- access: "create",
80368
- role: "admin"
80369
- },
80370
- "llm.upsertProfile": {
80371
- capName: "llm",
80372
- capScope: "system",
80373
- addonId: null,
80374
- access: "create",
80375
- role: "admin"
80376
- },
80377
80428
  "llmRuntime.appleAvailability": {
80378
80429
  capName: "llm-runtime",
80379
80430
  capScope: "system",
@@ -80878,13 +80929,6 @@ Object.freeze({
80878
80929
  access: "create",
80879
80930
  role: "admin"
80880
80931
  },
80881
- "meshNetwork.testConnection": {
80882
- capName: "mesh-network",
80883
- capScope: "system",
80884
- addonId: null,
80885
- access: "create",
80886
- role: "admin"
80887
- },
80888
80932
  "meter.getStatus": {
80889
80933
  capName: "meter",
80890
80934
  capScope: "device",
@@ -81543,13 +81587,6 @@ Object.freeze({
81543
81587
  access: "create",
81544
81588
  role: "admin"
81545
81589
  },
81546
- "notificationOutput.deleteTarget": {
81547
- capName: "notification-output",
81548
- capScope: "system",
81549
- addonId: null,
81550
- access: "delete",
81551
- role: "admin"
81552
- },
81553
81590
  "notificationOutput.discoverTargets": {
81554
81591
  capName: "notification-output",
81555
81592
  capScope: "system",
@@ -81557,13 +81594,6 @@ Object.freeze({
81557
81594
  access: "view",
81558
81595
  role: "admin"
81559
81596
  },
81560
- "notificationOutput.getTarget": {
81561
- capName: "notification-output",
81562
- capScope: "system",
81563
- addonId: null,
81564
- access: "view",
81565
- role: "admin"
81566
- },
81567
81597
  "notificationOutput.listTargetKinds": {
81568
81598
  capName: "notification-output",
81569
81599
  capScope: "system",
@@ -81571,13 +81601,6 @@ Object.freeze({
81571
81601
  access: "view",
81572
81602
  role: "admin"
81573
81603
  },
81574
- "notificationOutput.listTargets": {
81575
- capName: "notification-output",
81576
- capScope: "system",
81577
- addonId: null,
81578
- access: "view",
81579
- role: "admin"
81580
- },
81581
81604
  "notificationOutput.probeTargetConfig": {
81582
81605
  capName: "notification-output",
81583
81606
  capScope: "system",
@@ -81592,13 +81615,6 @@ Object.freeze({
81592
81615
  access: "create",
81593
81616
  role: "admin"
81594
81617
  },
81595
- "notificationOutput.setTargetEnabled": {
81596
- capName: "notification-output",
81597
- capScope: "system",
81598
- addonId: null,
81599
- access: "create",
81600
- role: "admin"
81601
- },
81602
81618
  "notificationOutput.testTarget": {
81603
81619
  capName: "notification-output",
81604
81620
  capScope: "system",
@@ -81606,13 +81622,6 @@ Object.freeze({
81606
81622
  access: "create",
81607
81623
  role: "admin"
81608
81624
  },
81609
- "notificationOutput.upsertTarget": {
81610
- capName: "notification-output",
81611
- capScope: "system",
81612
- addonId: null,
81613
- access: "create",
81614
- role: "admin"
81615
- },
81616
81625
  "notificationRules.activateAlarmProfile": {
81617
81626
  capName: "notification-rules",
81618
81627
  capScope: "system",
@@ -81872,41 +81881,6 @@ Object.freeze({
81872
81881
  access: "view",
81873
81882
  role: "admin"
81874
81883
  },
81875
- "oidcConnections.connectionFormSchema": {
81876
- capName: "oidc-connections",
81877
- capScope: "system",
81878
- addonId: null,
81879
- access: "view",
81880
- role: "admin"
81881
- },
81882
- "oidcConnections.createConnection": {
81883
- capName: "oidc-connections",
81884
- capScope: "system",
81885
- addonId: null,
81886
- access: "create",
81887
- role: "admin"
81888
- },
81889
- "oidcConnections.deleteConnection": {
81890
- capName: "oidc-connections",
81891
- capScope: "system",
81892
- addonId: null,
81893
- access: "delete",
81894
- role: "admin"
81895
- },
81896
- "oidcConnections.listConnections": {
81897
- capName: "oidc-connections",
81898
- capScope: "system",
81899
- addonId: null,
81900
- access: "view",
81901
- role: "admin"
81902
- },
81903
- "oidcConnections.updateConnection": {
81904
- capName: "oidc-connections",
81905
- capScope: "system",
81906
- addonId: null,
81907
- access: "create",
81908
- role: "admin"
81909
- },
81910
81884
  "osd.getStatus": {
81911
81885
  capName: "osd",
81912
81886
  capScope: "device",
@@ -84938,13 +84912,6 @@ Object.freeze({
84938
84912
  access: "delete",
84939
84913
  role: "admin"
84940
84914
  },
84941
- "storage.deleteLocation": {
84942
- capName: "storage",
84943
- capScope: "system",
84944
- addonId: null,
84945
- access: "delete",
84946
- role: "admin"
84947
- },
84948
84915
  "storage.endDownload": {
84949
84916
  capName: "storage",
84950
84917
  capScope: "system",
@@ -85001,13 +84968,6 @@ Object.freeze({
85001
84968
  access: "view",
85002
84969
  role: "admin"
85003
84970
  },
85004
- "storage.listProviders": {
85005
- capName: "storage",
85006
- capScope: "system",
85007
- addonId: null,
85008
- access: "view",
85009
- role: "admin"
85010
- },
85011
84971
  "storage.read": {
85012
84972
  capName: "storage",
85013
84973
  capScope: "system",
@@ -85029,27 +84989,6 @@ Object.freeze({
85029
84989
  access: "view",
85030
84990
  role: "admin"
85031
84991
  },
85032
- "storage.testConfig": {
85033
- capName: "storage",
85034
- capScope: "system",
85035
- addonId: null,
85036
- access: "view",
85037
- role: "admin"
85038
- },
85039
- "storage.testLocation": {
85040
- capName: "storage",
85041
- capScope: "system",
85042
- addonId: null,
85043
- access: "view",
85044
- role: "admin"
85045
- },
85046
- "storage.upsertLocation": {
85047
- capName: "storage",
85048
- capScope: "system",
85049
- addonId: null,
85050
- access: "create",
85051
- role: "admin"
85052
- },
85053
84992
  "storage.write": {
85054
84993
  capName: "storage",
85055
84994
  capScope: "system",
@@ -85827,27 +85766,6 @@ Object.freeze({
85827
85766
  access: "create",
85828
85767
  role: "admin"
85829
85768
  },
85830
- "terminalSession.createInstance": {
85831
- capName: "terminal-session",
85832
- capScope: "system",
85833
- addonId: null,
85834
- access: "create",
85835
- role: "admin"
85836
- },
85837
- "terminalSession.deleteInstance": {
85838
- capName: "terminal-session",
85839
- capScope: "system",
85840
- addonId: null,
85841
- access: "delete",
85842
- role: "admin"
85843
- },
85844
- "terminalSession.listInstances": {
85845
- capName: "terminal-session",
85846
- capScope: "system",
85847
- addonId: null,
85848
- access: "view",
85849
- role: "admin"
85850
- },
85851
85769
  "terminalSession.listLegacyCameras": {
85852
85770
  capName: "terminal-session",
85853
85771
  capScope: "system",
@@ -85890,20 +85808,6 @@ Object.freeze({
85890
85808
  access: "create",
85891
85809
  role: "admin"
85892
85810
  },
85893
- "terminalSession.setInstanceEnabled": {
85894
- capName: "terminal-session",
85895
- capScope: "system",
85896
- addonId: null,
85897
- access: "create",
85898
- role: "admin"
85899
- },
85900
- "terminalSession.updateInstance": {
85901
- capName: "terminal-session",
85902
- capScope: "system",
85903
- addonId: null,
85904
- access: "create",
85905
- role: "admin"
85906
- },
85907
85811
  "terminalSession.writeInput": {
85908
85812
  capName: "terminal-session",
85909
85813
  capScope: "system",
@@ -85918,6 +85822,13 @@ Object.freeze({
85918
85822
  access: "create",
85919
85823
  role: "viewer"
85920
85824
  },
85825
+ "timedRun.getOptions": {
85826
+ capName: "timed-run",
85827
+ capScope: "device",
85828
+ addonId: null,
85829
+ access: "view",
85830
+ role: "viewer"
85831
+ },
85921
85832
  "timedRun.getStatus": {
85922
85833
  capName: "timed-run",
85923
85834
  capScope: "device",
@@ -85932,6 +85843,13 @@ Object.freeze({
85932
85843
  access: "create",
85933
85844
  role: "viewer"
85934
85845
  },
85846
+ "timedRun.setSettings": {
85847
+ capName: "timed-run",
85848
+ capScope: "device",
85849
+ addonId: null,
85850
+ access: "create",
85851
+ role: "admin"
85852
+ },
85935
85853
  "toast.onToast": {
85936
85854
  capName: "toast",
85937
85855
  capScope: "system",
@@ -86862,6 +86780,7 @@ function offered(received) {
86862
86780
  };
86863
86781
  }
86864
86782
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
86783
+ string().min(1).max(4096);
86865
86784
  Object.freeze({
86866
86785
  "accessories.getStatus": [{
86867
86786
  name: "deviceId",
@@ -87545,6 +87464,11 @@ Object.freeze({
87545
87464
  form: "single",
87546
87465
  optional: false
87547
87466
  }],
87467
+ "deviceManager.setAlias": [{
87468
+ name: "deviceId",
87469
+ form: "single",
87470
+ optional: false
87471
+ }],
87548
87472
  "deviceManager.setChildLayout": [{
87549
87473
  name: "deviceId",
87550
87474
  form: "single",
@@ -89609,6 +89533,11 @@ Object.freeze({
89609
89533
  form: "single",
89610
89534
  optional: false
89611
89535
  }],
89536
+ "timedRun.getOptions": [{
89537
+ name: "deviceId",
89538
+ form: "single",
89539
+ optional: false
89540
+ }],
89612
89541
  "timedRun.getStatus": [{
89613
89542
  name: "deviceId",
89614
89543
  form: "single",
@@ -89619,6 +89548,11 @@ Object.freeze({
89619
89548
  form: "single",
89620
89549
  optional: false
89621
89550
  }],
89551
+ "timedRun.setSettings": [{
89552
+ name: "deviceId",
89553
+ form: "single",
89554
+ optional: false
89555
+ }],
89622
89556
  "update.getStatus": [{
89623
89557
  name: "deviceId",
89624
89558
  form: "single",
@@ -90481,6 +90415,7 @@ var STATE_SUMMARY_VARS = [
90481
90415
  [...RULE_TEXT],
90482
90416
  [...RULE_TEXT],
90483
90417
  [...RULE_TEXT],
90418
+ [...RULE_TEXT],
90484
90419
  [...RULE_BODY_ONLY],
90485
90420
  [...RULE_TEXT],
90486
90421
  ([...RULE_TEXT], [...DEVICE_EVENTS]),