@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.
@@ -46,7 +46,7 @@ let node_fs = require("node:fs");
46
46
  node_fs = __toESM(node_fs);
47
47
  let node_path = require("node:path");
48
48
  node_path = __toESM(node_path);
49
- //#region ../types/dist/event-category-BmPm98n0.mjs
49
+ //#region ../types/dist/event-category-78tcBemo.mjs
50
50
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
51
51
  EventCategory["SystemBoot"] = "system.boot";
52
52
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -167,6 +167,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
167
167
  */
168
168
  EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
169
169
  /**
170
+ * An addon is RUNNING although its installed package is below what the
171
+ * running server requires (`camstack.requires.addons` of the server, or the
172
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
173
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
174
+ * operator alert per node and package. The addon is never held back for it.
175
+ *
176
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
177
+ */
178
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
179
+ /**
180
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
181
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
182
+ *
183
+ * Payload: `{ packageName, addonId, nodeId }`.
184
+ */
185
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
186
+ /**
170
187
  * Monitor scheduled the next retry for a failed addon. Transient —
171
188
  * surfaced to the UI for live-updating the "next retry in Ns"
172
189
  * countdown on the Addons page row, NOT persisted as an alert.
@@ -277,7 +294,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
277
294
  EventCategory["ProcessRestarted"] = "process.restarted";
278
295
  /**
279
296
  * The SET of storage locations changed — one was created, edited, enabled,
280
- * disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
297
+ * disabled or deleted through the location instances or the forced-delete action.
281
298
  *
282
299
  * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
283
300
  * it must also converge on its own periodic path, because a dropped event
@@ -832,7 +849,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
832
849
  return EventCategory;
833
850
  }({});
834
851
  //#endregion
835
- //#region ../types/dist/device-type-D1fH5kaC.mjs
852
+ //#region ../types/dist/device-type-DxCT0krO.mjs
836
853
  /**
837
854
  import { errMsg } from '@camstack/types'
838
855
  * Extract a human-readable message from an unknown error value.
@@ -994,6 +1011,13 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
994
1011
  * filters that want "all devices with on-motion behaviour".
995
1012
  */
996
1013
  DeviceFeature["MotionTrigger"] = "motion-trigger";
1014
+ /**
1015
+ * Device can be run for a chosen time and then stopped. Mirrors the
1016
+ * `timed-run` wrapper cap — the cap is bound by the core-blocks addon, not by
1017
+ * a driver, so the flag is derived from the device's bindings
1018
+ * (`featuresOfBoundCaps`). Gates the quick-run control in the viewer.
1019
+ */
1020
+ DeviceFeature["TimedRun"] = "timed-run";
997
1021
  /** Light supports rgb-triplet color via `color` cap. */
998
1022
  DeviceFeature["LightColorRgb"] = "light-color-rgb";
999
1023
  /** Light supports HSV color via `color` cap. */
@@ -6028,7 +6052,7 @@ var ZodIssueCode = {
6028
6052
  var ZodFirstPartyTypeKind;
6029
6053
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6030
6054
  //#endregion
6031
- //#region ../types/dist/sleep-CdKka2ri.mjs
6055
+ //#region ../types/dist/sleep-7El4r5Bl.mjs
6032
6056
  /**
6033
6057
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6034
6058
  * window to float samples (D455).
@@ -6392,6 +6416,59 @@ Object.fromEntries([
6392
6416
  function isValuelessField(field) {
6393
6417
  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";
6394
6418
  }
6419
+ /**
6420
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6421
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6422
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6423
+ * pre-resolving) lets callers both read the effective value AND detect
6424
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6425
+ * malformed input yields an empty map, never throws.
6426
+ */
6427
+ function collectHydratedFieldEntries(schema) {
6428
+ const out = /* @__PURE__ */ new Map();
6429
+ if (typeof schema !== "object" || schema === null) return out;
6430
+ const sections = schema.sections;
6431
+ if (!Array.isArray(sections)) return out;
6432
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6433
+ return out;
6434
+ }
6435
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6436
+ function collectFieldEntriesInto(fields, into) {
6437
+ if (!Array.isArray(fields)) return;
6438
+ for (const field of fields) {
6439
+ if (typeof field !== "object" || field === null) continue;
6440
+ const type = field.type;
6441
+ if (type === "group") {
6442
+ collectFieldEntriesInto(field.fields, into);
6443
+ continue;
6444
+ }
6445
+ if (type === "sub-tabs") {
6446
+ const tabs = field.tabs;
6447
+ if (Array.isArray(tabs)) {
6448
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6449
+ }
6450
+ continue;
6451
+ }
6452
+ const key = field.key;
6453
+ if (typeof key !== "string" || key.length === 0) continue;
6454
+ if (!("value" in field)) continue;
6455
+ into.set(key, {
6456
+ value: field.value,
6457
+ default: field.default
6458
+ });
6459
+ }
6460
+ }
6461
+ /**
6462
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6463
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6464
+ * common "just give me the resolved values" case. Built on
6465
+ * {@link collectHydratedFieldEntries}.
6466
+ */
6467
+ function collectHydratedFieldValues(schema) {
6468
+ const out = /* @__PURE__ */ new Map();
6469
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6470
+ return out;
6471
+ }
6395
6472
  function hydrateSchema(schema, values, options = {}) {
6396
6473
  return {
6397
6474
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -6737,10 +6814,33 @@ function integrationRowsApiOver(api) {
6737
6814
  };
6738
6815
  }
6739
6816
  var PREFIX = "records/";
6817
+ /**
6818
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6819
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6820
+ * instance of the host's classes, not of the addon's. The brand plus the code
6821
+ * make `instanceof` answer by identity of the failure, not of the copy.
6822
+ */
6823
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6824
+ function carriesBrand(value) {
6825
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6826
+ }
6740
6827
  /** The base of every records failure. `detail` is the human part of the message. */
6741
6828
  var RecordsError = class extends Error {
6829
+ /** The code a subclass stands for; the base stands for every code. */
6830
+ static code = void 0;
6831
+ /**
6832
+ * `instanceof` across copies of this module: any branded error is a
6833
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6834
+ * subclass's code. Same-copy answers are unchanged.
6835
+ */
6836
+ static [Symbol.hasInstance](value) {
6837
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6838
+ if (!carriesBrand(value)) return false;
6839
+ return this.code === void 0 || value.code === this.code;
6840
+ }
6742
6841
  code;
6743
6842
  detail;
6843
+ [RECORDS_ERROR_BRAND] = true;
6744
6844
  constructor(code, detail) {
6745
6845
  super(`${PREFIX}${code}: ${detail}`);
6746
6846
  this.name = "RecordsError";
@@ -6749,22 +6849,25 @@ var RecordsError = class extends Error {
6749
6849
  }
6750
6850
  };
6751
6851
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6752
- var RecordSecretUnavailableError = class extends RecordsError {
6852
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6853
+ static code = "secret-unavailable";
6753
6854
  constructor(detail) {
6754
- super("secret-unavailable", detail);
6855
+ super(RecordSecretUnavailableError.code, detail);
6755
6856
  this.name = "RecordSecretUnavailableError";
6756
6857
  }
6757
6858
  };
6758
- var InvalidRecordError = class extends RecordsError {
6859
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6860
+ static code = "invalid-record";
6759
6861
  constructor(detail) {
6760
- super("invalid-record", detail);
6862
+ super(InvalidRecordError.code, detail);
6761
6863
  this.name = "InvalidRecordError";
6762
6864
  }
6763
6865
  };
6764
6866
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6765
- var DeclarationRefusedError = class extends RecordsError {
6867
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6868
+ static code = "declaration-refused";
6766
6869
  constructor(detail) {
6767
- super("declaration-refused", detail);
6870
+ super(DeclarationRefusedError.code, detail);
6768
6871
  this.name = "DeclarationRefusedError";
6769
6872
  }
6770
6873
  };
@@ -7242,17 +7345,35 @@ var RecordIntegrationInstances = class {
7242
7345
  ...kind !== void 0 ? { kind } : {}
7243
7346
  });
7244
7347
  if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
7348
+ const initial = this.hooks.initialBody(name, kind);
7349
+ const patch = this.patchOf(values ?? {});
7350
+ if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
7351
+ kind,
7352
+ body: {
7353
+ ...initial,
7354
+ ...patch
7355
+ }
7356
+ }));
7245
7357
  await this.handle.put({
7246
7358
  id: minted,
7247
- data: this.hooks.initialBody(name, kind)
7359
+ data: initial
7248
7360
  }, { ifRev: 0 });
7249
- const patch = this.patchOf(values ?? {});
7250
7361
  if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
7251
7362
  await this.hooks.onChanged?.(minted, "created");
7252
7363
  return { id: minted };
7253
7364
  };
7254
7365
  updateInstance = async ({ id, name, values }) => {
7255
7366
  const patch = this.patchOf(values);
7367
+ if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
7368
+ const stored = await this.handle.get(id);
7369
+ if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
7370
+ kind: void 0,
7371
+ body: {
7372
+ ...stored.data,
7373
+ ...patch
7374
+ }
7375
+ }));
7376
+ }
7256
7377
  if (name !== void 0) patch[this.nameField] = name;
7257
7378
  await this.handle.patch(id, patch);
7258
7379
  await this.hooks.onChanged?.(id, "updated");
@@ -7563,6 +7684,21 @@ var SCOPED_SETTINGS_SCOPES = [
7563
7684
  "device-defaults",
7564
7685
  "node"
7565
7686
  ];
7687
+ /** The one refusal of a key a scope did not declare. */
7688
+ function assertDeclaredKeys(keys, declared) {
7689
+ const undeclared = keys.filter((key) => !declared.has(key));
7690
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7691
+ }
7692
+ /** Build the handle over an addon's scoped read, write and unset. */
7693
+ function createScopedSettings(deps) {
7694
+ return {
7695
+ async get() {
7696
+ return deps.schema.parse(await deps.read());
7697
+ },
7698
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7699
+ unset: (keys) => deps.unset(keys)
7700
+ };
7701
+ }
7566
7702
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7567
7703
  var KeychainWriteRefusedError = class extends Error {
7568
7704
  fieldPath;
@@ -8138,7 +8274,16 @@ function createHandle(raw, opts) {
8138
8274
  return blob;
8139
8275
  }
8140
8276
  },
8141
- writeAddonStore: writeAddon
8277
+ writeAddonStore: writeAddon,
8278
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8279
+ const stored = await readStoredAddonBlob(raw);
8280
+ index.seen("addon", stored);
8281
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8282
+ if (removable.length === 0) return;
8283
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8284
+ await raw.writeAddonStore(patch);
8285
+ index.wrote("addon", patch);
8286
+ })
8142
8287
  };
8143
8288
  }
