@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.
- 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 +35 -20
- package/dist/builtins/device-manager/device-manager.addon.js +127 -152
- package/dist/builtins/device-manager/device-manager.addon.mjs +127 -152
- package/dist/builtins/device-manager/device-meta-actions.d.ts +1 -1
- package/dist/builtins/device-manager/linked-devices-manual-backfill.d.ts +16 -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-BLU9Dol1.mjs → dist-D0_hqbgc.mjs} +19 -5
- package/dist/{dist-0fjBDzUu.js → dist-DlL8iWQo.js} +19 -5
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{manifest-system-deps-BF-C5NDL.mjs → manifest-system-deps-CYKgsQ_b.mjs} +4 -2
- package/dist/{manifest-system-deps-KBuTXH-y.js → manifest-system-deps-CdIVy85V.js} +4 -2
- package/dist/{retired-settings-keys-rIzS2f51.mjs → retired-settings-keys-AE2Qy3Xd.mjs} +1 -1
- package/dist/{retired-settings-keys-pL2Nq6KT.js → retired-settings-keys-B5wVSU65.js} +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-CdIVy85V.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-CYKgsQ_b.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-DlL8iWQo.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-D0_hqbgc.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-DlL8iWQo.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-D0_hqbgc.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-DlL8iWQo.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-D0_hqbgc.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-DlL8iWQo.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-D0_hqbgc.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-DlL8iWQo.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-D0_hqbgc.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-DlL8iWQo.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-D0_hqbgc.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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
82
|
-
readonly
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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-
|
|
7
|
-
const require_retired_settings_keys = require("../../retired-settings-keys-
|
|
6
|
+
const require_dist = require("../../dist-DlL8iWQo.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-B5wVSU65.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 =
|
|
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,
|
|
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
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
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;
|
|
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
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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
|
|
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 (
|
|
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),
|