@camstack/system 1.2.186 → 1.2.188

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-runner.js +1 -1
  2. package/dist/addon-runner.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-linked-devices.d.ts +35 -20
  16. package/dist/builtins/device-manager/device-manager.addon.js +127 -152
  17. package/dist/builtins/device-manager/device-manager.addon.mjs +127 -152
  18. package/dist/builtins/device-manager/device-meta-actions.d.ts +1 -1
  19. package/dist/builtins/device-manager/linked-devices-manual-backfill.d.ts +16 -0
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  22. package/dist/builtins/hub-forwarder/index.js +1 -1
  23. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  28. package/dist/builtins/local-network/local-network.addon.js +1 -1
  29. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  30. package/dist/builtins/loki-logging/index.js +1 -1
  31. package/dist/builtins/loki-logging/index.mjs +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  34. package/dist/builtins/platform-probe/index.js +1 -1
  35. package/dist/builtins/platform-probe/index.mjs +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  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-BLU9Dol1.mjs → dist-D0_hqbgc.mjs} +19 -5
  51. package/dist/{dist-0fjBDzUu.js → dist-DlL8iWQo.js} +19 -5
  52. package/dist/index.js +2 -2
  53. package/dist/index.mjs +2 -2
  54. package/dist/{manifest-system-deps-BF-C5NDL.mjs → manifest-system-deps-CYKgsQ_b.mjs} +4 -2
  55. package/dist/{manifest-system-deps-KBuTXH-y.js → manifest-system-deps-CdIVy85V.js} +4 -2
  56. package/dist/{retired-settings-keys-rIzS2f51.mjs → retired-settings-keys-AE2Qy3Xd.mjs} +1 -1
  57. package/dist/{retired-settings-keys-pL2Nq6KT.js → retired-settings-keys-B5wVSU65.js} +1 -1
  58. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { $t as isDeviceConfigCap, Bt as DeviceRole, Ft as BaseAddon, H as deviceManagerCapability, J as enumerateSchemaFields, P as buildStreamParamsConfigSchema, Pt as errMsg, U as deviceStateCapability, Vt as DeviceType, W as deviceStatusCapability, Wt as WELL_KNOWN_TAB_MAP, a as CAMERA_SWITCH_ORDER, b as STREAM_PROFILE_META, f as DeviceStatusSchema, ft as normalizeUnit, mt as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, on as sleep, q as enumerateItemArrayFields, sn as EventCategory, t as ALL_CAPABILITY_DEFINITIONS, xt as runtimeStatePolicyFor, yt as resolveContainerPrimaryChild, zt as DeviceFeature } from "../../dist-BLU9Dol1.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-rIzS2f51.mjs";
1
+ import { $t as isDeviceConfigCap, Bt as DeviceRole, Ft as BaseAddon, H as deviceManagerCapability, J as enumerateSchemaFields, P as buildStreamParamsConfigSchema, Pt as errMsg, U as deviceStateCapability, Vt as DeviceType, W as deviceStatusCapability, Wt as WELL_KNOWN_TAB_MAP, a as CAMERA_SWITCH_ORDER, b as STREAM_PROFILE_META, f as DeviceStatusSchema, ft as normalizeUnit, mt as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, on as sleep, q as enumerateItemArrayFields, sn as EventCategory, t as ALL_CAPABILITY_DEFINITIONS, xt as runtimeStatePolicyFor, yt as resolveContainerPrimaryChild, zt as DeviceFeature } from "../../dist-D0_hqbgc.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-AE2Qy3Xd.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
@@ -2046,7 +2046,7 @@ var LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
2046
2046
  * ids may arrive as numbers or numeric strings (the multiselect field
2047
2047
  * stores string option values); anything else is dropped. */
