@camstack/system 1.2.304 → 1.2.306
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/addon-utils.d.ts +1 -1
- package/dist/addon-utils.js +3 -1
- package/dist/addon-utils.mjs +2 -2
- 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/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.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/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.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-extension-linked-slot.d.ts +27 -0
- package/dist/builtins/device-manager/device-linked-devices.d.ts +55 -13
- package/dist/builtins/device-manager/device-manager.addon.js +324 -136
- package/dist/builtins/device-manager/device-manager.addon.mjs +324 -136
- package/dist/builtins/doorbell/binding-mirror.d.ts +37 -7
- package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +66 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +17 -36
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +320 -124
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +320 -124
- 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/{child-cap-dispatch-C1c3rWaX.mjs → child-cap-dispatch-BCJjUtXV.mjs} +9 -1
- package/dist/{child-cap-dispatch-De5_HcvR.js → child-cap-dispatch-Be8e2cBP.js} +9 -1
- package/dist/{dist-Duqf80QM.mjs → dist-CLfJMMdf.mjs} +1056 -349
- package/dist/{dist-C571Y95Z.js → dist-vecbhDzu.js} +1079 -354
- package/dist/download/model-downloader.d.ts +15 -0
- package/dist/{event-loop-stall-monitor-GPrVvx14.mjs → event-loop-stall-monitor-EXjzIxDz.mjs} +25 -3
- package/dist/{event-loop-stall-monitor-CAsHdMwP.js → event-loop-stall-monitor-ey4mbofz.js} +36 -2
- package/dist/index.js +21 -3
- package/dist/index.mjs +21 -3
- package/dist/kernel/config-manager.d.ts +3 -1
- package/dist/{retired-settings-keys-0j8rNoa8.js → retired-settings-keys-Bm2p0Rot.js} +1 -1
- package/dist/{retired-settings-keys-CeO_dBHU.mjs → retired-settings-keys-C1ODcdnr.mjs} +1 -1
- package/package.json +7 -1
- package/dist/builtins/device-manager/device-store-sections.d.ts +0 -26
|
@@ -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-vecbhDzu.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-Bm2p0Rot.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
|
|
@@ -2720,6 +2720,16 @@ var LINKED_IDS_CONFIG_KEY = "_linkedDeviceIds";
|
|
|
2720
2720
|
* explicit operator OPT-IN (2026-07-22 flip: the old absent⇒ALL default
|
|
2721
2721
|
* auto-materialized sensor snapshots the operator never asked for). */
|
|
2722
2722
|
var LINKED_TRACKED_IDS_CONFIG_KEY = "_linkedDeviceTrackedIds";
|
|
2723
|
+
/**
|
|
2724
|
+
* The three reserved keys, as one set: they stay with a camera's NUMBER on a
|
|
2725
|
+
* hardware swap, and a settings save that still carries them (a page opened
|
|
2726
|
+
* before D655 retired the section) is refused by name, never dropped.
|
|
2727
|
+
*/
|
|
2728
|
+
var LINKED_DEVICES_RESERVED_KEYS = new Set([
|
|
2729
|
+
LINKED_MODE_CONFIG_KEY,
|
|
2730
|
+
LINKED_IDS_CONFIG_KEY,
|
|
2731
|
+
LINKED_TRACKED_IDS_CONFIG_KEY
|
|
2732
|
+
]);
|
|
2723
2733
|
/** Parse the two reserved keys out of a device-config blob. Tolerant:
|
|
2724
2734
|
* ids may arrive as numbers or numeric strings (the multiselect field
|
|
2725
2735
|
* stores string option values); anything else is dropped. */
|
|
@@ -2769,14 +2779,24 @@ function resolveLinkedDeviceIds(params) {
|
|
|
2769
2779
|
* are ignored.
|
|
2770
2780
|
*/
|
|
2771
2781
|
function resolveTrackedEventDeviceIds(config, linkedIds) {
|
|
2782
|
+
const picked = new Set(parseTrackedOptIn(config));
|
|
2783
|
+
return linkedIds.filter((id) => picked.has(id));
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* The persisted tracked-event opt-in AS STORED — not intersected with the
|
|
2787
|
+
* linked set, which is the resolver's job. Absent / non-array ⇒ `[]` (NONE,
|
|
2788
|
+
* the same answer the resolver gives). Ids may be numbers or numeric strings;
|
|
2789
|
+
* anything else is dropped; deduped, in stored order.
|
|
2790
|
+
*/
|
|
2791
|
+
function parseTrackedOptIn(config) {
|
|
2772
2792
|
const raw = config[LINKED_TRACKED_IDS_CONFIG_KEY];
|
|
2773
2793
|
if (!Array.isArray(raw)) return [];
|
|
2774
|
-
const
|
|
2794
|
+
const out = [];
|
|
2775
2795
|
for (const v of raw) {
|
|
2776
2796
|
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
2777
|
-
if (Number.isInteger(n))
|
|
2797
|
+
if (Number.isInteger(n) && !out.includes(n)) out.push(n);
|
|
2778
2798
|
}
|
|
2779
|
-
return
|
|
2799
|
+
return out;
|
|
2780
2800
|
}
|
|
2781
2801
|
/**
|
|
2782
2802
|
* The fleet listing every function here needs, and the ONLY projection any of
|
|
@@ -2869,115 +2889,56 @@ async function getLinkedDevicesBatch(pctx, input) {
|
|
|
2869
2889
|
});
|
|
2870
2890
|
}
|
|
2871
2891
|
/**
|
|
2872
|
-
*
|
|
2873
|
-
*
|
|
2874
|
-
*
|
|
2875
|
-
*
|
|
2876
|
-
|
|
2877
|
-
async function buildLinkedDevicesContribution(pctx, deviceId) {
|
|
2878
|
-
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
2879
|
-
if (!persisted || persisted.meta.type !== require_dist.DeviceType.Camera) return null;
|
|
2880
|
-
const [all, blob] = await Promise.all([listAll(pctx, LINK_CANDIDATE_PROJECTION), pctx.settings.readDeviceStore(deviceId)]);
|
|
2881
|
-
const config = parseLinkedDevicesConfig(blob);
|
|
2882
|
-
const self = all.find((d) => d.id === deviceId);
|
|
2883
|
-
const linkedIds = resolveLinkedDeviceIds({
|
|
2884
|
-
selfId: deviceId,
|
|
2885
|
-
config,
|
|
2886
|
-
candidates: all
|
|
2887
|
-
});
|
|
2888
|
-
const byId = new Map(all.map((d) => [d.id, d]));
|
|
2889
|
-
const trackedOptions = linkedIds.flatMap((id) => {
|
|
2890
|
-
const d = byId.get(id);
|
|
2891
|
-
if (!d) return [];
|
|
2892
|
-
return [{
|
|
2893
|
-
value: String(d.id),
|
|
2894
|
-
label: d.location !== null ? `${d.name} (${d.location})` : d.name
|
|
2895
|
-
}];
|
|
2896
|
-
});
|
|
2897
|
-
const trackedValue = resolveTrackedEventDeviceIds(blob, linkedIds).map(String);
|
|
2898
|
-
return { sections: [{
|
|
2899
|
-
id: "linked-devices",
|
|
2900
|
-
title: "Linked devices",
|
|
2901
|
-
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).",
|
|
2902
|
-
order: 70,
|
|
2903
|
-
fields: [{
|
|
2904
|
-
type: "device-multiselect",
|
|
2905
|
-
key: LINKED_IDS_CONFIG_KEY,
|
|
2906
|
-
label: "Linked devices",
|
|
2907
|
-
description: "Pick the companions of this camera. Same-location devices are suggested; device-tree children stay linked regardless.",
|
|
2908
|
-
deviceFilter: {
|
|
2909
|
-
excludeDeviceId: deviceId,
|
|
2910
|
-
excludeTypes: [require_dist.DeviceType.Container]
|
|
2911
|
-
},
|
|
2912
|
-
suggestLocation: self?.location ?? null,
|
|
2913
|
-
value: config.manualIds.map(String)
|
|
2914
|
-
}, {
|
|
2915
|
-
type: "multiselect",
|
|
2916
|
-
key: LINKED_TRACKED_IDS_CONFIG_KEY,
|
|
2917
|
-
label: "Devices that produce tracked events",
|
|
2918
|
-
description: "Linked devices whose state changes materialize a synthetic tracked event on this camera. Default: none (opt-in).",
|
|
2919
|
-
options: trackedOptions,
|
|
2920
|
-
value: trackedValue
|
|
2921
|
-
}]
|
|
2922
|
-
}] };
|
|
2923
|
-
}
|
|
2924
|
-
/**
|
|
2925
|
-
* Direct `applyDeviceSettingsPatch` normalization for the two reserved keys.
|
|
2926
|
-
* The admin-ui save path reaches the device config blob via the
|
|
2927
|
-
* `writerCapName: 'device-manager'` routing (never this method), but the cap
|
|
2928
|
-
* contract includes the method — keep it correct for direct callers.
|
|
2892
|
+
* THE write path for the linked-devices keys — the `linked-devices` slot's
|
|
2893
|
+
* `set` lands here (D648). Normalizes the pick list and the tracked-event
|
|
2894
|
+
* opt-in; an EXPLICIT empty opt-in is stored as `[]`, never deleted, because
|
|
2895
|
+
* an absent key and an empty one are read alike today but only `[]` says the
|
|
2896
|
+
* operator chose "none".
|
|
2929
2897
|
*/
|
|
2930
2898
|
async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
|
|
2931
2899
|
const next = { ...await pctx.settings.readDeviceStore(deviceId) };
|
|
2932
2900
|
if ("_linkedDeviceIds" in patch) next[LINKED_IDS_CONFIG_KEY] = [...parseLinkedDevicesConfig({ [LINKED_IDS_CONFIG_KEY]: patch[LINKED_IDS_CONFIG_KEY] }).manualIds];
|
|
2933
|
-
if ("_linkedDeviceTrackedIds" in patch)
|
|
2934
|
-
const raw = patch[LINKED_TRACKED_IDS_CONFIG_KEY];
|
|
2935
|
-
const ids = [];
|
|
2936
|
-
if (Array.isArray(raw)) for (const v of raw) {
|
|
2937
|
-
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
2938
|
-
if (Number.isInteger(n) && !ids.includes(n)) ids.push(n);
|
|
2939
|
-
}
|
|
2940
|
-
next[LINKED_TRACKED_IDS_CONFIG_KEY] = ids;
|
|
2941
|
-
}
|
|
2901
|
+
if ("_linkedDeviceTrackedIds" in patch) next[LINKED_TRACKED_IDS_CONFIG_KEY] = [...parseTrackedOptIn(patch)];
|
|
2942
2902
|
await pctx.settings.writeDeviceStore(deviceId, next);
|
|
2943
2903
|
return { success: true };
|
|
2944
2904
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
*
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2905
|
+
/**
|
|
2906
|
+
* Which NEW picks a linked-devices write must refuse: the camera itself, a
|
|
2907
|
+
* container (never a pick — its children are, D356 and the file header), and
|
|
2908
|
+
* an id no device has.
|
|
2909
|
+
*
|
|
2910
|
+
* Only ids that are not ALREADY persisted are judged. A list written before
|
|
2911
|
+
* this rule existed may hold a container or a device that has since been
|
|
2912
|
+
* deleted; the editor shows those as unavailable / missing, and keeping them
|
|
2913
|
+
* is not a new link. Removing them is the operator's call, never this
|
|
2914
|
+
* function's (a missing source is shown, never silently dropped).
|
|
2915
|
+
*
|
|
2916
|
+
* Since D655 every write of the list goes through the slot, the Locations
|
|
2917
|
+
* page's room rebuild included, so this is the only gate a pick passes.
|
|
2918
|
+
*/
|
|
2919
|
+
function refusedLinkedPicks(params) {
|
|
2920
|
+
const kept = new Set(params.persisted);
|
|
2921
|
+
const out = [];
|
|
2922
|
+
for (const deviceId of params.picked) {
|
|
2923
|
+
if (kept.has(deviceId)) continue;
|
|
2924
|
+
if (deviceId === params.selfId) {
|
|
2925
|
+
out.push({
|
|
2926
|
+
deviceId,
|
|
2927
|
+
reason: "self"
|
|
2928
|
+
});
|
|
2929
|
+
continue;
|
|
2969
2930
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2931
|
+
const device = params.fleet.get(deviceId);
|
|
2932
|
+
if (device === void 0) out.push({
|
|
2933
|
+
deviceId,
|
|
2934
|
+
reason: "unknown"
|
|
2935
|
+
});
|
|
2936
|
+
else if (device.type === require_dist.DeviceType.Container) out.push({
|
|
2937
|
+
deviceId,
|
|
2938
|
+
reason: "container"
|
|
2973
2939
|
});
|
|
2974
2940
|
}
|
|
2975
|
-
|
|
2976
|
-
for (const [key, value] of Object.entries(patch)) if (!claimed.has(key)) driverPatch[key] = value;
|
|
2977
|
-
return {
|
|
2978
|
-
storeGroups,
|
|
2979
|
-
driverPatch
|
|
2980
|
-
};
|
|
2941
|
+
return out;
|
|
2981
2942
|
}
|
|
2982
2943
|
//#endregion
|
|
2983
2944
|
//#region src/builtins/device-manager/device-aggregation.ts
|
|
@@ -3644,12 +3605,15 @@ async function updateDeviceFieldsBatch(deps, input) {
|
|
|
3644
3605
|
* registry lookup). Used by `updateDeviceFieldsBatch`. */
|
|
3645
3606
|
async function applyGroupPatch(deps, deviceId, group) {
|
|
3646
3607
|
if (group.writerCapName === "device-manager") {
|
|
3647
|
-
const
|
|
3648
|
-
if (
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3608
|
+
const retired = Object.keys(group.patch).filter((key) => LINKED_DEVICES_RESERVED_KEYS.has(key));
|
|
3609
|
+
if (retired.length > 0) {
|
|
3610
|
+
deps.ctx.logger.warn("device-manager: a save of the retired linked-devices section refused", {
|
|
3611
|
+
tags: { deviceId },
|
|
3612
|
+
meta: { keys: retired }
|
|
3613
|
+
});
|
|
3614
|
+
throw new Error(`[device-manager] ${retired.join(", ")} can no longer be saved from the settings section (D655) — reload the page and use the Extensions section`);
|
|
3652
3615
|
}
|
|
3616
|
+
const driverPatch = group.patch;
|
|
3653
3617
|
if (Object.keys(driverPatch).length === 0) return;
|
|
3654
3618
|
const hubRegistry = deps.ctx.kernel?.deviceRegistry;
|
|
3655
3619
|
const found = hubRegistry ? resolveDeviceById(hubRegistry, deviceId) : null;
|
|
@@ -3931,6 +3895,226 @@ var DeviceEventPropagator = class {
|
|
|
3931
3895
|
return chain;
|
|
3932
3896
|
}
|
|
3933
3897
|
};
|
|
3898
|
+
//#endregion
|
|
3899
|
+
//#region src/builtins/device-manager/device-extension-linked-slot.ts
|
|
3900
|
+
/** The routing prefix of this owner's `slotRef`s. */
|
|
3901
|
+
var DEVICE_MANAGER_EXTENSION_SOURCE = "device-manager";
|
|
3902
|
+
/** The store the slot reads and writes, named (D62). */
|
|
3903
|
+
var LINKED_DEVICES_AUTHORITY = `device-manager:${LINKED_IDS_CONFIG_KEY}`;
|
|
3904
|
+
/**
|
|
3905
|
+
* The picked ids' ledger fields, from ONE indexed read of the device manager's
|
|
3906
|
+
* own store (`rows.getMany`) — never the whole fleet to label a handful of ids.
|
|
3907
|
+
* An id no device has is absent from the map.
|
|
3908
|
+
*/
|
|
3909
|
+
function pickedFrom(rows) {
|
|
3910
|
+
const out = /* @__PURE__ */ new Map();
|
|
3911
|
+
for (const [id, row] of rows) out.set(id, {
|
|
3912
|
+
name: row.meta.name,
|
|
3913
|
+
type: row.meta.type
|
|
3914
|
+
});
|
|
3915
|
+
return out;
|
|
3916
|
+
}
|
|
3917
|
+
/** What a camera's picked id is, said once for the editor. */
|
|
3918
|
+
function sourceOf(deviceId, byId) {
|
|
3919
|
+
const device = byId.get(deviceId);
|
|
3920
|
+
if (device === void 0) return {
|
|
3921
|
+
deviceId,
|
|
3922
|
+
name: null,
|
|
3923
|
+
health: "missing",
|
|
3924
|
+
detail: "No device has this id any more — it links nothing until removed."
|
|
3925
|
+
};
|
|
3926
|
+
if (device.type === require_dist.DeviceType.Container) return {
|
|
3927
|
+
deviceId,
|
|
3928
|
+
name: device.name,
|
|
3929
|
+
health: "unavailable",
|
|
3930
|
+
detail: "A container is never a pick (D356): its children are linked in its place. Pick the children instead."
|
|
3931
|
+
};
|
|
3932
|
+
return {
|
|
3933
|
+
deviceId,
|
|
3934
|
+
name: device.name,
|
|
3935
|
+
health: "ok"
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Which ids of a written opt-in must be refused: a NEW opt-in for a device the
|
|
3940
|
+
* camera does not link. An id already stored is never refused — whatever was
|
|
3941
|
+
* read can be written back unchanged (a stale opt-in is removed by leaving it
|
|
3942
|
+
* out, never by being unable to save). The written list REPLACES the stored
|
|
3943
|
+
* one: since D655 the editor shows every stored opt-in, so nothing it sends
|
|
3944
|
+
* back is written blind.
|
|
3945
|
+
*/
|
|
3946
|
+
function refusedTrackedOptIns(params) {
|
|
3947
|
+
const allowed = new Set([
|
|
3948
|
+
...params.picked,
|
|
3949
|
+
...params.implied,
|
|
3950
|
+
...params.stored
|
|
3951
|
+
]);
|
|
3952
|
+
return params.tracked.filter((id) => !allowed.has(id));
|
|
3953
|
+
}
|
|
3954
|
+
/** A ledger row as the pure link resolver reads it, or null for an unknown type. */
|
|
3955
|
+
function toLinkCandidate(row) {
|
|
3956
|
+
const type = Object.values(require_dist.DeviceType).find((t) => t === row.meta.type);
|
|
3957
|
+
if (type === void 0) return null;
|
|
3958
|
+
return {
|
|
3959
|
+
id: row.meta.id,
|
|
3960
|
+
parentDeviceId: row.meta.parentDeviceId,
|
|
3961
|
+
location: row.meta.location,
|
|
3962
|
+
type,
|
|
3963
|
+
stableId: row.meta.stableId,
|
|
3964
|
+
...row.meta.primaryChildEntityId !== void 0 ? { primaryChildEntityId: row.meta.primaryChildEntityId } : {},
|
|
3965
|
+
...row.meta.role !== void 0 ? { role: row.meta.role } : {},
|
|
3966
|
+
name: row.meta.name
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
/** How deep a container nesting is walked — a cycle in the ledger must end. */
|
|
3970
|
+
var MAX_CONTAINER_DEPTH = 8;
|
|
3971
|
+
/**
|
|
3972
|
+
* The devices the camera links BY STRUCTURE, labelled: what the resolver adds
|
|
3973
|
+
* beyond the picks. Computed by the resolver itself (`resolveLinkedDeviceIds`)
|
|
3974
|
+
* over the only rows it can reach — the picks, the camera's children, and the
|
|
3975
|
+
* descendants of every container among them — read level by level with ONE
|
|
3976
|
+
* indexed `listByParentMany` each, never the fleet.
|
|
3977
|
+
*/
|
|
3978
|
+
async function impliedLinks(pctx, deviceId, manualIds, pickedRows) {
|
|
3979
|
+
const rows = new Map(pickedRows);
|
|
3980
|
+
let parents = [deviceId, ...[...pickedRows.values()].filter((r) => r.meta.type === require_dist.DeviceType.Container).map((r) => r.meta.id)];
|
|
3981
|
+
const walked = /* @__PURE__ */ new Set();
|
|
3982
|
+
for (let depth = 0; depth < MAX_CONTAINER_DEPTH && parents.length > 0; depth++) {
|
|
3983
|
+
for (const id of parents) walked.add(id);
|
|
3984
|
+
const byParent = await pctx.metaStore.rows.listByParentMany(parents);
|
|
3985
|
+
const next = [];
|
|
3986
|
+
for (const children of byParent.values()) for (const child of children) {
|
|
3987
|
+
rows.set(child.meta.id, child);
|
|
3988
|
+
if (child.meta.type === require_dist.DeviceType.Container && !walked.has(child.meta.id)) next.push(child.meta.id);
|
|
3989
|
+
}
|
|
3990
|
+
parents = next;
|
|
3991
|
+
}
|
|
3992
|
+
const candidates = [...rows.values()].flatMap((r) => {
|
|
3993
|
+
const c = toLinkCandidate(r);
|
|
3994
|
+
return c === null ? [] : [c];
|
|
3995
|
+
});
|
|
3996
|
+
const picks = new Set(manualIds);
|
|
3997
|
+
return resolveLinkedDeviceIds({
|
|
3998
|
+
selfId: deviceId,
|
|
3999
|
+
config: {
|
|
4000
|
+
mode: "manual",
|
|
4001
|
+
manualIds
|
|
4002
|
+
},
|
|
4003
|
+
candidates
|
|
4004
|
+
}).filter((id) => !picks.has(id)).map((id) => ({
|
|
4005
|
+
deviceId: id,
|
|
4006
|
+
name: rows.get(id)?.meta.name ?? null,
|
|
4007
|
+
health: "ok"
|
|
4008
|
+
}));
|
|
4009
|
+
}
|
|
4010
|
+
function describeRefusal(deviceId, reason) {
|
|
4011
|
+
if (reason === "self") return `${deviceId} is the camera itself`;
|
|
4012
|
+
if (reason === "container") return `${deviceId} is a container — pick its children`;
|
|
4013
|
+
return `${deviceId} is not a device`;
|
|
4014
|
+
}
|
|
4015
|
+
async function fleetOf(pctx) {
|
|
4016
|
+
const all = await listAll(pctx, LINK_CANDIDATE_PROJECTION);
|
|
4017
|
+
return new Map(all.map((d) => [d.id, d]));
|
|
4018
|
+
}
|
|
4019
|
+
function candidatesFor(deviceId, byId) {
|
|
4020
|
+
const location = byId.get(deviceId)?.location ?? null;
|
|
4021
|
+
const out = [];
|
|
4022
|
+
for (const device of byId.values()) {
|
|
4023
|
+
if (device.id === deviceId) continue;
|
|
4024
|
+
if (device.parentDeviceId === deviceId) continue;
|
|
4025
|
+
if (device.type === require_dist.DeviceType.Container) continue;
|
|
4026
|
+
const suggested = location !== null && device.location === location;
|
|
4027
|
+
const parent = device.parentDeviceId === null ? void 0 : byId.get(device.parentDeviceId);
|
|
4028
|
+
out.push({
|
|
4029
|
+
deviceId: device.id,
|
|
4030
|
+
name: device.name,
|
|
4031
|
+
type: device.type,
|
|
4032
|
+
location: device.location,
|
|
4033
|
+
parentName: parent?.name ?? null,
|
|
4034
|
+
suggested,
|
|
4035
|
+
reason: suggested ? "same location" : null
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
4038
|
+
return out.toSorted((a, b) => Number(b.suggested) - Number(a.suggested) || a.name.localeCompare(b.name));
|
|
4039
|
+
}
|
|
4040
|
+
/** Build the `device-extension` provider the device manager registers. */
|
|
4041
|
+
function createDeviceManagerExtensionProvider(pctx) {
|
|
4042
|
+
return require_dist.createDeviceExtensionProvider({
|
|
4043
|
+
source: DEVICE_MANAGER_EXTENSION_SOURCE,
|
|
4044
|
+
label: "Device manager",
|
|
4045
|
+
slots: [require_dist.defineExtensionSlotProvider("linked-devices", {
|
|
4046
|
+
list: async (deviceId) => {
|
|
4047
|
+
const blob = await pctx.settings.readDeviceStore(deviceId);
|
|
4048
|
+
const { manualIds } = parseLinkedDevicesConfig(blob);
|
|
4049
|
+
const pickedRows = await pctx.metaStore.rows.getMany(manualIds);
|
|
4050
|
+
const byId = pickedFrom(pickedRows);
|
|
4051
|
+
const implied = await impliedLinks(pctx, deviceId, manualIds, pickedRows);
|
|
4052
|
+
return {
|
|
4053
|
+
authority: LINKED_DEVICES_AUTHORITY,
|
|
4054
|
+
enabled: null,
|
|
4055
|
+
activation: null,
|
|
4056
|
+
value: {
|
|
4057
|
+
slot: "linked-devices",
|
|
4058
|
+
deviceIds: [...manualIds],
|
|
4059
|
+
trackedDeviceIds: [...parseTrackedOptIn(blob)]
|
|
4060
|
+
},
|
|
4061
|
+
sources: manualIds.map((id) => sourceOf(id, byId)),
|
|
4062
|
+
...implied.length > 0 ? { impliedSources: implied } : {}
|
|
4063
|
+
};
|
|
4064
|
+
},
|
|
4065
|
+
set: async (deviceId, value) => {
|
|
4066
|
+
const [pickedRows, blob] = await Promise.all([pctx.metaStore.rows.getMany(value.deviceIds), pctx.settings.readDeviceStore(deviceId)]);
|
|
4067
|
+
const byId = pickedFrom(pickedRows);
|
|
4068
|
+
const refused = refusedLinkedPicks({
|
|
4069
|
+
selfId: deviceId,
|
|
4070
|
+
picked: value.deviceIds,
|
|
4071
|
+
persisted: parseLinkedDevicesConfig(blob).manualIds,
|
|
4072
|
+
fleet: byId
|
|
4073
|
+
});
|
|
4074
|
+
if (refused.length > 0) {
|
|
4075
|
+
pctx.host.ctx.logger.warn("linked-devices: write refused — nothing persisted", {
|
|
4076
|
+
tags: { deviceId },
|
|
4077
|
+
meta: { refused }
|
|
4078
|
+
});
|
|
4079
|
+
throw new Error(`linked-devices on ${deviceId}: refused — ` + refused.map((r) => describeRefusal(r.deviceId, r.reason)).join("; "));
|
|
4080
|
+
}
|
|
4081
|
+
const tracked = value.trackedDeviceIds;
|
|
4082
|
+
if (tracked === void 0) {
|
|
4083
|
+
await applyLinkedDevicesPatch(pctx, deviceId, { [LINKED_IDS_CONFIG_KEY]: value.deviceIds });
|
|
4084
|
+
return {
|
|
4085
|
+
applied: true,
|
|
4086
|
+
reload: "none"
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
const implied = await impliedLinks(pctx, deviceId, value.deviceIds, pickedRows);
|
|
4090
|
+
const notLinked = refusedTrackedOptIns({
|
|
4091
|
+
tracked,
|
|
4092
|
+
picked: value.deviceIds,
|
|
4093
|
+
implied: implied.map((s) => s.deviceId),
|
|
4094
|
+
stored: parseTrackedOptIn(blob)
|
|
4095
|
+
});
|
|
4096
|
+
if (notLinked.length > 0) {
|
|
4097
|
+
pctx.host.ctx.logger.warn("linked-devices: write refused — nothing persisted", {
|
|
4098
|
+
tags: { deviceId },
|
|
4099
|
+
meta: { trackedNotLinked: notLinked }
|
|
4100
|
+
});
|
|
4101
|
+
throw new Error(`linked-devices on ${deviceId}: refused — tracked events can only be opted in for a linked device, not ${notLinked.join(", ")}`);
|
|
4102
|
+
}
|
|
4103
|
+
await applyLinkedDevicesPatch(pctx, deviceId, {
|
|
4104
|
+
[LINKED_IDS_CONFIG_KEY]: value.deviceIds,
|
|
4105
|
+
[LINKED_TRACKED_IDS_CONFIG_KEY]: [...new Set(tracked)]
|
|
4106
|
+
});
|
|
4107
|
+
return {
|
|
4108
|
+
applied: true,
|
|
4109
|
+
reload: "none"
|
|
4110
|
+
};
|
|
4111
|
+
},
|
|
4112
|
+
candidates: async (deviceId) => candidatesFor(deviceId, await fleetOf(pctx))
|
|
4113
|
+
})],
|
|
4114
|
+
hostTypeOf: async (deviceId) => (await pctx.metaStore.resolvePersistedById(deviceId))?.meta.type ?? null,
|
|
4115
|
+
logger: pctx.host.ctx.logger
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
3934
4118
|
/** A refusal that names a switch the camera does not offer, rather than one it
|
|
3935
4119
|
* could not be asked about. The cap documents rejecting the former; anything
|
|
3936
4120
|
* else is treated as the camera being out of reach, which is the safe reading:
|
|
@@ -4101,9 +4285,9 @@ async function reloadMigratedDevices(pctx, targets) {
|
|
|
4101
4285
|
*
|
|
4102
4286
|
* - The DRIVER config keys (host, port, credentials, `deviceCache`, every
|
|
4103
4287
|
* schema field) SWAP — they describe the box.
|
|
4104
|
-
* - The device-manager-owned
|
|
4105
|
-
*
|
|
4106
|
-
* "the driveway camera", not to a serial number.
|
|
4288
|
+
* - The device-manager-owned linked-devices keys (mode, ids, tracked-event
|
|
4289
|
+
* opt-in — {@link ROLE_SCOPED_CONFIG_KEYS}) STAY with the number — a sensor
|
|
4290
|
+
* is linked to "the driveway camera", not to a serial number.
|
|
4107
4291
|
* - `_profileMap` is DROPPED from both. It is the legacy spelling of the
|
|
4108
4292
|
* stream-profile assignment (the live authority is the stream broker's own
|
|
4109
4293
|
* map — see `StreamsTab.tsx`, which stopped writing it), and either copy
|
|
@@ -4121,7 +4305,7 @@ async function reloadMigratedDevices(pctx, targets) {
|
|
|
4121
4305
|
/** Legacy stream-profile assignment key (cf. `device-queries.ts`). */
|
|
4122
4306
|
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
4123
4307
|
/** Config keys that belong to the camera's ROLE and stay with the number. */
|
|
4124
|
-
var ROLE_SCOPED_CONFIG_KEYS =
|
|
4308
|
+
var ROLE_SCOPED_CONFIG_KEYS = LINKED_DEVICES_RESERVED_KEYS;
|
|
4125
4309
|
function splitConfig(config) {
|
|
4126
4310
|
const hardware = {};
|
|
4127
4311
|
const role = {};
|
|
@@ -7908,9 +8092,6 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
7908
8092
|
getChildrenBatch: (input) => getChildrenBatch(pctx, input),
|
|
7909
8093
|
getLinkedDevices: (input) => getLinkedDevices(pctx, input),
|
|
7910
8094
|
getLinkedDevicesBatch: (input) => getLinkedDevicesBatch(pctx, input),
|
|
7911
|
-
getDeviceSettingsContribution: (input) => buildLinkedDevicesContribution(pctx, input.deviceId),
|
|
7912
|
-
getDeviceLiveContribution: async () => null,
|
|
7913
|
-
applyDeviceSettingsPatch: (input) => applyLinkedDevicesPatch(pctx, input.deviceId, input.patch),
|
|
7914
8095
|
getStreamSources: (input) => getStreamSources(pctx, input),
|
|
7915
8096
|
getConfigSchema: (input) => getConfigSchema(pctx, input),
|
|
7916
8097
|
getSettingsSchema: (input) => getSettingsSchema(pctx, input),
|
|
@@ -8049,28 +8230,35 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
8049
8230
|
this.propagator.start();
|
|
8050
8231
|
this.ctx.logger.info("device-event-propagator started");
|
|
8051
8232
|
}
|
|
8052
|
-
return [
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8233
|
+
return [
|
|
8234
|
+
{
|
|
8235
|
+
capability: require_dist.deviceManagerCapability,
|
|
8236
|
+
provider
|
|
8237
|
+
},
|
|
8238
|
+
{
|
|
8239
|
+
capability: require_dist.deviceStateCapability,
|
|
8240
|
+
provider: {
|
|
8241
|
+
getSnapshot: async (input) => {
|
|
8242
|
+
return stateMirror.snapshotForDevice(input.deviceId);
|
|
8243
|
+
},
|
|
8244
|
+
getCapSlice: async (input) => {
|
|
8245
|
+
return stateMirror.capSlice(input.deviceId, input.capName);
|
|
8246
|
+
},
|
|
8247
|
+
getAllSnapshots: async () => {
|
|
8248
|
+
return stateMirror.allSnapshots();
|
|
8249
|
+
},
|
|
8250
|
+
setCapSlice: async (input) => {
|
|
8251
|
+
const { deviceId, capName, slice } = input;
|
|
8252
|
+
if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
|
|
8253
|
+
if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings, capName);
|
|
8254
|
+
}
|
|
8071
8255
|
}
|
|
8256
|
+
},
|
|
8257
|
+
{
|
|
8258
|
+
capability: require_dist.deviceExtensionCapability,
|
|
8259
|
+
provider: createDeviceManagerExtensionProvider(pctx)
|
|
8072
8260
|
}
|
|
8073
|
-
|
|
8261
|
+
];
|
|
8074
8262
|
}
|
|
8075
8263
|
async onShutdown() {
|
|
8076
8264
|
this.propagator?.stop();
|