@camstack/system 1.2.242 → 1.2.244

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 (53) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +437 -11
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +437 -11
  15. package/dist/builtins/device-manager/device-meta-actions.d.ts +97 -0
  16. package/dist/builtins/device-manager/device-meta-types.d.ts +66 -1
  17. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  18. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  19. package/dist/builtins/hub-forwarder/index.js +1 -1
  20. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  21. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  22. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  23. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  24. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  25. package/dist/builtins/local-network/local-network.addon.js +1 -1
  26. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  27. package/dist/builtins/loki-logging/index.js +1 -1
  28. package/dist/builtins/loki-logging/index.mjs +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  30. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  31. package/dist/builtins/platform-probe/index.js +1 -1
  32. package/dist/builtins/platform-probe/index.mjs +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  34. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  35. package/dist/builtins/snapshot/index.js +1 -1
  36. package/dist/builtins/snapshot/index.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  42. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  43. package/dist/builtins/system-config/system-config.addon.js +1 -1
  44. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  45. package/dist/builtins/winston-logging/index.js +1 -1
  46. package/dist/builtins/winston-logging/index.mjs +1 -1
  47. package/dist/{dist-DkGEz_3L.mjs → dist-4MS1GNc8.mjs} +308 -3
  48. package/dist/{dist-BETx_5mH.js → dist-Cz6h6Nc3.js} +325 -2
  49. package/dist/index.js +1 -1
  50. package/dist/index.mjs +1 -1
  51. package/dist/{retired-settings-keys-BnTBoLTv.js → retired-settings-keys-DBrIdOQp.js} +1 -1
  52. package/dist/{retired-settings-keys-BSsaf7n4.mjs → retired-settings-keys-D_5MkKII.mjs} +1 -1
  53. package/package.json +1 -1
@@ -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-BETx_5mH.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, bn as EventCategory, k as addonPagesCapability, qt as errMsg } from "../../dist-DkGEz_3L.mjs";
1
+ import { A as addonPagesCapability, Cn as EventCategory, Xt as errMsg, Zt as BaseAddon } from "../../dist-4MS1GNc8.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -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-BETx_5mH.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, bn as EventCategory, j as addonWidgetsCapability, qt as errMsg } from "../../dist-DkGEz_3L.mjs";
1
+ import { Cn as EventCategory, M as addonWidgetsCapability, Xt as errMsg, Zt as BaseAddon } from "../../dist-4MS1GNc8.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 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-BETx_5mH.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, M as alertsCapability, bn as EventCategory, n as AlertSchema, qt as errMsg, sn as createEvent } from "../../dist-DkGEz_3L.mjs";
1
+ import { Cn as EventCategory, N as alertsCapability, Xt as errMsg, Zt as BaseAddon, n as AlertSchema, un as createEvent } from "../../dist-4MS1GNc8.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -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-BETx_5mH.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { Bt as storageOccupancyCapability, Jt as BaseAddon, P as backupCapability, bn as EventCategory, yt as mayWriteToLocation } from "../../dist-DkGEz_3L.mjs";
1
+ import { Cn as EventCategory, F as backupCapability, Ut as storageOccupancyCapability, Zt as BaseAddon, xt as mayWriteToLocation } from "../../dist-4MS1GNc8.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -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-BETx_5mH.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Jt as BaseAddon, ht as logDestinationCapability } from "../../dist-DkGEz_3L.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/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -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-BETx_5mH.js");
3
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { B as coreBlockIdFromAddonId, Jt as BaseAddon, V as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, qt as errMsg, s as CORE_BLOCKS_ADDON_ID, z as coreBlockAddonId } from "../../dist-DkGEz_3L.mjs";
1
+ import { B as coreBlockAddonId, H as coreBlocksCapability, V as coreBlockIdFromAddonId, Xt as errMsg, Zt as BaseAddon, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-4MS1GNc8.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-BETx_5mH.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-BnTBoLTv.js");
6
+ const require_dist = require("../../dist-Cz6h6Nc3.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-DBrIdOQp.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -3928,7 +3928,8 @@ async function runLocationBackfillAtBoot(pctx, bootRows) {
3928
3928
  * setLocation, setType, setIntegrationId, setLinkDeviceId,
3929
3929
  * setPrimaryChildEntityId, setChildLayout, setRole, setDisplay,
3930
3930
  * applyInitialMeta, setMetadata, setDisabled), and the location registry
3931
- * (listLocations, addLocation, removeLocation).
3931
+ * (listLocations, addLocation, removeLocation, renameLocation) with its icon
3932
+ * map (listLocationIcons, setLocationIcon).
3932
3933
  *
3933
3934
  * EXACT ordering and side effects preserved — notably registerDevice's
3934
3935
  * name-precedence reconcile + field-preservation spreads, the
@@ -4980,6 +4981,311 @@ async function addLocation(pctx, input) {
4980
4981
  });
