@camstack/system 1.2.185 → 1.2.187

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-CCMYMmkQ.mjs → dist-C5PkvU4p.mjs} +62 -6
  51. package/dist/{dist-DE3OH-FG.js → dist-DNwhsXWi.js} +62 -6
  52. package/dist/index.js +2 -2
  53. package/dist/index.mjs +2 -2
  54. package/dist/{manifest-system-deps-BmXFnBQZ.mjs → manifest-system-deps-BF-C5NDL.mjs} +2 -55
  55. package/dist/{manifest-system-deps-DMWd5Ygf.js → manifest-system-deps-KBuTXH-y.js} +1 -54
  56. package/dist/{retired-settings-keys-C0uslzAd.mjs → retired-settings-keys-BZU4lIH7.mjs} +1 -1
  57. package/dist/{retired-settings-keys-BgUS9Rr3.js → retired-settings-keys-CUXClAIQ.js} +1 -1
  58. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_system_deps = require("./manifest-system-deps-DMWd5Ygf.js");
2
+ const require_manifest_system_deps = require("./manifest-system-deps-KBuTXH-y.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, Qt as emitHeapDiagnosticReport, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, j as LocalChildClient, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-BmXFnBQZ.mjs";
1
+ import { A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, Qt as emitHeapDiagnosticReport, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, j as LocalChildClient, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-BF-C5NDL.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
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-DE3OH-FG.js");
6
+ const require_dist = require("../../dist-DNwhsXWi.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 { E as addonPagesCapability, Ft as BaseAddon, Pt as errMsg, sn as EventCategory } from "../../dist-CCMYMmkQ.mjs";
1
+ import { E as addonPagesCapability, Ft as BaseAddon, Pt as errMsg, sn as EventCategory } from "../../dist-C5PkvU4p.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-DE3OH-FG.js");
6
+ const require_dist = require("../../dist-DNwhsXWi.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 { Ft as BaseAddon, O as addonWidgetsCapability, Pt as errMsg, sn as EventCategory } from "../../dist-CCMYMmkQ.mjs";
1
+ import { Ft as BaseAddon, O as addonWidgetsCapability, Pt as errMsg, sn as EventCategory } from "../../dist-C5PkvU4p.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-DE3OH-FG.js");
6
+ const require_dist = require("../../dist-DNwhsXWi.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 { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, k as alertsCapability, n as AlertSchema, sn as EventCategory } from "../../dist-CCMYMmkQ.mjs";
1
+ import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, k as alertsCapability, n as AlertSchema, sn as EventCategory } from "../../dist-C5PkvU4p.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-DE3OH-FG.js");
6
+ const require_dist = require("../../dist-DNwhsXWi.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 { Ft as BaseAddon, j as backupCapability, sn as EventCategory } from "../../dist-CCMYMmkQ.mjs";
1
+ import { Ft as BaseAddon, j as backupCapability, sn as EventCategory } from "../../dist-C5PkvU4p.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-DE3OH-FG.js");
6
+ const require_dist = require("../../dist-DNwhsXWi.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 { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-CCMYMmkQ.mjs";
1
+ import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-C5PkvU4p.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-DE3OH-FG.js");
3
+ const require_dist = require("../../dist-DNwhsXWi.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 { Ft as BaseAddon, I as coreBlockAddonId, L as coreBlockIdFromAddonId, Pt as errMsg, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-CCMYMmkQ.mjs";
1
+ import { Ft as BaseAddon, I as coreBlockAddonId, L as coreBlockIdFromAddonId, Pt as errMsg, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-C5PkvU4p.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";
@@ -9,6 +9,8 @@ export declare const LINKED_IDS_CONFIG_KEY = "_linkedDeviceIds";
9
9
  * auto-materialized sensor snapshots the operator never asked for). */
10
10
  export declare const LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
11
11
  export interface LinkedDevicesConfig {
12
+ /** Always `manual` since D356 — kept on the type because the cap output
13
+ * still carries it for clients already shipped. */
12
14
  readonly mode: LinkedDevicesMode;
13
15
  readonly manualIds: readonly number[];
14
16
  }
@@ -47,10 +49,21 @@ export interface LinkCandidate {
47
49
  */
48
50
  export declare function resolveLinkedDeviceIds(params: {
49
51
  readonly selfId: number;
50
- readonly selfLocation: string | null;
51
52
  readonly config: LinkedDevicesConfig;
52
53
  readonly candidates: readonly LinkCandidate[];
53
54
  }): readonly number[];
55
+ /**
56
+ * What the retired `auto` mode (pre-D356) added beyond the device-tree
57
+ * children: every non-container candidate sharing the camera's location, a
58
+ * container child standing in for its container's PRIMARY child. Kept ONLY
59
+ * for the one-shot boot backfill that materialises it into the manual list
60
+ * (`linked-devices-manual-backfill.ts`); nothing on the read path calls it.
61
+ */
62
+ export declare function legacyAutoLinkIds(params: {
63
+ readonly selfId: number;
64
+ readonly selfLocation: string | null;
65
+ readonly candidates: readonly LinkCandidate[];
66
+ }): readonly number[];
54
67
  /**
55
68
  * Which linked devices materialize synthetic tracked events on the camera.
56
69
  * Absent / non-array config key ⇒ NONE — the operator must explicitly pick
@@ -62,28 +75,30 @@ export declare function resolveLinkedDeviceIds(params: {
62
75
  * are ignored.
63
76
  */
64
77
  export declare function resolveTrackedEventDeviceIds(config: Record<string, unknown>, linkedIds: readonly number[]): readonly number[];
65
- /** The single resolved answer, before it is tagged with its camera. */
66
- export type ResolvedLinkedDevices = Awaited<ReturnType<IDeviceManagerProvider['getLinkedDevices']>>;
67
78
  /**
68
- * The broken state the 2026-08-26 design leaves possible, made DETECTABLE: a
69
- * container whose OWN location matches the camera's while its descendants
70
- * carry `location: null` (a location that predates the cascade, or a cascade
71
- * write that failed) is skipped by the container rule AND matched by nobody —
72
- * the whole appliance silently vanishes from auto-link. Measured live
73
- * 2026-09-05: 40 containers, 373 children, zero log lines.
79
+ * The fleet listing every function here needs, and the ONLY projection any of
80
+ * them may ask for.
81
+ *
82
+ * Linked-device policy reads five fields off a candidate — `id`,
83
+ * `parentDeviceId`, `location`, `name`, `features` — and `slim` carries all
84
+ * five. What `slim` drops is `config`, and `config` is not a column: `listAll`
85
+ * materialises it with ONE `readDeviceStore` per device, sequentially, on
86
+ * device-manager's event loop. This module asked for the full projection three
87
+ * times, so every linked-devices read swept the whole fleet's settings rows to
88
+ * use none of them.
74
89
  *
75
- * A container qualifies when it matches the camera's location, has at least
76
- * one descendant, contributed NONE of them to the resolved set, and at least
77
- * one descendant is location-less. A subtree whose members all carry a
78
- * DIFFERENT explicit location is excluded on purpose — that is an operator's
79
- * deliberate split, not the broken state.
90
+ * Measured on the live hub 2026-08-25: `query addon-device-settings` ran
91
+ * 36 814 times a minute against a 967-device fleet — ~38 full-fleet sweeps a
92
+ * minute, ~31 of them the `pipeline-analytics` reverse index rebuilding itself
93
+ * one camera at a time through `getLinkedDevices`. The per-camera `config`
94
+ * this query DOES need is the separate `readDeviceStore(deviceId)` below, and
95
+ * it is one row, not the fleet.
80
96
  */
81
- export declare function findLocationInvisibleContainers(params: {
82
- readonly selfId: number;
83
- readonly selfLocation: string | null;
84
- readonly resolvedIds: ReadonlySet<number>;
85
- readonly candidates: readonly LinkCandidate[];
86
- }): readonly number[];
97
+ export declare const LINK_CANDIDATE_PROJECTION: {
98
+ readonly projection: "slim";
99
+ };
100
+ /** The single resolved answer, before it is tagged with its camera. */
101
+ export type ResolvedLinkedDevices = Awaited<ReturnType<IDeviceManagerProvider['getLinkedDevices']>>;
87
102
  /** Cap implementation of `getLinkedDevices`. */
88
103
  export declare function getLinkedDevices(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getLinkedDevices']>[0]): ReturnType<IDeviceManagerProvider['getLinkedDevices']>;
89
104
  /**
@@ -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-DE3OH-FG.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-BgUS9Rr3.js");
6
+ const require_dist = require("../../dist-DNwhsXWi.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-CUXClAIQ.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
@@ -2051,7 +2051,7 @@ var LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
2051
2051
  * ids may arrive as numbers or numeric strings (the multiselect field
2052
2052
  * stores string option values); anything else is dropped. */
2053
2053
  function parseLinkedDevicesConfig(blob) {
2054
- const mode = blob["_linkedDevicesMode"] === "manual" ? "manual" : "auto";
2054
+ const mode = "manual";
2055
2055
  const rawIds = blob[LINKED_IDS_CONFIG_KEY];
2056
2056
  const manualIds = [];
2057
2057
  if (Array.isArray(rawIds)) for (const v of rawIds) {
@@ -2104,7 +2104,7 @@ function expandContainer(id, byId, byParent, visited, out) {
2104
2104
  * header). Order: children first, then mode additions.
2105
2105
  */
2106
2106
  function resolveLinkedDeviceIds(params) {
2107
- const { selfId, selfLocation, config, candidates } = params;
2107
+ const { selfId, config, candidates } = params;
2108
2108
  const byId = new Map(candidates.map((c) => [c.id, c]));
2109
2109
  const byParent = /* @__PURE__ */ new Map();
2110
2110
  for (const c of candidates) {
@@ -2116,18 +2116,41 @@ function resolveLinkedDeviceIds(params) {
2116
2116
  const ids = /* @__PURE__ */ new Set();
2117
2117
  const expand = (id) => expandContainer(id, byId, byParent, /* @__PURE__ */ new Set(), ids);
2118
2118
  for (const c of candidates) if (c.parentDeviceId === selfId) expand(c.id);
2119
- if (config.mode === "auto") {
2120
- if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
2121
- if (c.type === require_dist.DeviceType.Container) continue;
2122
- if (c.location !== selfLocation) continue;
2123
- const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
2124
- if (parent !== void 0 && parent.type === require_dist.DeviceType.Container) {
2125
- expand(parent.id);
2126
- continue;
2127
- }
2128
- ids.add(c.id);
2119
+ for (const id of config.manualIds) if (byId.has(id)) expand(id);
2120
+ ids.delete(selfId);
2121
+ return [...ids];
2122
+ }
2123
+ /**
2124
+ * What the retired `auto` mode (pre-D356) added beyond the device-tree
2125
+ * children: every non-container candidate sharing the camera's location, a
2126
+ * container child standing in for its container's PRIMARY child. Kept ONLY
2127
+ * for the one-shot boot backfill that materialises it into the manual list
2128
+ * (`linked-devices-manual-backfill.ts`); nothing on the read path calls it.
2129
+ */
2130
+ function legacyAutoLinkIds(params) {
2131
+ const { selfId, selfLocation, candidates } = params;
2132
+ if (selfLocation === null || selfLocation.length === 0) return [];
2133
+ const byId = new Map(candidates.map((c) => [c.id, c]));
2134
+ const byParent = /* @__PURE__ */ new Map();
2135
+ for (const c of candidates) {
2136
+ if (c.parentDeviceId === null) continue;
2137
+ const siblings = byParent.get(c.parentDeviceId);
2138
+ if (siblings) siblings.push(c);
2139
+ else byParent.set(c.parentDeviceId, [c]);
2140
+ }
2141
+ const ids = /* @__PURE__ */ new Set();
2142
+ const expand = (id) => expandContainer(id, byId, byParent, /* @__PURE__ */ new Set(), ids);
2143
+ for (const c of candidates) {
2144
+ if (c.type === require_dist.DeviceType.Container) continue;
2145
+ if (c.location !== selfLocation) continue;
2146
+ if (c.parentDeviceId === selfId) continue;
2147
+ const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
2148
+ if (parent !== void 0 && parent.type === require_dist.DeviceType.Container) {
2149
+ expand(parent.id);
2150
+ continue;
2129
2151
  }
2130
- } else for (const id of config.manualIds) if (byId.has(id)) expand(id);
2152
+ ids.add(c.id);
2153
+ }
2131
2154
  ids.delete(selfId);
2132
2155
  return [...ids];
2133
2156
  }
@@ -2172,91 +2195,6 @@ function resolveTrackedEventDeviceIds(config, linkedIds) {
2172
2195
  */
2173
2196
  var LINK_CANDIDATE_PROJECTION = { projection: "slim" };
2174
2197
  /**
2175
- * The broken state the 2026-08-26 design leaves possible, made DETECTABLE: a
2176
- * container whose OWN location matches the camera's while its descendants
2177
- * carry `location: null` (a location that predates the cascade, or a cascade
2178
- * write that failed) is skipped by the container rule AND matched by nobody —
2179
- * the whole appliance silently vanishes from auto-link. Measured live
2180
- * 2026-09-05: 40 containers, 373 children, zero log lines.
2181
- *
2182
- * A container qualifies when it matches the camera's location, has at least
2183
- * one descendant, contributed NONE of them to the resolved set, and at least
2184
- * one descendant is location-less. A subtree whose members all carry a
2185
- * DIFFERENT explicit location is excluded on purpose — that is an operator's
2186
- * deliberate split, not the broken state.
2187
- */
2188
- function findLocationInvisibleContainers(params) {
2189
- const { selfId, selfLocation, resolvedIds, candidates } = params;
2190
- if (selfLocation === null || selfLocation.length === 0) return [];
2191
- const byParent = /* @__PURE__ */ new Map();
2192
- for (const c of candidates) {
2193
- if (c.parentDeviceId === null) continue;
2194
- const bucket = byParent.get(c.parentDeviceId);
2195
- if (bucket) bucket.push(c);
2196
- else byParent.set(c.parentDeviceId, [c]);
2197
- }
2198
- const out = [];
2199
- for (const c of candidates) {
2200
- if (c.type !== require_dist.DeviceType.Container) continue;
2201
- if (c.id === selfId) continue;
2202
- if (c.location !== selfLocation) continue;
2203
- let anyResolved = false;
2204
- let anyNullLocation = false;
2205
- let descendantCount = 0;
2206
- const visited = new Set([c.id]);
2207
- let frontier = [c.id];
2208
- while (frontier.length > 0) {
2209
- const next = [];
2210
- for (const parentId of frontier) for (const child of byParent.get(parentId) ?? []) {
2211
- if (visited.has(child.id)) continue;
2212
- visited.add(child.id);
2213
- descendantCount += 1;
2214
- if (resolvedIds.has(child.id)) anyResolved = true;
2215
- if (child.location === null) anyNullLocation = true;
2216
- next.push(child.id);
2217
- }
2218
- frontier = next;
2219
- }
2220
- if (descendantCount > 0 && !anyResolved && anyNullLocation) out.push(c.id);
2221
- }
2222
- return out;
2223
- }
2224
- /**
2225
- * Per-process warn dedupe for {@link findLocationInvisibleContainers}. This
2226
- * read path runs continuously (the analytics reverse index resolves linked
2227
- * devices for every camera on a timer), so an undeduped warn would be a log
2228
- * flood about a state that only an operator action (or the boot backfill)
2229
- * changes. One line per (camera, container) pair per process is exactly as
2230
- * loud as the fact is. Bounded so pathological id churn cannot grow it
2231
- * forever; once full it stops ADDING, never stops the resolver.
2232
- */
2233
- var INVISIBLE_CONTAINER_WARN_CAP = 4096;
2234
- var warnedInvisibleContainers = /* @__PURE__ */ new Set();
2235
- function warnLocationInvisibleContainers(pctx, cameraId, byId, all, resolved) {
2236
- if (resolved.mode !== "auto") return;
2237
- const selfLocation = byId.get(cameraId)?.location ?? null;
2238
- if (selfLocation === null || selfLocation.length === 0) return;
2239
- const invisible = findLocationInvisibleContainers({
2240
- selfId: cameraId,
2241
- selfLocation,
2242
- resolvedIds: new Set(resolved.devices.map((d) => d.deviceId)),
2243
- candidates: all
2244
- });
2245
- for (const containerId of invisible) {
2246
- const key = `${cameraId}:${containerId}`;
2247
- if (warnedInvisibleContainers.has(key)) continue;
2248
- if (warnedInvisibleContainers.size >= INVISIBLE_CONTAINER_WARN_CAP) return;
2249
- warnedInvisibleContainers.add(key);
2250
- 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", {
2251
- tags: { deviceId: containerId },
2252
- meta: {
2253
- cameraId,
2254
- location: selfLocation
2255
- }
2256
- });
2257
- }
2258
- }
2259
- /**
2260
2198
  * Resolve ONE camera's linked set against an ALREADY-FETCHED fleet.
2261
2199
  *
2262
2200
  * The fleet is a parameter, not a fetch, because it is the entire cost of this
@@ -2268,7 +2206,6 @@ function resolveFor(deviceId, all, byId, blob) {
2268
2206
  const config = parseLinkedDevicesConfig(blob);
2269
2207
  const ids = resolveLinkedDeviceIds({
2270
2208
  selfId: deviceId,
2271
- selfLocation: byId.get(deviceId)?.location ?? null,
2272
2209
  config,
2273
2210
  candidates: all
2274
2211
  });
@@ -2294,10 +2231,7 @@ function resolveFor(deviceId, all, byId, blob) {
2294
2231
  async function getLinkedDevices(pctx, input) {
2295
2232
  const { deviceId } = input;
2296
2233
  const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
2297
- const byId = new Map(all.map((d) => [d.id, d]));
2298
- const resolved = resolveFor(deviceId, all, byId, blob);
2299
- warnLocationInvisibleContainers(pctx, deviceId, byId, all, resolved);
2300
- return resolved;
2234
+ return resolveFor(deviceId, all, new Map(all.map((d) => [d.id, d])), blob);
2301
2235
  }
2302
2236
  /**
2303
2237
  * Cap implementation of `getLinkedDevicesBatch` — ONE fleet listing for the
@@ -2324,11 +2258,9 @@ async function getLinkedDevicesBatch(pctx, input) {
2324
2258
  })))]);
2325
2259
  const byId = new Map(all.map((d) => [d.id, d]));
2326
2260
  return deviceIds.map((deviceId, index) => {
2327
- const resolved = resolveFor(deviceId, all, byId, blobs[index] ?? {});
2328
- warnLocationInvisibleContainers(pctx, deviceId, byId, all, resolved);
2329
2261
  return {
2330
2262
  deviceId,
2331
- ...resolved
2263
+ ...resolveFor(deviceId, all, byId, blobs[index] ?? {})
2332
2264
  };
2333
2265
  });
2334
2266
  }
@@ -2343,13 +2275,9 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2343
2275
  if (!persisted || persisted.meta.type !== require_dist.DeviceType.Camera) return null;
2344
2276
  const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
2345
2277
  const config = parseLinkedDevicesConfig(blob);
2346
- const options = all.filter((d) => d.id !== deviceId && d.type !== require_dist.DeviceType.Container).map((d) => ({
2347
- value: String(d.id),
2348
- label: d.location !== null ? `${d.name} (${d.location})` : d.name
2349
- }));
2278
+ const self = all.find((d) => d.id === deviceId);
2350
2279
  const linkedIds = resolveLinkedDeviceIds({
2351
2280
  selfId: deviceId,
2352
- selfLocation: all.find((d) => d.id === deviceId)?.location ?? null,
2353
2281
  config,
2354
2282
  candidates: all
2355
2283
  });
@@ -2366,44 +2294,27 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2366
2294
  return { sections: [{
2367
2295
  id: "linked-devices",
2368
2296
  title: "Linked devices",
2369
- 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.",
2297
+ 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).",
2370
2298
  order: 70,
2371
- fields: [
2372
- {
2373
- type: "select",
2374
- key: LINKED_MODE_CONFIG_KEY,
2375
- label: "Link mode",
2376
- description: "Auto: children + same-location devices. Manual: children + the list below.",
2377
- options: [{
2378
- value: "auto",
2379
- label: "Auto (location-based)"
2380
- }, {
2381
- value: "manual",
2382
- label: "Manual"
2383
- }],
2384
- value: config.mode
2385
- },
2386
- {
2387
- type: "multiselect",
2388
- key: LINKED_IDS_CONFIG_KEY,
2389
- label: "Manually linked devices",
2390
- description: "Used in manual mode (children stay linked regardless).",
2391
- showWhen: {
2392
- field: LINKED_MODE_CONFIG_KEY,
2393
- equals: "manual"
2394
- },
2395
- options,
2396
- value: config.manualIds.map(String)
2299
+ fields: [{
2300
+ type: "device-multiselect",
2301
+ key: LINKED_IDS_CONFIG_KEY,
2302
+ label: "Linked devices",
2303
+ description: "Pick the companions of this camera. Same-location devices are suggested; device-tree children stay linked regardless.",
2304
+ deviceFilter: {
2305
+ excludeDeviceId: deviceId,
2306
+ excludeTypes: [require_dist.DeviceType.Container]
2397
2307
  },
2398
- {
2399
- type: "multiselect",
2400
- key: LINKED_TRACKED_IDS_CONFIG_KEY,
2401
- label: "Devices that produce tracked events",
2402
- description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
2403
- options: trackedOptions,
2404
- value: trackedValue
2405
- }
2406
- ]
2308
+ suggestLocation: self?.location ?? null,
2309
+ value: config.manualIds.map(String)
2310
+ }, {
2311
+ type: "multiselect",
2312
+ key: LINKED_TRACKED_IDS_CONFIG_KEY,
2313
+ label: "Devices that produce tracked events",
2314
+ description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
2315
+ options: trackedOptions,
2316
+ value: trackedValue
2317
+ }]
2407
2318
  }] };
2408
2319
  }
2409
2320
  /**
@@ -2414,7 +2325,6 @@ async function buildLinkedDevicesContribution(pctx, deviceId) {
2414
2325
  */
2415
2326
  async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
2416
2327
  const next = { ...await pctx.settings.readDeviceStore(deviceId) };
2417
- if ("_linkedDevicesMode" in patch) next[LINKED_MODE_CONFIG_KEY] = patch["_linkedDevicesMode"] === "manual" ? "manual" : "auto";
2418
2328
  if ("_linkedDeviceIds" in patch) next[LINKED_IDS_CONFIG_KEY] = [...parseLinkedDevicesConfig({ [LINKED_IDS_CONFIG_KEY]: patch[LINKED_IDS_CONFIG_KEY] }).manualIds];
2419
2329
  if ("_linkedDeviceTrackedIds" in patch) {
2420
2330
  const raw = patch[LINKED_TRACKED_IDS_CONFIG_KEY];
@@ -3817,7 +3727,7 @@ async function runLocationBackfillAtBoot(pctx, bootRows) {
3817
3727
  });
3818
3728
  }
3819
3729
  const residual = planNullLocationFills((!repaired && bootRows !== void 0 ? bootRows : await pctx.metaStore.rows.listAll()).map((r) => r.meta));
3820
- 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", {
3730
+ 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", {
3821
3731
  tags: { deviceId: ancestorId },
3822
3732
  meta: {
3823
3733
  location: fills[0]?.location,
@@ -4376,7 +4286,7 @@ async function collectDescendants(metaStore, rootId) {
4376
4286
  * every failing id — never indistinguishable from a clean cascade. A
4377
4287
  * `DeviceMetaChanged` event fires for the parent itself and for every
4378
4288
  * descendant actually written (never for one that failed), so linked-device
4379
- * consumers (auto-link) see exactly what landed.
4289
+ * consumers (the linked-devices picker, which suggests by location) see exactly what landed.
4380
4290
  */
4381
4291
  async function setLocation(pctx, input) {
4382
4292
  const { deviceId, location } = input;
@@ -4974,6 +4884,66 @@ async function removeLocation(pctx, input) {
4974
4884
  });
4975
4885
  }
4976
4886
  //#endregion
4887
+ //#region src/builtins/device-manager/linked-devices-manual-backfill.ts
4888
+ var LINKED_MANUAL_BACKFILL_MARKER_KEY = "_linkedDevicesManualBackfillVersion";
4889
+ function planManualBackfill(params) {
4890
+ const { selfId, selfLocation, blob, candidates } = params;
4891
+ const current = parseLinkedDevicesConfig(blob).manualIds;
4892
+ if (blob["_linkedDevicesMode"] === "manual") return {
4893
+ write: false,
4894
+ manualIds: current
4895
+ };
4896
+ const merged = [...current];
4897
+ for (const id of legacyAutoLinkIds({
4898
+ selfId,
4899
+ selfLocation,
4900
+ candidates
4901
+ })) if (!merged.includes(id)) merged.push(id);
4902
+ return {
4903
+ write: true,
4904
+ manualIds: merged
4905
+ };
4906
+ }
4907
+ async function runLinkedDevicesManualBackfillAtBoot(pctx) {
4908
+ const logger = pctx.host.ctx.logger;
4909
+ const addonStore = await pctx.settings.readAddonStore();
4910
+ const markerRaw = addonStore[LINKED_MANUAL_BACKFILL_MARKER_KEY];
4911
+ if ((typeof markerRaw === "number" ? markerRaw : 0) >= 1) return;
4912
+ const all = await listAll(pctx, LINK_CANDIDATE_PROJECTION);
4913
+ let written = 0;
4914
+ let linked = 0;
4915
+ for (const cam of all) {
4916
+ if (cam.type !== require_dist.DeviceType.Camera) continue;
4917
+ const blob = await pctx.settings.readDeviceStore(cam.id);
4918
+ const plan = planManualBackfill({
4919
+ selfId: cam.id,
4920
+ selfLocation: cam.location ?? null,
4921
+ blob,
4922
+ candidates: all
4923
+ });
4924
+ if (!plan.write) continue;
4925
+ await pctx.settings.writeDeviceStore(cam.id, {
4926
+ ...blob,
4927
+ [LINKED_MODE_CONFIG_KEY]: "manual",
4928
+ [LINKED_IDS_CONFIG_KEY]: [...plan.manualIds]
4929
+ });
4930
+ written += 1;
4931
+ linked += plan.manualIds.length;
4932
+ logger.info("linked devices: auto links materialised into the manual list (D356)", {
4933
+ tags: { deviceId: cam.id },
4934
+ meta: { manualIds: plan.manualIds }
4935
+ });
4936
+ }
4937
+ await pctx.settings.writeAddonStore({
4938
+ ...addonStore,
4939
+ [LINKED_MANUAL_BACKFILL_MARKER_KEY]: 1
4940
+ });
4941
+ logger.info("linked devices: manual backfill complete (D356)", { meta: {
4942
+ camerasWritten: written,
4943
+ linksMaterialised: linked
4944
+ } });
4945
+ }
4946
+ //#endregion
4977
4947
  //#region src/builtins/device-manager/device-meta-types.ts
4978
4948
  /**
4979
4949
  * Decode the raw `ctx.settings.readAddonStore()` record into {@link AddonStore}.
@@ -6727,6 +6697,11 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
6727
6697
  } catch (err) {
6728
6698
  this.ctx.logger.warn("location backfill failed — fleet boots without it", { meta: { error: require_dist.errMsg(err) } });
6729
6699
  }
6700
+ try {
6701
+ await runLinkedDevicesManualBackfillAtBoot(pctx);
6702
+ } catch (err) {
6703
+ this.ctx.logger.warn("linked-devices manual backfill failed — auto links are NOT materialised", { meta: { error: require_dist.errMsg(err) } });
6704
+ }
6730
6705
  const provider = {
6731
6706
  resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
6732
6707
  resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),