@camstack/addon-matter-broker 0.2.36 → 0.2.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -5930,6 +5930,40 @@ var BaseAddon = class {
5930
5930
  deviceSettingsSchema() {
5931
5931
  return null;
5932
5932
  }
5933
+ /**
5934
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
5935
+ * ARE the configuration of its integration.
5936
+ *
5937
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
5938
+ * operator should find on the addon's integration page (System →
5939
+ * Integrations → <name>) rather than only in the cluster-wide list of every
5940
+ * addon. Empty (the default) means the addon has no integration-level
5941
+ * settings and no such surface is offered — this is opt-in, because whether
5942
+ * an addon's configuration IS its integration's configuration depends on the
5943
+ * nature of the integration.
5944
+ *
5945
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
5946
+ * the ONE global schema, in the ONE addon store, written by the ONE
5947
+ * `updateGlobalSettings` path. There is deliberately no
5948
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
5949
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
5950
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
5951
+ *
5952
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
5953
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
5954
+ * removed with the reason recorded at
5955
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
5956
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
5957
+ * marker sprinkled across sections also has to borrow a field that already
5958
+ * means something else; borrowing `section.tab` put the literal word
5959
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
5960
+ * GROUP this visually" and cannot also mean "where this lives" (D269
5961
+ * supersedes D268). One declaration, in one place, next to the schema whose
5962
+ * ids it names.
5963
+ */
5964
+ integrationSettingSections() {
5965
+ return [];
5966
+ }
5933
5967
  async getGlobalSettings(overlay, cap, nodeId) {
5934
5968
  const schema = this.globalSettingsSchema(cap);
5935
5969
  if (!schema) return { sections: [] };
@@ -5940,6 +5974,55 @@ var BaseAddon = class {
5940
5974
  } : projected);
5941
5975
  }
5942
5976
  /**
5977
+ * The integration-level view of this addon's settings: exactly the sections
5978
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
5979
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
5980
+ *
5981
+ * Returns `null` when the addon declared nothing — an addon that opts out has
5982
+ * no integration settings surface at all, rather than an empty one that reads
5983
+ * as a failed load.
5984
+ *
5985
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
5986
+ * and not in whichever UI happens to render this:
5987
+ *
5988
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
5989
+ * shown here is the same field, with the same bare key, that the addon's
5990
+ * own page shows. There is no integration-specific writer — callers save
5991
+ * through `updateGlobalSettings` — so a second store key is unreachable,
5992
+ * not merely discouraged.
5993
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
5994
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
5995
+ * such a field silently picked would be a wrong answer for the operator
5996
+ * who opened the page (D266).
5997
+ * 3. **No silent typo.** A declared id that names no section throws. The
5998
+ * alternative — skip it — turns a rename into a surface that quietly
5999
+ * empties, which looks exactly like an addon with nothing to configure.
6000
+ */
6001
+ async getIntegrationSettings(nodeId) {
6002
+ const declared = this.integrationSettingSections();
6003
+ if (declared.length === 0) return null;
6004
+ const schema = this.globalSettingsSchema();
6005
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
6006
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
6007
+ const sections = [];
6008
+ for (const id of declared) {
6009
+ const section = byId.get(id);
6010
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
6011
+ const fields = dropPerNodeFields(section.fields);
6012
+ if (fields.length === 0) continue;
6013
+ sections.push({
6014
+ ...section,
6015
+ fields
6016
+ });
6017
+ }
6018
+ if (sections.length === 0) return null;
6019
+ const projected = await this.resolveGlobalStore(nodeId);
6020
+ return hydrateSchema({
6021
+ ...schema,
6022
+ sections
6023
+ }, projected);
6024
+ }
6025
+ /**
5943
6026
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
5944
6027
  * every `perNode: true` field carries THAT node's scoped value on its bare
5945
6028
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -6243,6 +6326,41 @@ var BaseAddon = class {
6243
6326
  * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6244
6327
  * don't declare `perNode` and are excluded by the `in` narrowing.
6245
6328
  */