4981
4982
  }
4982
4983
  /**
4984
+ * Every location that carries an icon, as `(locationKey, icon)` pairs.
4985
+ *
4986
+ * ADDITIVE beside `listLocations`, which still answers `string[]` — the
4987
+ * autocomplete, the HA adoption import and the recorder all read a list of
4988
+ * LABELS and a shape change there would break every one of them. A surface
4989
+ * that wants icons asks for them and joins on `locationIconKey`.
4990
+ *
4991
+ * Returns only what was actually stored. A location with no assignment is
4992
+ * ABSENT, never `map-pin`: the fallback belongs to the renderer
4993
+ * (`resolveLocationIcon`), and folding it in here would make "the operator
4994
+ * picked the pin" indistinguishable from "the operator picked nothing" —
4995
+ * which is the difference an icon picker has to draw.
4996
+ */
4997
+ async function listLocationIcons(pctx) {
4998
+ const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
4999
+ return Object.entries(icons).map(([locationKey, icon]) => ({
5000
+ locationKey,
5001
+ icon
5002
+ })).toSorted((a, b) => a.locationKey.localeCompare(b.locationKey));
5003
+ }
5004
+ /**
5005
+ * Set (or clear) the icon of a location label.
5006
+ *
5007
+ * Keyed on `locationIconKey` — trimmed + lower-cased, the equality the
5008
+ * registry matches on everywhere — so a case-only rename keeps the icon and
5009
+ * two spellings of one room can never carry two different icons.
5010
+ *
5011
+ * The label need NOT be registered: `setLocation` accepts a free-form string
5012
+ * and `listLocations` folds in whatever devices carry, so requiring
5013
+ * registration here would leave exactly the ad-hoc rooms unable to have an
5014
+ * icon. An empty label throws — there is no room to name.
5015
+ *
5016
+ * Same read-modify-write and same lock as `addLocation`: the map is one key of
5017
+ * the addon store, and `writeAddonStore` re-inserts the whole key range, so an
5018
+ * unordered write carries a stale copy of every other key.
5019
+ */
5020
+ async function setLocationIcon(pctx, input) {
5021
+ const trimmed = input.location.trim();
5022
+ if (trimmed.length === 0) throw new Error("[device-manager] setLocationIcon: location must be non-empty");
5023
+ const key = require_dist.locationIconKey(trimmed);
5024
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5025
+ const icons = (await pctx.metaStore.readStore()).locationIcons ?? {};
5026
+ if (input.icon === null) {
5027
+ if (!(key in icons)) return;
5028
+ const { [key]: _cleared, ...kept } = icons;
5029
+ await pctx.settings.writeAddonStore({ locationIcons: kept });
5030
+ return;
5031
+ }
5032
+ if (icons[key] === input.icon) return;
5033
+ await pctx.settings.writeAddonStore({ locationIcons: {
5034
+ ...icons,
5035
+ [key]: input.icon
5036
+ } });
5037
+ });
5038
+ }
5039
+ /**
5040
+ * Every location that carries a description, sorted by key.
5041
+ *
5042
+ * Only what was WRITTEN — a location with no description is absent, and the
5043
+ * digest then tells the model the location name alone, exactly as it did
5044
+ * before descriptions existed.
5045
+ */
5046
+ async function listLocationDescriptions(pctx) {
5047
+ const descriptions = (await pctx.metaStore.readStore()).locationDescriptions ?? {};
5048
+ return Object.entries(descriptions).map(([locationKey, description]) => ({
5049
+ locationKey,
5050
+ description
5051
+ })).toSorted((a, b) => a.locationKey.localeCompare(b.locationKey));
5052
+ }
5053
+ /**
5054
+ * Set (or clear) a location's description.
5055
+ *
5056
+ * A BLANK description clears, exactly as `null` does: an empty line handed to
5057
+ * a vision model costs attention and says nothing, so the two must not be
5058
+ * different states. The label is folded with `locationIconKey` — the same key
5059
+ * the icon uses, which is the whole point of this living here.
5060
+ *
5061
+ * Same read-modify-write and same lock as `setLocationIcon`; `writeAddonStore`
5062
+ * re-inserts the whole key range, so an unordered write carries a stale copy
5063
+ * of every other key.
5064
+ */
5065
+ async function setLocationDescription(pctx, input) {
5066
+ const trimmed = input.location.trim();
5067
+ if (trimmed.length === 0) throw new Error("[device-manager] setLocationDescription: location must be non-empty");
5068
+ const key = require_dist.locationIconKey(trimmed);
5069
+ const text = input.description === null ? "" : input.description.trim();
5070
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5071
+ const descriptions = (await pctx.metaStore.readStore()).locationDescriptions ?? {};
5072
+ if (text.length === 0) {
5073
+ if (!(key in descriptions)) return;
5074
+ const { [key]: _cleared, ...kept } = descriptions;
5075
+ await pctx.settings.writeAddonStore({ locationDescriptions: kept });
5076
+ return;
5077
+ }
5078
+ if (descriptions[key] === text) return;
5079
+ await pctx.settings.writeAddonStore({ locationDescriptions: {
5080
+ ...descriptions,
5081
+ [key]: text
5082
+ } });
5083
+ });
5084
+ }
5085
+ /**
5086
+ * Every site zone, with its rooms, sorted by key.
5087
+ *
5088
+ * Membership is inverted HERE rather than stored both ways: the store keeps
5089
+ * `location → zone`, which is what makes "one zone per room" structural, and a
5090
+ * second copy keyed the other way is a second thing that can disagree with the
5091
+ * first.
5092
+ */
5093
+ async function listSiteZones(pctx) {
5094
+ const store = await pctx.metaStore.readStore();
5095
+ const zones = store.siteZones ?? {};
5096
+ const membership = store.locationZones ?? {};
5097
+ const rooms = /* @__PURE__ */ new Map();
5098
+ for (const [locationKey, zoneKey] of Object.entries(membership)) {
5099
+ const list = rooms.get(zoneKey);
5100
+ if (list === void 0) rooms.set(zoneKey, [locationKey]);
5101
+ else list.push(locationKey);
5102
+ }
5103
+ return Object.entries(zones).map(([zoneKey, record]) => ({
5104
+ zoneKey,
5105
+ label: record.label,
5106
+ icon: record.icon ?? null,
5107
+ locationKeys: (rooms.get(zoneKey) ?? []).toSorted((a, b) => a.localeCompare(b))
5108
+ })).toSorted((a, b) => a.zoneKey.localeCompare(b.zoneKey));
5109
+ }
5110
+ /**
5111
+ * Create a zone, or set the icon of one that already exists.
5112
+ *
5113
+ * `icon: null` clears the icon and KEEPS the zone. Conflating the two would
5114
+ * make a mis-click on "no icon" delete every room's membership — the icon is a
5115
+ * decoration and the grouping is the data.
5116
+ *
5117
+ * `icon` OMITTED leaves whatever icon is there: creating a zone from a screen
5118
+ * with no picker must not silently clear the glyph a previous screen set.
5119
+ */
5120
+ async function setSiteZone(pctx, input) {
5121
+ const label = input.name.trim();
5122
+ if (label.length === 0) throw new Error("[device-manager] setSiteZone: name must be non-empty");
5123
+ const key = require_dist.siteZoneKey(label);
5124
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5125
+ const zones = (await pctx.metaStore.readStore()).siteZones ?? {};
5126
+ const existing = zones[key];
5127
+ const icon = input.icon === void 0 ? existing?.icon : input.icon === null ? void 0 : input.icon;
5128
+ const next = icon === void 0 ? { label } : {
5129
+ label,
5130
+ icon
5131
+ };
5132
+ if (existing?.label === next.label && existing.icon === next.icon) return;
5133
+ await pctx.settings.writeAddonStore({ siteZones: {
5134
+ ...zones,
5135
+ [key]: next
5136
+ } });
5137
+ });
5138
+ }
5139
+ /**
5140
+ * Delete a zone. Its LOCATIONS survive, unattached.
5141
+ *
5142
+ * Deleting a grouping must never delete what was grouped — the rooms exist
5143
+ * independently and the devices in them were never touched to begin with.
5144
+ */
5145
+ async function removeSiteZone(pctx, input) {
5146
+ const key = require_dist.siteZoneKey(input.name);
5147
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5148
+ const store = await pctx.metaStore.readStore();
5149
+ const zones = store.siteZones ?? {};
5150
+ const membership = store.locationZones ?? {};
5151
+ const hadZone = key in zones;
5152
+ const orphaned = Object.entries(membership).filter(([, zoneKey]) => zoneKey === key);
5153
+ if (!hadZone && orphaned.length === 0) return;
5154
+ const { [key]: _dropped, ...keptZones } = zones;
5155
+ const keptMembership = Object.fromEntries(Object.entries(membership).filter(([, zoneKey]) => zoneKey !== key));
5156
+ await pctx.settings.writeAddonStore({
5157
+ ...hadZone ? { siteZones: keptZones } : {},
5158
+ ...orphaned.length > 0 ? { locationZones: keptMembership } : {}
5159
+ });
5160
+ });
5161
+ }
5162
+ /**
5163
+ * Rename a zone, carrying its icon AND every room in it.
5164
+ *
5165
+ * It exists so that renaming is not delete-then-recreate, which drops both.
5166
+ * A rename onto an existing zone MERGES: the target keeps its own icon and the
5167
+ * rooms are unioned, the same rule `renameLocation` follows.
5168
+ */
5169
+ async function renameSiteZone(pctx, input) {
5170
+ const toLabel = input.to.trim();
5171
+ if (toLabel.length === 0) throw new Error("[device-manager] renameSiteZone: target name must be non-empty");
5172
+ const fromKey = require_dist.siteZoneKey(input.from);
5173
+ const toKey = require_dist.siteZoneKey(toLabel);
5174
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5175
+ const store = await pctx.metaStore.readStore();
5176
+ const zones = store.siteZones ?? {};
5177
+ const moved = zones[fromKey];
5178
+ if (moved === void 0) return;
5179
+ const { [fromKey]: _dropped, ...rest } = zones;
5180
+ const target = rest[toKey];
5181
+ const nextZones = {
5182
+ ...rest,
5183
+ [toKey]: target ?? (moved.icon === void 0 ? { label: toLabel } : {
5184
+ label: toLabel,
5185
+ icon: moved.icon
5186
+ })
5187
+ };
5188
+ const membership = store.locationZones ?? {};
5189
+ const nextMembership = Object.fromEntries(Object.entries(membership).map(([locationKey, zoneKey]) => [locationKey, zoneKey === fromKey ? toKey : zoneKey]));
5190
+ await pctx.settings.writeAddonStore({
5191
+ siteZones: nextZones,
5192
+ locationZones: nextMembership
5193
+ });
5194
+ });
5195
+ }
5196
+ /**
5197
+ * Put a location in a zone, or take it out with `zone: null`.
5198
+ *
5199
+ * ONE slot per location, so a second call moves the room rather than listing it
5200
+ * twice. The zone is created if it does not exist — a screen that assigns
5201
+ * before creating is a normal order, and refusing it would be the only place in
5202
+ * this registry that demands registration first.
5203
+ */
5204
+ async function setLocationZone(pctx, input) {
5205
+ const location = input.location.trim();
5206
+ if (location.length === 0) throw new Error("[device-manager] setLocationZone: location must be non-empty");
5207
+ const locationKey = require_dist.locationIconKey(location);
5208
+ const label = input.zone === null ? null : input.zone.trim();
5209
+ if (label !== null && label.length === 0) throw new Error("[device-manager] setLocationZone: zone must be non-empty or null");
5210
+ await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5211
+ const store = await pctx.metaStore.readStore();
5212
+ const membership = store.locationZones ?? {};
5213
+ if (label === null) {
5214
+ if (!(locationKey in membership)) return;
5215
+ const { [locationKey]: _cleared, ...kept } = membership;
5216
+ await pctx.settings.writeAddonStore({ locationZones: kept });
5217
+ return;
5218
+ }
5219
+ const zoneKey = require_dist.siteZoneKey(label);
5220
+ const zones = store.siteZones ?? {};
5221
+ const needsZone = !(zoneKey in zones);
5222
+ if (membership[locationKey] === zoneKey && !needsZone) return;
5223
+ await pctx.settings.writeAddonStore({
5224
+ ...needsZone ? { siteZones: {
5225
+ ...zones,
5226
+ [zoneKey]: { label }
5227
+ } } : {},
5228
+ locationZones: {
5229
+ ...membership,
5230
+ [locationKey]: zoneKey
5231
+ }
5232
+ });
5233
+ });
5234
+ }
5235
+ /**
5236
+ * Move a location's zone membership from one key to another, for
5237
+ * `renameLocation`. Same shape as the icon and description movers.
5238
+ */
5239
+ function renameLocationZone(membership, fromKey, toKey) {
5240
+ const moved = membership[fromKey];
5241
+ if (moved === void 0) return null;
5242
+ if (fromKey === toKey) return null;
5243
+ const { [fromKey]: _dropped, ...rest } = membership;
5244
+ return toKey in rest ? rest : {
5245
+ ...rest,
5246
+ [toKey]: moved
5247
+ };
5248
+ }
5249
+ /**
5250
+ * Move a location's description from one key to another, for `renameLocation`.
5251
+ *
5252
+ * Identical rule to {@link renameLocationIcon}, deliberately: a merge keeps the
5253
+ * TARGET's description, because renaming "Sala" into "Salone" moves devices
5254
+ * into Salone and does not re-describe it. Split from the icon mover only
5255
+ * because the value types differ; if one ever changes, the other must.
5256
+ */
5257
+ function renameLocationDescription(descriptions, fromKey, toKey) {
5258
+ const moved = descriptions[fromKey];
5259
+ if (moved === void 0) return null;
5260
+ if (fromKey === toKey) return null;
5261
+ const { [fromKey]: _dropped, ...rest } = descriptions;
5262
+ return toKey in rest ? rest : {
5263
+ ...rest,
5264
+ [toKey]: moved
5265
+ };
5266
+ }
5267
+ /**
5268
+ * Move a location's icon from one key to another, for `renameLocation`.
5269
+ *
5270
+ * Returns `null` when NOTHING changes, so the caller can leave the key out of
5271
+ * its `writeAddonStore` patch instead of re-writing an identical map.
5272
+ *
5273
+ * A rename onto a room that already has an icon is a MERGE, and the surviving
5274
+ * room keeps its own: the operator renaming "Sala" into "Salone" is moving
5275
+ * devices into Salone, not restyling it. The source key is dropped either way.
5276
+ * Pure — builds a new record, mutates nothing.
5277
+ */
5278
+ function renameLocationIcon(icons, fromKey, toKey) {
5279
+ const moved = icons[fromKey];
5280
+ if (moved === void 0) return null;
5281
+ if (fromKey === toKey) return null;
5282
+ const { [fromKey]: _dropped, ...rest } = icons;
5283
+ return toKey in rest ? rest : {
5284
+ ...rest,
5285
+ [toKey]: moved
5286
+ };
5287
+ }
5288
+ /**
4983
5289
  * Remove a label from the curated registry. Match is
4984
5290
  * case-insensitive. Devices that still reference this label keep
4985
5291
  * their `meta.location` value (the registry is a suggestion
@@ -5010,13 +5316,23 @@ async function renameLocation(pctx, input) {
5010
5316
  const fromKey = from.toLowerCase();
5011
5317
  const toKey = to.toLowerCase();
5012
5318
  await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5013
- const current = (await pctx.metaStore.readStore()).locations ?? [];
5014
- if (!current.some((l) => l.trim().toLowerCase() === fromKey)) return;
5319
+ const store = await pctx.metaStore.readStore();
5320
+ const current = store.locations ?? [];
5321
+ const registered = current.some((l) => l.trim().toLowerCase() === fromKey);
5322
+ const movedIcons = renameLocationIcon(store.locationIcons ?? {}, fromKey, toKey);
5323
+ const movedDescriptions = renameLocationDescription(store.locationDescriptions ?? {}, fromKey, toKey);
5324
+ const movedZones = renameLocationZone(store.locationZones ?? {}, fromKey, toKey);
5325
+ if (!registered && movedIcons === null && movedDescriptions === null && movedZones === null) return;
5015
5326
  const kept = current.filter((l) => {
5016
5327
  const key = l.trim().toLowerCase();
5017
5328
  return key !== fromKey && key !== toKey;
5018
5329
  });
5019
- await pctx.settings.writeAddonStore({ locations: [...kept, to] });
5330
+ await pctx.settings.writeAddonStore({
5331
+ ...registered ? { locations: [...kept, to] } : {},
5332
+ ...movedIcons !== null ? { locationIcons: movedIcons } : {},
5333
+ ...movedDescriptions !== null ? { locationDescriptions: movedDescriptions } : {},
5334
+ ...movedZones !== null ? { locationZones: movedZones } : {}
5335
+ });
5020
5336
  });
5021
5337
  const moved = await pctx.metaStore.withMetaWriteLock(async () => {
5022
5338
  const out = [];
@@ -5054,10 +5370,27 @@ async function removeLocation(pctx, input) {
5054
5370
  const trimmed = input.name.trim();
5055
5371
  if (trimmed.length === 0) return;
5056
5372
  await pctx.bindingsDeps.withAddonStoreWriteLock(async () => {
5057
- const current = (await pctx.metaStore.readStore()).locations ?? [];
5058
- const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
5059
- if (remaining.length === current.length) return;
5060
- await pctx.settings.writeAddonStore({ locations: remaining });
5373
+ const store = await pctx.metaStore.readStore();
5374
+ const current = store.locations ?? [];
5375
+ const key = require_dist.locationIconKey(trimmed);
5376
+ const remaining = current.filter((l) => l.toLowerCase() !== key);
5377
+ const droppedLabel = remaining.length !== current.length;
5378
+ const icons = store.locationIcons ?? {};
5379
+ const droppedIcon = key in icons;
5380
+ const descriptions = store.locationDescriptions ?? {};
5381
+ const droppedDescription = key in descriptions;
5382
+ const membership = store.locationZones ?? {};
5383
+ const droppedZone = key in membership;
5384
+ if (!droppedLabel && !droppedIcon && !droppedDescription && !droppedZone) return;
5385
+ const { [key]: _removedIcon, ...keptIcons } = icons;
5386
+ const { [key]: _removedDescription, ...keptDescriptions } = descriptions;
5387
+ const { [key]: _removedZone, ...keptMembership } = membership;
5388
+ await pctx.settings.writeAddonStore({
5389
+ ...droppedLabel ? { locations: remaining } : {},
5390
+ ...droppedIcon ? { locationIcons: keptIcons } : {},
5391
+ ...droppedDescription ? { locationDescriptions: keptDescriptions } : {},
5392
+ ...droppedZone ? { locationZones: keptMembership } : {}
5393
+ });
5061
5394
  });
5062
5395
  if (input.cascade !== true) return;
5063
5396
  const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
@@ -5159,12 +5492,96 @@ function decodeAddonStore(raw) {
5159
5492
  const nextDeviceId = raw["nextDeviceId"];
5160
5493
  const locations = raw["locations"];
5161
5494
  const roleDisplayDefaults = raw["roleDisplayDefaults"];
5495
+ const locationIcons = decodeLocationIcons(raw["locationIcons"]);
5496
+ const locationDescriptions = decodeLocationDescriptions(raw["locationDescriptions"]);
5497
+ const siteZones = decodeSiteZones(raw["siteZones"]);
5498
+ const locationZones = decodeStringMap(raw["locationZones"]);
5162
5499
  return {
5163
5500
  ...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
5164
5501
  ...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
5165
- ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
5502
+ ...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {},
5503
+ ...locationIcons !== null ? { locationIcons } : {},
5504
+ ...locationDescriptions !== null ? { locationDescriptions } : {},
5505
+ ...siteZones !== null ? { siteZones } : {},
5506
+ ...locationZones !== null ? { locationZones } : {}
5166
5507
  };
5167
5508
  }
5509
+ /**
5510
+ * Decode the `locationIcons` map, dropping every entry whose value is not a
5511
+ * member of the closed vocabulary.
5512
+ *
5513
+ * Dropping, not passing through: the icon's whole point is that both surfaces
5514
+ * can draw it, and an id from a hand-edit or a newer hub is exactly the value
5515
+ * that draws nothing. Absent reads as "no icon chosen", which every renderer
5516
+ * already degrades to the fallback pin. Returns `null` when the key itself is
5517
+ * absent or of the wrong shape, so the field stays OMITTED rather than being
5518
+ * written back as an empty object.
5519
+ */
5520
+ function decodeLocationIcons(raw) {
5521
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5522
+ const out = {};
5523
+ for (const [key, value] of Object.entries(raw)) {
5524
+ const parsed = require_dist.LocationIconIdSchema.safeParse(value);
5525
+ if (!parsed.success) continue;
5526
+ out[key] = parsed.data;
5527
+ }
5528
+ return out;
5529
+ }
5530
+ /**
5531
+ * Decode the `locationDescriptions` map, dropping every entry that is not a
5532
+ * non-blank string.
5533
+ *
5534
+ * Same posture as the icons: a blank description and a missing one are the
5535
+ * same instruction to every reader, and storing the first would send an empty
5536
+ * line to a vision model. Over-long text is TRUNCATED rather than dropped —
5537
+ * losing the whole description because a hand-edit ran long is a worse answer
5538
+ * than losing its tail.
5539
+ */
5540
+ function decodeLocationDescriptions(raw) {
5541
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5542
+ const out = {};
5543
+ for (const [key, value] of Object.entries(raw)) {
5544
+ if (typeof value !== "string") continue;
5545
+ const text = value.trim();
5546
+ if (text.length === 0) continue;
5547
+ out[key] = text.slice(0, 400);
5548
+ }
5549
+ return out;
5550
+ }
5551
+ /**
5552
+ * Decode the zone registry, dropping any entry without a usable label.
5553
+ *
5554
+ * A zone whose label did not survive a hand-edit cannot be shown or renamed —
5555
+ * it would be a row the operator can see the effects of and never name. An
5556
+ * icon outside the vocabulary is dropped on its own, leaving the zone: losing
5557
+ * a glyph is a smaller loss than losing the grouping.
5558
+ */
5559
+ function decodeSiteZones(raw) {
5560
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5561
+ const out = {};
5562
+ for (const [key, value] of Object.entries(raw)) {
5563
+ if (value === null || typeof value !== "object" || Array.isArray(value)) continue;
5564
+ const rec = value;
5565
+ const label = typeof rec["label"] === "string" ? rec["label"].trim() : "";
5566
+ if (label.length === 0) continue;
5567
+ const icon = require_dist.LocationIconIdSchema.safeParse(rec["icon"]);
5568
+ out[key] = icon.success ? {
5569
+ label,
5570
+ icon: icon.data
5571
+ } : { label };
5572
+ }
5573
+ return out;
5574
+ }
5575
+ /** Decode a plain `string → string` map, dropping anything else. */
5576
+ function decodeStringMap(raw) {
5577
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
5578
+ const out = {};
5579
+ for (const [key, value] of Object.entries(raw)) {
5580
+ if (typeof value !== "string" || value.trim().length === 0) continue;
5581
+ out[key] = value;
5582
+ }
5583
+ return out;
5584
+ }
5168
5585
  function isRoleDisplayDefaults(value) {
5169
5586
  return value !== null && typeof value === "object" && !Array.isArray(value);
5170
5587
  }
@@ -6997,6 +7414,15 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
6997
7414
  addLocation: (input) => addLocation(pctx, input),
6998
7415
  removeLocation: (input) => removeLocation(pctx, input),
6999
7416
  renameLocation: (input) => renameLocation(pctx, input),
7417
+ listLocationIcons: () => listLocationIcons(pctx),
7418
+ setLocationIcon: (input) => setLocationIcon(pctx, input),
7419
+ listLocationDescriptions: () => listLocationDescriptions(pctx),
7420
+ setLocationDescription: (input) => setLocationDescription(pctx, input),
7421
+ listSiteZones: () => listSiteZones(pctx),
7422
+ setSiteZone: (input) => setSiteZone(pctx, input),
7423
+ removeSiteZone: (input) => removeSiteZone(pctx, input),
7424
+ renameSiteZone: (input) => renameSiteZone(pctx, input),
7425
+ setLocationZone: (input) => setLocationZone(pctx, input),
7000
7426
  listPersistedByAddon: (input) => listPersistedByAddon(pctx, input),
7001
7427
  listAll: (input) => listAll(pctx, input),
7002
7428
  getDevice: (input) => getDevice(pctx, input),