@camstack/system 1.2.179 → 1.2.180
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.d.ts +56 -10
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +657 -62
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +658 -63
- package/dist/builtins/backup-orchestrator/run-queue.d.ts +103 -0
- package/dist/builtins/backup-orchestrator/staging-paths.d.ts +36 -0
- 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-manager.addon.js +146 -5
- package/dist/builtins/device-manager/device-manager.addon.mjs +146 -5
- package/dist/builtins/device-manager/device-meta-actions.d.ts +1 -1
- package/dist/builtins/device-manager/migrate-device.d.ts +33 -0
- package/dist/builtins/device-manager/migrate-hardware-state.d.ts +24 -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-backup/system-backup.service.d.ts +55 -2
- 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-L_39vRmH.mjs → dist-DqNTelT4.mjs} +167 -1
- package/dist/{dist-C7qWwrK1.js → dist-TEwQZL3g.js} +167 -1
- package/dist/index.js +530 -2
- package/dist/index.mjs +533 -5
- package/dist/kernel/addon-installer.d.ts +69 -0
- package/dist/kernel/deps/closure-dedupe.d.ts +105 -0
- package/dist/{manifest-system-deps-PAIfmNun.js → manifest-system-deps-BVIr3MYJ.js} +1 -1
- package/dist/{manifest-system-deps-CMOGc5nC.mjs → manifest-system-deps-Ci-jhrxY.mjs} +1 -1
- package/dist/{retired-settings-keys-CnbKAxmX.mjs → retired-settings-keys-CeSITpIX.mjs} +1 -1
- package/dist/{retired-settings-keys-3soU9pfP.js → retired-settings-keys-DauPtbFR.js} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type BackupRunState = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
2
|
+
export type BackupRunPhase = 'queued' | 'building' | 'uploading' | 'done';
|
|
3
|
+
/** What a trigger asks for — the queue keys joining on this. */
|
|
4
|
+
export interface BackupRunRequest {
|
|
5
|
+
readonly destinations?: readonly string[];
|
|
6
|
+
readonly locations?: readonly string[];
|
|
7
|
+
readonly label?: string;
|
|
8
|
+
readonly retentionCount?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Immutable snapshot of one run — the wire shape of `backup.listRuns`. */
|
|
11
|
+
export interface BackupRunSnapshot {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly state: BackupRunState;
|
|
14
|
+
readonly phase: BackupRunPhase;
|
|
15
|
+
readonly destinationIds: readonly string[];
|
|
16
|
+
readonly label?: string;
|
|
17
|
+
readonly requestedAt: number;
|
|
18
|
+
readonly startedAt?: number;
|
|
19
|
+
readonly finishedAt?: number;
|
|
20
|
+
readonly stagedBytes: number;
|
|
21
|
+
readonly archiveSizeBytes?: number;
|
|
22
|
+
readonly uploadedBytes: number;
|
|
23
|
+
readonly completedDestinationIds: readonly string[];
|
|
24
|
+
readonly failedDestinationIds: readonly string[];
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
/**
|
|
27
|
+
* 1-based place in the execution queue — 1 = runs next. Present only
|
|
28
|
+
* while `state === 'queued'`, and derived from the queue's OWN
|
|
29
|
+
* pending order at read time (never from timestamps), so a consumer
|
|
30
|
+
* can only ever show the order the executor will actually honour.
|
|
31
|
+
*/
|
|
32
|
+
readonly queuePosition?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Progress sink handed to the executor. Every update replaces the
|
|
36
|
+
* stored snapshot (no shared mutable state leaks out of the queue).
|
|
37
|
+
*/
|
|
38
|
+
export interface BackupRunProgress {
|
|
39
|
+
setPhase(phase: BackupRunPhase): void;
|
|
40
|
+
/** Resolved destination targets — known only once the run starts. */
|
|
41
|
+
setDestinations(ids: readonly string[]): void;
|
|
42
|
+
setStagedBytes(bytes: number): void;
|
|
43
|
+
setArchiveSize(bytes: number): void;
|
|
44
|
+
setUploadedBytes(bytes: number): void;
|
|
45
|
+
destinationCompleted(id: string): void;
|
|
46
|
+
destinationFailed(id: string): void;
|
|
47
|
+
}
|
|
48
|
+
export interface SubmitResult<T> {
|
|
49
|
+
readonly runId: string;
|
|
50
|
+
/** True when the run had to wait behind an in-flight run. */
|
|
51
|
+
readonly queued: boolean;
|
|
52
|
+
/** True when this submit was coalesced onto an identical queued run. */
|
|
53
|
+
readonly joined: boolean;
|
|
54
|
+
/** Settles when the run terminates. Rejects `BackupCancelledError` on cancel-before-any-result. */
|
|
55
|
+
readonly result: Promise<T>;
|
|
56
|
+
}
|
|
57
|
+
export type BackupRunExecutor<T> = (request: BackupRunRequest, signal: AbortSignal, progress: BackupRunProgress) => Promise<T>;
|
|
58
|
+
export interface BackupRunQueueOptions {
|
|
59
|
+
readonly maxFinished?: number;
|
|
60
|
+
readonly now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* FIFO queue with concurrency 1. `submit` never starts a second
|
|
64
|
+
* executor while one is in flight; an identical request already
|
|
65
|
+
* WAITING (same destinations/locations/label/retention) is joined
|
|
66
|
+
* instead of queued twice — two schedule ticks due on the same minute
|
|
67
|
+
* produce one run, not two.
|
|
68
|
+
*/
|
|
69
|
+
export declare class BackupRunQueue<T> {
|
|
70
|
+
private readonly executor;
|
|
71
|
+
private readonly runs;
|
|
72
|
+
private readonly pending;
|
|
73
|
+
private readonly finished;
|
|
74
|
+
private activeId;
|
|
75
|
+
private readonly maxFinished;
|
|
76
|
+
private readonly now;
|
|
77
|
+
constructor(executor: BackupRunExecutor<T>, opts?: BackupRunQueueOptions);
|
|
78
|
+
submit(request: BackupRunRequest): SubmitResult<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Cancel a run. Queued: removed before it ever starts (its result
|
|
81
|
+
* rejects `BackupCancelledError`). Running: the abort signal fires —
|
|
82
|
+
* the executor's tar/upload streams stop and its cleanup runs.
|
|
83
|
+
* Unknown or already terminal: `false`.
|
|
84
|
+
*/
|
|
85
|
+
cancel(runId: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* EXECUTION order, not timestamp order: the running run first, then
|
|
88
|
+
* every queued run in the exact order the pump will start them (each
|
|
89
|
+
* stamped with `queuePosition`, 1 = next), then the bounded finished
|
|
90
|
+
* history newest-first. The queued section is read straight off
|
|
91
|
+
* `this.pending` — a consumer sorting by `requestedAt` descending
|
|
92
|
+
* would put the run that starts NEXT at the BOTTOM, which is
|
|
93
|
+
* backwards exactly when the queue matters (more than one waiter).
|
|
94
|
+
*/
|
|
95
|
+
listRuns(): readonly BackupRunSnapshot[];
|
|
96
|
+
getRun(runId: string): BackupRunSnapshot | undefined;
|
|
97
|
+
private createRecord;
|
|
98
|
+
private pump;
|
|
99
|
+
private execute;
|
|
100
|
+
private progressFor;
|
|
101
|
+
private patch;
|
|
102
|
+
private finish;
|
|
103
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { IScopedLogger } from '@camstack/types';
|
|
2
|
+
/** Basename of the staging dir under the node data root. */
|
|
3
|
+
export declare const STAGING_DIR_NAME = "backup-staging";
|
|
4
|
+
/** Canonical staging dir for a node data root. */
|
|
5
|
+
export declare function resolveStagingDir(nodeDataDir: string): string;
|
|
6
|
+
export interface StagingSweepResult {
|
|
7
|
+
/** Top-level staging entries removed. */
|
|
8
|
+
readonly removedCount: number;
|
|
9
|
+
/** File bytes reclaimed (recursive for removed subdirectories). */
|
|
10
|
+
readonly freedBytes: number;
|
|
11
|
+
/** Entries spared because `keepPaths` named them. */
|
|
12
|
+
readonly keptCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DirectoryFootprint {
|
|
15
|
+
readonly present: boolean;
|
|
16
|
+
readonly totalBytes: number;
|
|
17
|
+
readonly fileCount: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Delete every top-level entry of `stagingDir` except those whose
|
|
21
|
+
* resolved path appears in `keepPaths`. Per-entry failures are logged
|
|
22
|
+
* and skipped — a locked file must not abort the sweep. A missing
|
|
23
|
+
* staging dir is a clean no-op.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sweepStagingDir(input: {
|
|
26
|
+
stagingDir: string;
|
|
27
|
+
keepPaths: readonly string[];
|
|
28
|
+
logger: IScopedLogger;
|
|
29
|
+
}): Promise<StagingSweepResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Recursive size + file count of a path (file or directory). Absent
|
|
32
|
+
* path → `present: false` with zeros. Used by the sweep to report
|
|
33
|
+
* reclaimed bytes and by the orchestrator to surface the legacy
|
|
34
|
+
* staging dir's footprint without touching it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function measureDirectoryFootprint(dir: string): Promise<DirectoryFootprint>;
|
|
@@ -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-TEwQZL3g.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-DqNTelT4.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-TEwQZL3g.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-DqNTelT4.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";
|
|
@@ -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-TEwQZL3g.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-DauPtbFR.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
|
|
@@ -2114,7 +2114,13 @@ function resolveLinkedDeviceIds(params) {
|
|
|
2114
2114
|
if (config.mode === "auto") {
|
|
2115
2115
|
if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
|
|
2116
2116
|
if (c.type === require_dist.DeviceType.Container) continue;
|
|
2117
|
-
if (c.location
|
|
2117
|
+
if (c.location !== selfLocation) continue;
|
|
2118
|
+
const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
|
|
2119
|
+
if (parent !== void 0 && parent.type === require_dist.DeviceType.Container) {
|
|
2120
|
+
expand(parent.id);
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
ids.add(c.id);
|
|
2118
2124
|
}
|
|
2119
2125
|
} else for (const id of config.manualIds) if (byId.has(id)) expand(id);
|
|
2120
2126
|
ids.delete(selfId);
|
|
@@ -3327,6 +3333,16 @@ async function migrateDevice$1(deps, input) {
|
|
|
3327
3333
|
const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
|
|
3328
3334
|
const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
|
|
3329
3335
|
await deps.swapIds(sourceId, targetId);
|
|
3336
|
+
await deps.swapHardwareState(sourceId, targetId);
|
|
3337
|
+
for (const deviceId of [sourceId, targetId]) try {
|
|
3338
|
+
await deps.forgetStreamState(deviceId);
|
|
3339
|
+
} catch (err) {
|
|
3340
|
+
deps.log("migration: broker hardware state NOT forgotten — stale stream state survives", {
|
|
3341
|
+
deviceId,
|
|
3342
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3343
|
+
remedy: "delete the device's derived streams and reassign its stream profiles, or update the stream-broker addon and re-run streamBroker.forgetDeviceHardware"
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3330
3346
|
const up = await applyAll(deps, sourceId, true);
|
|
3331
3347
|
await deps.respawnRunners([sourceId, targetId]);
|
|
3332
3348
|
deps.log("camera migrated", {
|
|
@@ -3344,6 +3360,102 @@ async function migrateDevice$1(deps, input) {
|
|
|
3344
3360
|
};
|
|
3345
3361
|
}
|
|
3346
3362
|
//#endregion
|
|
3363
|
+
//#region src/builtins/device-manager/migrate-hardware-state.ts
|
|
3364
|
+
/**
|
|
3365
|
+
* The per-device state half of a migration (`deviceManager.migrateDevice`).
|
|
3366
|
+
*
|
|
3367
|
+
* ## The rule
|
|
3368
|
+
*
|
|
3369
|
+
* **Role-scoped state follows the NUMBER. Hardware-scoped state follows the
|
|
3370
|
+
* DEVICE.** `swapIds` moves the device ROWS — `addonId` and `stableId` follow
|
|
3371
|
+
* the hardware — but the per-device CONFIG blob and the runtime-state blob are
|
|
3372
|
+
* keyed by the number in a separate store, and until 2026-09-01 they simply
|
|
3373
|
+
* stayed put. Measured on the live hub after replacing a Hikvision (id 618)
|
|
3374
|
+
* with a Reolink: device 618 — Reolink hardware — was dialled at
|
|
3375
|
+
* `192.168.1.245:80` with `deviceCache.model="DS-2DE2A404IW-DE3/W"`, the
|
|
3376
|
+
* Hikvision's address, credentials and cached model, and never came online.
|
|
3377
|
+
* Device 4362 wore the Reolink's config on Hikvision hardware. The two devices
|
|
3378
|
+
* were wearing each other's identity.
|
|
3379
|
+
*
|
|
3380
|
+
* The proof the config belongs to the hardware, not the number: writing the
|
|
3381
|
+
* Hikvision blob onto the Reolink was REJECTED by the Reolink driver's own
|
|
3382
|
+
* zod schema (`motionSensitivity: 60` vs max 50). A config that fails the
|
|
3383
|
+
* other provider's schema is a config for different hardware.
|
|
3384
|
+
*
|
|
3385
|
+
* ## What swaps, what stays
|
|
3386
|
+
*
|
|
3387
|
+
* - The DRIVER config keys (host, port, credentials, `deviceCache`, every
|
|
3388
|
+
* schema field) SWAP — they describe the box.
|
|
3389
|
+
* - The device-manager-owned section keys ({@link DEVICE_STORE_SECTIONS}:
|
|
3390
|
+
* linked-devices mode/ids) STAY with the number — a sensor is linked to
|
|
3391
|
+
* "the driveway camera", not to a serial number.
|
|
3392
|
+
* - `_profileMap` is DROPPED from both. It is the legacy spelling of the
|
|
3393
|
+
* stream-profile assignment (the live authority is the stream broker's own
|
|
3394
|
+
* map — see `StreamsTab.tsx`, which stopped writing it), and either copy
|
|
3395
|
+
* names stream ids of the wrong hardware after a swap.
|
|
3396
|
+
* - The runtime-state blobs (`__device-state:<id>` — battery snapshot, sleep
|
|
3397
|
+
* flag, feature probe) SWAP wholesale: every slice mirrors the box.
|
|
3398
|
+
*
|
|
3399
|
+
* ## Crash points are loud, same as `swapIds`
|
|
3400
|
+
*
|
|
3401
|
+
* There is no transaction: each blob is cleared and rewritten. A crash between
|
|
3402
|
+
* clear and write leaves a device with NO config — a provider that fails to
|
|
3403
|
+
* construct and says so — never two devices wearing each other's, which is the
|
|
3404
|
+
* state that reads as plausible and costs a day.
|
|
3405
|
+
*/
|
|
3406
|
+
/** Legacy stream-profile assignment key (cf. `device-queries.ts`). */
|
|
3407
|
+
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
3408
|
+
/** Config keys that belong to the camera's ROLE and stay with the number. */
|
|
3409
|
+
var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
|
|
3410
|
+
function splitConfig(config) {
|
|
3411
|
+
const hardware = {};
|
|
3412
|
+
const role = {};
|
|
3413
|
+
for (const [key, value] of Object.entries(config)) {
|
|
3414
|
+
if (key === PROFILE_MAP_CONFIG_KEY) continue;
|
|
3415
|
+
if (ROLE_SCOPED_CONFIG_KEYS.has(key)) role[key] = value;
|
|
3416
|
+
else hardware[key] = value;
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
hardware,
|
|
3420
|
+
role
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Swap the hardware-scoped halves of two devices' persisted state. Runs right
|
|
3425
|
+
* after `swapIds`, under the same meta write lock. THROWS on failure: a
|
|
3426
|
+
* migration whose rows swapped but whose configs did not is exactly the
|
|
3427
|
+
* incident this module exists to prevent, and it must never be reported as a
|
|
3428
|
+
* success.
|
|
3429
|
+
*/
|
|
3430
|
+
async function swapHardwareState(settings, sourceId, targetId) {
|
|
3431
|
+
const sourceConfig = await settings.readDeviceStore(sourceId);
|
|
3432
|
+
const targetConfig = await settings.readDeviceStore(targetId);
|
|
3433
|
+
const source = splitConfig(sourceConfig);
|
|
3434
|
+
const target = splitConfig(targetConfig);
|
|
3435
|
+
await settings.clearDeviceStore(sourceId);
|
|
3436
|
+
await settings.writeDeviceStore(sourceId, {
|
|
3437
|
+
...target.hardware,
|
|
3438
|
+
...source.role
|
|
3439
|
+
});
|
|
3440
|
+
await settings.clearDeviceStore(targetId);
|
|
3441
|
+
await settings.writeDeviceStore(targetId, {
|
|
3442
|
+
...source.hardware,
|
|
3443
|
+
...target.role
|
|
3444
|
+
});
|
|
3445
|
+
const sourceRuntimeState = await settings.readDeviceRuntimeState(sourceId);
|
|
3446
|
+
const targetRuntimeState = await settings.readDeviceRuntimeState(targetId);
|
|
3447
|
+
await settings.writeDeviceRuntimeState(sourceId, targetRuntimeState);
|
|
3448
|
+
await settings.writeDeviceRuntimeState(targetId, sourceRuntimeState);
|
|
3449
|
+
return {
|
|
3450
|
+
runtimeStateNowAtSource: targetRuntimeState,
|
|
3451
|
+
runtimeStateNowAtTarget: sourceRuntimeState,
|
|
3452
|
+
hardwareKeysToSource: Object.keys(target.hardware).toSorted(),
|
|
3453
|
+
hardwareKeysToTarget: Object.keys(source.hardware).toSorted(),
|
|
3454
|
+
roleKeysKeptAtSource: Object.keys(source.role).toSorted(),
|
|
3455
|
+
roleKeysKeptAtTarget: Object.keys(target.role).toSorted()
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
//#endregion
|
|
3347
3459
|
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
3348
3460
|
/**
|
|
3349
3461
|
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
@@ -3423,7 +3535,7 @@ async function allocateDeviceId(pctx, input) {
|
|
|
3423
3535
|
* The composition lives in `migrate-device.ts`, deliberately free of this
|
|
3424
3536
|
* file's context so the decisions it carries — above all "never report a switch
|
|
3425
3537
|
* as off when it could not be written" — are testable without a hub. This is
|
|
3426
|
-
* the wiring: the
|
|
3538
|
+
* the wiring: the real dependencies, and the write lock.
|
|
3427
3539
|
*
|
|
3428
3540
|
* The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
|
|
3429
3541
|
* value a crash could park a row on was never used and will never be reused.
|
|
@@ -3446,13 +3558,42 @@ async function migrateDevice(pctx, input) {
|
|
|
3446
3558
|
swapIds: async (a, b) => {
|
|
3447
3559
|
await pctx.metaStore.rows.swapIds(a, b, tempId);
|
|
3448
3560
|
},
|
|
3561
|
+
swapHardwareState: async (a, b) => {
|
|
3562
|
+
const swapped = await swapHardwareState(pctx.settings, a, b);
|
|
3563
|
+
pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
|
|
3564
|
+
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
3565
|
+
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
3566
|
+
tags: { deviceId: a },
|
|
3567
|
+
meta: {
|
|
3568
|
+
sourceId: a,
|
|
3569
|
+
targetId: b,
|
|
3570
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
3571
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
3572
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
3573
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
},
|
|
3577
|
+
forgetStreamState: async (deviceId) => {
|
|
3578
|
+
await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
|
|
3579
|
+
},
|
|
3449
3580
|
respawnRunners: async (deviceIds) => {
|
|
3450
3581
|
const owners = /* @__PURE__ */ new Set();
|
|
3451
3582
|
for (const deviceId of deviceIds) {
|
|
3452
3583
|
const addonId = pctx.metaStore.idToAddonId.get(deviceId);
|
|
3453
3584
|
if (addonId !== void 0) owners.add(addonId);
|
|
3454
3585
|
}
|
|
3455
|
-
for (const addonId of owners)
|
|
3586
|
+
for (const addonId of owners) {
|
|
3587
|
+
const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
|
|
3588
|
+
ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
|
|
3589
|
+
tags: { deviceId: sourceId },
|
|
3590
|
+
meta: {
|
|
3591
|
+
addonId,
|
|
3592
|
+
devicesTakenDown: owned
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
3596
|
+
}
|
|
3456
3597
|
},
|
|
3457
3598
|
log: (message, meta) => {
|
|
3458
3599
|
ctx.logger.info(message, {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $t as isDeviceConfigCap, Bt as DeviceRole, Ft as BaseAddon, H as deviceManagerCapability, J as enumerateSchemaFields, P as buildStreamParamsConfigSchema, Pt as errMsg, U as deviceStateCapability, Vt as DeviceType, W as deviceStatusCapability, Wt as WELL_KNOWN_TAB_MAP, a as CAMERA_SWITCH_ORDER, b as STREAM_PROFILE_META, f as DeviceStatusSchema, ft as normalizeUnit, mt as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, on as sleep, q as enumerateItemArrayFields, sn as EventCategory, t as ALL_CAPABILITY_DEFINITIONS, xt as runtimeStatePolicyFor, yt as resolveContainerPrimaryChild, zt as DeviceFeature } from "../../dist-
|
|
2
|
-
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-
|
|
1
|
+
import { $t as isDeviceConfigCap, Bt as DeviceRole, Ft as BaseAddon, H as deviceManagerCapability, J as enumerateSchemaFields, P as buildStreamParamsConfigSchema, Pt as errMsg, U as deviceStateCapability, Vt as DeviceType, W as deviceStatusCapability, Wt as WELL_KNOWN_TAB_MAP, a as CAMERA_SWITCH_ORDER, b as STREAM_PROFILE_META, f as DeviceStatusSchema, ft as normalizeUnit, mt as parseStreamParamsFormPatch, o as CAP_NAMES_WITH_STATUS, on as sleep, q as enumerateItemArrayFields, sn as EventCategory, t as ALL_CAPABILITY_DEFINITIONS, xt as runtimeStatePolicyFor, yt as resolveContainerPrimaryChild, zt as DeviceFeature } from "../../dist-DqNTelT4.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-CeSITpIX.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
5
5
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -2109,7 +2109,13 @@ function resolveLinkedDeviceIds(params) {
|
|
|
2109
2109
|
if (config.mode === "auto") {
|
|
2110
2110
|
if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
|
|
2111
2111
|
if (c.type === DeviceType.Container) continue;
|
|
2112
|
-
if (c.location
|
|
2112
|
+
if (c.location !== selfLocation) continue;
|
|
2113
|
+
const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
|
|
2114
|
+
if (parent !== void 0 && parent.type === DeviceType.Container) {
|
|
2115
|
+
expand(parent.id);
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
ids.add(c.id);
|
|
2113
2119
|
}
|
|
2114
2120
|
} else for (const id of config.manualIds) if (byId.has(id)) expand(id);
|
|
2115
2121
|
ids.delete(selfId);
|
|
@@ -3322,6 +3328,16 @@ async function migrateDevice$1(deps, input) {
|
|
|
3322
3328
|
const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
|
|
3323
3329
|
const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
|
|
3324
3330
|
await deps.swapIds(sourceId, targetId);
|
|
3331
|
+
await deps.swapHardwareState(sourceId, targetId);
|
|
3332
|
+
for (const deviceId of [sourceId, targetId]) try {
|
|
3333
|
+
await deps.forgetStreamState(deviceId);
|
|
3334
|
+
} catch (err) {
|
|
3335
|
+
deps.log("migration: broker hardware state NOT forgotten — stale stream state survives", {
|
|
3336
|
+
deviceId,
|
|
3337
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3338
|
+
remedy: "delete the device's derived streams and reassign its stream profiles, or update the stream-broker addon and re-run streamBroker.forgetDeviceHardware"
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3325
3341
|
const up = await applyAll(deps, sourceId, true);
|
|
3326
3342
|
await deps.respawnRunners([sourceId, targetId]);
|
|
3327
3343
|
deps.log("camera migrated", {
|
|
@@ -3339,6 +3355,102 @@ async function migrateDevice$1(deps, input) {
|
|
|
3339
3355
|
};
|
|
3340
3356
|
}
|
|
3341
3357
|
//#endregion
|
|
3358
|
+
//#region src/builtins/device-manager/migrate-hardware-state.ts
|
|
3359
|
+
/**
|
|
3360
|
+
* The per-device state half of a migration (`deviceManager.migrateDevice`).
|
|
3361
|
+
*
|
|
3362
|
+
* ## The rule
|
|
3363
|
+
*
|
|
3364
|
+
* **Role-scoped state follows the NUMBER. Hardware-scoped state follows the
|
|
3365
|
+
* DEVICE.** `swapIds` moves the device ROWS — `addonId` and `stableId` follow
|
|
3366
|
+
* the hardware — but the per-device CONFIG blob and the runtime-state blob are
|
|
3367
|
+
* keyed by the number in a separate store, and until 2026-09-01 they simply
|
|
3368
|
+
* stayed put. Measured on the live hub after replacing a Hikvision (id 618)
|
|
3369
|
+
* with a Reolink: device 618 — Reolink hardware — was dialled at
|
|
3370
|
+
* `192.168.1.245:80` with `deviceCache.model="DS-2DE2A404IW-DE3/W"`, the
|
|
3371
|
+
* Hikvision's address, credentials and cached model, and never came online.
|
|
3372
|
+
* Device 4362 wore the Reolink's config on Hikvision hardware. The two devices
|
|
3373
|
+
* were wearing each other's identity.
|
|
3374
|
+
*
|
|
3375
|
+
* The proof the config belongs to the hardware, not the number: writing the
|
|
3376
|
+
* Hikvision blob onto the Reolink was REJECTED by the Reolink driver's own
|
|
3377
|
+
* zod schema (`motionSensitivity: 60` vs max 50). A config that fails the
|
|
3378
|
+
* other provider's schema is a config for different hardware.
|
|
3379
|
+
*
|
|
3380
|
+
* ## What swaps, what stays
|
|
3381
|
+
*
|
|
3382
|
+
* - The DRIVER config keys (host, port, credentials, `deviceCache`, every
|
|
3383
|
+
* schema field) SWAP — they describe the box.
|
|
3384
|
+
* - The device-manager-owned section keys ({@link DEVICE_STORE_SECTIONS}:
|
|
3385
|
+
* linked-devices mode/ids) STAY with the number — a sensor is linked to
|
|
3386
|
+
* "the driveway camera", not to a serial number.
|
|
3387
|
+
* - `_profileMap` is DROPPED from both. It is the legacy spelling of the
|
|
3388
|
+
* stream-profile assignment (the live authority is the stream broker's own
|
|
3389
|
+
* map — see `StreamsTab.tsx`, which stopped writing it), and either copy
|
|
3390
|
+
* names stream ids of the wrong hardware after a swap.
|
|
3391
|
+
* - The runtime-state blobs (`__device-state:<id>` — battery snapshot, sleep
|
|
3392
|
+
* flag, feature probe) SWAP wholesale: every slice mirrors the box.
|
|
3393
|
+
*
|
|
3394
|
+
* ## Crash points are loud, same as `swapIds`
|
|
3395
|
+
*
|
|
3396
|
+
* There is no transaction: each blob is cleared and rewritten. A crash between
|
|
3397
|
+
* clear and write leaves a device with NO config — a provider that fails to
|
|
3398
|
+
* construct and says so — never two devices wearing each other's, which is the
|
|
3399
|
+
* state that reads as plausible and costs a day.
|
|
3400
|
+
*/
|
|
3401
|
+
/** Legacy stream-profile assignment key (cf. `device-queries.ts`). */
|
|
3402
|
+
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
3403
|
+
/** Config keys that belong to the camera's ROLE and stay with the number. */
|
|
3404
|
+
var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
|
|
3405
|
+
function splitConfig(config) {
|
|
3406
|
+
const hardware = {};
|
|
3407
|
+
const role = {};
|
|
3408
|
+
for (const [key, value] of Object.entries(config)) {
|
|
3409
|
+
if (key === PROFILE_MAP_CONFIG_KEY) continue;
|
|
3410
|
+
if (ROLE_SCOPED_CONFIG_KEYS.has(key)) role[key] = value;
|
|
3411
|
+
else hardware[key] = value;
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
hardware,
|
|
3415
|
+
role
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
/**
|
|
3419
|
+
* Swap the hardware-scoped halves of two devices' persisted state. Runs right
|
|
3420
|
+
* after `swapIds`, under the same meta write lock. THROWS on failure: a
|
|
3421
|
+
* migration whose rows swapped but whose configs did not is exactly the
|
|
3422
|
+
* incident this module exists to prevent, and it must never be reported as a
|
|
3423
|
+
* success.
|
|
3424
|
+
*/
|
|
3425
|
+
async function swapHardwareState(settings, sourceId, targetId) {
|
|
3426
|
+
const sourceConfig = await settings.readDeviceStore(sourceId);
|
|
3427
|
+
const targetConfig = await settings.readDeviceStore(targetId);
|
|
3428
|
+
const source = splitConfig(sourceConfig);
|
|
3429
|
+
const target = splitConfig(targetConfig);
|
|
3430
|
+
await settings.clearDeviceStore(sourceId);
|
|
3431
|
+
await settings.writeDeviceStore(sourceId, {
|
|
3432
|
+
...target.hardware,
|
|
3433
|
+
...source.role
|
|
3434
|
+
});
|
|
3435
|
+
await settings.clearDeviceStore(targetId);
|
|
3436
|
+
await settings.writeDeviceStore(targetId, {
|
|
3437
|
+
...source.hardware,
|
|
3438
|
+
...target.role
|
|
3439
|
+
});
|
|
3440
|
+
const sourceRuntimeState = await settings.readDeviceRuntimeState(sourceId);
|
|
3441
|
+
const targetRuntimeState = await settings.readDeviceRuntimeState(targetId);
|
|
3442
|
+
await settings.writeDeviceRuntimeState(sourceId, targetRuntimeState);
|
|
3443
|
+
await settings.writeDeviceRuntimeState(targetId, sourceRuntimeState);
|
|
3444
|
+
return {
|
|
3445
|
+
runtimeStateNowAtSource: targetRuntimeState,
|
|
3446
|
+
runtimeStateNowAtTarget: sourceRuntimeState,
|
|
3447
|
+
hardwareKeysToSource: Object.keys(target.hardware).toSorted(),
|
|
3448
|
+
hardwareKeysToTarget: Object.keys(source.hardware).toSorted(),
|
|
3449
|
+
roleKeysKeptAtSource: Object.keys(source.role).toSorted(),
|
|
3450
|
+
roleKeysKeptAtTarget: Object.keys(target.role).toSorted()
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
//#endregion
|
|
3342
3454
|
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
3343
3455
|
/**
|
|
3344
3456
|
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
@@ -3418,7 +3530,7 @@ async function allocateDeviceId(pctx, input) {
|
|
|
3418
3530
|
* The composition lives in `migrate-device.ts`, deliberately free of this
|
|
3419
3531
|
* file's context so the decisions it carries — above all "never report a switch
|
|
3420
3532
|
* as off when it could not be written" — are testable without a hub. This is
|
|
3421
|
-
* the wiring: the
|
|
3533
|
+
* the wiring: the real dependencies, and the write lock.
|
|
3422
3534
|
*
|
|
3423
3535
|
* The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
|
|
3424
3536
|
* value a crash could park a row on was never used and will never be reused.
|
|
@@ -3441,13 +3553,42 @@ async function migrateDevice(pctx, input) {
|
|
|
3441
3553
|
swapIds: async (a, b) => {
|
|
3442
3554
|
await pctx.metaStore.rows.swapIds(a, b, tempId);
|
|
3443
3555
|
},
|
|
3556
|
+
swapHardwareState: async (a, b) => {
|
|
3557
|
+
const swapped = await swapHardwareState(pctx.settings, a, b);
|
|
3558
|
+
pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
|
|
3559
|
+
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
3560
|
+
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
3561
|
+
tags: { deviceId: a },
|
|
3562
|
+
meta: {
|
|
3563
|
+
sourceId: a,
|
|
3564
|
+
targetId: b,
|
|
3565
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
3566
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
3567
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
3568
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
3569
|
+
}
|
|
3570
|
+
});
|
|
3571
|
+
},
|
|
3572
|
+
forgetStreamState: async (deviceId) => {
|
|
3573
|
+
await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
|
|
3574
|
+
},
|
|
3444
3575
|
respawnRunners: async (deviceIds) => {
|
|
3445
3576
|
const owners = /* @__PURE__ */ new Set();
|
|
3446
3577
|
for (const deviceId of deviceIds) {
|
|
3447
3578
|
const addonId = pctx.metaStore.idToAddonId.get(deviceId);
|
|
3448
3579
|
if (addonId !== void 0) owners.add(addonId);
|
|
3449
3580
|
}
|
|
3450
|
-
for (const addonId of owners)
|
|
3581
|
+
for (const addonId of owners) {
|
|
3582
|
+
const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
|
|
3583
|
+
ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
|
|
3584
|
+
tags: { deviceId: sourceId },
|
|
3585
|
+
meta: {
|
|
3586
|
+
addonId,
|
|
3587
|
+
devicesTakenDown: owned
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
3591
|
+
}
|
|
3451
3592
|
},
|
|
3452
3593
|
log: (message, meta) => {
|
|
3453
3594
|
ctx.logger.info(message, {
|
|
@@ -19,7 +19,7 @@ export declare function allocateDeviceId(pctx: ProviderContext, input: Parameter
|
|
|
19
19
|
* The composition lives in `migrate-device.ts`, deliberately free of this
|
|
20
20
|
* file's context so the decisions it carries — above all "never report a switch
|
|
21
21
|
* as off when it could not be written" — are testable without a hub. This is
|
|
22
|
-
* the wiring: the
|
|
22
|
+
* the wiring: the real dependencies, and the write lock.
|
|
23
23
|
*
|
|
24
24
|
* The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
|
|
25
25
|
* value a crash could park a row on was never used and will never be reused.
|
|
@@ -26,6 +26,39 @@ export interface MigrateDeviceDeps {
|
|
|
26
26
|
readonly setSwitch: (deviceId: number, switchId: CameraSwitchId, enabled: boolean) => Promise<void>;
|
|
27
27
|
/** `DeviceRowStore.swapIds`, already under the meta write lock. */
|
|
28
28
|
readonly swapIds: (a: number, b: number) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Swap the HARDWARE-scoped per-device state along with the rows: the driver
|
|
31
|
+
* config blob (host/port/credentials/`deviceCache` — everything the
|
|
32
|
+
* provider's schema owns) and the runtime-state blob. Role-scoped keys stay
|
|
33
|
+
* with the number. See `migrate-hardware-state.ts` for the rule, and for
|
|
34
|
+
* the incident where the rows swapped and the configs did not — the two
|
|
35
|
+
* devices came out wearing each other's identity.
|
|
36
|
+
*
|
|
37
|
+
* A failure here THROWS, and the migration throws with it: rows swapped
|
|
38
|
+
* with configs unswapped is exactly that incident, and it must never be
|
|
39
|
+
* reported as a success.
|
|
40
|
+
*/
|
|
41
|
+
readonly swapHardwareState: (sourceId: number, targetId: number) => Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Tell the stream broker the hardware behind this number is gone
|
|
44
|
+
* (`streamBroker.forgetDeviceHardware`): every DERIVED stream definition is
|
|
45
|
+
* DELETED, the profile-slot assignment entry is purged, and the probed
|
|
46
|
+
* snapshots and RTSP token rows of the old streams go with it.
|
|
47
|
+
*
|
|
48
|
+
* Measured on the live hub after replacing a Hikvision with a Reolink on id
|
|
49
|
+
* 618: the assignment still said `mid → native:slot-3`, a raw slot only the
|
|
50
|
+
* Hikvision had, and the `derived:Mid to low` feeder — defined against the
|
|
51
|
+
* old profile layout — respawned every ten seconds forever (observed at
|
|
52
|
+
* attempt 2732). The recorder reported `no profile attached for device
|
|
53
|
+
* 618`, live view answered `No broker for stream "618/adaptive"`, and no
|
|
54
|
+
* line named the stale assignment as the cause.
|
|
55
|
+
*
|
|
56
|
+
* Forgetting is deliberately what this does, rather than guessing a
|
|
57
|
+
* mapping: the catalog is the authority on which stream is which, and it
|
|
58
|
+
* belongs to the camera that is now there. An RPC, never an event — a
|
|
59
|
+
* dropped event (D8) would leave the feeder respawning forever.
|
|
60
|
+
*/
|
|
61
|
+
readonly forgetStreamState: (deviceId: number) => Promise<void>;
|
|
29
62
|
/** Respawn the runners that hold either number in memory. Disabling stops
|
|
30
63
|
* data PRODUCTION; it does not flush a cache that maps id → addon. */
|
|
31
64
|
readonly respawnRunners: (deviceIds: readonly number[]) => Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DeviceManagerSettings } from './device-meta-store.js';
|
|
2
|
+
/** The narrow settings slice the swap needs — the seam the spec fakes. */
|
|
3
|
+
export type HardwareStateStore = Pick<DeviceManagerSettings, 'readDeviceStore' | 'writeDeviceStore' | 'clearDeviceStore' | 'readDeviceRuntimeState' | 'writeDeviceRuntimeState'>;
|
|
4
|
+
export interface SwappedHardwareState {
|
|
5
|
+
/** Runtime-state blob now persisted at `sourceId` (was the target's). */
|
|
6
|
+
readonly runtimeStateNowAtSource: Record<string, unknown>;
|
|
7
|
+
/** Runtime-state blob now persisted at `targetId` (was the source's). */
|
|
8
|
+
readonly runtimeStateNowAtTarget: Record<string, unknown>;
|
|
9
|
+
/** Hardware config keys that moved target → source. */
|
|
10
|
+
readonly hardwareKeysToSource: readonly string[];
|
|
11
|
+
/** Hardware config keys that moved source → target. */
|
|
12
|
+
readonly hardwareKeysToTarget: readonly string[];
|
|
13
|
+
/** Role config keys that stayed on each number. */
|
|
14
|
+
readonly roleKeysKeptAtSource: readonly string[];
|
|
15
|
+
readonly roleKeysKeptAtTarget: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Swap the hardware-scoped halves of two devices' persisted state. Runs right
|
|
19
|
+
* after `swapIds`, under the same meta write lock. THROWS on failure: a
|
|
20
|
+
* migration whose rows swapped but whose configs did not is exactly the
|
|
21
|
+
* incident this module exists to prevent, and it must never be reported as a
|
|
22
|
+
* success.
|
|
23
|
+
*/
|
|
24
|
+
export declare function swapHardwareState(settings: HardwareStateStore, sourceId: number, targetId: number): Promise<SwappedHardwareState>;
|
|
@@ -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-TEwQZL3g.js");
|
|
7
7
|
//#region src/builtins/doorbell/trigger-engine.ts
|
|
8
8
|
/**
|
|
9
9
|
* Pure trigger logic for the virtual-doorbell builtin — edge detection over
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ft as BaseAddon, G as doorbellCapability, Jt as createEvent, M as bareAddonId, Pt as errMsg, Vt as DeviceType, Y as evaluateSensorEdge, _ as SOURCE_CAPS, nt as isSameAddonId, rt as isSourceCap, sn as EventCategory, v as SOURCE_DEVICE_TYPES } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, G as doorbellCapability, Jt as createEvent, M as bareAddonId, Pt as errMsg, Vt as DeviceType, Y as evaluateSensorEdge, _ as SOURCE_CAPS, nt as isSameAddonId, rt as isSourceCap, sn as EventCategory, v as SOURCE_DEVICE_TYPES } from "../../dist-DqNTelT4.mjs";
|
|
2
2
|
//#region src/builtins/doorbell/trigger-engine.ts
|
|
3
3
|
/**
|
|
4
4
|
* Pure trigger logic for the virtual-doorbell builtin — edge detection over
|