6329
+ /**
6330
+ * The same fields with every `perNode: true` one removed, recursing into layout
6331
+ * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
6332
+ * with no child is dropped rather than rendered empty.
6333
+ *
6334
+ * Used by `getIntegrationSettings`: an integration is not a node, so a field
6335
+ * whose store key is `<key>@<nodeId>` has no node to belong to there.
6336
+ */
6337
+ function dropPerNodeFields(fields) {
6338
+ const kept = [];
6339
+ for (const field of fields) {
6340
+ if (field.type === "group") {
6341
+ const inner = dropPerNodeFields(field.fields);
6342
+ if (inner.length > 0) kept.push({
6343
+ ...field,
6344
+ fields: inner
6345
+ });
6346
+ continue;
6347
+ }
6348
+ if (field.type === "sub-tabs") {
6349
+ const tabs = field.tabs.map((tab) => ({
6350
+ ...tab,
6351
+ fields: dropPerNodeFields(tab.fields)
6352
+ })).filter((tab) => tab.fields.length > 0);
6353
+ if (tabs.length > 0) kept.push({
6354
+ ...field,
6355
+ tabs
6356
+ });
6357
+ continue;
6358
+ }
6359
+ if ("perNode" in field && field.perNode === true) continue;
6360
+ kept.push(field);
6361
+ }
6362
+ return kept;
6363
+ }
6246
6364
  function collectPerNodeFieldKeys(fields) {
6247
6365
  const collected = [];
6248
6366
  for (const field of fields) {
@@ -9412,6 +9530,9 @@ method(object({
9412
9530
  kind: "mutation",
9413
9531
  auth: "admin"
9414
9532
  }), method(object({
9533
+ addonId: string$2(),
9534
+ nodeId: string$2().optional()
9535
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
9415
9536
  addonId: string$2(),
9416
9537
  deviceId: number(),
9417
9538
  nodeId: string$2().optional()
@@ -31588,6 +31709,12 @@ Object.freeze({
31588
31709
  addonId: null,
31589
31710
  access: "view"
31590
31711
  },
31712
+ "addonSettings.getIntegrationSettings": {
31713
+ capName: "addon-settings",
31714
+ capScope: "system",
31715
+ addonId: null,
31716
+ access: "view"
31717
+ },
31591
31718
  "addonSettings.updateDeviceSettings": {
31592
31719
  capName: "addon-settings",
31593
31720
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -5928,6 +5928,40 @@ var BaseAddon = class {
5928
5928
  deviceSettingsSchema() {
5929
5929
  return null;
5930
5930
  }
5931
+ /**
5932
+ * INTEGRATION-LEVEL SETTINGS — declare which of this addon's global sections
5933
+ * ARE the configuration of its integration.
5934
+ *
5935
+ * Return the `ConfigSection.id`s, from {@link globalSettingsSchema}, that an
5936
+ * operator should find on the addon's integration page (System →
5937
+ * Integrations → <name>) rather than only in the cluster-wide list of every
5938
+ * addon. Empty (the default) means the addon has no integration-level
5939
+ * settings and no such surface is offered — this is opt-in, because whether
5940
+ * an addon's configuration IS its integration's configuration depends on the
5941
+ * nature of the integration.
5942
+ *
5943
+ * WHAT THIS IS NOT. It is not a scope. The selected sections keep living in
5944
+ * the ONE global schema, in the ONE addon store, written by the ONE
5945
+ * `updateGlobalSettings` path. There is deliberately no
5946
+ * `updateIntegrationSettings`: a second write path is how a surface acquires
5947
+ * a second store key, and this repo has shipped that twice (`btmPath@hub`,
5948
+ * D266). Selecting sections cannot introduce a key that selecting cannot.
5949
+ *
5950
+ * WHY IT IS A LIST OF SECTION IDS AND NOT A MARKER ON THE SECTION.
5951
+ * `ConfigFieldBase` used to carry `scope?: 'device' | 'global'` and it was
5952
+ * removed with the reason recorded at
5953
+ * `packages/types/src/interfaces/config-ui.ts:249` — *"a field's scope is
5954
+ * determined by WHICH schema it lives in, not by a field-level marker."* A
5955
+ * marker sprinkled across sections also has to borrow a field that already
5956
+ * means something else; borrowing `section.tab` put the literal word
5957
+ * "integration" into an operator-facing tab bar, because `tab` means "how to
5958
+ * GROUP this visually" and cannot also mean "where this lives" (D269
5959
+ * supersedes D268). One declaration, in one place, next to the schema whose
5960
+ * ids it names.
5961
+ */
5962
+ integrationSettingSections() {
5963
+ return [];
5964
+ }
5931
5965
  async getGlobalSettings(overlay, cap, nodeId) {
5932
5966
  const schema = this.globalSettingsSchema(cap);
5933
5967
  if (!schema) return { sections: [] };
@@ -5938,6 +5972,55 @@ var BaseAddon = class {
5938
5972
  } : projected);
5939
5973
  }
5940
5974
  /**
5975
+ * The integration-level view of this addon's settings: exactly the sections
5976
+ * named by {@link integrationSettingSections}, hydrated from the SAME store
5977
+ * `getGlobalSettings` reads, and narrowed to cluster-scoped fields.
5978
+ *
5979
+ * Returns `null` when the addon declared nothing — an addon that opts out has
5980
+ * no integration settings surface at all, rather than an empty one that reads
5981
+ * as a failed load.
5982
+ *
5983
+ * Three properties hold BY CONSTRUCTION, which is why they are here in core
5984
+ * and not in whichever UI happens to render this:
5985
+ *
5986
+ * 1. **One key.** The payload is a SUBSET of the global schema, so a field
5987
+ * shown here is the same field, with the same bare key, that the addon's
5988
+ * own page shows. There is no integration-specific writer — callers save
5989
+ * through `updateGlobalSettings` — so a second store key is unreachable,
5990
+ * not merely discouraged.
5991
+ * 2. **No node scope.** `perNode: true` fields are DROPPED. Their store key
5992
+ * is `<key>@<nodeId>` and an integration is not a node; whichever node
5993
+ * such a field silently picked would be a wrong answer for the operator
5994
+ * who opened the page (D266).
5995
+ * 3. **No silent typo.** A declared id that names no section throws. The
5996
+ * alternative — skip it — turns a rename into a surface that quietly
5997
+ * empties, which looks exactly like an addon with nothing to configure.
5998
+ */
5999
+ async getIntegrationSettings(nodeId) {
6000
+ const declared = this.integrationSettingSections();
6001
+ if (declared.length === 0) return null;
6002
+ const schema = this.globalSettingsSchema();
6003
+ if (!schema) throw new Error(`${this.constructor.name}: integrationSettingSections() names [${declared.join(", ")}] but globalSettingsSchema() returns null.`);
6004
+ const byId = new Map(schema.sections.map((section) => [section.id, section]));
6005
+ const sections = [];
6006
+ for (const id of declared) {
6007
+ const section = byId.get(id);
6008
+ if (!section) throw new Error(`${this.constructor.name}: integrationSettingSections() names unknown section "${id}". Known sections: [${[...byId.keys()].join(", ")}].`);
6009
+ const fields = dropPerNodeFields(section.fields);
6010
+ if (fields.length === 0) continue;
6011
+ sections.push({
6012
+ ...section,
6013
+ fields
6014
+ });
6015
+ }
6016
+ if (sections.length === 0) return null;
6017
+ const projected = await this.resolveGlobalStore(nodeId);
6018
+ return hydrateSchema({
6019
+ ...schema,
6020
+ sections
6021
+ }, projected);
6022
+ }
6023
+ /**
5941
6024
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
5942
6025
  * every `perNode: true` field carries THAT node's scoped value on its bare
5943
6026
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -6241,6 +6324,41 @@ var BaseAddon = class {
6241
6324
  * `hydrateSchema` does. Valueless structural fields (separator/info/…)
6242
6325
  * don't declare `perNode` and are excluded by the `in` narrowing.
6243
6326
  */
6327
+ /**
6328
+ * The same fields with every `perNode: true` one removed, recursing into layout
6329
+ * containers exactly as {@link collectPerNodeFieldKeys} does. A container left
6330
+ * with no child is dropped rather than rendered empty.
6331
+ *
6332
+ * Used by `getIntegrationSettings`: an integration is not a node, so a field
6333
+ * whose store key is `<key>@<nodeId>` has no node to belong to there.
6334
+ */
6335
+ function dropPerNodeFields(fields) {
6336
+ const kept = [];
6337
+ for (const field of fields) {
6338
+ if (field.type === "group") {
6339
+ const inner = dropPerNodeFields(field.fields);
6340
+ if (inner.length > 0) kept.push({
6341
+ ...field,
6342
+ fields: inner
6343
+ });
6344
+ continue;
6345
+ }
6346
+ if (field.type === "sub-tabs") {
6347
+ const tabs = field.tabs.map((tab) => ({
6348
+ ...tab,
6349
+ fields: dropPerNodeFields(tab.fields)
6350
+ })).filter((tab) => tab.fields.length > 0);
6351
+ if (tabs.length > 0) kept.push({
6352
+ ...field,
6353
+ tabs
6354
+ });
6355
+ continue;
6356
+ }
6357
+ if ("perNode" in field && field.perNode === true) continue;
6358
+ kept.push(field);
6359
+ }
6360
+ return kept;
6361
+ }
6244
6362
  function collectPerNodeFieldKeys(fields) {
6245
6363
  const collected = [];
6246
6364
  for (const field of fields) {
@@ -9410,6 +9528,9 @@ method(object({
9410
9528
  kind: "mutation",
9411
9529
  auth: "admin"
9412
9530
  }), method(object({
9531
+ addonId: string$2(),
9532
+ nodeId: string$2().optional()
9533
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
9413
9534
  addonId: string$2(),
9414
9535
  deviceId: number(),
9415
9536
  nodeId: string$2().optional()
@@ -31586,6 +31707,12 @@ Object.freeze({
31586
31707
  addonId: null,
31587
31708
  access: "view"
31588
31709
  },
31710
+ "addonSettings.getIntegrationSettings": {
31711
+ capName: "addon-settings",
31712
+ capScope: "system",
31713
+ addonId: null,
31714
+ access: "view"
31715
+ },
31589
31716
  "addonSettings.updateDeviceSettings": {
31590
31717
  capName: "addon-settings",
31591
31718
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-matter-broker",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "Matter broker addon for CamStack — owns a Matter fabric (commissioning + the long-lived controller) via the matter.js controller and brokers commissioned Matter nodes into CamStack",
5
5
  "keywords": [
6
6
  "camstack",