@camstack/system 1.2.184 → 1.2.186
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-linked-devices.d.ts +20 -0
- package/dist/builtins/device-manager/device-manager.addon.js +371 -12
- package/dist/builtins/device-manager/device-manager.addon.mjs +371 -12
- package/dist/builtins/device-manager/location-alignment.d.ts +61 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-DE3OH-FG.js → dist-0fjBDzUu.js} +56 -5
- package/dist/{dist-CCMYMmkQ.mjs → dist-BLU9Dol1.mjs} +56 -5
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{manifest-system-deps-BmXFnBQZ.mjs → manifest-system-deps-BF-C5NDL.mjs} +2 -55
- package/dist/{manifest-system-deps-DMWd5Ygf.js → manifest-system-deps-KBuTXH-y.js} +1 -54
- package/dist/{retired-settings-keys-BgUS9Rr3.js → retired-settings-keys-pL2Nq6KT.js} +1 -1
- package/dist/{retired-settings-keys-C0uslzAd.mjs → retired-settings-keys-rIzS2f51.mjs} +1 -1
- package/package.json +1 -1
package/dist/addon-runner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_manifest_system_deps = require("./manifest-system-deps-
|
|
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);
|
package/dist/addon-runner.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
6
|
+
const require_dist = require("../../dist-0fjBDzUu.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-
|
|
1
|
+
import { E as addonPagesCapability, Ft as BaseAddon, Pt as errMsg, sn as EventCategory } from "../../dist-BLU9Dol1.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-
|
|
6
|
+
const require_dist = require("../../dist-0fjBDzUu.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-
|
|
1
|
+
import { Ft as BaseAddon, O as addonWidgetsCapability, Pt as errMsg, sn as EventCategory } from "../../dist-BLU9Dol1.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-
|
|
6
|
+
const require_dist = require("../../dist-0fjBDzUu.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-
|
|
1
|
+
import { Ft as BaseAddon, Jt as createEvent, Pt as errMsg, k as alertsCapability, n as AlertSchema, sn as EventCategory } from "../../dist-BLU9Dol1.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-
|
|
6
|
+
const require_dist = require("../../dist-0fjBDzUu.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-
|
|
1
|
+
import { Ft as BaseAddon, j as backupCapability, sn as EventCategory } from "../../dist-BLU9Dol1.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-
|
|
6
|
+
const require_dist = require("../../dist-0fjBDzUu.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-
|
|
1
|
+
import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-BLU9Dol1.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-
|
|
3
|
+
const require_dist = require("../../dist-0fjBDzUu.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-
|
|
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-BLU9Dol1.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";
|
|
@@ -64,6 +64,26 @@ export declare function resolveLinkedDeviceIds(params: {
|
|
|
64
64
|
export declare function resolveTrackedEventDeviceIds(config: Record<string, unknown>, linkedIds: readonly number[]): readonly number[];
|
|
65
65
|
/** The single resolved answer, before it is tagged with its camera. */
|
|
66
66
|
export type ResolvedLinkedDevices = Awaited<ReturnType<IDeviceManagerProvider['getLinkedDevices']>>;
|
|
67
|
+
/**
|
|
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.
|
|
74
|
+
*
|
|
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.
|
|
80
|
+
*/
|
|
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[];
|
|
67
87
|
/** Cap implementation of `getLinkedDevices`. */
|
|
68
88
|
export declare function getLinkedDevices(pctx: ProviderContext, input: Parameters<IDeviceManagerProvider['getLinkedDevices']>[0]): ReturnType<IDeviceManagerProvider['getLinkedDevices']>;
|
|
69
89
|
/**
|
|
@@ -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-
|
|
7
|
-
const require_retired_settings_keys = require("../../retired-settings-keys-
|
|
6
|
+
const require_dist = require("../../dist-0fjBDzUu.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-pL2Nq6KT.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
|
|
@@ -2172,6 +2172,91 @@ function resolveTrackedEventDeviceIds(config, linkedIds) {
|
|
|
2172
2172
|
*/
|
|
2173
2173
|
var LINK_CANDIDATE_PROJECTION = { projection: "slim" };
|
|
2174
2174
|
/**
|
|
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
|
+
/**
|
|
2175
2260
|
* Resolve ONE camera's linked set against an ALREADY-FETCHED fleet.
|
|
2176
2261
|
*
|
|
2177
2262
|
* The fleet is a parameter, not a fetch, because it is the entire cost of this
|
|
@@ -2209,7 +2294,10 @@ function resolveFor(deviceId, all, byId, blob) {
|
|
|
2209
2294
|
async function getLinkedDevices(pctx, input) {
|
|
2210
2295
|
const { deviceId } = input;
|
|
2211
2296
|
const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
|
|
2212
|
-
|
|
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;
|
|
2213
2301
|
}
|
|
2214
2302
|
/**
|
|
2215
2303
|
* Cap implementation of `getLinkedDevicesBatch` — ONE fleet listing for the
|
|
@@ -2235,10 +2323,14 @@ async function getLinkedDevicesBatch(pctx, input) {
|
|
|
2235
2323
|
return {};
|
|
2236
2324
|
})))]);
|
|
2237
2325
|
const byId = new Map(all.map((d) => [d.id, d]));
|
|
2238
|
-
return deviceIds.map((deviceId, index) =>
|
|
2239
|
-
deviceId,
|
|
2240
|
-
|
|
2241
|
-
|
|
2326
|
+
return deviceIds.map((deviceId, index) => {
|
|
2327
|
+
const resolved = resolveFor(deviceId, all, byId, blobs[index] ?? {});
|
|
2328
|
+
warnLocationInvisibleContainers(pctx, deviceId, byId, all, resolved);
|
|
2329
|
+
return {
|
|
2330
|
+
deviceId,
|
|
2331
|
+
...resolved
|
|
2332
|
+
};
|
|
2333
|
+
});
|
|
2242
2334
|
}
|
|
2243
2335
|
/**
|
|
2244
2336
|
* "Linked devices" settings section for a CAMERA (null for every other
|
|
@@ -3525,6 +3617,215 @@ async function swapHardwareState(settings, sourceId, targetId) {
|
|
|
3525
3617
|
};
|
|
3526
3618
|
}
|
|
3527
3619
|
//#endregion
|
|
3620
|
+
//#region src/builtins/device-manager/location-alignment.ts
|
|
3621
|
+
/**
|
|
3622
|
+
* Location alignment — closes the RETROACTIVITY hole D252/D329 left open, and
|
|
3623
|
+
* makes the broken state LOUD wherever it survives.
|
|
3624
|
+
*
|
|
3625
|
+
* The cascade (`setLocation` → every descendant's own `location` column,
|
|
3626
|
+
* device-meta-actions.ts) runs only when `setLocation` is CALLED. Both records
|
|
3627
|
+
* said so out loud ("Nothing retroactive") and the fleet paid for it: measured
|
|
3628
|
+
* on the live hub 2026-09-04, 40 containers whose location predated the
|
|
3629
|
+
* cascade carried 373 children at `location: null` — every one of those
|
|
3630
|
+
* containers invisible to auto-mode linking-by-location (the container itself
|
|
3631
|
+
* is deliberately never a link; its children never matched), and not one log
|
|
3632
|
+
* line said so. Camera 615 showed one siren where four appliances belonged.
|
|
3633
|
+
*
|
|
3634
|
+
* Two mechanisms close it, both WRITE-time — read-time inheritance stays
|
|
3635
|
+
* rejected (D252's context: a live-resolved location is invisible everywhere
|
|
3636
|
+
* except the one call site that resolves it):
|
|
3637
|
+
*
|
|
3638
|
+
* 1. **A one-shot boot backfill** (`runLocationBackfillAtBoot`). For every
|
|
3639
|
+
* device whose `location` is null and whose parent chain reaches a located
|
|
3640
|
+
* CONTAINER ancestor (container-only — see `planNullLocationFills` for
|
|
3641
|
+
* why a camera's subtree is deliberately left alone), the NEAREST located
|
|
3642
|
+
* ancestor's location is written onto the
|
|
3643
|
+
* row — once per install, marker-guarded. It fills NULL ONLY: an automated
|
|
3644
|
+
* pass carries no operator intent, so it must never overwrite an explicit
|
|
3645
|
+
* different location the way an operator's `setLocation` deliberately
|
|
3646
|
+
* does. Runs under `withMetaWriteLock` and re-verifies each row is still
|
|
3647
|
+
* null before patching, so it cannot fight a concurrent edit. The marker
|
|
3648
|
+
* is only advanced when every planned write LANDED — a partial repair
|
|
3649
|
+
* retries next boot (bounded: once per boot), never silently half-done.
|
|
3650
|
+
*
|
|
3651
|
+
* 2. **Registration-time inheritance** (`resolveInheritedLocation`, consumed
|
|
3652
|
+
* by `registerDevice` in device-meta-actions.ts). A child adopted AFTER
|
|
3653
|
+
* its parent's location was set is the same hole reopened — the cascade
|
|
3654
|
+
* only fires on `setLocation`, and nobody re-saves a container because HA
|
|
3655
|
+
* grew an entity. So a newly-registered (or re-parented) row whose own
|
|
3656
|
+
* location is null inherits the nearest located CONTAINER ancestor's
|
|
3657
|
+
* location the instant it registers, and pulls it down onto its own
|
|
3658
|
+
* null-location descendants (out-of-order adoption registers children
|
|
3659
|
+
* before parents).
|
|
3660
|
+
*
|
|
3661
|
+
* And one detection: every boot, read-only, the fleet is checked for the
|
|
3662
|
+
* broken state and each misaligned ancestor is WARNED about, device-tagged —
|
|
3663
|
+
* the state can re-enter (a partially-failed cascade, a hand-edited ledger)
|
|
3664
|
+
* and a branch that drops work must say so.
|
|
3665
|
+
*/
|
|
3666
|
+
/** Addon-store key of the one-shot marker. A number: the highest backfill
|
|
3667
|
+
* version that COMPLETED (every planned write landed). */
|
|
3668
|
+
var LOCATION_BACKFILL_MARKER_KEY = "locationBackfillVersion";
|
|
3669
|
+
/** Walk the parent chain of `startParentId` over an in-memory fleet snapshot
|
|
3670
|
+
* and return the nearest ancestor carrying a non-null location. Cycle-guarded:
|
|
3671
|
+
* a chain that revisits a node answers null rather than spinning. */
|
|
3672
|
+
function nearestLocatedAncestor(byId, startParentId) {
|
|
3673
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3674
|
+
let cursor = startParentId;
|
|
3675
|
+
while (cursor !== null && !visited.has(cursor)) {
|
|
3676
|
+
visited.add(cursor);
|
|
3677
|
+
const ancestor = byId.get(cursor);
|
|
3678
|
+
if (!ancestor) return null;
|
|
3679
|
+
if (ancestor.location !== null) return ancestor;
|
|
3680
|
+
cursor = ancestor.parentDeviceId;
|
|
3681
|
+
}
|
|
3682
|
+
return null;
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Pure planning core of the backfill: every row whose own `location` is null
|
|
3686
|
+
* and whose parent chain reaches a located CONTAINER ancestor gets that
|
|
3687
|
+
* ancestor's location. Nearest located ancestor wins — and DECIDES: a located
|
|
3688
|
+
* non-container between the row and a located container blocks the fill.
|
|
3689
|
+
*
|
|
3690
|
+
* The container-only predicate is an operator decision (2026-09-05), the
|
|
3691
|
+
* automated mirror of the structural-children rule: a non-container parent's
|
|
3692
|
+
* subtree (a camera and its siren/floodlight) is already fully reachable
|
|
3693
|
+
* through `resolveLinkedDeviceIds`' children-of-self rule, so a location buys
|
|
3694
|
+
* its own camera nothing — and COSTS cross-camera noise, verified against the
|
|
3695
|
+
* real fleet shape with the real resolver: a camera child that gains the room
|
|
3696
|
+
* matches every OTHER camera in that room individually (`ids.add`; only a
|
|
3697
|
+
* Container parent routes through `expand`), so camera B lists camera A's
|
|
3698
|
+
* siren as a companion. A container's descendants, by contrast, are reachable
|
|
3699
|
+
* by location ONLY through their own columns — the container itself is never
|
|
3700
|
+
* a link — which is the whole reason the cascade writes them (D252). Note the
|
|
3701
|
+
* asymmetry with the OPERATOR's cascade, which has no type predicate (D329):
|
|
3702
|
+
* an operator re-saving a camera accepts that trade explicitly; an automated
|
|
3703
|
+
* pass must not manufacture it.
|
|
3704
|
+
*/
|
|
3705
|
+
function planNullLocationFills(rows) {
|
|
3706
|
+
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
3707
|
+
const plan = [];
|
|
3708
|
+
for (const row of rows) {
|
|
3709
|
+
if (row.location !== null) continue;
|
|
3710
|
+
if (row.parentDeviceId === null) continue;
|
|
3711
|
+
const hit = nearestLocatedAncestor(byId, row.parentDeviceId);
|
|
3712
|
+
if (hit === null) continue;
|
|
3713
|
+
if (hit.type !== require_dist.DeviceType.Container) continue;
|
|
3714
|
+
if (hit.location === null) continue;
|
|
3715
|
+
plan.push({
|
|
3716
|
+
id: row.id,
|
|
3717
|
+
location: hit.location,
|
|
3718
|
+
ancestorId: hit.id
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
return plan;
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Async ancestor walk for `registerDevice`: the nearest located CONTAINER
|
|
3725
|
+
* ancestor's location, read row by row (registration has no fleet snapshot in
|
|
3726
|
+
* hand and must not pay for one). Same container-only predicate as
|
|
3727
|
+
* {@link planNullLocationFills}, and for the same reason — a child adopted
|
|
3728
|
+
* under a located CAMERA is already linked structurally to that camera, and a
|
|
3729
|
+
* location on its own column would make it a companion of every other camera
|
|
3730
|
+
* in the room. The nearest located ancestor decides: a located non-container
|
|
3731
|
+
* on the way up answers null. Cycle-guarded like the pure walk; a missing row
|
|
3732
|
+
* — a parent registered out of order — answers null, and the parent's own
|
|
3733
|
+
* later registration pulls the location down instead (see `registerDevice`).
|
|
3734
|
+
*/
|
|
3735
|
+
async function resolveInheritedLocation(rows, parentDeviceId) {
|
|
3736
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3737
|
+
let cursor = parentDeviceId;
|
|
3738
|
+
while (cursor !== null && !visited.has(cursor)) {
|
|
3739
|
+
visited.add(cursor);
|
|
3740
|
+
const ancestor = await rows.get(cursor);
|
|
3741
|
+
if (ancestor === null) return null;
|
|
3742
|
+
if (ancestor.meta.location !== null) return ancestor.meta.type === require_dist.DeviceType.Container ? ancestor.meta.location : null;
|
|
3743
|
+
cursor = ancestor.meta.parentDeviceId;
|
|
3744
|
+
}
|
|
3745
|
+
return null;
|
|
3746
|
+
}
|
|
3747
|
+
/** `fills` grouped by the ancestor whose location they inherit — the unit the
|
|
3748
|
+
* logs speak in, because the ancestor is the device an operator would act on. */
|
|
3749
|
+
function groupByAncestor(fills) {
|
|
3750
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3751
|
+
for (const fill of fills) {
|
|
3752
|
+
const bucket = grouped.get(fill.ancestorId);
|
|
3753
|
+
if (bucket) bucket.push(fill);
|
|
3754
|
+
else grouped.set(fill.ancestorId, [fill]);
|
|
3755
|
+
}
|
|
3756
|
+
return grouped;
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Boot entrypoint: the one-shot repair (marker-guarded) plus the every-boot
|
|
3760
|
+
* detection. `bootRows` is the fleet listing the addon's warm-boot pass
|
|
3761
|
+
* already paid for — reused for the detection so a clean boot costs no extra
|
|
3762
|
+
* fleet read; ignored by the repair, which takes its own listing UNDER the
|
|
3763
|
+
* meta write lock (the snapshot must not predate the lock).
|
|
3764
|
+
*/
|
|
3765
|
+
async function runLocationBackfillAtBoot(pctx, bootRows) {
|
|
3766
|
+
const logger = pctx.host.ctx.logger;
|
|
3767
|
+
const markerRaw = (await pctx.settings.readAddonStore())[LOCATION_BACKFILL_MARKER_KEY];
|
|
3768
|
+
const completedVersion = typeof markerRaw === "number" ? markerRaw : 0;
|
|
3769
|
+
let repaired = false;
|
|
3770
|
+
if (completedVersion < 1) {
|
|
3771
|
+
const patched = [];
|
|
3772
|
+
const failed = [];
|
|
3773
|
+
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3774
|
+
const plan = planNullLocationFills((await pctx.metaStore.rows.listAll()).map((r) => r.meta));
|
|
3775
|
+
for (const fill of plan) try {
|
|
3776
|
+
const current = await pctx.metaStore.rows.get(fill.id);
|
|
3777
|
+
if (current === null || current.meta.location !== null) continue;
|
|
3778
|
+
await pctx.metaStore.rows.patch(fill.id, { location: fill.location });
|
|
3779
|
+
patched.push(fill);
|
|
3780
|
+
} catch (err) {
|
|
3781
|
+
failed.push({
|
|
3782
|
+
id: fill.id,
|
|
3783
|
+
error: err
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
repaired = true;
|
|
3788
|
+
for (const [ancestorId, fills] of groupByAncestor(patched)) {
|
|
3789
|
+
pctx.host.ctx.eventBus.emit({
|
|
3790
|
+
id: (0, node_crypto.randomUUID)(),
|
|
3791
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3792
|
+
source: {
|
|
3793
|
+
type: "device",
|
|
3794
|
+
id: ancestorId
|
|
3795
|
+
},
|
|
3796
|
+
category: require_dist.EventCategory.DeviceMetaChanged,
|
|
3797
|
+
data: {
|
|
3798
|
+
deviceId: ancestorId,
|
|
3799
|
+
field: "location",
|
|
3800
|
+
value: fills[0]?.location ?? null
|
|
3801
|
+
}
|
|
3802
|
+
});
|
|
3803
|
+
logger.info("location backfill: aligned location-less descendants to their ancestor", {
|
|
3804
|
+
tags: { deviceId: ancestorId },
|
|
3805
|
+
meta: {
|
|
3806
|
+
location: fills[0]?.location,
|
|
3807
|
+
updatedIds: fills.map((f) => f.id)
|
|
3808
|
+
}
|
|
3809
|
+
});
|
|
3810
|
+
}
|
|
3811
|
+
if (failed.length > 0) logger.error("location backfill partially failed — retrying on the next boot", { meta: {
|
|
3812
|
+
failedIds: failed.map((f) => f.id),
|
|
3813
|
+
errors: failed.map((f) => f.error instanceof Error ? f.error.message : String(f.error))
|
|
3814
|
+
} });
|
|
3815
|
+
else await pctx.metaStore.withAddonStoreWriteLock(async () => {
|
|
3816
|
+
await pctx.settings.writeAddonStore({ [LOCATION_BACKFILL_MARKER_KEY]: 1 });
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
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", {
|
|
3821
|
+
tags: { deviceId: ancestorId },
|
|
3822
|
+
meta: {
|
|
3823
|
+
location: fills[0]?.location,
|
|
3824
|
+
misalignedDescendantIds: fills.map((f) => f.id)
|
|
3825
|
+
}
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
//#endregion
|
|
3528
3829
|
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
3529
3830
|
/**
|
|
3530
3831
|
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
@@ -3728,7 +4029,7 @@ async function migrateDevice(pctx, input) {
|
|
|
3728
4029
|
async function registerDevice(pctx, input) {
|
|
3729
4030
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
3730
4031
|
const featuresArr = Array.isArray(features) ? [...features] : [];
|
|
3731
|
-
const { isFirstRegistration, exportFingerprint, fingerprintChanged } = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
4032
|
+
const { isFirstRegistration, exportFingerprint, fingerprintChanged, inheritedLocation, inheritedDescendantIds, inheritFailed } = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3732
4033
|
const existingRow = await pctx.metaStore.rows.get(id);
|
|
3733
4034
|
const existingMeta = existingRow?.meta;
|
|
3734
4035
|
const wasRegistered = existingRow?.registered ?? false;
|
|
@@ -3740,6 +4041,10 @@ async function registerDevice(pctx, input) {
|
|
|
3740
4041
|
deviceType: type,
|
|
3741
4042
|
features: featuresArr
|
|
3742
4043
|
});
|
|
4044
|
+
const priorLocation = existingMeta?.location ?? null;
|
|
4045
|
+
const resolvedParentId = parentDeviceId ?? null;
|
|
4046
|
+
const parentChanged = existingMeta !== void 0 && (existingMeta.parentDeviceId ?? null) !== resolvedParentId;
|
|
4047
|
+
const inherited = priorLocation === null && resolvedParentId !== null && (isFirst || parentChanged) ? await resolveInheritedLocation(pctx.metaStore.rows, resolvedParentId) : null;
|
|
3743
4048
|
await pctx.metaStore.rows.upsertRegistration({
|
|
3744
4049
|
deviceId: id,
|
|
3745
4050
|
addonId,
|
|
@@ -3747,21 +4052,69 @@ async function registerDevice(pctx, input) {
|
|
|
3747
4052
|
type,
|
|
3748
4053
|
name: resolvedName,
|
|
3749
4054
|
userNamed: wasUserNamed,
|
|
3750
|
-
location:
|
|
4055
|
+
location: priorLocation ?? inherited,
|
|
3751
4056
|
disabled: existingMeta?.disabled ?? false,
|
|
3752
|
-
parentDeviceId:
|
|
4057
|
+
parentDeviceId: resolvedParentId,
|
|
3753
4058
|
registered: true,
|
|
3754
4059
|
features: featuresArr,
|
|
3755
4060
|
exportFingerprint: fingerprint
|
|
3756
4061
|
});
|
|
4062
|
+
const descendantsFilled = [];
|
|
4063
|
+
const descendantsFailed = [];
|
|
4064
|
+
if (inherited !== null) for (const descendant of await collectDescendants(pctx.metaStore, id)) {
|
|
4065
|
+
if (descendant.location !== null) continue;
|
|
4066
|
+
try {
|
|
4067
|
+
await pctx.metaStore.rows.patch(descendant.id, { location: inherited });
|
|
4068
|
+
descendantsFilled.push(descendant.id);
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
descendantsFailed.push({
|
|
4071
|
+
id: descendant.id,
|
|
4072
|
+
error: err
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
3757
4076
|
return {
|
|
3758
4077
|
isFirstRegistration: isFirst,
|
|
3759
4078
|
exportFingerprint: fingerprint,
|
|
3760
|
-
fingerprintChanged: existingMeta?.exportFingerprint !== fingerprint
|
|
4079
|
+
fingerprintChanged: existingMeta?.exportFingerprint !== fingerprint,
|
|
4080
|
+
inheritedLocation: inherited,
|
|
4081
|
+
inheritedDescendantIds: descendantsFilled,
|
|
4082
|
+
inheritFailed: descendantsFailed
|
|
3761
4083
|
};
|
|
3762
4084
|
});
|
|
3763
4085
|
if (Object.keys(config).length > 0) await pctx.settings.writeDeviceStore(id, config);
|
|
3764
4086
|
pctx.metaStore.idToAddonId.set(id, addonId);
|
|
4087
|
+
if (inheritedLocation !== null) {
|
|
4088
|
+
for (const changedId of [id, ...inheritedDescendantIds]) pctx.host.ctx.eventBus.emit({
|
|
4089
|
+
id: (0, node_crypto.randomUUID)(),
|
|
4090
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
4091
|
+
source: {
|
|
4092
|
+
type: "device",
|
|
4093
|
+
id: changedId
|
|
4094
|
+
},
|
|
4095
|
+
category: require_dist.EventCategory.DeviceMetaChanged,
|
|
4096
|
+
data: {
|
|
4097
|
+
deviceId: changedId,
|
|
4098
|
+
field: "location",
|
|
4099
|
+
value: inheritedLocation
|
|
4100
|
+
}
|
|
4101
|
+
});
|
|
4102
|
+
pctx.host.ctx.logger.info("registerDevice: inherited located-ancestor location", {
|
|
4103
|
+
tags: { deviceId: id },
|
|
4104
|
+
meta: {
|
|
4105
|
+
location: inheritedLocation,
|
|
4106
|
+
descendantsFilled: inheritedDescendantIds.length
|
|
4107
|
+
}
|
|
4108
|
+
});
|
|
4109
|
+
if (inheritFailed.length > 0) pctx.host.ctx.logger.error("registerDevice: inherited-location fill partially failed — some descendants NOT aligned", {
|
|
4110
|
+
tags: { deviceId: id },
|
|
4111
|
+
meta: {
|
|
4112
|
+
location: inheritedLocation,
|
|
4113
|
+
failedIds: inheritFailed.map((f) => f.id),
|
|
4114
|
+
errors: inheritFailed.map((f) => f.error instanceof Error ? f.error.message : String(f.error))
|
|
4115
|
+
}
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
3765
4118
|
if (isFirstRegistration) pctx.host.ctx.eventBus.emit({
|
|
3766
4119
|
id: (0, node_crypto.randomUUID)(),
|
|
3767
4120
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -6353,7 +6706,8 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
6353
6706
|
const stateMirror = this.stateMirrorImpl;
|
|
6354
6707
|
const resolvePersistedById = metaStore.resolvePersistedById;
|
|
6355
6708
|
const idToAddonId = metaStore.idToAddonId;
|
|
6356
|
-
|
|
6709
|
+
const bootRows = await deviceRows.listAll();
|
|
6710
|
+
for (const row of bootRows) idToAddonId.set(row.meta.id, row.meta.addonId);
|
|
6357
6711
|
const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, this.ctx, deviceId, integrationId);
|
|
6358
6712
|
const pctx = {
|
|
6359
6713
|
host: this.providerHost,
|
|
@@ -6368,6 +6722,11 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
6368
6722
|
return provider;
|
|
6369
6723
|
}
|
|
6370
6724
|
};
|
|
6725
|
+
try {
|
|
6726
|
+
await runLocationBackfillAtBoot(pctx, bootRows);
|
|
6727
|
+
} catch (err) {
|
|
6728
|
+
this.ctx.logger.warn("location backfill failed — fleet boots without it", { meta: { error: require_dist.errMsg(err) } });
|
|
6729
|
+
}
|
|
6371
6730
|
const provider = {
|
|
6372
6731
|
resolveDeviceOwnerSync: (deviceId) => resolveDeviceOwnerSync(pctx, deviceId),
|
|
6373
6732
|
resolveNativeCapOwnerSync: (capName, deviceId) => resolveNativeCapOwnerSync(pctx, capName, deviceId),
|