@camstack/system 1.2.243 → 1.2.245

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/addon-utils.js +1 -1
  2. package/dist/addon-utils.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +315 -7
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +315 -7
  17. package/dist/builtins/device-manager/device-meta-actions.d.ts +65 -0
  18. package/dist/builtins/device-manager/device-meta-types.d.ts +49 -0
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +1 -1
  28. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +2 -2
  38. package/dist/builtins/snapshot/index.mjs +2 -2
  39. package/dist/builtins/snapshot/snapshot-courtesy.d.ts +12 -2
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-B4htkDVv.mjs → dist-4MS1GNc8.mjs} +206 -1
  51. package/dist/{dist-D6kqBW1G.js → dist-Cz6h6Nc3.js} +211 -0
  52. package/dist/index.js +1 -1
  53. package/dist/index.mjs +1 -1
  54. package/dist/{retired-settings-keys-BPIyHR4_.js → retired-settings-keys-DBrIdOQp.js} +1 -1
  55. package/dist/{retired-settings-keys-C9w4b4TV.mjs → retired-settings-keys-D_5MkKII.mjs} +1 -1
  56. package/dist/{snapshot-courtesy-LoJNgHHZ.mjs → snapshot-courtesy-CZPvteq2.mjs} +34 -7
  57. package/dist/{snapshot-courtesy-oeTWCXPv.js → snapshot-courtesy-Cqs7GkOV.js} +34 -7
  58. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { C as STREAM_PROFILE_META, Et as parseStreamParamsFormPatch, J as deviceStatusCapability, K as deviceManagerCapability, Pt as runtimeStatePolicyFor, Q as enumerateSchemaFields, R as buildStreamParamsConfigSchema, Sn as EventCategory, Xt as BaseAddon, Yt as errMsg, Z as enumerateItemArrayFields, a as CAMERA_SWITCH_ORDER, an as WELL_KNOWN_TAB_MAP, en as DeviceFeature, f as DeviceStatusSchema, gt as locationIconKey, h as LocationIconIdSchema, jt as resolveContainerPrimaryChild, mn as isDeviceConfigCap, nn as DeviceType, o as CAP_NAMES_WITH_STATUS, q as deviceStateCapability, t as ALL_CAPABILITY_DEFINITIONS, tn as DeviceRole, wt as normalizeUnit, xn as sleep$1 } from "../../dist-B4htkDVv.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-C9w4b4TV.mjs";
1
+ import { C as STREAM_PROFILE_META, Cn as EventCategory, Et as parseStreamParamsFormPatch, J as deviceStatusCapability, K as deviceManagerCapability, Pt as runtimeStatePolicyFor, Q as enumerateSchemaFields, R as buildStreamParamsConfigSchema, Sn as sleep$1, Xt as errMsg, Z as enumerateItemArrayFields, Zt as BaseAddon, a as CAMERA_SWITCH_ORDER, f as DeviceStatusSchema, gt as locationIconKey, h as LocationIconIdSchema, hn as isDeviceConfigCap, jt as resolveContainerPrimaryChild, nn as DeviceRole, o as CAP_NAMES_WITH_STATUS, on as WELL_KNOWN_TAB_MAP, q as deviceStateCapability, rn as DeviceType, t as ALL_CAPABILITY_DEFINITIONS, tn as DeviceFeature, wt as normalizeUnit, zt as siteZoneKey } from "../../dist-4MS1GNc8.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-D_5MkKII.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -5032,6 +5032,234 @@ async function setLocationIcon(pctx, input) {
5032
5032
  });
5033
5033
  }