2048
2048
  function parseLinkedDevicesConfig(blob) {
2049
- const mode = blob["_linkedDevicesMode"] === "manual" ? "manual" : "auto";
2049
+ const mode = "manual";
2050
2050
  const rawIds = blob[LINKED_IDS_CONFIG_KEY];
2051
2051
  const manualIds = [];
2052
2052
  if (Array.isArray(rawIds)) for (const v of rawIds) {
@@ -2099,7 +2099,7 @@ function expandContainer(id, byId, byParent, visited, out) {
2099
2099
  * header). Order: children first, then mode additions.
2100
2100
  */
2101
2101
  function resolveLinkedDeviceIds(params) {
2102
- const { selfId, selfLocation, config, candidates } = params;
2102
+ const { selfId, config, candidates } = params;
2103
2103
  const byId = new Map(candidates.map((c) => [c.id, c]));
2104
2104
  const byParent = /* @__PURE__ */ new Map();
2105
2105
  for (const c of candidates) {
@@ -2111,18 +2111,41 @@ function resolveLinkedDeviceIds(params) {
2111
2111
  const ids = /* @__PURE__ */ new Set();
2112
2112
  const expand = (id) => expandContainer(id, byId, byParent, /* @__PURE__ */ new Set(), ids);
2113
2113
  for (const c of candidates) if (c.parentDeviceId === selfId) expand(c.id);
2114
- if (config.mode === "auto") {
2115
- if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
2116
- if (c.type === DeviceType.Container) continue;
2117
- if (c.location !== selfLocation) continue;
2118
- const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
2119
- if (parent !== void 0 && parent.type === DeviceType.Container) {
2120
- expand(parent.id);
2121
- continue;
2122
- }
2123
- ids.add(c.id);
2114
+ for (const id of config.manualIds) if (byId.has(id)) expand(id);
2115
+ ids.delete(selfId);
2116
+ return [...ids];
2117
+ }
2118
+ /**
2119
+ * What the retired `auto` mode (pre-D356) added beyond the device-tree
2120
+ * children: every non-container candidate sharing the camera's location, a
2121
+ * container child standing in for its container's PRIMARY child. Kept ONLY
2122
+ * for the one-shot boot backfill that materialises it into the manual list
2123
+ * (`linked-devices-manual-backfill.ts`); nothing on the read path calls it.
2124
+ */
2125
+ function legacyAutoLinkIds(params) {
2126
+ const { selfId, selfLocation, candidates } = params;
2127
+ if (selfLocation === null || selfLocation.length === 0) return [];
2128
+ const byId = new Map(candidates.map((c) => [c.id, c]));
2129
+ const byParent = /* @__PURE__ */ new Map();
2130
+ for (const c of candidates) {
2131
+ if (c.parentDeviceId === null) continue;
2132
+ const siblings = byParent.get(c.parentDeviceId);
2133
+ if (siblings) siblings.push(c);
2134
+ else byParent.set(c.parentDeviceId, [c]);
2135
+ }
2136
+ const ids = /* @__PURE__ */ new Set();
2137
+ const expand = (id) => expandContainer(id, byId, byParent, /* @__PURE__ */ new Set(), ids);
2138
+ for (const c of candidates) {
2139
+ if (c.type === DeviceType.Container) continue;
2140
+ if (c.location !== selfLocation) continue;
2141
+ if (c.parentDeviceId === selfId) continue;
2142
+ const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
2143
+ if (parent !== void 0 && parent.type === DeviceType.Container) {
2144
+ expand(parent.id);
2145
+ continue;
2124
2146
  }
2125
- } else for (const id of config.manualIds) if (byId.has(id)) expand(id);
2147
+ ids.add(c.id);
2148
+ }
2126
2149
  ids.delete(selfId);
2127
2150
  return [...ids];
2128
2151
  }
@@ -2167,91 +2190,6 @@ function resolveTrackedEventDeviceIds(config, linkedIds) {
2167
2190
  */
2168
2191
  var LINK_CANDIDATE_PROJECTION = { projection: "slim" };
2169
2192
  /**
2170
- * The broken state the 2026-08-26 design leaves possible, made DETECTABLE: a
2171
- * container whose OWN location matches the camera's while its descendants
2172
- * carry `location: null` (a location that predates the cascade, or a cascade
2173
- * write that failed) is skipped by the container rule AND matched by nobody —
2174
- * the whole appliance silently vanishes from auto-link. Measured live
2175
- * 2026-09-05: 40 containers, 373 children, zero log lines.
2176
- *
2177
- * A container qualifies when it matches the camera's location, has at least
2178
- * one descendant, contributed NONE of them to the resolved set, and at least
2179
- * one descendant is location-less. A subtree whose members all carry a
2180
- * DIFFERENT explicit location is excluded on purpose — that is an operator's
2181
- * deliberate split, not the broken state.
2182
- */
2183
- function findLocationInvisibleContainers(params) {
2184
- const { selfId, selfLocation, resolvedIds, candidates } = params;
2185
- if (selfLocation === null || selfLocation.length === 0) return [];
2186
- const byParent = /* @__PURE__ */ new Map();
2187
- for (const c of candidates) {
2188
- if (c.parentDeviceId === null) continue;
2189
- const bucket = byParent.get(c.parentDeviceId);
2190
- if (bucket) bucket.push(c);
2191
- else byParent.set(c.parentDeviceId, [c]);
2192
- }
2193
- const out = [];
2194
- for (const c of candidates) {
2195
- if (c.type !== DeviceType.Container) continue;
2196
- if (c.id === selfId) continue;
2197
- if (c.location !== selfLocation) continue;
2198
- let anyResolved = false;
2199
- let anyNullLocation = false;
2200
- let descendantCount = 0;
2201
- const visited = new Set([c.id]);
2202
- let frontier = [c.id];
2203
- while (frontier.length > 0) {
2204
- const next = [];
2205
- for (const parentId of frontier) for (const child of byParent.get(parentId) ?? []) {
2206
- if (visited.has(child.id)) continue;
2207
- visited.add(child.id);
2208
- descendantCount += 1;
2209
- if (resolvedIds.has(child.id)) anyResolved = true;
2210
- if (child.location === null) anyNullLocation = true;
2211
- next.push(child.id);
2212
- }
2213
- frontier = next;
2214
- }
2215
- if (descendantCount > 0 && !anyResolved && anyNullLocation) out.push(c.id);
2216
- }
2217
- return out;
2218
- }
2219
- /**
2220
- * Per-process warn dedupe for {@link findLocationInvisibleContainers}. This
2221
- * read path runs continuously (the analytics reverse index resolves linked
2222
- * devices for every camera on a timer), so an undeduped warn would be a log
2223
- * flood about a state that only an operator action (or the boot backfill)
2224
- * changes. One line per (camera, container) pair per process is exactly as
2225
- * loud as the fact is. Bounded so pathological id churn cannot grow it
2226
- * forever; once full it stops ADDING, never stops the resolver.
2227
- */
2228
- var INVISIBLE_CONTAINER_WARN_CAP = 4096;
2229
- var warnedInvisibleContainers = /* @__PURE__ */ new Set();
2230
- function warnLocationInvisibleContainers(pctx, cameraId, byId, all, resolved) {
2231
- if (resolved.mode !== "auto") return;
2232
- const selfLocation = byId.get(cameraId)?.location ?? null;
2233
- if (selfLocation === null || selfLocation.length === 0) return;
2234
- const invisible = findLocationInvisibleContainers({
2235
- selfId: cameraId,
2236
- selfLocation,
2237
- resolvedIds: new Set(resolved.devices.map((d) => d.deviceId)),
2238
- candidates: all
2239
- });
2240
- for (const containerId of invisible) {
2241
- const key = `${cameraId}:${containerId}`;
2242
- if (warnedInvisibleContainers.has(key)) continue;
2243
- if (warnedInvisibleContainers.size >= INVISIBLE_CONTAINER_WARN_CAP) return;
2244
- warnedInvisibleContainers.add(key);
2245
- pctx.host.ctx.logger.warn("getLinkedDevices: container matches the camera location but is invisible to auto-link — its children carry no location; re-save the container location to realign", {
2246
- tags: { deviceId: containerId },
2247
- meta: {
2248
- cameraId,
2249
- location: selfLocation
2250
- }
2251
- });
2252
- }
2253
- }
2254
- /**
2255
2193
  * Resolve ONE camera's linked set against an ALREADY-FETCHED fleet.
2256
2194
  *
2257
2195
  * The fleet is a parameter, not a fetch, because it is the entire cost of this
@@ -2263,7 +2201,6 @@ function resolveFor(deviceId, all, byId, blob) {
2263
2201
  const config = parseLinkedDevicesConfig(blob);
2264
2202
  const ids = resolveLinkedDeviceIds({
2265
2203
  selfId: deviceId,
2266
- selfLocation: byId.get(deviceId)?.location ?? null,
2267
2204
  config,
2268
2205
  candidates: all
2269
2206
  });
@@ -2289,10 +2226,7 @@ function resolveFor(deviceId, all, byId, blob) {
2289
2226
  async function getLinkedDevices(pctx, input) {
2290
2227
  const { deviceId } = input;
2291
2228
  const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
2292
- const byId = new Map(all.map((d) => [d.id, d]));
2293
- const resolved = resolveFor(deviceId, all, byId, blob);
2294
- warnLocationInvisibleContainers(pctx, deviceId, byId, all, resolved);
2295
- return resolved;
2229
+ return resolveFor(deviceId, all, new Map(all.map((d) => [d.id, d])), blob);
2296
2230
  }
2297
2231
  /**
2298
2232
  * Cap implementation of `getLinkedDevicesBatch` — ONE fleet listing for the
@@ -2319,11 +2253,9 @@ async function getLinkedDevicesBatch(pctx, input) {
2319
2253
  })))]);
2320
2254
  const byId = new Map(all.map((d) => [d.id, d]));
2321
2255
  return deviceIds.map((deviceId, index) => {
2322
- const resolved = resolveFor(deviceId, all, byId, blobs[index] ?? {});
2323
- warnLocationInvisibleContainers(pctx, deviceId, byId, all, resolved);
2324
2256
  return {
2325
2257
  deviceId,
2326
- ...resolved
2258
+ ...resolveFor(deviceId, all, byId, blobs[index] ?? {})
2327
2259
  };
2328
2260
  });
2329
2261
  }
@@ -2338,13 +2270,9 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2338
2270
  if (!persisted || persisted.meta.type !== DeviceType.Camera) return null;
2339
2271
  const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
2340
2272
  const config = parseLinkedDevicesConfig(blob);
2341
- const options = all.filter((d) => d.id !== deviceId && d.type !== DeviceType.Container).map((d) => ({
2342
- value: String(d.id),
2343
- label: d.location !== null ? `${d.name} (${d.location})` : d.name
2344
- }));
2273
+ const self = all.find((d) => d.id === deviceId);
2345
2274
  const linkedIds = resolveLinkedDeviceIds({
2346
2275
  selfId: deviceId,
2347
- selfLocation: all.find((d) => d.id === deviceId)?.location ?? null,
2348
2276
  config,
2349
2277
  candidates: all
2350
2278
  });
@@ -2361,44 +2289,27 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2361
2289
  return { sections: [{
2362
2290
  id: "linked-devices",
2363
2291
  title: "Linked devices",
2364
- description: "Companion devices associated with this camera. Device-tree children are always linked; auto mode also links every device sharing the camera location, manual mode links the picked list instead.",
2292
+ description: "Companion devices associated with this camera. Device-tree children are always linked; everything else is the list you pick below. Devices in the same location are suggested first, never linked on their own (D356).",
2365
2293
  order: 70,
2366
- fields: [
2367
- {
2368
- type: "select",
2369
- key: LINKED_MODE_CONFIG_KEY,
2370
- label: "Link mode",
2371
- description: "Auto: children + same-location devices. Manual: children + the list below.",
2372
- options: [{
2373
- value: "auto",
2374
- label: "Auto (location-based)"
2375
- }, {
2376
- value: "manual",
2377
- label: "Manual"
2378
- }],
2379
- value: config.mode
2380
- },
2381
- {
2382
- type: "multiselect",
2383
- key: LINKED_IDS_CONFIG_KEY,
2384
- label: "Manually linked devices",
2385
- description: "Used in manual mode (children stay linked regardless).",
2386
- showWhen: {
2387
- field: LINKED_MODE_CONFIG_KEY,
2388
- equals: "manual"
2389
- },
2390
- options,
2391
- value: config.manualIds.map(String)
2294
+ fields: [{
2295
+ type: "device-multiselect",
2296
+ key: LINKED_IDS_CONFIG_KEY,
2297
+ label: "Linked devices",
2298
+ description: "Pick the companions of this camera. Same-location devices are suggested; device-tree children stay linked regardless.",
2299
+ deviceFilter: {
2300
+ excludeDeviceId: deviceId,
2301
+ excludeTypes: [DeviceType.Container]
2392
2302
  },
2393
- {
2394
- type: "multiselect",
2395
- key: LINKED_TRACKED_IDS_CONFIG_KEY,
2396
- label: "Devices that produce tracked events",
2397
- description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
2398
- options: trackedOptions,
2399
- value: trackedValue
2400
- }
2401
- ]
2303
+ suggestLocation: self?.location ?? null,
2304
+ value: config.manualIds.map(String)
2305
+ }, {
2306
+ type: "multiselect",
2307
+ key: LINKED_TRACKED_IDS_CONFIG_KEY,
2308
+ label: "Devices that produce tracked events",
2309
+ description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
2310
+ options: trackedOptions,
2311
+ value: trackedValue
2312
+ }]
2402
2313
  }] };
2403
2314
  }
2404
2315
  /**
@@ -2409,7 +2320,6 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2409
2320
  */
2410
2321
  async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
2411
2322
  const next = { ...await pctx.settings.readDeviceStore(deviceId) };
2412
- if ("_linkedDevicesMode" in patch) next[LINKED_MODE_CONFIG_KEY] = patch["_linkedDevicesMode"] === "manual" ? "manual" : "auto";
2413
2323
  if ("_linkedDeviceIds" in patch) next[LINKED_IDS_CONFIG_KEY] = [...parseLinkedDevicesConfig({ [LINKED_IDS_CONFIG_KEY]: patch[LINKED_IDS_CONFIG_KEY] }).manualIds];
2414
2324
  if ("_linkedDeviceTrackedIds" in patch) {
2415
2325
  const raw = patch[LINKED_TRACKED_IDS_CONFIG_KEY];
@@ -3812,7 +3722,7 @@ async function runLocationBackfillAtBoot(pctx, bootRows) {
3812
3722
  });
3813
3723
  }
3814
3724
  const residual = planNullLocationFills((!repaired && bootRows !== void 0 ? bootRows : await pctx.metaStore.rows.listAll()).map((r) => r.meta));
3815
- for (const [ancestorId, fills] of groupByAncestor(residual)) logger.warn("location cascade misaligned — location-less descendants under a located ancestor never match location auto-link; re-save the ancestor location to realign", {
3725
+ for (const [ancestorId, fills] of groupByAncestor(residual)) logger.warn("location cascade misaligned — location-less descendants under a located ancestor never reach the same-location suggestions of the picker; re-save the ancestor location to realign", {
3816
3726
  tags: { deviceId: ancestorId },
3817
3727
  meta: {
3818
3728
  location: fills[0]?.location,
@@ -4371,7 +4281,7 @@ async function collectDescendants(metaStore, rootId) {
4371
4281
  * every failing id — never indistinguishable from a clean cascade. A
4372
4282
  * `DeviceMetaChanged` event fires for the parent itself and for every
4373
4283
  * descendant actually written (never for one that failed), so linked-device
4374
- * consumers (auto-link) see exactly what landed.
4284
+ * consumers (the linked-devices picker, which suggests by location) see exactly what landed.
4375
4285
  */
4376
4286
  async function setLocation(pctx, input) {
4377
4287
  const { deviceId, location } = input;
@@ -4969,6 +4879,66 @@ async function removeLocation(pctx, input) {
4969
4879
  });
4970
4880
  }
4971
4881
  //#endregion
4882
+ //#region src/builtins/device-manager/linked-devices-manual-backfill.ts
4883
+ var LINKED_MANUAL_BACKFILL_MARKER_KEY = "_linkedDevicesManualBackfillVersion";
4884
+ function planManualBackfill(params) {
4885
+ const { selfId, selfLocation, blob, candidates } = params;
4886
+ const current = parseLinkedDevicesConfig(blob).manualIds;
4887
+ if (blob["_linkedDevicesMode"] === "manual") return {
4888
+ write: false,
4889
+ manualIds: current
4890
+ };
4891
+ const merged = [...current];
4892
+ for (const id of legacyAutoLinkIds({
4893
+ selfId,
4894
+ selfLocation,
4895
+ candidates
4896
+ })) if (!merged.includes(id)) merged.push(id);
4897
+ return {
4898
+ write: true,
4899
+ manualIds: merged
4900
+ };
4901
+ }
4902
+ async function runLinkedDevicesManualBackfillAtBoot(pctx) {
4903
+ const logger = pctx.host.ctx.logger;
4904
+ const addonStore = await pctx.settings.readAddonStore();
4905
+ const markerRaw = addonStore[LINKED_MANUAL_BACKFILL_MARKER_KEY];
4906
+ if ((typeof markerRaw === "number" ? markerRaw : 0) >= 1) return;
4907
+ const all = await listAll(pctx, LINK_CANDIDATE_PROJECTION);
4908
+ let written = 0;
4909
+ let linked = 0;
4910
+ for (const cam of all) {
4911
+ if (cam.type !== DeviceType.Camera) continue;
4912
+ const blob = await pctx.settings.readDeviceStore(cam.id);
4913
+ const plan = planManualBackfill({
4914
+ selfId: cam.id,
4915
+ selfLocation: cam.location ?? null,
4916
+ blob,
4917
+ candidates: all
4918
+ });
4919
+ if (!plan.write) continue;
4920
+ await pctx.settings.writeDeviceStore(cam.id, {
4921
+ ...blob,
4922
+ [LINKED_MODE_CONFIG_KEY]: "manual",
4923
+ [LINKED_IDS_CONFIG_KEY]: [...plan.manualIds]
4924
+ });
4925
+ written += 1;
4926
+ linked += plan.manualIds.length;
4927
+ logger.info("linked devices: auto links materialised into the manual list (D356)", {
4928
+ tags: { deviceId: cam.id },
4929
+ meta: { manualIds: plan.manualIds }
4930
+ });
4931
+ }
4932
+ await pctx.settings.writeAddonStore({
4933
+ ...addonStore,
4934
+ [LINKED_MANUAL_BACKFILL_MARKER_KEY]: 1
4935
+ });
4936
+ logger.info("linked devices: manual backfill complete (D356)", { meta: {
4937
+ camerasWritten: written,
4938
+ linksMaterialised: linked
4939
+ } });
4940
+ }
4941
+ //#endregion
4972
4942
  //#region src/builtins/device-manager/device-meta-types.ts
4973
4943
  /**
4974
4944
  * Decode the raw `ctx.settings.readAddonStore()` record into {@link AddonStore}.
@@ -6722,6 +6692,11 @@ var DeviceManagerAddon = class extends BaseAddon {
6722
6692
  } catch (err) {
6723
6693
  this.ctx.logger.warn("location backfill failed — fleet boots without it", { meta: { error: errMsg(err) } });
6724
6694
  }
6695
+ try {
6696
+ await runLinkedDevicesManualBackfillAtBoot(pctx);
6697
+ } catch (err) {
6698
+ this.ctx.logger.warn("linked-devices manual backfill failed — auto links are NOT materialised", { meta: { error: errMsg(err) } });
6699
+ }
6725
6700
  const provider = {
6726
6701
  resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
6727
6702
  resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
@@ -95,7 +95,7 @@ export declare function setName(pctx: ProviderContext, input: Parameters<IDevice
95
95
  * every failing id — never indistinguishable from a clean cascade. A
96
96
  * `DeviceMetaChanged` event fires for the parent itself and for every
97
97
  * descendant actually written (never for one that failed), so linked-device
98
- * consumers (auto-link) see exactly what landed.
98
+ * consumers (the linked-devices picker, which suggests by location) see exactly what landed.
99
99
  */
100
100
  export declare function setLocation(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['setLocation']>[0]): ReturnType<IDeviceManagerProvider['setLocation']>;
101
101
  /**
@@ -0,0 +1,16 @@
1
+ import { LinkCandidate } from './device-linked-devices.js';
2
+ import { ProviderContext } from './device-provider-context.js';
3
+ export declare const LINKED_MANUAL_BACKFILL_MARKER_KEY = "_linkedDevicesManualBackfillVersion";
4
+ export declare const LINKED_MANUAL_BACKFILL_VERSION = 1;
5
+ export interface ManualBackfillPlan {
6
+ /** `false` = the camera was already on `manual`; nothing to write. */
7
+ readonly write: boolean;
8
+ readonly manualIds: readonly number[];
9
+ }
10
+ export declare function planManualBackfill(params: {
11
+ readonly selfId: number;
12
+ readonly selfLocation: string | null;
13
+ readonly blob: Record<string, unknown>;
14
+ readonly candidates: readonly LinkCandidate[];
15
+ }): ManualBackfillPlan;
16
+ export declare function runLinkedDevicesManualBackfillAtBoot(pctx: ProviderContext): Promise<void>;
@@ -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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.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 { Ft as BaseAddon, G as doorbellCapability, Jt as createEvent, M as bareAddonId, Pt as errMsg, Vt as DeviceType, Y as evaluateSensorEdge, _ as SOURCE_CAPS, nt as isSameAddonId, rt as isSourceCap, sn as EventCategory, v as SOURCE_DEVICE_TYPES } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, G as doorbellCapability, Jt as createEvent, M as bareAddonId, Pt as errMsg, Vt as DeviceType, Y as evaluateSensorEdge, _ as SOURCE_CAPS, nt as isSameAddonId, rt as isSourceCap, sn as EventCategory, v as SOURCE_DEVICE_TYPES } from "../../dist-D0_hqbgc.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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.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 { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-D0_hqbgc.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-0fjBDzUu.js");
3
+ const require_dist = require("../../dist-DlL8iWQo.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 { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, sn as EventCategory } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, sn as EventCategory } from "../../dist-D0_hqbgc.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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.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 { A as authProviderCapability, Ft as BaseAddon, T as UserRecordSchema, jt as userManagementCapability, r as ApiKeyRecordSchema, x as ScopedTokenSchema } from "../../dist-BLU9Dol1.mjs";
2
+ import { A as authProviderCapability, Ft as BaseAddon, T as UserRecordSchema, jt as userManagementCapability, r as ApiKeyRecordSchema, x as ScopedTokenSchema } from "../../dist-D0_hqbgc.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-0fjBDzUu.js");
3
+ const require_dist = require("../../dist-DlL8iWQo.js");
4
4
  const require_tls = require("../../tls-BxQlomxd.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 { Ft as BaseAddon, sn as EventCategory, st as localNetworkCapability } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, sn as EventCategory, st as localNetworkCapability } from "../../dist-D0_hqbgc.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-DOTmtLCW.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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.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 { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-D0_hqbgc.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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_child_process = require("node:child_process");
9
9
  let node_util = require("node:util");
@@ -1,4 +1,4 @@
1
- import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, Q as foldSnapshotByFunction, _t as reducePoints, dt as metricsProviderCapability, sn as EventCategory, vt as resolveBucketMs } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, Q as foldSnapshotByFunction, _t as reducePoints, dt as metricsProviderCapability, sn as EventCategory, vt as resolveBucketMs } from "../../dist-D0_hqbgc.mjs";
2
2
  import { readFile, readdir } from "node:fs/promises";
3
3
  import { execFile, execFileSync } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_util = require("node:util");
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Ft as BaseAddon, K as enumerateInferenceDevices, Pt as errMsg, Xt as emitReadiness, ht as platformProbeCapability, sn as EventCategory, wt as scoreRuntimes } from "../../dist-BLU9Dol1.mjs";
2
+ import { Ft as BaseAddon, K as enumerateInferenceDevices, Pt as errMsg, Xt as emitReadiness, ht as platformProbeCapability, sn as EventCategory, wt as scoreRuntimes } from "../../dist-D0_hqbgc.mjs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
5
  import * as fs from "node:fs";
@@ -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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Ft as BaseAddon, sn as EventCategory } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, sn as EventCategory } from "../../dist-D0_hqbgc.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.js");
7
7
  let zod = require("zod");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
@@ -1,4 +1,4 @@
1
- import { At as streamQualityLabel, Et as snapshotCapability, Ft as BaseAddon, M as bareAddonId, N as batteryCapability, Pt as errMsg, V as deriveBatteryPresence, Vt as DeviceType, en as nodePin, i as BatteryStatusSchema, sn as EventCategory, zt as DeviceFeature } from "../../dist-BLU9Dol1.mjs";
1
+ import { At as streamQualityLabel, Et as snapshotCapability, Ft as BaseAddon, M as bareAddonId, N as batteryCapability, Pt as errMsg, V as deriveBatteryPresence, Vt as DeviceType, en as nodePin, i as BatteryStatusSchema, sn as EventCategory, zt as DeviceFeature } from "../../dist-D0_hqbgc.mjs";
2
2
  import { z } from "zod";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
@@ -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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Ft as BaseAddon, Z as filesystemBrowseCapability, kt as storageProviderCapability } from "../../dist-BLU9Dol1.mjs";
1
+ import { Ft as BaseAddon, Z as filesystemBrowseCapability, kt as storageProviderCapability } from "../../dist-D0_hqbgc.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
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-0fjBDzUu.js");
6
+ const require_dist = require("../../dist-DlL8iWQo.js");
7
7
  const require_hub_cap_forward = require("../../hub-cap-forward-DmHNjbB0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { C as StorageLocationTypeSchema, Dt as storageCapability, F as collectSecretConfigKeys, Ft as BaseAddon, Ot as storageMigrationCapability, S as StorageCleanupJobSchema, Tt as settingsStoreCapability, h as REDACTED_SECRET, tn as parseJsonObject, w as StorageMigrationJobSchema } from "../../dist-BLU9Dol1.mjs";
1
+ import { C as StorageLocationTypeSchema, Dt as storageCapability, F as collectSecretConfigKeys, Ft as BaseAddon, Ot as storageMigrationCapability, S as StorageCleanupJobSchema, Tt as settingsStoreCapability, h as REDACTED_SECRET, tn as parseJsonObject, w as StorageMigrationJobSchema } from "../../dist-D0_hqbgc.mjs";
2
2
  import { t as HUB_CAP_FWD_ACTION } from "../../hub-cap-forward-BdT320dq.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";