8144
8289
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8268,7 +8413,7 @@ function boundOf(list) {
8268
8413
  * Base class for CamStack addons. Eliminates settings boilerplate:
8269
8414
  *
8270
8415
  * - Typed `config` property with automatic resolution from store + defaults
8271
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
8416
+ * - the scoped settings API (`clusterSettings` / `nodeSettings` / `deviceDefaults`) auto-implemented
8272
8417
  * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
8273
8418
  * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
8274
8419
  * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
@@ -8278,7 +8423,7 @@ function boundOf(list) {
8278
8423
  * Subclasses override:
8279
8424
  * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
8280
8425
  * - `onShutdown()` — cleanup
8281
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8426
+ * - `clusterSettingsSchema()` / `nodeSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
8282
8427
  *
8283
8428
  * @example
8284
8429
  * ```ts
@@ -8292,7 +8437,7 @@ function boundOf(list) {
8292
8437
  * super({ maxRetries: 3, endpoint: 'http://localhost' })
8293
8438
  * }
8294
8439
  *
8295
- * protected globalSettingsSchema() {
8440
+ * protected clusterSettingsSchema() {
8296
8441
  * return this.schema({
8297
8442
  * sections: [{
8298
8443
  * id: 'main', title: 'Settings',
@@ -8335,7 +8480,7 @@ var BaseAddon = class {
8335
8480
  defaults;
8336
8481
  /**
8337
8482
  * Hidden-state paths that hold secrets (D787) — values the addon writes
8338
- * itself (`updateGlobalSettings`, `state()`) that no form field declares.
8483
+ * itself (`state()`, the scoped writers) that no form field declares.
8339
8484
  * Stored in the keychain exactly like a `password` field. Syntax:
8340
8485
  * `'snapshotSigningKey'`, `'identity.pincode'`, `'tokens[userId].accessToken'`
8341
8486
  * (`key[<id field>]` addresses array items by id, never by index).
@@ -8345,7 +8490,7 @@ var BaseAddon = class {
8345
8490
  /**
8346
8491
  * D986 M4: the scoped schemas this addon declared, resolved once per capability
8347
8492
  * (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
8348
- * see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
8493
+ * see them. Empty until loaded.
8349
8494
  */
8350
8495
  _scopedByCap = /* @__PURE__ */ new Map();
8351
8496
  _nodeKeysByCap = /* @__PURE__ */ new Map();
@@ -8531,7 +8676,7 @@ var BaseAddon = class {
8531
8676
  /** Addon-specific cleanup. Override if needed. */
8532
8677
  async onShutdown() {}
8533
8678
  /**
8534
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
8679
+ * Called after config is resolved during a settings write.
8535
8680
  * Override to react to config changes (e.g. restart a sampler, reconnect a service).
8536
8681
  * Not called during initialize() — use onInitialize() for initial setup.
8537
8682
  */
@@ -8560,82 +8705,12 @@ var BaseAddon = class {
8560
8705
  scopedSettingsCapabilities() {
8561
8706
  return [];
8562
8707
  }
8563
- /** Override to provide global-level settings UI schema. */
8564
- globalSettingsSchema(_cap) {
8565
- return null;
8566
- }
8567
8708
  /** Override to provide device-level settings UI schema. */
8568
8709
  deviceSettingsSchema() {
8569
8710
  return null;
8570
8711
  }
8571
8712
  /**
8572
- * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
8573
- * ARE the configuration of its integration.
8574
- *
8575
- * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
8576
- * operator should find on the addon's integration page (System →
8577
- * Integrations → <name>) rather than only in the cluster-wide list of every
8578
- * addon. Empty (the default) means the addon has no integration-level
8579
- * settings and no such surface is offered — this is opt-in, because whether
8580
- * an addon's configuration IS its integration's configuration depends on the
8581
- * nature of the integration.
8582
- *
8583
- * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
8584
- * the ONE global schema, in the ONE addon store, written by the ONE
8585
- * `updateGlobalSettings` path. There is deliberately no
8586
- * `updateIntegrationSettings`: a second write path is how a surface acquires
8587
- * a second store key, and this repo has shipped that twice (`btmPath@hub`,
8588
- * D266). Selecting sections cannot introduce a key that selecting cannot.
8589
- *
8590
- * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
8591
- * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
8592
- * removed with the reason recorded at
8593
- * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
8594
- * determined by WHICH schema it lives in, not by a field-level marker."* A
8595
- * marker sprinkled across sections also has to borrow a field that already
8596
- * means something else; borrowing `section.tab` put the literal word
8597
- * "integration" into an operator-facing tab bar, because `tab` means "how to
8598
- * GROUP this visually" and cannot also mean "where this lives" (D269
8599
- * supersedes D268). One declaration, in one place, next to the schema whose
8600
- * ids it names.
8601
- */
8602
- integrationSettingSections() {
8603
- return [];
8604
- }
8605
- async getGlobalSettings(overlay, cap, nodeId) {
8606
- await this.loadScopedSchemas(cap);
8607
- const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
8608
- if (!schema) return { sections: [] };
8609
- const projected = await this.readRawProjectedStore(nodeId, cap);
8610
- return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
8611
- }
8612
- /**
8613
- * The integration-level view of this addon's settings: exactly the sections
8614
- * named by {@link integrationSettingSections}, hydrated from the SAME store
8615
- * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
8616
- *
8617
- * Returns `null` when the addon declared nothing — an addon that opts out has
8618
- * no integration settings surface at all, rather than an empty one that reads
8619
- * as a failed load.
8620
- *
8621
- * Three properties hold BY CONSTRUCTION, which is why they are here in core
8622
- * and not in whichever UI happens to render this:
8623
- *
8624
- * 1. **One key.** The payload is a SUBSET of the global schema, so a field
8625
- * shown here is the same field, with the same bare key, that the addon's
8626
- * own page shows. There is no integration-specific writer — callers save
8627
- * through `updateGlobalSettings` — so a second store key is unreachable,
8628
- * not merely discouraged.
8629
- * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
8630
- * is `<key>@<nodeId>` and an integration is not a node; whichever node
8631
- * such a field silently picked would be a wrong answer for the operator
8632
- * who opened the page (D266).
8633
- * 3. **No silent typo.** A declared id that names no section throws. The
8634
- * alternative — skip it — turns a rename into a surface that quietly
8635
- * empties, which looks exactly like an addon with nothing to configure.
8636
- */
8637
- /**
8638
- * The form schema of ONE integration instance, or `null` to use {@link globalSettingsSchema}.
8713
+ * The form schema of ONE integration instance, or `null` to use the scoped form.
8639
8714
  *
8640
8715
  * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8641
8716
  * needs its connection fields, the default integration the addon's own sections. It is the
@@ -8683,37 +8758,178 @@ var BaseAddon = class {
8683
8758
  }
8684
8759
  }
8685
8760
  /**
8761
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8762
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8763
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8764
+ */
8765
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8766
+ return null;
8767
+ }
8768
+ /**
8769
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8770
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8771
+ */
8772
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8773
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8774
+ clusterSettings(cap, schema) {
8775
+ return this.scopedHandle("cluster", cap, schema);
8776
+ }
8777
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8778
+ deviceDefaults(cap, schema) {
8779
+ return this.scopedHandle("device-defaults", cap, schema);
8780
+ }
8781
+ /**
8782
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8783
+ * with no row yields the schema defaults — never a bare-key fallback.
8784
+ */
8785
+ nodeSettings(cap, schema, nodeId) {
8786
+ return this.scopedHandle("node", cap, schema, nodeId);
8787
+ }
8788
+ /**
8789
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8790
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8791
+ * central delete. `nodeId` omitted ⇒ the local node.
8792
+ */
8793
+ nodeState(key, schema, fallback, nodeId) {
8794
+ this._declaredStateKeys.add(key);
8795
+ return createDurableState({
8796
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8797
+ schema,
8798
+ fallback,
8799
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8800
+ write: async (patch) => {
8801
+ await this._ctx?.settings?.writeAddonStore(patch);
8802
+ },
8803
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8804
+ addonId: this._ctx?.id,
8805
+ key: k,
8806
+ error: String(e)
8807
+ } })
8808
+ });
8809
+ }
8810
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8811
+ async declaredScopedSchema(scope, cap) {
8812
+ await this.loadScopedSchemas(cap);
8813
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8814
+ }
8815
+ async requireScopedSchema(scope, cap) {
8816
+ const schema = await this.declaredScopedSchema(scope, cap);
8817
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8818
+ return schema;
8819
+ }
8820
+ scopedKeys(schema) {
8821
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8822
+ }
8823
+ /** `store` narrowed to the keys `schema` declares. */
8824
+ narrowToScope(store, schema) {
8825
+ const keys = this.scopedKeys(schema);
8826
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8827
+ }
8828
+ scopedHandle(scope, cap, schema, nodeId) {
8829
+ return createScopedSettings({
8830
+ schema,
8831
+ read: async () => {
8832
+ const declared = await this.requireScopedSchema(scope, cap);
8833
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8834
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8835
+ return {
8836
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8837
+ ...stored
8838
+ };
8839
+ },
8840
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8841
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8842
+ });
8843
+ }
8844
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8845
+ async writeScoped(scope, cap, patch, nodeId) {
8846
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8847
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8848
+ }
8849
+ /**
8850
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8851
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8852
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8853
+ */
8854
+ async unsetScoped(scope, cap, keys, nodeId) {
8855
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8856
+ const target = scope === "node" ? nodeId : void 0;
8857
+ const tree = this.addonSecretTree();
8858
+ const secretKeys = keys.filter((key) => tree.has(key));
8859
+ const plainKeys = keys.filter((key) => !tree.has(key));
8860
+ let changed = false;
8861
+ if (plainKeys.length > 0) {
8862
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8863
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8864
+ }
8865
+ if (secretKeys.length > 0) {
8866
+ await this.clearSecretKeys(secretKeys, target, cap);
8867
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8868
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8869
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8870
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8871
+ changed = true;
8872
+ }
8873
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8874
+ }
8875
+ /**
8876
+ * The hydrated form of one scope of one capability's settings (any
8877
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8878
+ */
8879
+ async getScopedSettings(scope, cap, nodeId) {
8880
+ const schema = await this.declaredScopedSchema(scope, cap);
8881
+ if (schema === null) return null;
8882
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8883
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8884
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8885
+ }
8886
+ /**
8887
+ * The scoped schemas this addon serves: one entry per loaded (capability, scope), capabilities
8888
+ * in declaration order, scopes in {@link SCOPED_SETTINGS_SCOPES} order. `[]` when it declares
8889
+ * nothing.
8890
+ */
8891
+ async listScopedSettings() {
8892
+ await this.loadScopedSchemas();
8893
+ return [...this._scopedByCap].flatMap(([cap, entries]) => entries.map((entry) => ({
8894
+ cap,
8895
+ scope: entry.scope,
8896
+ sectionIds: entry.schema.sections.map((section) => section.id)
8897
+ })));
8898
+ }
8899
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8900
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8901
+ this._operatorWriteDepth++;
8902
+ try {
8903
+ await this.writeScoped(scope, cap, patch, nodeId);
8904
+ } finally {
8905
+ this._operatorWriteDepth--;
8906
+ }
8907
+ }
8908
+ /**
8909
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
8910
+ * its value. `null` when the capability declares nothing for the scope.
8911
+ */
8912
+ async readScopedValues(scope, cap, nodeId) {
8913
+ const schema = await this.declaredScopedSchema(scope, cap);
8914
+ if (schema === null) return null;
8915
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
8916
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8917
+ }
8918
+ /**
8686
8919
  * What the create wizard and the integration edit page render for one instance: the addon's
8687
8920
  * per-instance schema when it declares one, else its global schema (what they always showed).
8688
8921
  */
8689
8922
  async getIntegrationFormSchema(instance, nodeId) {
8690
8923
  const scoped = await this.integrationFormSchema(instance);
8691
- if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
8924
+ if (scoped === null) return this.scopedForm(nodeId);
8692
8925
  return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8693
8926
  }
8694
- async getIntegrationSettings(nodeId) {
8695
- const declared = this.integrationSettingSections();
8696
- if (declared.length === 0) return null;
8697
- const schema = this.globalSettingsSchema();
8698
- if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
8699
- const byId = new Map(schema.sections.map((section) => [section.id, section]));
8700
- const sections = [];
8701
- for (const id of declared) {
8702
- const section = byId.get(id);
8703
- if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
8704
- const fields = dropPerNodeFields(section.fields);
8705
- if (fields.length === 0) continue;
8706
- sections.push({
8707
- ...section,
8708
- fields
8709
- });
8710
- }
8711
- if (sections.length === 0) return null;
8712
- const projected = await this.readRawProjectedStore(nodeId);
8713
- return hydrateSchema({
8714
- ...schema,
8715
- sections
8716
- }, projected, { inlineSecretsAsViews: true });
8927
+ /** The form of the scoped schemas this addon declared, hydrated from its store. */
8928
+ async scopedForm(nodeId) {
8929
+ await this.loadScopedSchemas();
8930
+ const schema = this.scopedFormSchema();
8931
+ if (!schema) return { sections: [] };
8932
+ return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8717
8933
  }
8718
8934
  /**
8719
8935
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
@@ -8722,8 +8938,8 @@ var BaseAddon = class {
8722
8938
  * bare fallback), all `@`-scoped and stray bare per-node keys are dropped.
8723
8939
  * A no-op passthrough when the schema declares no `perNode` field.
8724
8940
  *
8725
- * This is the sanctioned way for a `getGlobalSettings` OVERRIDE that needs
8726
- * the store for custom option logic (option narrowing, value snapping) to
8941
+ * This is the sanctioned way for an addon that needs
8942
+ * the store for custom logic (option narrowing, value snapping) to
8727
8943
  * read it per-node — never `ctx.settings.readAddonStore()` directly.
8728
8944
  * `nodeId` omitted ⇒ the local node (node-blind injection from ctx).
8729
8945
  *
@@ -8775,7 +8991,7 @@ var BaseAddon = class {
8775
8991
  }
8776
8992
  /**
8777
8993
  * Whether ONE global key of this addon's store was written, read three-way
8778
- * (D651). For a question a hydrated view cannot answer: `getGlobalSettings`
8994
+ * (D651). For a question a hydrated view cannot answer: a hydrated form
8779
8995
  * reports `value ?? default`, so "the operator chose the default" and "nobody
8780
8996
  * chose" look the same, and a failed read looks like both. A `perNode` key is
8781
8997
  * not supported here: its stored name is scoped.
@@ -8809,7 +9025,7 @@ var BaseAddon = class {
8809
9025
  }
8810
9026
  /**
8811
9027
  * The OPERATOR's write — a settings form or an admin API call, dispatched by
8812
- * the kernel. Identical to {@link updateGlobalSettings}, except that a secret
9028
+ * the kernel. The one in-process write of the addon blob that is not a scoped write; a secret
8813
9029
  * the operator sends is stored as sent even when it equals a pre-keychain
8814
9030
  * inline value: the operator re-entering a password is a change, an addon
8815
9031
  * writing back what it read is not (D787).
@@ -8822,9 +9038,6 @@ var BaseAddon = class {
8822
9038
  this._operatorWriteDepth--;
8823
9039
  }
8824
9040
  }
8825
- async updateGlobalSettings(patch, nodeId) {
8826
- await this.applyGlobalPatch(patch, nodeId);
8827
- }
8828
9041
  /**
8829
9042
  * REMOVE stored secrets — their keychain values and their markers (D787).
8830
9043
  *
@@ -8834,6 +9047,9 @@ var BaseAddon = class {
8834
9047
  * a keychain (an older kernel) it writes `''`, as such code always did.
8835
9048
  */
8836
9049
  async clearSecrets(keys, nodeId) {
9050
+ await this.clearSecretKeys(keys, nodeId);
9051
+ }
9052
+ async clearSecretKeys(keys, nodeId, cap) {
8837
9053
  const tree = this.addonSecretTree();
8838
9054
  const keychain = this._ctx?.secrets !== void 0;
8839
9055
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8846,25 +9062,26 @@ var BaseAddon = class {
8846
9062
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8847
9063
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8848
9064
  }
8849
- await this.applyGlobalPatch(patch, nodeId);
9065
+ await this.applyGlobalPatch(patch, nodeId, cap);
8850
9066
  }
8851
- async applyGlobalPatch(barePatch, nodeId) {
8852
- await this.loadScopedSchemas();
8853
- const keys = this.perNodeKeys();
9067
+ /** Resolves `true` when the write changed stored values. */
9068
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9069
+ await this.loadScopedSchemas(cap);
9070
+ const keys = this.perNodeKeys(cap);
8854
9071
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8855
9072
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8856
9073
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8857
9074
  const handle = this._secretSettings;
8858
9075
  if (handle !== null) {
8859
9076
  const { changed } = await handle.writeAddonStore(scoped);
8860
- if (!changed) return;
9077
+ if (!changed) return false;
8861
9078
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8862
- if (target !== localNode) return;
9079
+ if (target !== localNode) return true;
8863
9080
  await this.resolveConfig();
8864
9081
  await this.onConfigChanged();
8865
9082
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8866
- const legacy = this.globalSettingsSchema();
8867
- this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9083
+ this.maybeAutoRestart(Object.keys(barePatch), this.loadedScopedSchemas().map((entry) => entry.schema));
9084
+ return true;
8868
9085
  }
8869
9086
  /**
8870
9087
  * The set of field keys the global settings schema declares `perNode: true`
@@ -8901,10 +9118,7 @@ var BaseAddon = class {
8901
9118
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
8902
9119
  this._addonSecretTree = null;
8903
9120
  }
8904
- /**
8905
- * The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
8906
- * sections of its scoped schemas, so dropping the old hook does not blank the form.
8907
- */
9121
+ /** The form of an addon's scoped schemas: their sections, each shown once. */
8908
9122
  scopedFormSchema(cap) {
8909
9123
  const byId = /* @__PURE__ */ new Map();
8910
9124
  for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
@@ -8916,8 +9130,7 @@ var BaseAddon = class {
8916
9130
  return [...this._scopedByCap.values()].flat();
8917
9131
  }
8918
9132
  /**
8919
- * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
8920
- * the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
9133
+ * The field keys that are stored per node: every field of a {@link nodeSettingsSchema}. Derived once
8921
9134
  * per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
8922
9135
  * per-node code path is bypassed and the settings API behaves like the node-agnostic one.
8923
9136
  * Call {@link loadScopedSchemas} first (the async seams do).
@@ -8926,8 +9139,7 @@ var BaseAddon = class {
8926
9139
  const cacheKey = cap ?? "";
8927
9140
  const cached = this._nodeKeysByCap.get(cacheKey);
8928
9141
  if (cached) return cached;
8929
- const legacy = this.globalSettingsSchema(cap);
8930
- const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
9142
+ const keys = /* @__PURE__ */ new Set();
8931
9143
  for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
8932
9144
  if (scope !== "node") continue;
8933
9145
  for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
@@ -9145,8 +9357,6 @@ var BaseAddon = class {
9145
9357
  }
9146
9358
  declaredKeySet() {
9147
9359
  const keys = new Set(Object.keys(this.defaults));
9148
- const schema = this.globalSettingsSchema();
9149
- if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9150
9360
  for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
9151
9361
  for (const key of this._declaredStateKeys) keys.add(key);
9152
9362
  for (const path of this.secretStatePaths) {
@@ -9244,7 +9454,7 @@ var BaseAddon = class {
9244
9454
  /** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
9245
9455
  addonSecretTree() {
9246
9456
  if (this._addonSecretTree === null) {
9247
- const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
9457
+ const tree = /* @__PURE__ */ new Map();
9248
9458
  for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
9249
9459
  this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
9250
9460
  }
@@ -9456,47 +9666,6 @@ function classifyStoreError(err) {
9456
9666
  if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
9457
9667
  return "permanent";
9458
9668
  }
9459
- /**
9460
- * Collect the keys of every field marked `perNode: true`, recursing into
9461
- * layout containers (`group` fields and `sub-tabs` tabs) the same way
9462
- * `hydrateSchema` does. Valueless structural fields (separator/info/…)
9463
- * don't declare `perNode` and are excluded by the `in` narrowing.
9464
- */
9465
- /**
9466
- * The same fields with every `perNode: true` one removed, recursing into layout
9467
- * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
9468
- * with no child is dropped rather than rendered empty.
9469
- *
9470
- * Used by `getIntegrationSettings`: an integration is not a node, so a field
9471
- * whose store key is `<key>@<nodeId>` has no node to belong to there.
9472
- */
9473
- function dropPerNodeFields(fields) {
9474
- const kept = [];
9475
- for (const field of fields) {
9476
- if (field.type === "group") {
9477
- const inner = dropPerNodeFields(field.fields);
9478
- if (inner.length > 0) kept.push({
9479
- ...field,
9480
- fields: inner
9481
- });
9482
- continue;
9483
- }
9484
- if (field.type === "sub-tabs") {
9485
- const tabs = field.tabs.map((tab) => ({
9486
- ...tab,
9487
- fields: dropPerNodeFields(tab.fields)
9488
- })).filter((tab) => tab.fields.length > 0);
9489
- if (tabs.length > 0) kept.push({
9490
- ...field,
9491
- tabs
9492
- });
9493
- continue;
9494
- }
9495
- if ("perNode" in field && field.perNode === true) continue;
9496
- kept.push(field);
9497
- }
9498
- return kept;
9499
- }
9500
9669
  /** The keys of the value-holding fields, recursing into layout containers. */
9501
9670
  function collectValueFieldKeys(fields) {
9502
9671
  const collected = [];
@@ -9529,21 +9698,6 @@ function collectFieldKeys(fields) {
9529
9698
  }
9530
9699
  return collected;
9531
9700
  }
9532
- function collectPerNodeFieldKeys(fields) {
9533
- const collected = [];
9534
- for (const field of fields) {
9535
- if (field.type === "group") {
9536
- collected.push(...collectPerNodeFieldKeys(field.fields));
9537
- continue;
9538
- }
9539
- if (field.type === "sub-tabs") {
9540
- for (const tab of field.tabs) collected.push(...collectPerNodeFieldKeys(tab.fields));
9541
- continue;
9542
- }
9543
- if ("perNode" in field && field.perNode === true) collected.push(field.key);
9544
- }
9545
- return collected;
9546
- }
9547
9701
  /**
9548
9702
  * Normalize an `ICamstackAddon.initialize()` return value into the
9549
9703
  * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
@@ -10594,7 +10748,6 @@ var CAP_NAMES = Object.freeze([
10594
10748
  "notifier",
10595
10749
  "numeric-sensor",
10596
10750
  "oauth-integration",
10597
- "oidc-connections",
10598
10751
  "osd",
10599
10752
  "osd-manager",
10600
10753
  "pet-feeder",
@@ -12301,13 +12454,25 @@ Object.freeze({
12301
12454
  "path": "temperature",
12302
12455
  "valueType": "number"
12303
12456
  }],
12304
- "timed-run": [{
12305
- "path": "activeUntil",
12306
- "valueType": "number"
12307
- }, {
12308
- "path": "requestedBy",
12309
- "valueType": "string"
12310
- }],
12457
+ "timed-run": [
12458
+ {
12459
+ "path": "activeUntil",
12460
+ "valueType": "number"
12461
+ },
12462
+ {
12463
+ "path": "requestedBy",
12464
+ "valueType": "string"
12465
+ },
12466
+ {
12467
+ "path": "quickRun.duration",
12468
+ "valueType": "number"
12469
+ },
12470
+ {
12471
+ "path": "quickRun.unit",
12472
+ "valueType": "enum",
12473
+ "enumValues": ["s", "min"]
12474
+ }
12475
+ ],
12311
12476
  "update": [
12312
12477
  {
12313
12478
  "path": "currentVersion",
@@ -12486,8 +12651,7 @@ Object.freeze({
12486
12651
  "device-provider": { "reloadDevice": 18e4 },
12487
12652
  "llm": {
12488
12653
  "generate": 6e5,
12489
- "generateVision": 6e5,
12490
- "testProfile": 6e5
12654
+ "generateVision": 6e5
12491
12655
  },
12492
12656
  "llm-runtime": {
12493
12657
  "complete": 6e5,
@@ -12532,7 +12696,7 @@ object({
12532
12696
  params: record(string(), union([string(), number()])).optional()
12533
12697
  });
12534
12698
  //#endregion
12535
- //#region ../types/dist/composition-BmBHuDTQ.mjs
12699
+ //#region ../types/dist/composition-Bx0JvKUU.mjs
12536
12700
  /**
12537
12701
  * The ffmpeg build every CamStack node downloads, per platform and arch.
12538
12702
  *
@@ -13694,6 +13858,387 @@ var NodeColorIdSchema = _enum([
13694
13858
  "node-h"
13695
13859
  ]);
13696
13860
  //#endregion
13861
+ //#region ../types/dist/convert-BAzH15HU.mjs
13862
+ /**
13863
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
13864
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
13865
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
13866
+ * decimals). K derives from `°C = K − 273.15`.
13867
+ */
13868
+ var UNIT_TABLE = {
13869
+ "°C": {
13870
+ dimension: "temperature",
13871
+ factor: 1,
13872
+ offset: 0
13873
+ },
13874
+ "°F": {
13875
+ dimension: "temperature",
13876
+ factor: 5 / 9,
13877
+ offset: -160 / 9
13878
+ },
13879
+ K: {
13880
+ dimension: "temperature",
13881
+ factor: 1,
13882
+ offset: -273.15
13883
+ },
13884
+ hPa: {
13885
+ dimension: "pressure",
13886
+ factor: 1,
13887
+ offset: 0
13888
+ },
13889
+ kPa: {
13890
+ dimension: "pressure",
13891
+ factor: 10,
13892
+ offset: 0
13893
+ },
13894
+ Pa: {
13895
+ dimension: "pressure",
13896
+ factor: .01,
13897
+ offset: 0
13898
+ },
13899
+ mbar: {
13900
+ dimension: "pressure",
13901
+ factor: 1,
13902
+ offset: 0
13903
+ },
13904
+ bar: {
13905
+ dimension: "pressure",
13906
+ factor: 1e3,
13907
+ offset: 0
13908
+ },
13909
+ inHg: {
13910
+ dimension: "pressure",
13911
+ factor: 33.8639,
13912
+ offset: 0
13913
+ },
13914
+ mmHg: {
13915
+ dimension: "pressure",
13916
+ factor: 1.33322,
13917
+ offset: 0
13918
+ },
13919
+ psi: {
13920
+ dimension: "pressure",
13921
+ factor: 68.9476,
13922
+ offset: 0
13923
+ },
13924
+ "m/s": {
13925
+ dimension: "speed",
13926
+ factor: 1,
13927
+ offset: 0
13928
+ },
13929
+ "km/h": {
13930
+ dimension: "speed",
13931
+ factor: 1 / 3.6,
13932
+ offset: 0
13933
+ },
13934
+ mph: {
13935
+ dimension: "speed",
13936
+ factor: .44704,
13937
+ offset: 0
13938
+ },
13939
+ kn: {
13940
+ dimension: "speed",
13941
+ factor: .514444,
13942
+ offset: 0
13943
+ },
13944
+ "mm/h": {
13945
+ dimension: "precipitation-rate",
13946
+ factor: 1,
13947
+ offset: 0
13948
+ },
13949
+ "in/h": {
13950
+ dimension: "precipitation-rate",
13951
+ factor: 25.4,
13952
+ offset: 0
13953
+ },
13954
+ m: {
13955
+ dimension: "length",
13956
+ factor: 1,
13957
+ offset: 0
13958
+ },
13959
+ mm: {
13960
+ dimension: "length",
13961
+ factor: .001,
13962
+ offset: 0
13963
+ },
13964
+ cm: {
13965
+ dimension: "length",
13966
+ factor: .01,
13967
+ offset: 0
13968
+ },
13969
+ km: {
13970
+ dimension: "length",
13971
+ factor: 1e3,
13972
+ offset: 0
13973
+ },
13974
+ in: {
13975
+ dimension: "length",
13976
+ factor: .0254,
13977
+ offset: 0
13978
+ },
13979
+ ft: {
13980
+ dimension: "length",
13981
+ factor: .3048,
13982
+ offset: 0
13983
+ },
13984
+ mi: {
13985
+ dimension: "length",
13986
+ factor: 1609.344,
13987
+ offset: 0
13988
+ },
13989
+ "m²": {
13990
+ dimension: "area",
13991
+ factor: 1,
13992
+ offset: 0
13993
+ },
13994
+ lx: {
13995
+ dimension: "illuminance",
13996
+ factor: 1,
13997
+ offset: 0
13998
+ },
13999
+ "W/m²": {
14000
+ dimension: "irradiance",
14001
+ factor: 1,
14002
+ offset: 0
14003
+ },
14004
+ W: {
14005
+ dimension: "power",
14006
+ factor: 1,
14007
+ offset: 0
14008
+ },
14009
+ kW: {
14010
+ dimension: "power",
14011
+ factor: 1e3,
14012
+ offset: 0
14013
+ },
14014
+ VA: {
14015
+ dimension: "apparent-power",
14016
+ factor: 1,
14017
+ offset: 0
14018
+ },
14019
+ Wh: {
14020
+ dimension: "energy",
14021
+ factor: 1,
14022
+ offset: 0
14023
+ },
14024
+ kWh: {
14025
+ dimension: "energy",
14026
+ factor: 1e3,
14027
+ offset: 0
14028
+ },
14029
+ MWh: {
14030
+ dimension: "energy",
14031
+ factor: 1e6,
14032
+ offset: 0
14033
+ },
14034
+ V: {
14035
+ dimension: "voltage",
14036
+ factor: 1,
14037
+ offset: 0
14038
+ },
14039
+ mV: {
14040
+ dimension: "voltage",
14041
+ factor: .001,
14042
+ offset: 0
14043
+ },
14044
+ A: {
14045
+ dimension: "current",
14046
+ factor: 1,
14047
+ offset: 0
14048
+ },
14049
+ mA: {
14050
+ dimension: "current",
14051
+ factor: .001,
14052
+ offset: 0
14053
+ },
14054
+ ppm: {
14055
+ dimension: "concentration-volume",
14056
+ factor: 1,
14057
+ offset: 0
14058
+ },
14059
+ ppb: {
14060
+ dimension: "concentration-volume",
14061
+ factor: .001,
14062
+ offset: 0
14063
+ },
14064
+ "µg/m³": {
14065
+ dimension: "concentration-mass",
14066
+ factor: 1,
14067
+ offset: 0
14068
+ },
14069
+ "mg/m³": {
14070
+ dimension: "concentration-mass",
14071
+ factor: 1e3,
14072
+ offset: 0
14073
+ },
14074
+ kg: {
14075
+ dimension: "mass",
14076
+ factor: 1,
14077
+ offset: 0
14078
+ },
14079
+ g: {
14080
+ dimension: "mass",
14081
+ factor: .001,
14082
+ offset: 0
14083
+ },
14084
+ lb: {
14085
+ dimension: "mass",
14086
+ factor: .453592,
14087
+ offset: 0
14088
+ },
14089
+ oz: {
14090
+ dimension: "mass",
14091
+ factor: .0283495,
14092
+ offset: 0
14093
+ },
14094
+ "%": {
14095
+ dimension: "percentage",
14096
+ factor: 1,
14097
+ offset: 0
14098
+ },
14099
+ L: {
14100
+ dimension: "volume",
14101
+ factor: 1,
14102
+ offset: 0
14103
+ },
14104
+ mL: {
14105
+ dimension: "volume",
14106
+ factor: .001,
14107
+ offset: 0
14108
+ },
14109
+ gal: {
14110
+ dimension: "volume",
14111
+ factor: 3.785411784,
14112
+ offset: 0
14113
+ },
14114
+ s: {
14115
+ dimension: "duration",
14116
+ factor: 1,
14117
+ offset: 0
14118
+ },
14119
+ ms: {
14120
+ dimension: "duration",
14121
+ factor: .001,
14122
+ offset: 0
14123
+ },
14124
+ min: {
14125
+ dimension: "duration",
14126
+ factor: 60,
14127
+ offset: 0
14128
+ },
14129
+ h: {
14130
+ dimension: "duration",
14131
+ factor: 3600,
14132
+ offset: 0
14133
+ },
14134
+ d: {
14135
+ dimension: "duration",
14136
+ factor: 86400,
14137
+ offset: 0
14138
+ },
14139
+ B: {
14140
+ dimension: "information",
14141
+ factor: 1,
14142
+ offset: 0
14143
+ },
14144
+ KiB: {
14145
+ dimension: "information",
14146
+ factor: 1024,
14147
+ offset: 0
14148
+ },
14149
+ MiB: {
14150
+ dimension: "information",
14151
+ factor: 1024 ** 2,
14152
+ offset: 0
14153
+ },
14154
+ GiB: {
14155
+ dimension: "information",
14156
+ factor: 1024 ** 3,
14157
+ offset: 0
14158
+ },
14159
+ TiB: {
14160
+ dimension: "information",
14161
+ factor: 1024 ** 4,
14162
+ offset: 0
14163
+ },
14164
+ kB: {
14165
+ dimension: "information",
14166
+ factor: 1e3,
14167
+ offset: 0
14168
+ },
14169
+ MB: {
14170
+ dimension: "information",
14171
+ factor: 1e6,
14172
+ offset: 0
14173
+ },
14174
+ GB: {
14175
+ dimension: "information",
14176
+ factor: 1e9,
14177
+ offset: 0
14178
+ },
14179
+ "°": {
14180
+ dimension: "angle",
14181
+ factor: 1,
14182
+ offset: 0
14183
+ },
14184
+ dBFS: {
14185
+ dimension: "sound-level",
14186
+ factor: 1,
14187
+ offset: 0
14188
+ },
14189
+ dBm: {
14190
+ dimension: "signal-level",
14191
+ factor: 1,
14192
+ offset: 0
14193
+ },
14194
+ mired: {
14195
+ dimension: "colour-temperature",
14196
+ factor: 1,
14197
+ offset: 0
14198
+ },
14199
+ kbps: {
14200
+ dimension: "bitrate",
14201
+ factor: 1,
14202
+ offset: 0
14203
+ },
14204
+ Mbps: {
14205
+ dimension: "bitrate",
14206
+ factor: 1e3,
14207
+ offset: 0
14208
+ },
14209
+ fps: {
14210
+ dimension: "rate",
14211
+ factor: 1,
14212
+ offset: 0
14213
+ }
14214
+ };
14215
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
14216
+ function specFor(unit) {
14217
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
14218
+ }
14219
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
14220
+ function toCanonical(value, spec) {
14221
+ return value * spec.factor + spec.offset;
14222
+ }
14223
+ /** Canonical-unit value → the unit described by `spec`. */
14224
+ function fromCanonical(canonical, spec) {
14225
+ return (canonical - spec.offset) / spec.factor;
14226
+ }
14227
+ /**
14228
+ * Graceful variant for the render path: returns the converted value, or
14229
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
14230
+ * Never throws — a render caller falls back to the raw value + source unit.
14231
+ */
14232
+ function tryConvertUnit(value, from, to) {
14233
+ if (from === to) return value;
14234
+ const fromSpec = specFor(from);
14235
+ if (!fromSpec) return void 0;
14236
+ const toSpec = specFor(to);
14237
+ if (!toSpec) return void 0;
14238
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
14239
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
14240
+ }
14241
+ //#endregion
13697
14242
  //#region ../types/dist/index.mjs
13698
14243
  var SMTP_INBOUND_DEFAULT_PORT = 2525;
13699
14244
  var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
@@ -13890,17 +14435,18 @@ var IntegrationInstanceRefSchema = object({
13890
14435
  * message to what the kind supports — callers never special-case a service.
13891
14436
  *
13892
14437
  * DESIGN DECISIONS (locked):
13893
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13894
- * `setTargetEnabled`), each provider persisting via the `settings-store`
13895
- * cap. Rationale: the admin UI needs one uniform surface across the
13896
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13897
- * alternative would fork the UI per addon and cannot host the
13898
- * discovery→adopt flow.
13899
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13900
- * the generated cap-mount auto-`concatCollection`-fans them across every
13901
- * registered provider (notifiers addon + HA addon) so one catalog is
13902
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13903
- * `addonId` the generated collection router extracts from the call input.
14438
+ * - A target is an INSTANCE of this function cap (epic #440, step 6): the
14439
+ * providers serve their targets as `integration-instances` records, one
14440
+ * record kind per target kind, and the admin lists, adds, edits, switches
14441
+ * and deletes them through that cap. This cap keeps what an instance cannot
14442
+ * say: the kind catalogue (`listTargetKinds`: capabilities, delivery
14443
+ * parameters, brand icon), dispatch (`send`, `testTarget`) and the form
14444
+ * helpers (`discoverTargets`, `probeTargetConfig`).
14445
+ * - `listTargetKinds` / `discoverTargets` return arrays → the generated
14446
+ * cap-mount auto-`concatCollection`-fans them across every registered
14447
+ * provider so one catalog is routable. `send` / `testTarget` /
14448
+ * `probeTargetConfig` route to ONE provider by the `addonId` the generated
14449
+ * collection router extracts from the call input.
13904
14450
  * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13905
14451
  * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13906
14452
  * `storage` / `storage-provider` / `recording` caps over the same path. No
@@ -14145,7 +14691,7 @@ var TargetKindCapsSchema = object({
14145
14691
  * the union is large and not meant for runtime validation here; the exported
14146
14692
  * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14147
14693
  */
14148
- var ConfigSchemaPassthrough$4 = unknown();
14694
+ var ConfigSchemaPassthrough$3 = unknown();
14149
14695
  var TargetKindSchema = object({
14150
14696
  kind: string(),
14151
14697
  label: string(),
@@ -14182,7 +14728,7 @@ var TargetKindSchema = object({
14182
14728
  * its raster path, which is the safe default for an unknown image.
14183
14729
  */
14184
14730
  iconMediaType: string().optional(),
14185
- configSchema: ConfigSchemaPassthrough$4,
14731
+ configSchema: ConfigSchemaPassthrough$3,
14186
14732
  /**
14187
14733
  * The config keys that decide WHERE this kind's secrets are sent (server,
14188
14734
  * base URL, instance, chat, recipient). A save that changes any of them
@@ -14204,9 +14750,11 @@ var TargetKindSchema = object({
14204
14750
  caps: TargetKindCapsSchema
14205
14751
  });
14206
14752
  /**
14207
- * A persisted target. `config` holds secrets; providers REDACT secret fields
14208
- * (return a presence marker only) when serving `listTargets` — never
14209
- * round-trip a stored secret to the UI.
14753
+ * A target as a provider's own code sees it (the notification-center bridge, the
14754
+ * send path): the flat record body split into the name, the switch and the
14755
+ * kind's fields. `config` holds secrets; a reader that serves it to a client
14756
+ * REDACTS secret fields (a presence marker only) — never round-trip a stored
14757
+ * secret to the UI.
14210
14758
  */
14211
14759
  var TargetSchema = object({
14212
14760
  id: string(),
@@ -14249,13 +14797,13 @@ var ProbeTargetConfigResultSchema = object({
14249
14797
  stateInstance: string().optional(),
14250
14798
  error: string().optional()
14251
14799
  });
14252
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14800
+ method(object({}), array(TargetKindSchema)), method(object({
14253
14801
  kind: string(),
14254
14802
  config: record(string(), unknown()).optional(),
14255
14803
  /**
14256
14804
  * When editing / duplicating, the blank password field never round-trips
14257
14805
  * the stored token. Pass the source target id so the provider can restore
14258
- * secrets server-side before dialling (same merge as upsert).
14806
+ * secrets server-side before dialling.
14259
14807
  */
14260
14808
  targetId: string().optional()
14261
14809
  }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
@@ -14278,27 +14826,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
14278
14826
  }), TestResultSchema, {
14279
14827
  kind: "mutation",
14280
14828
  auth: "admin"
14281
- }), method(object({
14282
- target: TargetSchema,
14283
- /**
14284
- * Duplicate flow: create a NEW target id while copying secrets from an
14285
- * existing row. The UI never holds the stored token; blank / redacted
14286
- * password fields are restored from this source (D634 still applies if
14287
- * destinations diverge without a re-typed secret).
14288
- */
14289
- cloneSecretsFrom: string().optional()
14290
- }), TargetSchema, {
14291
- kind: "mutation",
14292
- auth: "admin"
14293
- }), method(object({ targetId: string() }), _void(), {
14294
- kind: "mutation",
14295
- auth: "admin"
14296
- }), method(object({
14297
- targetId: string(),
14298
- enabled: boolean()
14299
- }), _void(), {
14300
- kind: "mutation",
14301
- auth: "admin"
14302
14829
  });
14303
14830
  looseObject({
14304
14831
  name: string().min(1),
@@ -22566,7 +23093,6 @@ var SYSTEM_CAP_VOCABULARY = {
22566
23093
  "notification-output": system("notification delivery — reached through notifier devices and rules"),
22567
23094
  "notification-rules": system("notification rules — authored on their own page"),
22568
23095
  "oauth-integration": system(IDENTITY),
22569
- "oidc-connections": system(IDENTITY),
22570
23096
  "osd-manager": system(PIPELINE),
22571
23097
  "pipeline-executor": system(PIPELINE),
22572
23098
  "pipeline-orchestrator": system(PIPELINE),
@@ -42587,6 +43113,7 @@ var ExpressionBindingSourceSchema = union([
42587
43113
  object({
42588
43114
  kind: literal("global"),
42589
43115
  sourceStableId: string(),
43116
+ addonId: string().optional(),
42590
43117
  cap: string(),
42591
43118
  fieldPath: string()
42592
43119
  })
@@ -42653,385 +43180,6 @@ var SourceInfoSchema = object({
42653
43180
  raw: record(string(), unknown()).optional()
42654
43181
  });
42655
43182
  /**
42656
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
42657
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
42658
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
42659
- * decimals). K derives from `°C = K − 273.15`.
42660
- */
42661
- var UNIT_TABLE = {
42662
- "°C": {
42663
- dimension: "temperature",
42664
- factor: 1,
42665
- offset: 0
42666
- },
42667
- "°F": {
42668
- dimension: "temperature",
42669
- factor: 5 / 9,
42670
- offset: -160 / 9
42671
- },
42672
- K: {
42673
- dimension: "temperature",
42674
- factor: 1,
42675
- offset: -273.15
42676
- },
42677
- hPa: {
42678
- dimension: "pressure",
42679
- factor: 1,
42680
- offset: 0
42681
- },
42682
- kPa: {
42683
- dimension: "pressure",
42684
- factor: 10,
42685
- offset: 0
42686
- },
42687
- Pa: {
42688
- dimension: "pressure",
42689
- factor: .01,
42690
- offset: 0
42691
- },
42692
- mbar: {
42693
- dimension: "pressure",
42694
- factor: 1,
42695
- offset: 0
42696
- },
42697
- bar: {
42698
- dimension: "pressure",
42699
- factor: 1e3,
42700
- offset: 0
42701
- },
42702
- inHg: {
42703
- dimension: "pressure",
42704
- factor: 33.8639,
42705
- offset: 0
42706
- },
42707
- mmHg: {
42708
- dimension: "pressure",
42709
- factor: 1.33322,
42710
- offset: 0
42711
- },
42712
- psi: {
42713
- dimension: "pressure",
42714
- factor: 68.9476,
42715
- offset: 0
42716
- },
42717
- "m/s": {
42718
- dimension: "speed",
42719
- factor: 1,
42720
- offset: 0
42721
- },
42722
- "km/h": {
42723
- dimension: "speed",
42724
- factor: 1 / 3.6,
42725
- offset: 0
42726
- },
42727
- mph: {
42728
- dimension: "speed",
42729
- factor: .44704,
42730
- offset: 0
42731
- },
42732
- kn: {
42733
- dimension: "speed",
42734
- factor: .514444,
42735
- offset: 0
42736
- },
42737
- "mm/h": {
42738
- dimension: "precipitation-rate",
42739
- factor: 1,
42740
- offset: 0
42741
- },
42742
- "in/h": {
42743
- dimension: "precipitation-rate",
42744
- factor: 25.4,
42745
- offset: 0
42746
- },
42747
- m: {
42748
- dimension: "length",
42749
- factor: 1,
42750
- offset: 0
42751
- },
42752
- mm: {
42753
- dimension: "length",
42754
- factor: .001,
42755
- offset: 0
42756
- },
42757
- cm: {
42758
- dimension: "length",
42759
- factor: .01,
42760
- offset: 0
42761
- },
42762
- km: {
42763
- dimension: "length",
42764
- factor: 1e3,
42765
- offset: 0
42766
- },
42767
- in: {
42768
- dimension: "length",
42769
- factor: .0254,
42770
- offset: 0
42771
- },
42772
- ft: {
42773
- dimension: "length",
42774
- factor: .3048,
42775
- offset: 0
42776
- },
42777
- mi: {
42778
- dimension: "length",
42779
- factor: 1609.344,
42780
- offset: 0
42781
- },
42782
- "m²": {
42783
- dimension: "area",
42784
- factor: 1,
42785
- offset: 0
42786
- },
42787
- lx: {
42788
- dimension: "illuminance",
42789
- factor: 1,
42790
- offset: 0
42791
- },
42792
- "W/m²": {
42793
- dimension: "irradiance",
42794
- factor: 1,
42795
- offset: 0
42796
- },
42797
- W: {
42798
- dimension: "power",
42799
- factor: 1,
42800
- offset: 0
42801
- },
42802
- kW: {
42803
- dimension: "power",
42804
- factor: 1e3,
42805
- offset: 0
42806
- },
42807
- VA: {
42808
- dimension: "apparent-power",
42809
- factor: 1,
42810
- offset: 0
42811
- },
42812
- Wh: {
42813
- dimension: "energy",
42814
- factor: 1,
42815
- offset: 0
42816
- },
42817
- kWh: {
42818
- dimension: "energy",
42819
- factor: 1e3,
42820
- offset: 0
42821
- },
42822
- MWh: {
42823
- dimension: "energy",
42824
- factor: 1e6,
42825
- offset: 0
42826
- },
42827
- V: {
42828
- dimension: "voltage",
42829
- factor: 1,
42830
- offset: 0
42831
- },
42832
- mV: {
42833
- dimension: "voltage",
42834
- factor: .001,
42835
- offset: 0
42836
- },
42837
- A: {
42838
- dimension: "current",
42839
- factor: 1,
42840
- offset: 0
42841
- },
42842
- mA: {
42843
- dimension: "current",
42844
- factor: .001,
42845
- offset: 0
42846
- },
42847
- ppm: {
42848
- dimension: "concentration-volume",
42849
- factor: 1,
42850
- offset: 0
42851
- },
42852
- ppb: {
42853
- dimension: "concentration-volume",
42854
- factor: .001,
42855
- offset: 0
42856
- },
42857
- "µg/m³": {
42858
- dimension: "concentration-mass",
42859
- factor: 1,
42860
- offset: 0
42861
- },
42862
- "mg/m³": {
42863
- dimension: "concentration-mass",
42864
- factor: 1e3,
42865
- offset: 0
42866
- },
42867
- kg: {
42868
- dimension: "mass",
42869
- factor: 1,
42870
- offset: 0
42871
- },
42872
- g: {
42873
- dimension: "mass",
42874
- factor: .001,
42875
- offset: 0
42876
- },
42877
- lb: {
42878
- dimension: "mass",
42879
- factor: .453592,
42880
- offset: 0
42881
- },
42882
- oz: {
42883
- dimension: "mass",
42884
- factor: .0283495,
42885
- offset: 0
42886
- },
42887
- "%": {
42888
- dimension: "percentage",
42889
- factor: 1,
42890
- offset: 0
42891
- },
42892
- L: {
42893
- dimension: "volume",
42894
- factor: 1,
42895
- offset: 0
42896
- },
42897
- mL: {
42898
- dimension: "volume",
42899
- factor: .001,
42900
- offset: 0
42901
- },
42902
- gal: {
42903
- dimension: "volume",
42904
- factor: 3.785411784,
42905
- offset: 0
42906
- },
42907
- s: {
42908
- dimension: "duration",
42909
- factor: 1,
42910
- offset: 0
42911
- },
42912
- ms: {
42913
- dimension: "duration",
42914
- factor: .001,
42915
- offset: 0
42916
- },
42917
- min: {
42918
- dimension: "duration",
42919
- factor: 60,
42920
- offset: 0
42921
- },
42922
- h: {
42923
- dimension: "duration",
42924
- factor: 3600,
42925
- offset: 0
42926
- },
42927
- d: {
42928
- dimension: "duration",
42929
- factor: 86400,
42930
- offset: 0
42931
- },
42932
- B: {
42933
- dimension: "information",
42934
- factor: 1,
42935
- offset: 0
42936
- },
42937
- KiB: {
42938
- dimension: "information",
42939
- factor: 1024,
42940
- offset: 0
42941
- },
42942
- MiB: {
42943
- dimension: "information",
42944
- factor: 1024 ** 2,
42945
- offset: 0
42946
- },
42947
- GiB: {
42948
- dimension: "information",
42949
- factor: 1024 ** 3,
42950
- offset: 0
42951
- },
42952
- TiB: {
42953
- dimension: "information",
42954
- factor: 1024 ** 4,
42955
- offset: 0
42956
- },
42957
- kB: {
42958
- dimension: "information",
42959
- factor: 1e3,
42960
- offset: 0
42961
- },
42962
- MB: {
42963
- dimension: "information",
42964
- factor: 1e6,
42965
- offset: 0
42966
- },
42967
- GB: {
42968
- dimension: "information",
42969
- factor: 1e9,
42970
- offset: 0
42971
- },
42972
- "°": {
42973
- dimension: "angle",
42974
- factor: 1,
42975
- offset: 0
42976
- },
42977
- dBFS: {
42978
- dimension: "sound-level",
42979
- factor: 1,
42980
- offset: 0
42981
- },
42982
- dBm: {
42983
- dimension: "signal-level",
42984
- factor: 1,
42985
- offset: 0
42986
- },
42987
- mired: {
42988
- dimension: "colour-temperature",
42989
- factor: 1,
42990
- offset: 0
42991
- },
42992
- kbps: {
42993
- dimension: "bitrate",
42994
- factor: 1,
42995
- offset: 0
42996
- },
42997
- Mbps: {
42998
- dimension: "bitrate",
42999
- factor: 1e3,
43000
- offset: 0
43001
- },
43002
- fps: {
43003
- dimension: "rate",
43004
- factor: 1,
43005
- offset: 0
43006
- }
43007
- };
43008
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
43009
- function specFor(unit) {
43010
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
43011
- }
43012
- /** Value in `spec`'s unit → its dimension's canonical unit. */
43013
- function toCanonical(value, spec) {
43014
- return value * spec.factor + spec.offset;
43015
- }
43016
- /** Canonical-unit value → the unit described by `spec`. */
43017
- function fromCanonical(canonical, spec) {
43018
- return (canonical - spec.offset) / spec.factor;
43019
- }
43020
- /**
43021
- * Graceful variant for the render path: returns the converted value, or
43022
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
43023
- * Never throws — a render caller falls back to the raw value + source unit.
43024
- */
43025
- function tryConvertUnit(value, from, to) {
43026
- if (from === to) return value;
43027
- const fromSpec = specFor(from);
43028
- if (!fromSpec) return void 0;
43029
- const toSpec = specFor(to);
43030
- if (!toSpec) return void 0;
43031
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
43032
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
43033
- }
43034
- /**
43035
43183
  * A number a person types for a physical quantity — the input side of the
43036
43184
  * value + unit contract (D972).
43037
43185
  *
@@ -44708,15 +44856,28 @@ var SettingsSchemaWithValuesSchema = object({
44708
44856
  });
44709
44857
  /** Patch object — keys are field names, values are the new field values. */
44710
44858
  var SettingsPatchSchema = record(string(), unknown());
44859
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
44860
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
44861
+ /** One scoped schema an addon serves: its capability, scope and section ids. */
44862
+ var ScopedSettingsDeclarationSchema = object({
44863
+ cap: string(),
44864
+ scope: ScopedSettingsScopeSchema,
44865
+ sectionIds: array(string())
44866
+ });
44711
44867
  /** Standard success response for update operations. */
44712
44868
  var SettingsUpdateResultSchema = object({ success: literal(true) });
44713
44869
  method(object({
44714
44870
  addonId: string(),
44715
- nodeId: string().optional(),
44716
- overlay: record(string(), unknown()).optional(),
44717
- cap: string().optional()
44871
+ cap: string(),
44872
+ scope: ScopedSettingsScopeSchema,
44873
+ nodeId: string().optional()
44718
44874
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44719
44875
  addonId: string(),
44876
+ nodeId: string().optional()
44877
+ }), array(ScopedSettingsDeclarationSchema).nullable()), method(object({
44878
+ addonId: string(),
44879
+ cap: string(),
44880
+ scope: ScopedSettingsScopeSchema,
44720
44881
  nodeId: string().optional(),
44721
44882
  patch: SettingsPatchSchema
44722
44883
  }), SettingsUpdateResultSchema, {
@@ -44724,8 +44885,10 @@ method(object({
44724
44885
  auth: "admin"
44725
44886
  }), method(object({
44726
44887
  addonId: string(),
44888
+ cap: string(),
44889
+ scope: ScopedSettingsScopeSchema,
44727
44890
  nodeId: string().optional()
44728
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
44891
+ }), record(string(), unknown()).nullable()), method(object({
44729
44892
  addonId: string(),
44730
44893
  nodeId: string().optional(),
44731
44894
  instance: IntegrationInstanceRefSchema
@@ -46065,7 +46228,7 @@ var BackupDestinationInfoSchema = object({
46065
46228
  id: string(),
46066
46229
  /**
46067
46230
  * Manifest id of the owning addon. The Settings button on the
46068
- * destination card opens that addon's `globalSettingsSchema` panel.
46231
+ * destination card opens that addon's scoped settings panel.
46069
46232
  */
46070
46233
  addonId: string(),
46071
46234
  /** Operator-toggled enable flag from the destination-policy table. */
@@ -50535,6 +50698,7 @@ method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(Expor
50535
50698
  kind: "mutation",
50536
50699
  auth: "admin"
50537
50700
  });
50701
+ var DeviceAliasSchema = string().max(64).regex(/^[a-z][a-z0-9_]*$/);
50538
50702
  /**
50539
50703
  * A SITE ZONE — a named part of the property that several locations belong to.
50540
50704
  *
@@ -50928,6 +51092,12 @@ var DeviceInfoSchema = object({
50928
51092
  addonId: string(),
50929
51093
  type: _enum(DeviceType),
50930
51094
  name: string(),
51095
+ /** The name a value template uses for this device (`{{ stufa_veranda.temperature }}`),
51096
+ * entity_id style. Generated once from the name at first registration, unique
51097
+ * cluster-wide, editable through `setAlias`, NOT changed by a rename (D1021).
51098
+ * Optional: a record produced by an older build omits it until the boot
51099
+ * backfill has run. */
51100
+ alias: DeviceAliasSchema.optional(),
50931
51101
  /** Operator-organisational location label. `null` when unset. */
50932
51102
  location: string().nullable(),
50933
51103
  /** Soft-disabled flag. */
@@ -51247,6 +51417,25 @@ method(object({
51247
51417
  }), _void(), {
51248
51418
  kind: "mutation",
51249
51419
  auth: "admin"
51420
+ }), method(object({
51421
+ deviceId: number(),
51422
+ alias: string()
51423
+ }), discriminatedUnion("ok", [object({
51424
+ ok: literal(true),
51425
+ alias: DeviceAliasSchema
51426
+ }), object({
51427
+ ok: literal(false),
51428
+ reason: _enum([
51429
+ "empty",
51430
+ "too-long",
51431
+ "grammar",
51432
+ "reserved",
51433
+ "taken"
51434
+ ]),
51435
+ takenBy: number().optional()
51436
+ })]), {
51437
+ kind: "mutation",
51438
+ auth: "admin"
51250
51439
  }), method(object({
51251
51440
  deviceId: number(),
51252
51441
  location: string().nullable()
@@ -54272,7 +54461,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54272
54461
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
54273
54462
  * clears it.
54274
54463
  */
54275
- var ConfigSchemaPassthrough$3 = custom();
54464
+ var ConfigSchemaPassthrough$2 = custom();
54276
54465
  var IntegrationInstanceIdSchema = string().min(1);
54277
54466
  var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
54278
54467
  /**
@@ -54318,7 +54507,16 @@ var IntegrationInstanceKindSchema = object({
54318
54507
  /** Relative hub path to start an interactive account link of an instance of this kind. */
54319
54508
  connectPath: string().min(1).optional(),
54320
54509
  /** Relative hub path to disconnect that account. */
54321
- disconnectPath: string().min(1).optional()
54510
+ disconnectPath: string().min(1).optional(),
54511
+ /**
54512
+ * Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
54513
+ * GENERIC widget id the client maps to a component; a client that knows none of them shows none.
54514
+ * `connection` widgets write connection values, `model` widgets own the model choice.
54515
+ */
54516
+ widgets: array(object({
54517
+ id: string().min(1),
54518
+ placement: _enum(["connection", "model"])
54519
+ })).optional()
54322
54520
  });
54323
54521
  /**
54324
54522
  * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
@@ -54423,7 +54621,7 @@ var integrationInstancesCapability = {
54423
54621
  id: IntegrationInstanceIdSchema.nullable(),
54424
54622
  /** The kind of the create form (`id: null`) of a provider with several. */
54425
54623
  kind: string().min(1).optional()
54426
- }), ConfigSchemaPassthrough$3, { auth: "admin" }),
54624
+ }), ConfigSchemaPassthrough$2, { auth: "admin" }),
54427
54625
  listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
54428
54626
  getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
54429
54627
  /**
@@ -55374,12 +55572,12 @@ var LlmModelInfoSchema = object({
55374
55572
  });
55375
55573
  /**
55376
55574
  * The provider-specific part of a profile, validated by the provider that owns
55377
- * the kind (`upsertProfile` refuses what it does not accept). A field its
55575
+ * the kind (saving an instance refuses what it does not accept). A field its
55378
55576
  * `connectionSchema` declares `type: 'password'` is a secret: it is held in the
55379
55577
  * keychain, never in the row, and reads back as the redaction marker.
55380
55578
  */
55381
55579
  var LlmProviderConfigSchema = record(string(), unknown());
55382
- var LlmProfileSchema = object({
55580
+ object({
55383
55581
  id: string(),
55384
55582
  name: string(),
55385
55583
  /** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
@@ -55473,8 +55671,8 @@ var LlmProviderWidgetSchema = object({
55473
55671
  /** ConfigUISchema tree passed through untyped on the wire (the
55474
55672
  * notification-output `ConfigSchemaPassthrough` precedent at
55475
55673
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
55476
- var ConfigSchemaPassthrough$2 = unknown();
55477
- var LlmProfileKindDescriptorSchema = object({
55674
+ var ConfigSchemaPassthrough$1 = unknown();
55675
+ object({
55478
55676
  kind: LlmProfileKindSchema,
55479
55677
  label: string(),
55480
55678
  /** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
@@ -55482,9 +55680,9 @@ var LlmProfileKindDescriptorSchema = object({
55482
55680
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
55483
55681
  addonId: string(),
55484
55682
  /** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
55485
- connectionSchema: ConfigSchemaPassthrough$2,
55683
+ connectionSchema: ConfigSchemaPassthrough$1,
55486
55684
  /** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
55487
- advancedSchema: ConfigSchemaPassthrough$2.optional(),
55685
+ advancedSchema: ConfigSchemaPassthrough$1.optional(),
55488
55686
  /** The kind's defaults; a model from `listModels` may override any of them. */
55489
55687
  features: LlmModelFeaturesSchema,
55490
55688
  /** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
@@ -55613,20 +55811,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
55613
55811
  }), method(object({
55614
55812
  addonId: string().optional(),
55615
55813
  requestId: string()
55616
- }), _void(), { kind: "mutation" }), method(object({}), array(LlmProfileKindDescriptorSchema)), method(object({}), array(LlmProfileSchema)), method(object({ profile: LlmProfileSchema }), LlmProfileSchema, {
55617
- kind: "mutation",
55618
- auth: "admin"
55619
- }), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
55620
- kind: "mutation",
55621
- auth: "admin"
55622
- }), method(ProfileRefInputSchema, _void(), {
55623
- kind: "mutation",
55624
- auth: "admin"
55625
- }), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
55626
- kind: "mutation",
55627
- auth: "admin",
55628
- timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
55629
- }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55814
+ }), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
55630
55815
  kind: "mutation",
55631
55816
  auth: "admin"
55632
55817
  }), method(object({}), array(LlmDefaultSchema)), method(object({
@@ -56621,20 +56806,7 @@ method(_void(), MeshStatusSchema), method(object({
56621
56806
  /** Optional hostname override the host should advertise once joined. */
56622
56807
  hostname: string().optional() }), object({
56623
56808
  /** Authentication URL the operator should open in a browser. */
56624
- 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({
56625
- /** Optional auth key — when provided, probes the key validity
56626
- * against the provider's API. Omit when already joined to
56627
- * just ping the daemon. */
56628
- authKey: string().optional() }), object({
56629
- ok: boolean(),
56630
- /** Provider-side identifier resolved by the probe (tailnet
56631
- * name for Tailscale, network id for ZeroTier, etc.). */
56632
- tenant: string().optional(),
56633
- /** Daemon binary version, when reachable. */
56634
- daemonVersion: string().optional(),
56635
- /** Human-readable error when `ok: false`. */
56636
- error: string().optional()
56637
- }), { kind: "mutation" });
56809
+ 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() }));
56638
56810
  object({
56639
56811
  value: number().nullable().meta(readingMeta("device", { label: "Total" })),
56640
56812
  lastFetchedAt: number()
@@ -64013,6 +64185,11 @@ var NcScheduleSchema = object({
64013
64185
  * as a STATE SUMMARY at the end of each window of `schedule`.
64014
64186
  */
64015
64187
  var NcSystemEventDeliveryModeSchema = _enum(["instant", "summary"]);
64188
+ _enum([
64189
+ "activation",
64190
+ "deactivation",
64191
+ "schedule"
64192
+ ]);
64016
64193
  /**
64017
64194
  * How a summary window is SENT (D698): `rule` (absent) = ONE notification with
64018
64195
  * every section; `section` = one notification per section with something to
@@ -64029,11 +64206,26 @@ var NcStateSummarySplitSchema = _enum(["rule", "section"]);
64029
64206
  * midnight crossing); the send time is each window's END. Refused on the
64030
64207
  * `alarm` family, which stays instant (`systemEventDeliveryProblems`).
64031
64208
  */
64032
- var NcSystemEventDeliverySchema = discriminatedUnion("mode", [object({ mode: literal("instant") }), object({
64033
- mode: literal("summary"),
64034
- schedule: NcScheduleSchema,
64035
- split: NcStateSummarySplitSchema.optional()
64036
- })]);
64209
+ var NcSystemEventDeliverySchema = discriminatedUnion("mode", [
64210
+ object({ mode: literal("instant") }),
64211
+ object({
64212
+ mode: literal("summary"),
64213
+ schedule: NcScheduleSchema,
64214
+ split: NcStateSummarySplitSchema.optional()
64215
+ }),
64216
+ object({
64217
+ mode: literal("modes"),
64218
+ /** The bad edge, and every kind that has a single edge. */
64219
+ onActivation: boolean(),
64220
+ /** The recovery edges of the classes of the selected kinds (derived, never stored per kind). */
64221
+ onDeactivation: boolean(),
64222
+ /** A state summary at the end of each window; absent = no summary. */
64223
+ onSchedule: object({
64224
+ schedule: NcScheduleSchema,
64225
+ grouping: NcStateSummarySplitSchema
64226
+ }).optional()
64227
+ })
64228
+ ]);
64037
64229
  /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
64038
64230
  var NcPlateMatcherSchema = object({
64039
64231
  /**
@@ -66310,61 +66502,6 @@ var OauthIntegrationDescriptorSchema = object({
66310
66502
  });
66311
66503
  method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
66312
66504
  /**
66313
- * `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
66314
- *
66315
- * One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
66316
- * ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
66317
- * Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
66318
- * immutable after create.
66319
- */
66320
- var ConfigSchemaPassthrough$1 = unknown();
66321
- var OidcConnectionSchema = object({
66322
- id: string().min(1),
66323
- displayName: string(),
66324
- icon: string(),
66325
- issuerUrl: string(),
66326
- clientId: string(),
66327
- /** Client id + secret present — never the secret value. */
66328
- configured: boolean(),
66329
- /**
66330
- * Values for the connection form schema. `clientSecret` is a keychain
66331
- * {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
66332
- */
66333
- formValues: record(string(), unknown()).optional()
66334
- });
66335
- /**
66336
- * Patch one connection. Omitted or empty `clientSecret` keeps the stored
66337
- * secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
66338
- */
66339
- var OidcConnectionUpdateInputSchema = object({
66340
- id: string().min(1),
66341
- displayName: string().min(1),
66342
- icon: string().optional(),
66343
- issuerUrl: string().optional(),
66344
- clientId: string().optional(),
66345
- clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
66346
- redirectUri: string().optional(),
66347
- scopes: string().optional(),
66348
- usernameClaim: _enum([
66349
- "preferred_username",
66350
- "email",
66351
- "sub"
66352
- ]).optional()
66353
- });
66354
- method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
66355
- id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
66356
- displayName: string().min(1)
66357
- }), object({ id: string() }), {
66358
- kind: "mutation",
66359
- auth: "admin"
66360
- }), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
66361
- kind: "mutation",
66362
- auth: "admin"
66363
- }), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
66364
- kind: "mutation",
66365
- auth: "admin"
66366
- });
66367
- /**
66368
66505
  * Generic on-screen-display (video overlay) cap. Each camera exposes
66369
66506
  * a firmware-defined list of overlays (channel name, timestamp,
66370
66507
  * watermark, logo, …); the cap models them as a homogeneous array of
@@ -72537,21 +72674,6 @@ method(object({
72537
72674
  */
72538
72675
  ttlSec: union([number().int().positive(), literal("never")]).optional()
72539
72676
  }), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
72540
- var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
72541
- providerId: string().min(1),
72542
- displayName: string().min(1),
72543
- configSchema: unknown(),
72544
- nodeLocal: boolean(),
72545
- shouldSaveDiskSpace: literal(true),
72546
- minFreePercent: number().min(0).max(100)
72547
- }), object({
72548
- providerId: string().min(1),
72549
- displayName: string().min(1),
72550
- configSchema: unknown(),
72551
- nodeLocal: boolean(),
72552
- shouldSaveDiskSpace: literal(false),
72553
- minFreePercent: literal(null)
72554
- })]);
72555
72677
  method(object({
72556
72678
  location: StorageLocationRefSchema,
72557
72679
  relativePath: string()
@@ -72613,42 +72735,7 @@ method(object({
72613
72735
  }), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
72614
72736
  kind: "mutation",
72615
72737
  auth: "admin"
72616
- }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
72617
- createdAt: true,
72618
- updatedAt: true
72619
- }), StorageLocationSchema, {
72620
- kind: "mutation",
72621
- auth: "admin"
72622
- }), method(object({
72623
- id: string(),
72624
- force: boolean().optional()
72625
- }), _void(), {
72626
- kind: "mutation",
72627
- auth: "admin"
72628
- }), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
72629
- ok: boolean(),
72630
- error: string().optional()
72631
- }), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
72632
- providerId: string(),
72633
- /**
72634
- * The location this config is an UNSAVED edit of, when there is one.
72635
- *
72636
- * `listLocations` replaces every declared secret with the redaction
72637
- * sentinel, so the edit modal's form state holds the sentinel for any
72638
- * credential the operator did not retype — and posting that here
72639
- * without a way to resolve it makes the provider try to authenticate
72640
- * as `__camstack_redacted__` and report the operator's own working
72641
- * password as wrong. Given this id, the orchestrator restores each
72642
- * sentinel from the stored config (same rule as `upsertLocation`)
72643
- * before dispatching. Omitted by the "Add location" wizard, where
72644
- * every value was typed just now and nothing is stored yet.
72645
- */
72646
- locationId: string().optional(),
72647
- config: record(string(), unknown())
72648
- }), object({
72649
- ok: boolean(),
72650
- error: string().optional()
72651
- }), { auth: "admin" });
72738
+ }), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
72652
72739
  /** One table of the settings database. */
72653
72740
  var StorageTableSchema = object({
72654
72741
  /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
@@ -73879,7 +73966,7 @@ var TerminalInstanceInfoSchema = object({
73879
73966
  /**
73880
73967
  * False while the node the instance is bound to is not in the cluster
73881
73968
  * topology (forgotten, renamed, re-paired). The instance is kept; the
73882
- * operator re-points it with `updateInstance({ targetNodeId })`.
73969
+ * operator re-points it on the Terminal page.
73883
73970
  */
73884
73971
  nodeInCluster: boolean(),
73885
73972
  profileId: string(),
@@ -73917,41 +74004,7 @@ var TerminalOutputBatchSchema = object({
73917
74004
  snapshot: string().optional(),
73918
74005
  events: array(TerminalOutputEventSchema).readonly()
73919
74006
  });
73920
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
73921
- targetNodeId: string().min(1),
73922
- profileId: string().min(1),
73923
- name: string().trim().min(1).max(160).optional(),
73924
- executable: string().max(1024).optional(),
73925
- args: array(string().max(2048)).max(64).optional(),
73926
- cwd: string().max(1024).optional(),
73927
- environment: array(string().max(4096)).max(64).optional(),
73928
- profileSettings: ProfileSettingsBagSchema.optional()
73929
- }), TerminalInstanceInfoSchema, {
73930
- kind: "mutation",
73931
- auth: "admin"
73932
- }), method(object({
73933
- instanceId: string().min(1),
73934
- /** Re-point the instance to another node; the profile must exist there. */
73935
- targetNodeId: string().min(1).max(256).optional(),
73936
- name: string().trim().min(1).max(160).optional(),
73937
- executable: string().max(1024).optional(),
73938
- args: array(string().max(2048)).max(64).optional(),
73939
- cwd: string().max(1024).optional(),
73940
- environment: array(string().max(4096)).max(64).optional(),
73941
- profileSettings: ProfileSettingsBagSchema.optional()
73942
- }), TerminalInstanceInfoSchema, {
73943
- kind: "mutation",
73944
- auth: "admin"
73945
- }), method(object({ instanceId: string().min(1) }), _void(), {
73946
- kind: "mutation",
73947
- auth: "admin"
73948
- }), method(object({
73949
- instanceId: string().min(1),
73950
- enabled: boolean()
73951
- }), TerminalInstanceInfoSchema, {
73952
- kind: "mutation",
73953
- auth: "admin"
73954
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
74007
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
73955
74008
  stableId: string().min(1),
73956
74009
  name: string().trim().min(1).max(160).optional()
73957
74010
  }), TerminalInstanceInfoSchema, {
@@ -74020,6 +74073,11 @@ var TimedRunUnitSchema = _enum(["s", "min"]);
74020
74073
  _enum(["switch", "pet-fountain"]);
74021
74074
  DeviceType.Heater, DeviceType.PetFountain;
74022
74075
  DeviceType.Heater, DeviceType.PetFountain;
74076
+ /** One duration, as the one-tap run asks for it. */
74077
+ var TimedRunQuickRunSchema = object({
74078
+ duration: number().positive().finite(),
74079
+ unit: TimedRunUnitSchema
74080
+ });
74023
74081
  /** Why a request was not carried out. Typed, so a caller can say it in words. */
74024
74082
  var TimedRunRefusalSchema = _enum([
74025
74083
  "unsupported-device",
@@ -74040,9 +74098,30 @@ object({
74040
74098
  activeUntil: number().nullable(),
74041
74099
  /** Who asked for the run (a user id or an addon id); `null` when nothing is armed or it did not say. */
74042
74100
  requestedBy: string().nullable(),
74101
+ /**
74102
+ * The one-tap run of THIS device: what a "Run" button on any surface starts. `null` when
74103
+ * the device is gone or its type has no timed run; a status from before this field
74104
+ * existed has none, and a surface falls back to the type's default.
74105
+ */
74106
+ quickRun: TimedRunQuickRunSchema.nullable(),
74043
74107
  lastFetchedAt: number()
74044
74108
  });
74045
- method(object({
74109
+ /** What the settings form needs: the unit, the bounds and the type's default. */
74110
+ var TimedRunOptionsSchema = object({
74111
+ unit: TimedRunUnitSchema,
74112
+ /** The longest run, in `unit`. */
74113
+ maxDuration: number(),
74114
+ defaultDuration: number()
74115
+ });
74116
+ /** The one-tap run duration, in the type's unit; `null` returns to the type's default. */
74117
+ var TimedRunSettingsPatchSchema = object({ quickRunDuration: number().positive().finite().nullable().optional() });
74118
+ method(object({ deviceId: number().int() }), TimedRunOptionsSchema), method(object({
74119
+ deviceId: number().int(),
74120
+ settings: TimedRunSettingsPatchSchema
74121
+ }), _void(), {
74122
+ kind: "mutation",
74123
+ auth: "admin"
74124
+ }), method(object({
74046
74125
  deviceId: number().int(),
74047
74126
  duration: number().positive().finite(),
74048
74127
  unit: TimedRunUnitSchema
@@ -76748,21 +76827,28 @@ Object.freeze({
76748
76827
  access: "view",
76749
76828
  role: "admin"
76750
76829
  },
76751
- "addonSettings.getGlobalSettings": {
76830
+ "addonSettings.getIntegrationFormSchema": {
76831
+ capName: "addon-settings",
76832
+ capScope: "system",
76833
+ addonId: null,
76834
+ access: "view",
76835
+ role: "admin"
76836
+ },
76837
+ "addonSettings.getScopedSettings": {
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.getIntegrationFormSchema": {
76844
+ "addonSettings.listScopedSettings": {
76759
76845
  capName: "addon-settings",
76760
76846
  capScope: "system",
76761
76847
  addonId: null,
76762
76848
  access: "view",
76763
76849
  role: "admin"
76764
76850
  },
76765
- "addonSettings.getIntegrationSettings": {
76851
+ "addonSettings.readScopedValues": {
76766
76852
  capName: "addon-settings",
76767
76853
  capScope: "system",
76768
76854
  addonId: null,
@@ -76783,7 +76869,7 @@ Object.freeze({
76783
76869
  access: "create",
76784
76870
  role: "admin"
76785
76871
  },
76786
- "addonSettings.updateGlobalSettings": {
76872
+ "addonSettings.updateScopedSettings": {
76787
76873
  capName: "addon-settings",
76788
76874
  capScope: "system",
76789
76875
  addonId: null,
@@ -78953,6 +79039,13 @@ Object.freeze({
78953
79039
  access: "create",
78954
79040
  role: "admin"
78955
79041
  },
79042
+ "deviceManager.setAlias": {
79043
+ capName: "device-manager",
79044
+ capScope: "system",
79045
+ addonId: null,
79046
+ access: "create",
79047
+ role: "admin"
79048
+ },
78956
79049
  "deviceManager.setChildLayout": {
78957
79050
  capName: "device-manager",
78958
79051
  capScope: "system",
@@ -80241,13 +80334,6 @@ Object.freeze({
80241
80334
  access: "delete",
80242
80335
  role: "admin"
80243
80336
  },
80244
- "llm.deleteProfile": {
80245
- capName: "llm",
80246
- capScope: "system",
80247
- addonId: null,
80248
- access: "delete",
80249
- role: "admin"
80250
- },
80251
80337
  "llm.generate": {
80252
80338
  capName: "llm",
80253
80339
  capScope: "system",
@@ -80311,20 +80397,6 @@ Object.freeze({
80311
80397
  access: "view",
80312
80398
  role: "admin"
80313
80399
  },
80314
- "llm.listProfileKinds": {
80315
- capName: "llm",
80316
- capScope: "system",
80317
- addonId: null,
80318
- access: "view",
80319
- role: "admin"
80320
- },
80321
- "llm.listProfiles": {
80322
- capName: "llm",
80323
- capScope: "system",
80324
- addonId: null,
80325
- access: "view",
80326
- role: "admin"
80327
- },
80328
80400
  "llm.listRuntimeNodes": {
80329
80401
  capName: "llm",
80330
80402
  capScope: "system",
@@ -80332,13 +80404,6 @@ Object.freeze({
80332
80404
  access: "view",
80333
80405
  role: "admin"
80334
80406
  },
80335
- "llm.migrateRawSecrets": {
80336
- capName: "llm",
80337
- capScope: "system",
80338
- addonId: null,
80339
- access: "create",
80340
- role: "admin"
80341
- },
80342
80407
  "llm.resolveModelRef": {
80343
80408
  capName: "llm",
80344
80409
  capScope: "system",
@@ -80367,20 +80432,6 @@ Object.freeze({
80367
80432
  access: "create",
80368
80433
  role: "admin"
80369
80434
  },
80370
- "llm.testProfile": {
80371
- capName: "llm",
80372
- capScope: "system",
80373
- addonId: null,
80374
- access: "create",
80375
- role: "admin"
80376
- },
80377
- "llm.upsertProfile": {
80378
- capName: "llm",
80379
- capScope: "system",
80380
- addonId: null,
80381
- access: "create",
80382
- role: "admin"
80383
- },
80384
80435
  "llmRuntime.appleAvailability": {
80385
80436
  capName: "llm-runtime",
80386
80437
  capScope: "system",
@@ -80885,13 +80936,6 @@ Object.freeze({
80885
80936
  access: "create",
80886
80937
  role: "admin"
80887
80938
  },
80888
- "meshNetwork.testConnection": {
80889
- capName: "mesh-network",
80890
- capScope: "system",
80891
- addonId: null,
80892
- access: "create",
80893
- role: "admin"
80894
- },
80895
80939
  "meter.getStatus": {
80896
80940
  capName: "meter",
80897
80941
  capScope: "device",
@@ -81550,13 +81594,6 @@ Object.freeze({
81550
81594
  access: "create",
81551
81595
  role: "admin"
81552
81596
  },
81553
- "notificationOutput.deleteTarget": {
81554
- capName: "notification-output",
81555
- capScope: "system",
81556
- addonId: null,
81557
- access: "delete",
81558
- role: "admin"
81559
- },
81560
81597
  "notificationOutput.discoverTargets": {
81561
81598
  capName: "notification-output",
81562
81599
  capScope: "system",
@@ -81564,13 +81601,6 @@ Object.freeze({
81564
81601
  access: "view",
81565
81602
  role: "admin"
81566
81603
  },
81567
- "notificationOutput.getTarget": {
81568
- capName: "notification-output",
81569
- capScope: "system",
81570
- addonId: null,
81571
- access: "view",
81572
- role: "admin"
81573
- },
81574
81604
  "notificationOutput.listTargetKinds": {
81575
81605
  capName: "notification-output",
81576
81606
  capScope: "system",
@@ -81578,13 +81608,6 @@ Object.freeze({
81578
81608
  access: "view",
81579
81609
  role: "admin"
81580
81610
  },
81581
- "notificationOutput.listTargets": {
81582
- capName: "notification-output",
81583
- capScope: "system",
81584
- addonId: null,
81585
- access: "view",
81586
- role: "admin"
81587
- },
81588
81611
  "notificationOutput.probeTargetConfig": {
81589
81612
  capName: "notification-output",
81590
81613
  capScope: "system",
@@ -81599,13 +81622,6 @@ Object.freeze({
81599
81622
  access: "create",
81600
81623
  role: "admin"
81601
81624
  },
81602
- "notificationOutput.setTargetEnabled": {
81603
- capName: "notification-output",
81604
- capScope: "system",
81605
- addonId: null,
81606
- access: "create",
81607
- role: "admin"
81608
- },
81609
81625
  "notificationOutput.testTarget": {
81610
81626
  capName: "notification-output",
81611
81627
  capScope: "system",
@@ -81613,13 +81629,6 @@ Object.freeze({
81613
81629
  access: "create",
81614
81630
  role: "admin"
81615
81631
  },
81616
- "notificationOutput.upsertTarget": {
81617
- capName: "notification-output",
81618
- capScope: "system",
81619
- addonId: null,
81620
- access: "create",
81621
- role: "admin"
81622
- },
81623
81632
  "notificationRules.activateAlarmProfile": {
81624
81633
  capName: "notification-rules",
81625
81634
  capScope: "system",
@@ -81879,41 +81888,6 @@ Object.freeze({
81879
81888
  access: "view",
81880
81889
  role: "admin"
81881
81890
  },
81882
- "oidcConnections.connectionFormSchema": {
81883
- capName: "oidc-connections",
81884
- capScope: "system",
81885
- addonId: null,
81886
- access: "view",
81887
- role: "admin"
81888
- },
81889
- "oidcConnections.createConnection": {
81890
- capName: "oidc-connections",
81891
- capScope: "system",
81892
- addonId: null,
81893
- access: "create",
81894
- role: "admin"
81895
- },
81896
- "oidcConnections.deleteConnection": {
81897
- capName: "oidc-connections",
81898
- capScope: "system",
81899
- addonId: null,
81900
- access: "delete",
81901
- role: "admin"
81902
- },
81903
- "oidcConnections.listConnections": {
81904
- capName: "oidc-connections",
81905
- capScope: "system",
81906
- addonId: null,
81907
- access: "view",
81908
- role: "admin"
81909
- },
81910
- "oidcConnections.updateConnection": {
81911
- capName: "oidc-connections",
81912
- capScope: "system",
81913
- addonId: null,
81914
- access: "create",
81915
- role: "admin"
81916
- },
81917
81891
  "osd.getStatus": {
81918
81892
  capName: "osd",
81919
81893
  capScope: "device",
@@ -84945,13 +84919,6 @@ Object.freeze({
84945
84919
  access: "delete",
84946
84920
  role: "admin"
84947
84921
  },
84948
- "storage.deleteLocation": {
84949
- capName: "storage",
84950
- capScope: "system",
84951
- addonId: null,
84952
- access: "delete",
84953
- role: "admin"
84954
- },
84955
84922
  "storage.endDownload": {
84956
84923
  capName: "storage",
84957
84924
  capScope: "system",
@@ -85008,13 +84975,6 @@ Object.freeze({
85008
84975
  access: "view",
85009
84976
  role: "admin"
85010
84977
  },
85011
- "storage.listProviders": {
85012
- capName: "storage",
85013
- capScope: "system",
85014
- addonId: null,
85015
- access: "view",
85016
- role: "admin"
85017
- },
85018
84978
  "storage.read": {
85019
84979
  capName: "storage",
85020
84980
  capScope: "system",
@@ -85036,27 +84996,6 @@ Object.freeze({
85036
84996
  access: "view",
85037
84997
  role: "admin"
85038
84998
  },
85039
- "storage.testConfig": {
85040
- capName: "storage",
85041
- capScope: "system",
85042
- addonId: null,
85043
- access: "view",
85044
- role: "admin"
85045
- },
85046
- "storage.testLocation": {
85047
- capName: "storage",
85048
- capScope: "system",
85049
- addonId: null,
85050
- access: "view",
85051
- role: "admin"
85052
- },
85053
- "storage.upsertLocation": {
85054
- capName: "storage",
85055
- capScope: "system",
85056
- addonId: null,
85057
- access: "create",
85058
- role: "admin"
85059
- },
85060
84999
  "storage.write": {
85061
85000
  capName: "storage",
85062
85001
  capScope: "system",
@@ -85834,27 +85773,6 @@ Object.freeze({
85834
85773
  access: "create",
85835
85774
  role: "admin"
85836
85775
  },
85837
- "terminalSession.createInstance": {
85838
- capName: "terminal-session",
85839
- capScope: "system",
85840
- addonId: null,
85841
- access: "create",
85842
- role: "admin"
85843
- },
85844
- "terminalSession.deleteInstance": {
85845
- capName: "terminal-session",
85846
- capScope: "system",
85847
- addonId: null,
85848
- access: "delete",
85849
- role: "admin"
85850
- },
85851
- "terminalSession.listInstances": {
85852
- capName: "terminal-session",
85853
- capScope: "system",
85854
- addonId: null,
85855
- access: "view",
85856
- role: "admin"
85857
- },
85858
85776
  "terminalSession.listLegacyCameras": {
85859
85777
  capName: "terminal-session",
85860
85778
  capScope: "system",
@@ -85897,20 +85815,6 @@ Object.freeze({
85897
85815
  access: "create",
85898
85816
  role: "admin"
85899
85817
  },
85900
- "terminalSession.setInstanceEnabled": {
85901
- capName: "terminal-session",
85902
- capScope: "system",
85903
- addonId: null,
85904
- access: "create",
85905
- role: "admin"
85906
- },
85907
- "terminalSession.updateInstance": {
85908
- capName: "terminal-session",
85909
- capScope: "system",
85910
- addonId: null,
85911
- access: "create",
85912
- role: "admin"
85913
- },
85914
85818
  "terminalSession.writeInput": {
85915
85819
  capName: "terminal-session",
85916
85820
  capScope: "system",
@@ -85925,6 +85829,13 @@ Object.freeze({
85925
85829
  access: "create",
85926
85830
  role: "viewer"
85927
85831
  },
85832
+ "timedRun.getOptions": {
85833
+ capName: "timed-run",
85834
+ capScope: "device",
85835
+ addonId: null,
85836
+ access: "view",
85837
+ role: "viewer"
85838
+ },
85928
85839
  "timedRun.getStatus": {
85929
85840
  capName: "timed-run",
85930
85841
  capScope: "device",
@@ -85939,6 +85850,13 @@ Object.freeze({
85939
85850
  access: "create",
85940
85851
  role: "viewer"
85941
85852
  },
85853
+ "timedRun.setSettings": {
85854
+ capName: "timed-run",
85855
+ capScope: "device",
85856
+ addonId: null,
85857
+ access: "create",
85858
+ role: "admin"
85859
+ },
85942
85860
  "toast.onToast": {
85943
85861
  capName: "toast",
85944
85862
  capScope: "system",
@@ -86869,6 +86787,7 @@ function offered(received) {
86869
86787
  };
86870
86788
  }
86871
86789
  offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
86790
+ string().min(1).max(4096);
86872
86791
  Object.freeze({
86873
86792
  "accessories.getStatus": [{
86874
86793
  name: "deviceId",
@@ -87552,6 +87471,11 @@ Object.freeze({
87552
87471
  form: "single",
87553
87472
  optional: false
87554
87473
  }],
87474
+ "deviceManager.setAlias": [{
87475
+ name: "deviceId",
87476
+ form: "single",
87477
+ optional: false
87478
+ }],
87555
87479
  "deviceManager.setChildLayout": [{
87556
87480
  name: "deviceId",
87557
87481
  form: "single",
@@ -89616,6 +89540,11 @@ Object.freeze({
89616
89540
  form: "single",
89617
89541
  optional: false
89618
89542
  }],
89543
+ "timedRun.getOptions": [{
89544
+ name: "deviceId",
89545
+ form: "single",
89546
+ optional: false
89547
+ }],
89619
89548
  "timedRun.getStatus": [{
89620
89549
  name: "deviceId",
89621
89550
  form: "single",
@@ -89626,6 +89555,11 @@ Object.freeze({
89626
89555
  form: "single",
89627
89556
  optional: false
89628
89557
  }],
89558
+ "timedRun.setSettings": [{
89559
+ name: "deviceId",
89560
+ form: "single",
89561
+ optional: false
89562
+ }],
89629
89563
  "update.getStatus": [{
89630
89564
  name: "deviceId",
89631
89565
  form: "single",
@@ -90488,6 +90422,7 @@ var STATE_SUMMARY_VARS = [
90488
90422
  [...RULE_TEXT],
90489
90423
  [...RULE_TEXT],
90490
90424
  [...RULE_TEXT],
90425
+ [...RULE_TEXT],
90491
90426
  [...RULE_BODY_ONLY],
90492
90427
  [...RULE_TEXT],
90493
90428
  ([...RULE_TEXT], [...DEVICE_EVENTS]),