5034
5034
  /**
5035
+ * Every location that carries a description, sorted by key.
5036
+ *
5037
+ * Only what was WRITTEN — a location with no description is absent, and the
5038
+ * digest then tells the model the location name alone, exactly as it did
5039
+ * before descriptions existed.
5040
+ */
5041
+ async function listLocationDescriptions(pctx) {
5042
+ const descriptions = (await pctx.metaStore.readStore()).locationDescriptions ?? {};
5043
+ return Object.entries(descriptions).map(([locationKey, description]) => ({
5044
+ locationKey,
5045
+ description
5046
+ })).toSorted((a, b) => a.locationKey.localeCompare(b.locationKey));
5047
+ }
5048
+ /**
5049
+ * Set (or clear) a location's description.
5050
+ *
5051
+ * A BLANK description clears, exactly as `null` does: an empty line handed to
5052
+ * a vision model costs attention and says nothing, so the two must not be
5053
+ * different states. The label is folded with `locationIconKey` — the same key
5054
+ * the icon uses, which is the whole point of this living here.
5055
+ *
5056
+ * Same read-modify-write and same lock as `setLocationIcon`; `writeAddonStore`
5057
+ * re-inserts the whole key range, so an unordered write carries a stale copy
5058
+ * of every other key.
5059
+ */
5060
+ async function setLocationDescription(pctx, input) {
5061
+ const trimmed = input.location.trim();
5062
+ if (trimmed.length === 0) throw new Error("[device-manager] setLocationDescription: location must be non-empty");
5063
+ const key = locationIconKey(trimmed);
5064
+ const text = input.description === null ? "" : input.description.trim();
5065
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5066
+ const descriptions = (await pctx.metaStore.readStore()).locationDescriptions ?? {};
5067
+ if (text.length === 0) {
5068
+ if (!(key in descriptions)) return;
5069
+ const { [key]: _cleared, ...kept } = descriptions;
5070
+ await pctx.settings.writeAddonStore({ locationDescriptions: kept });
5071
+ return;
5072
+ }
5073
+ if (descriptions[key] === text) return;
5074
+ await pctx.settings.writeAddonStore({ locationDescriptions: {
5075
+ ...descriptions,
5076
+ [key]: text
5077
+ } });
5078
+ });
5079
+ }
5080
+ /**
5081
+ * Every site zone, with its rooms, sorted by key.
5082
+ *
5083
+ * Membership is inverted HERE rather than stored both ways: the store keeps
5084
+ * `location → zone`, which is what makes "one zone per room" structural, and a
5085
+ * second copy keyed the other way is a second thing that can disagree with the
5086
+ * first.
5087
+ */
5088
+ async function listSiteZones(pctx) {
5089
+ const store = await pctx.metaStore.readStore();
5090
+ const zones = store.siteZones ?? {};
5091
+ const membership = store.locationZones ?? {};
5092
+ const rooms = /* @__PURE__ */ new Map();
5093
+ for (const [locationKey, zoneKey] of Object.entries(membership)) {
5094
+ const list = rooms.get(zoneKey);
5095
+ if (list === void 0) rooms.set(zoneKey, [locationKey]);
5096
+ else list.push(locationKey);
5097
+ }
5098
+ return Object.entries(zones).map(([zoneKey, record]) => ({
5099
+ zoneKey,
5100
+ label: record.label,
5101
+ icon: record.icon ?? null,
5102
+ locationKeys: (rooms.get(zoneKey) ?? []).toSorted((a, b) => a.localeCompare(b))
5103
+ })).toSorted((a, b) => a.zoneKey.localeCompare(b.zoneKey));
5104
+ }
5105
+ /**
5106
+ * Create a zone, or set the icon of one that already exists.
5107
+ *
5108
+ * `icon: null` clears the icon and KEEPS the zone. Conflating the two would
5109
+ * make a mis-click on "no icon" delete every room's membership — the icon is a
5110
+ * decoration and the grouping is the data.
5111
+ *
5112
+ * `icon` OMITTED leaves whatever icon is there: creating a zone from a screen
5113
+ * with no picker must not silently clear the glyph a previous screen set.
5114
+ */
5115
+ async function setSiteZone(pctx, input) {
5116
+ const label = input.name.trim();
5117
+ if (label.length === 0) throw new Error("[device-manager] setSiteZone: name must be non-empty");
5118
+ const key = siteZoneKey(label);
5119
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5120
+ const zones = (await pctx.metaStore.readStore()).siteZones ?? {};
5121
+ const existing = zones[key];
5122
+ const icon = input.icon === void 0 ? existing?.icon : input.icon === null ? void 0 : input.icon;
5123
+ const next = icon === void 0 ? { label } : {
5124
+ label,
5125
+ icon
5126
+ };
5127
+ if (existing?.label === next.label && existing.icon === next.icon) return;
5128
+ await pctx.settings.writeAddonStore({ siteZones: {
5129
+ ...zones,
5130
+ [key]: next
5131
+ } });
5132
+ });
5133
+ }
5134
+ /**
5135
+ * Delete a zone. Its LOCATIONS survive, unattached.
5136
+ *
5137
+ * Deleting a grouping must never delete what was grouped — the rooms exist
5138
+ * independently and the devices in them were never touched to begin with.
5139
+ */
5140
+ async function removeSiteZone(pctx, input) {
5141
+ const key = siteZoneKey(input.name);
5142
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5143
+ const store = await pctx.metaStore.readStore();
5144
+ const zones = store.siteZones ?? {};
5145
+ const membership = store.locationZones ?? {};
5146
+ const hadZone = key in zones;
5147
+ const orphaned = Object.entries(membership).filter(([, zoneKey]) => zoneKey === key);
5148
+ if (!hadZone && orphaned.length === 0) return;
5149
+ const { [key]: _dropped, ...keptZones } = zones;
5150
+ const keptMembership = Object.fromEntries(Object.entries(membership).filter(([, zoneKey]) => zoneKey !== key));
5151
+ await pctx.settings.writeAddonStore({
5152
+ ...hadZone ? { siteZones: keptZones } : {},
5153
+ ...orphaned.length > 0 ? { locationZones: keptMembership } : {}
5154
+ });
5155
+ });
5156
+ }
5157
+ /**
5158
+ * Rename a zone, carrying its icon AND every room in it.
5159
+ *
5160
+ * It exists so that renaming is not delete-then-recreate, which drops both.
5161
+ * A rename onto an existing zone MERGES: the target keeps its own icon and the
5162
+ * rooms are unioned, the same rule `renameLocation` follows.
5163
+ */
5164
+ async function renameSiteZone(pctx, input) {
5165
+ const toLabel = input.to.trim();
5166
+ if (toLabel.length === 0) throw new Error("[device-manager] renameSiteZone: target name must be non-empty");
5167
+ const fromKey = siteZoneKey(input.from);
5168
+ const toKey = siteZoneKey(toLabel);
5169
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5170
+ const store = await pctx.metaStore.readStore();
5171
+ const zones = store.siteZones ?? {};
5172
+ const moved = zones[fromKey];
5173
+ if (moved === void 0) return;
5174
+ const { [fromKey]: _dropped, ...rest } = zones;
5175
+ const target = rest[toKey];
5176
+ const nextZones = {
5177
+ ...rest,
5178
+ [toKey]: target ?? (moved.icon === void 0 ? { label: toLabel } : {
5179
+ label: toLabel,
5180
+ icon: moved.icon
5181
+ })
5182
+ };
5183
+ const membership = store.locationZones ?? {};
5184
+ const nextMembership = Object.fromEntries(Object.entries(membership).map(([locationKey, zoneKey]) => [locationKey, zoneKey === fromKey ? toKey : zoneKey]));
5185
+ await pctx.settings.writeAddonStore({
5186
+ siteZones: nextZones,
5187
+ locationZones: nextMembership
5188
+ });
5189
+ });
5190
+ }
5191
+ /**
5192
+ * Put a location in a zone, or take it out with `zone: null`.
5193
+ *
5194
+ * ONE slot per location, so a second call moves the room rather than listing it
5195
+ * twice. The zone is created if it does not exist — a screen that assigns
5196
+ * before creating is a normal order, and refusing it would be the only place in
5197
+ * this registry that demands registration first.
5198
+ */
5199
+ async function setLocationZone(pctx, input) {
5200
+ const location = input.location.trim();
5201
+ if (location.length === 0) throw new Error("[device-manager] setLocationZone: location must be non-empty");
5202
+ const locationKey = locationIconKey(location);
5203
+ const label = input.zone === null ? null : input.zone.trim();
5204
+ if (label !== null && label.length === 0) throw new Error("[device-manager] setLocationZone: zone must be non-empty or null");
5205
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5206
+ const store = await pctx.metaStore.readStore();
5207
+ const membership = store.locationZones ?? {};
5208
+ if (label === null) {
5209
+ if (!(locationKey in membership)) return;
5210
+ const { [locationKey]: _cleared, ...kept } = membership;
5211
+ await pctx.settings.writeAddonStore({ locationZones: kept });
5212
+ return;
5213
+ }
5214
+ const zoneKey = siteZoneKey(label);
5215
+ const zones = store.siteZones ?? {};
5216
+ const needsZone = !(zoneKey in zones);
5217
+ if (membership[locationKey] === zoneKey && !needsZone) return;
5218
+ await pctx.settings.writeAddonStore({
5219
+ ...needsZone ? { siteZones: {
5220
+ ...zones,
5221
+ [zoneKey]: { label }
5222
+ } } : {},
5223
+ locationZones: {
5224
+ ...membership,
5225
+ [locationKey]: zoneKey
5226
+ }
5227
+ });
5228
+ });
5229
+ }
5230
+ /**
5231
+ * Move a location's zone membership from one key to another, for
5232
+ * `renameLocation`. Same shape as the icon and description movers.
5233
+ */
5234
+ function renameLocationZone(membership, fromKey, toKey) {
5235
+ const moved = membership[fromKey];
5236
+ if (moved === void 0) return null;
5237
+ if (fromKey === toKey) return null;
5238
+ const { [fromKey]: _dropped, ...rest } = membership;
5239
+ return toKey in rest ? rest : {
5240
+ ...rest,
5241
+ [toKey]: moved
5242
+ };
5243
+ }
5244
+ /**
5245
+ * Move a location's description from one key to another, for `renameLocation`.
5246
+ *
5247
+ * Identical rule to {@link renameLocationIcon}, deliberately: a merge keeps the
5248
+ * TARGET's description, because renaming "Sala" into "Salone" moves devices
5249
+ * into Salone and does not re-describe it. Split from the icon mover only
5250
+ * because the value types differ; if one ever changes, the other must.
5251
+ */
5252
+ function renameLocationDescription(descriptions, fromKey, toKey) {
5253
+ const moved = descriptions[fromKey];
5254
+ if (moved === void 0) return null;
5255
+ if (fromKey === toKey) return null;
5256
+ const { [fromKey]: _dropped, ...rest } = descriptions;
5257
+ return toKey in rest ? rest : {
5258
+ ...rest,
5259
+ [toKey]: moved
5260
+ };
5261
+ }
5262
+ /**
5035
5263
  * Move a location's icon from one key to another, for `renameLocation`.
5036
5264
  *
5037
5265
  * Returns `null` when NOTHING changes, so the caller can leave the key out of
@@ -5087,14 +5315,18 @@ async function renameLocation(pctx, input) {
5087
5315
  const current = store.locations ?? [];
5088
5316
  const registered = current.some((l) => l.trim().toLowerCase() === fromKey);
5089
5317
  const movedIcons = renameLocationIcon(store.locationIcons ?? {}, fromKey, toKey);
5090
- if (!registered && movedIcons === null) return;
5318
+ const movedDescriptions = renameLocationDescription(store.locationDescriptions ?? {}, fromKey, toKey);
5319
+ const movedZones = renameLocationZone(store.locationZones ?? {}, fromKey, toKey);
5320
+ if (!registered && movedIcons === null && movedDescriptions === null && movedZones === null) return;
5091
5321
  const kept = current.filter((l) => {
5092
5322
  const key = l.trim().toLowerCase();
5093
5323
  return key !== fromKey && key !== toKey;
5094
5324
  });
5095
5325
  await pctx.settings.writeAddonStore({
5096
5326
  ...registered ? { locations: [...kept, to] } : {},
5097
- ...movedIcons !== null ? { locationIcons: movedIcons } : {}
5327
+ ...movedIcons !== null ? { locationIcons: movedIcons } : {},
5328
+ ...movedDescriptions !== null ? { locationDescriptions: movedDescriptions } : {},
5329
+ ...movedZones !== null ? { locationZones: movedZones } : {}
5098
5330
  });
5099
5331
  });
5100
5332
  const moved = await pctx.metaStore.withMetaWriteLock(async () => {
@@ -5140,11 +5372,19 @@ async function removeLocation(pctx, input) {
5140
5372
  const droppedLabel = remaining.length !== current.length;
5141
5373
  const icons = store.locationIcons ?? {};
5142
5374
  const droppedIcon = key in icons;
5143
- if (!droppedLabel && !droppedIcon) return;
5375
+ const descriptions = store.locationDescriptions ?? {};
5376
+ const droppedDescription = key in descriptions;
5377
+ const membership = store.locationZones ?? {};
5378
+ const droppedZone = key in membership;
5379
+ if (!droppedLabel && !droppedIcon && !droppedDescription && !droppedZone) return;
5144
5380
  const { [key]: _removedIcon, ...keptIcons } = icons;
5381
+ const { [key]: _removedDescription, ...keptDescriptions } = descriptions;
5382
+ const { [key]: _removedZone, ...keptMembership } = membership;
5145
5383
  await pctx.settings.writeAddonStore({
5146
5384
  ...droppedLabel ? { locations: remaining } : {},
5147
- ...droppedIcon ? { locationIcons: keptIcons } : {}
5385
+ ...droppedIcon ? { locationIcons: keptIcons } : {},
5386
+ ...droppedDescription ? { locationDescriptions: keptDescriptions } : {},
5387
+ ...droppedZone ? { locationZones: keptMembership } : {}
5148
5388
  });
5149
5389
  });
5150
5390
  if (input.cascade !== true) return;
@@ -5248,11 +5488,17 @@ function decodeAddonStore(raw) {
5248
5488
  const locations = raw["locations"];
5249
5489
  const roleDisplayDefaults = raw["roleDisplayDefaults"];
5250
5490
  const locationIcons = decodeLocationIcons(raw["locationIcons"]);
5491
+ const locationDescriptions = decodeLocationDescriptions(raw["locationDescriptions"]);
5492
+ const siteZones = decodeSiteZones(raw["siteZones"]);
5493
+ const locationZones = decodeStringMap(raw["locationZones"]);
5251
5494
  return {
5252
5495
  ...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
5253
5496
  ...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
5254
5497
  ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {},
5255
- ...locationIcons !== null ? { locationIcons } : {}
5498
+ ...locationIcons !== null ? { locationIcons } : {},
5499
+ ...locationDescriptions !== null ? { locationDescriptions } : {},
5500
+ ...siteZones !== null ? { siteZones } : {},
5501
+ ...locationZones !== null ? { locationZones } : {}
5256
5502
  };
5257
5503
  }
5258
5504
  /**
@@ -5276,6 +5522,61 @@ function decodeLocationIcons(raw) {
5276
5522
  }
5277
5523
  return out;
5278
5524
  }
5525
+ /**
5526
+ * Decode the `locationDescriptions` map, dropping every entry that is not a
5527
+ * non-blank string.
5528
+ *
5529
+ * Same posture as the icons: a blank description and a missing one are the
5530
+ * same instruction to every reader, and storing the first would send an empty
5531
+ * line to a vision model. Over-long text is TRUNCATED rather than dropped —
5532
+ * losing the whole description because a hand-edit ran long is a worse answer
5533
+ * than losing its tail.
5534
+ */
5535
+ function decodeLocationDescriptions(raw) {
5536
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5537
+ const out = {};
5538
+ for (const [key, value] of Object.entries(raw)) {
5539
+ if (typeof value !== "string") continue;
5540
+ const text = value.trim();
5541
+ if (text.length === 0) continue;
5542
+ out[key] = text.slice(0, 400);
5543
+ }
5544
+ return out;
5545
+ }
5546
+ /**
5547
+ * Decode the zone registry, dropping any entry without a usable label.
5548
+ *
5549
+ * A zone whose label did not survive a hand-edit cannot be shown or renamed —
5550
+ * it would be a row the operator can see the effects of and never name. An
5551
+ * icon outside the vocabulary is dropped on its own, leaving the zone: losing
5552
+ * a glyph is a smaller loss than losing the grouping.
5553
+ */
5554
+ function decodeSiteZones(raw) {
5555
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5556
+ const out = {};
5557
+ for (const [key, value] of Object.entries(raw)) {
5558
+ if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
5559
+ const rec = value;
5560
+ const label = typeof rec["label"] === "string" ? rec["label"].trim() : "";
5561
+ if (label.length === 0) continue;
5562
+ const icon = LocationIconIdSchema.safeParse(rec["icon"]);
5563
+ out[key] = icon.success ? {
5564
+ label,
5565
+ icon: icon.data
5566
+ } : { label };
5567
+ }
5568
+ return out;
5569
+ }
5570
+ /** Decode a plain `string → string` map, dropping anything else. */
5571
+ function decodeStringMap(raw) {
5572
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5573
+ const out = {};
5574
+ for (const [key, value] of Object.entries(raw)) {
5575
+ if (typeof value !== "string" || value.trim().length === 0) continue;
5576
+ out[key] = value;
5577
+ }
5578
+ return out;
5579
+ }
5279
5580
  function isRoleDisplayDefaults(value) {
5280
5581
  return value !== null && typeof value === "object" && !Array.isArray(value);
5281
5582
  }
@@ -7110,6 +7411,13 @@ var DeviceManagerAddon = class extends BaseAddon {
7110
7411
  renameLocation: (input) => renameLocation(pctx, input),
7111
7412
  listLocationIcons: () => listLocationIcons(pctx),
7112
7413
  setLocationIcon: (input) => setLocationIcon(pctx, input),
7414
+ listLocationDescriptions: () => listLocationDescriptions(pctx),
7415
+ setLocationDescription: (input) => setLocationDescription(pctx, input),
7416
+ listSiteZones: () => listSiteZones(pctx),
7417
+ setSiteZone: (input) => setSiteZone(pctx, input),
7418
+ removeSiteZone: (input) => removeSiteZone(pctx, input),
7419
+ renameSiteZone: (input) => renameSiteZone(pctx, input),
7420
+ setLocationZone: (input) => setLocationZone(pctx, input),
7113
7421
  listPersistedByAddon: (input) => listPersistedByAddon(pctx, input),
7114
7422
  listAll: (input) => listAll(pctx, input),
7115
7423
  getDevice: (input) => getDevice(pctx, input),
@@ -244,6 +244,71 @@ export declare function listLocationIcons(pctx: ProviderContext): ReturnType<IDe
244
244
  * unordered write carries a stale copy of every other key.
245
245
  */
246
246
  export declare function setLocationIcon(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocationIcon']>[0]): ReturnType<IDeviceManagerProvider['setLocationIcon']>;
247
+ /**
248
+ * Every location that carries a description, sorted by key.
249
+ *
250
+ * Only what was WRITTEN — a location with no description is absent, and the
251
+ * digest then tells the model the location name alone, exactly as it did
252
+ * before descriptions existed.
253
+ */
254
+ export declare function listLocationDescriptions(pctx: ProviderContext): ReturnType<IDeviceManagerProvider['listLocationDescriptions']>;
255
+ /**
256
+ * Set (or clear) a location's description.
257
+ *
258
+ * A BLANK description clears, exactly as `null` does: an empty line handed to
259
+ * a vision model costs attention and says nothing, so the two must not be
260
+ * different states. The label is folded with `locationIconKey` — the same key
261
+ * the icon uses, which is the whole point of this living here.
262
+ *
263
+ * Same read-modify-write and same lock as `setLocationIcon`; `writeAddonStore`
264
+ * re-inserts the whole key range, so an unordered write carries a stale copy
265
+ * of every other key.
266
+ */
267
+ export declare function setLocationDescription(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocationDescription']>[0]): ReturnType<IDeviceManagerProvider['setLocationDescription']>;
268
+ /**
269
+ * Every site zone, with its rooms, sorted by key.
270
+ *
271
+ * Membership is inverted HERE rather than stored both ways: the store keeps
272
+ * `location → zone`, which is what makes "one zone per room" structural, and a
273
+ * second copy keyed the other way is a second thing that can disagree with the
274
+ * first.
275
+ */
276
+ export declare function listSiteZones(pctx: ProviderContext): ReturnType<IDeviceManagerProvider['listSiteZones']>;
277
+ /**
278
+ * Create a zone, or set the icon of one that already exists.
279
+ *
280
+ * `icon: null` clears the icon and KEEPS the zone. Conflating the two would
281
+ * make a mis-click on "no icon" delete every room's membership — the icon is a
282
+ * decoration and the grouping is the data.
283
+ *
284
+ * `icon` OMITTED leaves whatever icon is there: creating a zone from a screen
285
+ * with no picker must not silently clear the glyph a previous screen set.
286
+ */
287
+ export declare function setSiteZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setSiteZone']>[0]): ReturnType<IDeviceManagerProvider['setSiteZone']>;
288
+ /**
289
+ * Delete a zone. Its LOCATIONS survive, unattached.
290
+ *
291
+ * Deleting a grouping must never delete what was grouped — the rooms exist
292
+ * independently and the devices in them were never touched to begin with.
293
+ */
294
+ export declare function removeSiteZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['removeSiteZone']>[0]): ReturnType<IDeviceManagerProvider['removeSiteZone']>;
295
+ /**
296
+ * Rename a zone, carrying its icon AND every room in it.
297
+ *
298
+ * It exists so that renaming is not delete-then-recreate, which drops both.
299
+ * A rename onto an existing zone MERGES: the target keeps its own icon and the
300
+ * rooms are unioned, the same rule `renameLocation` follows.
301
+ */
302
+ export declare function renameSiteZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['renameSiteZone']>[0]): ReturnType<IDeviceManagerProvider['renameSiteZone']>;
303
+ /**
304
+ * Put a location in a zone, or take it out with `zone: null`.
305
+ *
306
+ * ONE slot per location, so a second call moves the room rather than listing it
307
+ * twice. The zone is created if it does not exist — a screen that assigns
308
+ * before creating is a normal order, and refusing it would be the only place in
309
+ * this registry that demands registration first.
310
+ */
311
+ export declare function setLocationZone(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocationZone']>[0]): ReturnType<IDeviceManagerProvider['setLocationZone']>;
247
312
  /**
248
313
  * Remove a label from the curated registry. Match is
249
314
  * case-insensitive. Devices that still reference this label keep
@@ -177,6 +177,47 @@ export interface AddonStore {
177
177
  * hole. Dropping it degrades the location to the fallback pin instead.
178
178
  */
179
179
  locationIcons?: Readonly<Record<string, LocationIconId>>;
180
+ /**
181
+ * What each location LOOKS like, in the operator's own words, keyed the same
182
+ * way as {@link AddonStore.locationIcons}.
183
+ *
184
+ * Beside the icon and not in the consumer that reads it (the summary AI)
185
+ * because it is a fact about the PLACE, not about the digest. It lived in
186
+ * the post-analysis addon's settings, keyed by a hand-typed label matched
187
+ * exactly and case-sensitively, and the cost was invisible: a rename in the
188
+ * Locations page carried the icon and left the description on a key no
189
+ * location would ever carry again.
190
+ *
191
+ * A value that is not a non-empty string is DROPPED on decode — an empty
192
+ * description costs the model attention and says nothing, so it must never
193
+ * reach it.
194
+ */
195
+ locationDescriptions?: Readonly<Record<string, string>>;
196
+ /**
197
+ * SITE ZONES — named parts of the property that group locations, keyed by
198
+ * `siteZoneKey(label)`. The label is stored as the operator typed it; the
199
+ * key is what everything else joins on.
200
+ *
201
+ * Split from {@link AddonStore.locationZones} on purpose: a zone EXISTS
202
+ * independently of anything being in it. Folding the two into one map would
203
+ * make an empty zone unrepresentable, so creating one and then filling it —
204
+ * the obvious order — would be impossible, and clearing the last room would
205
+ * silently delete the zone and its icon.
206
+ */
207
+ siteZones?: Readonly<Record<string, SiteZoneRecord>>;
208
+ /**
209
+ * Which zone each location belongs to: `locationKey → zoneKey`.
210
+ *
211
+ * This direction, not `zone → locations`, because it makes "at most one zone
212
+ * per location" true BY CONSTRUCTION rather than by a check somebody has to
213
+ * remember — there is one slot, so a second assignment moves the room
214
+ * instead of duplicating it.
215
+ *
216
+ * Keyed on the folded location label, like the icon and the description, and
217
+ * carried by `renameLocation` / dropped by `removeLocation` for the same
218
+ * reason they are: this repo has already lost an icon to a rename once.
219
+ */
220
+ locationZones?: Readonly<Record<string, string>>;
180
221
  /** Operator-authored per-role display defaults (unit/precision/icon), keyed
181
222
  * by `DeviceRole` string. Resolution merges these UNDER any per-device
182
223
  * `display` override. Written whole-record by `setRoleDisplayDefaults`
@@ -192,3 +233,11 @@ export interface AddonStore {
192
233
  * cost the caller its default, never a downstream `TypeError`.
193
234
  */
194
235
  export declare function decodeAddonStore(raw: Record<string, unknown>): AddonStore;
236
+ /**
237
+ * One zone on disk. The icon is OPTIONAL and absent means "nobody chose one" —
238
+ * never the fallback pin, which is the renderer's business.
239
+ */
240
+ export interface SiteZoneRecord {
241
+ readonly label: string;
242
+ readonly icon?: LocationIconId;
243
+ }
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D6kqBW1G.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { $ as evaluateSensorEdge, I as bareAddonId, Sn as EventCategory, Xt as BaseAddon, Y as doorbellCapability, Yt as errMsg, b as SOURCE_CAPS, ln as createEvent, lt as isSameAddonId, nn as DeviceType, ut as isSourceCap, x as SOURCE_DEVICE_TYPES } from "../../dist-B4htkDVv.mjs";
1
+ import { $ as evaluateSensorEdge, Cn as EventCategory, I as bareAddonId, Xt as errMsg, Y as doorbellCapability, Zt as BaseAddon, b as SOURCE_CAPS, lt as isSameAddonId, rn as DeviceType, un as createEvent, ut as isSourceCap, x as SOURCE_DEVICE_TYPES } from "../../dist-4MS1GNc8.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D6kqBW1G.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { Xt as BaseAddon, _t as logDestinationCapability } from "../../dist-B4htkDVv.mjs";
1
+ import { Zt as BaseAddon, _t as logDestinationCapability } from "../../dist-4MS1GNc8.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D6kqBW1G.js");
3
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { Sn as EventCategory, Xt as BaseAddon, Yt as errMsg, ln as createEvent, y as RecordingStatusSchema } from "../../dist-B4htkDVv.mjs";
1
+ import { Cn as EventCategory, Xt as errMsg, Zt as BaseAddon, un as createEvent, y as RecordingStatusSchema } from "../../dist-4MS1GNc8.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D6kqBW1G.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Gt as userManagementCapability, P as authProviderCapability, Xt as BaseAddon, k as UserRecordSchema, r as ApiKeyRecordSchema, w as ScopedTokenSchema } from "../../dist-B4htkDVv.mjs";
2
+ import { Kt as userManagementCapability, P as authProviderCapability, Zt as BaseAddon, k as UserRecordSchema, r as ApiKeyRecordSchema, w as ScopedTokenSchema } from "../../dist-4MS1GNc8.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-D6kqBW1G.js");
3
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
4
4
  const require_tls = require("../../tls-BFs4PzoW.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Sn as EventCategory, Xt as BaseAddon, ht as localNetworkCapability } from "../../dist-B4htkDVv.mjs";
1
+ import { Cn as EventCategory, Zt as BaseAddon, ht as localNetworkCapability } from "../../dist-4MS1GNc8.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-2xbgg78V.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D6kqBW1G.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { Xt as BaseAddon, _t as logDestinationCapability } from "../../dist-B4htkDVv.mjs";
1
+ import { Zt as BaseAddon, _t as logDestinationCapability } from "../../dist-4MS1GNc8.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D6kqBW1G.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { At as resolveBucketMs, Sn as EventCategory, St as metricsProviderCapability, Xt as BaseAddon, Yt as errMsg, kt as reducePoints, ln as createEvent, rt as foldSnapshotByFunction } from "../../dist-B4htkDVv.mjs";
1
+ import { At as resolveBucketMs, Cn as EventCategory, St as metricsProviderCapability, Xt as errMsg, Zt as BaseAddon, kt as reducePoints, rt as foldSnapshotByFunction, un as createEvent } from "../../dist-4MS1GNc8.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import * as fs from "node:fs";
4
4
  import { constants, readFileSync } from "node:fs";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-D6kqBW1G.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Dt as platformProbeCapability, Lt as scoreRuntimes, Sn as EventCategory, X as enumerateInferenceDevices, Xt as BaseAddon, Yt as errMsg, dn as emitReadiness } from "../../dist-B4htkDVv.mjs";
2
+ import { Cn as EventCategory, Dt as platformProbeCapability, Lt as scoreRuntimes, X as enumerateInferenceDevices, Xt as errMsg, Zt as BaseAddon, fn as emitReadiness } from "../../dist-4MS1GNc8.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { execFile } from "node:child_process";
5
5
  import * as os from "node:os";