@camstack/addon-post-analysis 1.2.327 → 1.2.330
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/{clip-model-registry-Cn5n0a0Q.mjs → clip-model-registry-CGaaUvAD.mjs} +111 -16
- package/dist/{clip-model-registry-CDG2krro.js → clip-model-registry-DQmmIYXS.js} +128 -15
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-TZMtQBvr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BNJdXVq8.mjs} +3 -3
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BdFzCOK8.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-B61IObaQ.mjs → hostInit-Bj7szOak.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +434 -118
- package/dist/pipeline-analytics/index.mjs +434 -118
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DK7fUvI-.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-DQmmIYXS.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -659,7 +659,8 @@ var NcAlarmArmOutcomeSchema = require_clip_model_registry.discriminatedUnion("ou
|
|
|
659
659
|
require_clip_model_registry.object({
|
|
660
660
|
outcome: require_clip_model_registry.literal("rejected"),
|
|
661
661
|
reason: NcAlarmArmAnywayRejectionSchema
|
|
662
|
-
}).readonly()
|
|
662
|
+
}).readonly(),
|
|
663
|
+
require_clip_model_registry.object({ outcome: require_clip_model_registry.literal("outside-schedule") }).readonly()
|
|
663
664
|
]);
|
|
664
665
|
/**
|
|
665
666
|
* The devices a mode covers, ascending and de-duplicated.
|
|
@@ -1518,7 +1519,25 @@ function modeDelaysFromPersisted(raw) {
|
|
|
1518
1519
|
}
|
|
1519
1520
|
kept[field] = value;
|
|
1520
1521
|
}
|
|
1521
|
-
if (
|
|
1522
|
+
if (item.postTriggeredAction !== void 0) {
|
|
1523
|
+
const action = require_clip_model_registry.NcAlarmPostTriggeredActionSchema.safeParse(item.postTriggeredAction);
|
|
1524
|
+
if (action.success) kept.postTriggeredAction = action.data;
|
|
1525
|
+
else dropped.push({
|
|
1526
|
+
mode: item.mode,
|
|
1527
|
+
why: "out-of-bounds",
|
|
1528
|
+
field: "triggeredDurationSec"
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
if (item.schedule !== void 0) {
|
|
1532
|
+
const schedule = require_clip_model_registry.NcScheduleSchema.safeParse(item.schedule);
|
|
1533
|
+
if (schedule.success) kept.schedule = schedule.data;
|
|
1534
|
+
else dropped.push({
|
|
1535
|
+
mode: item.mode,
|
|
1536
|
+
why: "out-of-bounds",
|
|
1537
|
+
field: "schedule"
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
if (kept.exitDelaySec !== void 0 || kept.entryDelaySec !== void 0 || kept.triggeredDurationSec !== void 0 || kept.postTriggeredAction !== void 0 || kept.schedule !== void 0) entries.push(kept);
|
|
1522
1541
|
}
|
|
1523
1542
|
return {
|
|
1524
1543
|
entries,
|
|
@@ -1534,16 +1553,15 @@ function modeDelaysFromPersisted(raw) {
|
|
|
1534
1553
|
* (`resolveModeDelays`): `home` has no exit delay on purpose — nobody is
|
|
1535
1554
|
* leaving — and says so with its own entry, not by this shared value.
|
|
1536
1555
|
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1540
|
-
* every existing alarm after N seconds would be a behaviour change nobody
|
|
1541
|
-
* asked for.
|
|
1556
|
+
* Default post-triggered action is `rearmAfter` 10 s — every mode inherits
|
|
1557
|
+
* that unless the operator overrides. Older blobs that still store the stock
|
|
1558
|
+
* `0` + absent/`doNothing` seed are migrated once at panel boot (D701).
|
|
1542
1559
|
*/
|
|
1543
1560
|
var DEFAULT_DELAYS = {
|
|
1544
1561
|
exitDelaySec: 30,
|
|
1545
1562
|
entryDelaySec: 20,
|
|
1546
|
-
triggeredDurationSec:
|
|
1563
|
+
triggeredDurationSec: 10,
|
|
1564
|
+
postTriggeredAction: "rearmAfter"
|
|
1547
1565
|
};
|
|
1548
1566
|
var DISARMED = {
|
|
1549
1567
|
target: "disarmed",
|
|
@@ -1561,7 +1579,7 @@ function armedState(mode) {
|
|
|
1561
1579
|
* answer is still correct the moment anyone asks.
|
|
1562
1580
|
*/
|
|
1563
1581
|
function stateAt(m, now) {
|
|
1564
|
-
if (m.clearAt !== void 0 && now >= m.clearAt) return m.target;
|
|
1582
|
+
if (m.clearAt !== void 0 && now >= m.clearAt) return m.clearToDisarmed === true ? "disarmed" : m.target;
|
|
1565
1583
|
if (m.fired) return "triggered";
|
|
1566
1584
|
if (m.triggerAt !== void 0) return now >= m.triggerAt ? "triggered" : "pending";
|
|
1567
1585
|
if (m.armedAt !== void 0) return now >= m.armedAt ? m.target : "arming";
|
|
@@ -1657,10 +1675,18 @@ function trigger(m, delays, now) {
|
|
|
1657
1675
|
changedAt: now
|
|
1658
1676
|
};
|
|
1659
1677
|
}
|
|
1660
|
-
/**
|
|
1678
|
+
/**
|
|
1679
|
+
* `clearAt` (+ optional disarm) when the operator asked for a bounded
|
|
1680
|
+
* post-triggered action — see {@link AlarmDelays}.
|
|
1681
|
+
*/
|
|
1661
1682
|
function autoClear(delays, firesAt) {
|
|
1662
|
-
if (delays.
|
|
1663
|
-
|
|
1683
|
+
if (delays.postTriggeredAction === "doNothing") return {};
|
|
1684
|
+
const clearAt = firesAt + Math.max(0, delays.triggeredDurationSec) * 1e3;
|
|
1685
|
+
if (delays.postTriggeredAction === "disableAfter") return {
|
|
1686
|
+
clearAt,
|
|
1687
|
+
clearToDisarmed: true
|
|
1688
|
+
};
|
|
1689
|
+
return { clearAt };
|
|
1664
1690
|
}
|
|
1665
1691
|
/**
|
|
1666
1692
|
* Settle the machine so a stored value never depends on when it is read.
|
|
@@ -1670,16 +1696,23 @@ function autoClear(delays, firesAt) {
|
|
|
1670
1696
|
* than only in the reading of it.
|
|
1671
1697
|
*/
|
|
1672
1698
|
function settle(m, now) {
|
|
1673
|
-
if (m.clearAt !== void 0 && now >= m.clearAt)
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1699
|
+
if (m.clearAt !== void 0 && now >= m.clearAt) {
|
|
1700
|
+
if (m.clearToDisarmed === true) return {
|
|
1701
|
+
...DISARMED,
|
|
1702
|
+
changedAt: m.clearAt
|
|
1703
|
+
};
|
|
1704
|
+
return {
|
|
1705
|
+
target: m.target,
|
|
1706
|
+
fired: false,
|
|
1707
|
+
changedAt: m.clearAt
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1678
1710
|
if (m.triggerAt !== void 0 && now >= m.triggerAt) return {
|
|
1679
1711
|
target: m.target,
|
|
1680
1712
|
fired: true,
|
|
1681
1713
|
changedAt: m.triggerAt,
|
|
1682
|
-
...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {}
|
|
1714
|
+
...m.clearAt !== void 0 ? { clearAt: m.clearAt } : {},
|
|
1715
|
+
...m.clearToDisarmed === true ? { clearToDisarmed: true } : {}
|
|
1683
1716
|
};
|
|
1684
1717
|
if (m.armedAt !== void 0 && now >= m.armedAt) return {
|
|
1685
1718
|
target: m.target,
|
|
@@ -1826,7 +1859,8 @@ function alarmTransition(input) {
|
|
|
1826
1859
|
kind: "alarm-disarmed",
|
|
1827
1860
|
state: next,
|
|
1828
1861
|
previous,
|
|
1829
|
-
at
|
|
1862
|
+
at,
|
|
1863
|
+
...input.autoCleared === true ? { autoCleared: true } : {}
|
|
1830
1864
|
};
|
|
1831
1865
|
return null;
|
|
1832
1866
|
}
|
|
@@ -1917,6 +1951,17 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
|
|
|
1917
1951
|
entryDelaySec: require_clip_model_registry.number().int().min(0).max(600).default(DEFAULT_DELAYS.entryDelaySec),
|
|
1918
1952
|
triggeredDurationSec: require_clip_model_registry.number().int().min(0).max(3600).default(DEFAULT_DELAYS.triggeredDurationSec),
|
|
1919
1953
|
/**
|
|
1954
|
+
* What happens when the triggered window ends. Absent on older blobs →
|
|
1955
|
+
* derived from {@link triggeredDurationSec} at read time (never defaulted
|
|
1956
|
+
* here: a Zod default would force `doNothing` onto installs that already
|
|
1957
|
+
* had a positive `triggeredDurationSec` and silently stop re-arming).
|
|
1958
|
+
*/
|
|
1959
|
+
postTriggeredAction: require_clip_model_registry._enum([
|
|
1960
|
+
"rearmAfter",
|
|
1961
|
+
"disableAfter",
|
|
1962
|
+
"doNothing"
|
|
1963
|
+
]).optional(),
|
|
1964
|
+
/**
|
|
1920
1965
|
* VESTIGIAL (D630): nothing reads it to decide anything any more — every
|
|
1921
1966
|
* transition is announced to `announceTargets`. Kept so a persisted blob
|
|
1922
1967
|
* and the settings cap still round-trip it, and read once at boot to tell
|
|
@@ -1975,7 +2020,14 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
|
|
|
1975
2020
|
mode: require_clip_model_registry.string(),
|
|
1976
2021
|
exitDelaySec: require_clip_model_registry.number().optional(),
|
|
1977
2022
|
entryDelaySec: require_clip_model_registry.number().optional(),
|
|
1978
|
-
triggeredDurationSec: require_clip_model_registry.number().optional()
|
|
2023
|
+
triggeredDurationSec: require_clip_model_registry.number().optional(),
|
|
2024
|
+
postTriggeredAction: require_clip_model_registry._enum([
|
|
2025
|
+
"rearmAfter",
|
|
2026
|
+
"disableAfter",
|
|
2027
|
+
"doNothing"
|
|
2028
|
+
]).optional(),
|
|
2029
|
+
/** Loose — re-validated through {@link modeDelaysFromPersisted}. */
|
|
2030
|
+
schedule: require_clip_model_registry.unknown().optional()
|
|
1979
2031
|
})).default([]),
|
|
1980
2032
|
/**
|
|
1981
2033
|
* Active alarm configuration profile id. Empty / absent on a blob older than
|
|
@@ -2047,12 +2099,18 @@ function settingsBlobFromPersisted(raw) {
|
|
|
2047
2099
|
if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
|
|
2048
2100
|
if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
|
|
2049
2101
|
if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
|
|
2102
|
+
if (typeof rec["postTriggeredAction"] === "string") entry.postTriggeredAction = rec["postTriggeredAction"];
|
|
2103
|
+
if (rec["schedule"] !== void 0) entry.schedule = rec["schedule"];
|
|
2050
2104
|
modeDelaysLoose.push(entry);
|
|
2051
2105
|
}
|
|
2106
|
+
const truncatedTriggered = Math.max(0, Math.min(3600, Math.trunc(triggered)));
|
|
2107
|
+
const rawAction = raw["postTriggeredAction"];
|
|
2108
|
+
const postTriggeredAction = rawAction === "rearmAfter" || rawAction === "disableAfter" || rawAction === "doNothing" ? rawAction : void 0;
|
|
2052
2109
|
return {
|
|
2053
2110
|
exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
|
|
2054
2111
|
entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
|
|
2055
|
-
triggeredDurationSec:
|
|
2112
|
+
triggeredDurationSec: truncatedTriggered,
|
|
2113
|
+
...postTriggeredAction !== void 0 ? { postTriggeredAction } : {},
|
|
2056
2114
|
announceTargets,
|
|
2057
2115
|
nonBlocking: lists("nonBlocking"),
|
|
2058
2116
|
blocking: lists("blocking"),
|
|
@@ -2065,6 +2123,7 @@ function settingsToPersisted(settings) {
|
|
|
2065
2123
|
exitDelaySec: settings.exitDelaySec,
|
|
2066
2124
|
entryDelaySec: settings.entryDelaySec,
|
|
2067
2125
|
triggeredDurationSec: settings.triggeredDurationSec,
|
|
2126
|
+
...settings.postTriggeredAction !== void 0 ? { postTriggeredAction: settings.postTriggeredAction } : {},
|
|
2068
2127
|
announceTargets: [...settings.announceTargets],
|
|
2069
2128
|
nonBlocking: settings.nonBlocking.map((l) => ({
|
|
2070
2129
|
mode: l.mode,
|
|
@@ -2149,11 +2208,13 @@ function machineFromPersisted(raw) {
|
|
|
2149
2208
|
const armedAt = optionalInstant(rec["armedAt"]);
|
|
2150
2209
|
const triggerAt = optionalInstant(rec["triggerAt"]);
|
|
2151
2210
|
const clearAt = optionalInstant(rec["clearAt"]);
|
|
2211
|
+
const clearToDisarmed = rec["clearToDisarmed"] === true;
|
|
2152
2212
|
return {
|
|
2153
2213
|
target: rec["target"],
|
|
2154
2214
|
...armedAt !== void 0 ? { armedAt } : {},
|
|
2155
2215
|
...triggerAt !== void 0 ? { triggerAt } : {},
|
|
2156
2216
|
...clearAt !== void 0 ? { clearAt } : {},
|
|
2217
|
+
...clearToDisarmed ? { clearToDisarmed: true } : {},
|
|
2157
2218
|
fired: rec["fired"] === true,
|
|
2158
2219
|
changedAt: rec["changedAt"]
|
|
2159
2220
|
};
|
|
@@ -2193,6 +2254,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2193
2254
|
super(ctx, ncAlarmPanelSchema, { type: require_clip_model_registry.DeviceType.AlarmPanel });
|
|
2194
2255
|
this.logDroppedModeDelays();
|
|
2195
2256
|
this.restore();
|
|
2257
|
+
this.migrateStockAfterTriggerDefaults();
|
|
2196
2258
|
this.registerAlarmCap();
|
|
2197
2259
|
this.publish();
|
|
2198
2260
|
if (this.config.get("announceArm") === false && this.config.get("announceTargets").length > 0) this.ctx.logger.info("alarm announcements now cover every transition (D630) — the announceArm switch is gone", { tags: { deviceId: this.id } });
|
|
@@ -2204,10 +2266,36 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2204
2266
|
this.config.set("announceTargets", []);
|
|
2205
2267
|
}
|
|
2206
2268
|
}
|
|
2269
|
+
/**
|
|
2270
|
+
* One-shot: the pre-D701 stock seed was `triggeredDurationSec: 0` + absent /
|
|
2271
|
+
* `doNothing` (stay triggered forever). That left After trigger looking dead
|
|
2272
|
+
* in the UI. Flip only that seed to {@link DEFAULT_DELAYS} — never a custom
|
|
2273
|
+
* duration or an explicit `disableAfter`.
|
|
2274
|
+
*/
|
|
2275
|
+
migrateStockAfterTriggerDefaults() {
|
|
2276
|
+
const duration = this.config.get("triggeredDurationSec");
|
|
2277
|
+
const action = this.config.get("postTriggeredAction");
|
|
2278
|
+
if (!(duration === 0 && (action === void 0 || action === "doNothing"))) return;
|
|
2279
|
+
this.config.set("triggeredDurationSec", DEFAULT_DELAYS.triggeredDurationSec);
|
|
2280
|
+
this.config.set("postTriggeredAction", DEFAULT_DELAYS.postTriggeredAction);
|
|
2281
|
+
this.ctx.logger.info("alarm after-trigger defaults migrated to rearmAfter 10s", {
|
|
2282
|
+
tags: { deviceId: this.id },
|
|
2283
|
+
meta: {
|
|
2284
|
+
fromDurationSec: duration,
|
|
2285
|
+
fromAction: action ?? "absent",
|
|
2286
|
+
toDurationSec: DEFAULT_DELAYS.triggeredDurationSec,
|
|
2287
|
+
toAction: DEFAULT_DELAYS.postTriggeredAction
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2207
2291
|
/** The operator-visible configuration, as the cap serves it. */
|
|
2208
2292
|
settings() {
|
|
2293
|
+
const storedAction = this.config.get("postTriggeredAction");
|
|
2209
2294
|
return {
|
|
2210
|
-
|
|
2295
|
+
exitDelaySec: this.config.get("exitDelaySec"),
|
|
2296
|
+
entryDelaySec: this.config.get("entryDelaySec"),
|
|
2297
|
+
triggeredDurationSec: this.config.get("triggeredDurationSec"),
|
|
2298
|
+
...storedAction !== void 0 ? { postTriggeredAction: storedAction } : {},
|
|
2211
2299
|
announceArm: this.config.get("announceArm"),
|
|
2212
2300
|
announceTargets: [...this.config.get("announceTargets")],
|
|
2213
2301
|
nonBlocking: modeDeviceListFromPersisted(this.config.get("nonBlocking")),
|
|
@@ -2356,6 +2444,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2356
2444
|
if (patch.exitDelaySec !== void 0) await this.config.set("exitDelaySec", patch.exitDelaySec);
|
|
2357
2445
|
if (patch.entryDelaySec !== void 0) await this.config.set("entryDelaySec", patch.entryDelaySec);
|
|
2358
2446
|
if (patch.triggeredDurationSec !== void 0) await this.config.set("triggeredDurationSec", patch.triggeredDurationSec);
|
|
2447
|
+
if (patch.postTriggeredAction !== void 0) await this.config.set("postTriggeredAction", patch.postTriggeredAction);
|
|
2359
2448
|
if (patch.announceArm !== void 0) await this.config.set("announceArm", patch.announceArm);
|
|
2360
2449
|
if (patch.announceTargets !== void 0) await this.config.set("announceTargets", [...patch.announceTargets]);
|
|
2361
2450
|
if (membership !== void 0 && membershipChanged !== null) this.ctx.logger.info("alarm non-blocking list changed", {
|
|
@@ -2531,6 +2620,29 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2531
2620
|
}
|
|
2532
2621
|
});
|
|
2533
2622
|
}
|
|
2623
|
+
if (edit.postTriggeredAction !== void 0) {
|
|
2624
|
+
const from = was?.postTriggeredAction ?? null;
|
|
2625
|
+
const to = now?.postTriggeredAction ?? null;
|
|
2626
|
+
if (from !== to) this.ctx.logger.info("alarm mode delays changed", {
|
|
2627
|
+
tags: { deviceId: this.id },
|
|
2628
|
+
meta: {
|
|
2629
|
+
mode: edit.mode,
|
|
2630
|
+
field: "postTriggeredAction",
|
|
2631
|
+
from,
|
|
2632
|
+
to
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
if (edit.schedule !== void 0) {
|
|
2637
|
+
if ((was?.schedule === void 0 ? null : "set") !== (now?.schedule === void 0 ? null : "set") || edit.schedule !== null) this.ctx.logger.info("alarm mode activation schedule changed", {
|
|
2638
|
+
tags: { deviceId: this.id },
|
|
2639
|
+
meta: {
|
|
2640
|
+
mode: edit.mode,
|
|
2641
|
+
cleared: edit.schedule === null,
|
|
2642
|
+
windows: now?.schedule?.windows.length ?? 0
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2534
2646
|
}
|
|
2535
2647
|
}
|
|
2536
2648
|
/** One INFO line per persisted per-mode delay entry the guard refused to trust. */
|
|
@@ -2589,6 +2701,13 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2589
2701
|
*/
|
|
2590
2702
|
async armWith(mode) {
|
|
2591
2703
|
const now = Date.now();
|
|
2704
|
+
if (!this.modeScheduleAllows(mode, now)) {
|
|
2705
|
+
this.ctx.logger.info("alarm arm refused — outside activation schedule", {
|
|
2706
|
+
tags: { deviceId: this.id },
|
|
2707
|
+
meta: { mode }
|
|
2708
|
+
});
|
|
2709
|
+
return { outcome: "outside-schedule" };
|
|
2710
|
+
}
|
|
2592
2711
|
const read = await this.applyArmGuard(mode);
|
|
2593
2712
|
if (read.kind === "unguarded") return this.settleArm(mode, null, now);
|
|
2594
2713
|
if (read.kind === "check-failed") {
|
|
@@ -2602,6 +2721,11 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2602
2721
|
why: this.exclusionWhyFor(mode, device)
|
|
2603
2722
|
})), now);
|
|
2604
2723
|
}
|
|
2724
|
+
/** Absent schedule = always allowed (`isScheduleActive`). */
|
|
2725
|
+
modeScheduleAllows(mode, now) {
|
|
2726
|
+
const schedule = modeDelaysFromPersisted(this.config.get("modeDelays")).entries.find((e) => e.mode === mode)?.schedule;
|
|
2727
|
+
return require_clip_model_registry.isScheduleActive(schedule, now);
|
|
2728
|
+
}
|
|
2605
2729
|
/**
|
|
2606
2730
|
* The Arm anyway on ONE refusal's notification — the only path that arms
|
|
2607
2731
|
* around a device that blocks.
|
|
@@ -2627,6 +2751,17 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
2627
2751
|
const reason = refusalId === this.spentRefusalId ? "used" : "unknown-refusal";
|
|
2628
2752
|
return this.rejectArmAnyway(refusalId, reason);
|
|
2629
2753
|
}
|
|
2754
|
+
if (!this.modeScheduleAllows(held.mode, now)) {
|
|
2755
|
+
this.refusal = null;
|
|
2756
|
+
this.ctx.logger.info("alarm arm-anyway refused — outside activation schedule", {
|
|
2757
|
+
tags: { deviceId: this.id },
|
|
2758
|
+
meta: {
|
|
2759
|
+
mode: held.mode,
|
|
2760
|
+
refusalId
|
|
2761
|
+
}
|
|
2762
|
+
});
|
|
2763
|
+
return { outcome: "outside-schedule" };
|
|
2764
|
+
}
|
|
2630
2765
|
if (this.machine.changedAt !== held.panelChangedAt) {
|
|
2631
2766
|
this.refusal = null;
|
|
2632
2767
|
return this.rejectArmAnyway(refusalId, "stale");
|
|
@@ -3022,11 +3157,12 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3022
3157
|
* rather than fired only once somebody looks.
|
|
3023
3158
|
*/
|
|
3024
3159
|
publish(now = Date.now()) {
|
|
3160
|
+
const autoCleared = this.machine.clearAt !== void 0 && now >= this.machine.clearAt && this.machine.clearToDisarmed === true;
|
|
3025
3161
|
this.machine = settle(this.machine, now);
|
|
3026
3162
|
const state = stateAt(this.machine, now);
|
|
3027
3163
|
this.runtimeState.setCapState("alarm-panel", this.status(now));
|
|
3028
3164
|
this.persist();
|
|
3029
|
-
this.report(state, now);
|
|
3165
|
+
this.report(state, now, autoCleared);
|
|
3030
3166
|
if (this.machine.armedAt !== void 0 || this.machine.triggerAt !== void 0 || this.machine.clearAt !== void 0) this.startTicking();
|
|
3031
3167
|
else this.stopTicking();
|
|
3032
3168
|
}
|
|
@@ -3038,7 +3174,7 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3038
3174
|
* panel's own state update with it would make a delivery failure look like
|
|
3039
3175
|
* an alarm failure.
|
|
3040
3176
|
*/
|
|
3041
|
-
report(state, now) {
|
|
3177
|
+
report(state, now, autoCleared = false) {
|
|
3042
3178
|
const previous = this.lastPublished;
|
|
3043
3179
|
this.lastPublished = state;
|
|
3044
3180
|
const hook = this.onPublish;
|
|
@@ -3053,7 +3189,8 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3053
3189
|
at: now,
|
|
3054
3190
|
...source !== null ? { source } : {},
|
|
3055
3191
|
...arming !== void 0 ? { arming } : {},
|
|
3056
|
-
...pending !== void 0 ? { pending } : {}
|
|
3192
|
+
...pending !== void 0 ? { pending } : {},
|
|
3193
|
+
...autoCleared ? { autoCleared: true } : {}
|
|
3057
3194
|
});
|
|
3058
3195
|
if (transition?.kind === "alarm-triggered") this.attribution.clear();
|
|
3059
3196
|
try {
|
|
@@ -3139,8 +3276,12 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3139
3276
|
}
|
|
3140
3277
|
/** The global durations plus the per-mode overrides, read from the persisted config NOW. */
|
|
3141
3278
|
delaySettings() {
|
|
3279
|
+
const storedAction = this.config.get("postTriggeredAction");
|
|
3142
3280
|
return {
|
|
3143
|
-
|
|
3281
|
+
exitDelaySec: this.config.get("exitDelaySec"),
|
|
3282
|
+
entryDelaySec: this.config.get("entryDelaySec"),
|
|
3283
|
+
triggeredDurationSec: this.config.get("triggeredDurationSec"),
|
|
3284
|
+
...storedAction !== void 0 ? { postTriggeredAction: storedAction } : {},
|
|
3144
3285
|
modeDelays: modeDelaysFromPersisted(this.config.get("modeDelays")).entries
|
|
3145
3286
|
};
|
|
3146
3287
|
}
|
|
@@ -3150,24 +3291,32 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
|
|
|
3150
3291
|
*/
|
|
3151
3292
|
delaysFor(mode) {
|
|
3152
3293
|
const settings = this.delaySettings();
|
|
3153
|
-
if (mode === null)
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3294
|
+
if (mode === null) {
|
|
3295
|
+
const delays = this.readDelays();
|
|
3296
|
+
return {
|
|
3297
|
+
exitDelaySec: delays.exitDelaySec,
|
|
3298
|
+
entryDelaySec: delays.entryDelaySec,
|
|
3299
|
+
triggeredDurationSec: delays.triggeredDurationSec,
|
|
3300
|
+
postTriggeredAction: delays.postTriggeredAction,
|
|
3301
|
+
source: {
|
|
3302
|
+
exitDelaySec: "default",
|
|
3303
|
+
entryDelaySec: "default",
|
|
3304
|
+
triggeredDurationSec: "default",
|
|
3305
|
+
postTriggeredAction: "default"
|
|
3306
|
+
}
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3163
3309
|
return require_clip_model_registry.resolveModeDelays(settings, mode);
|
|
3164
3310
|
}
|
|
3165
|
-
/** The
|
|
3311
|
+
/** The global durations + post-triggered action, read from the persisted config. */
|
|
3166
3312
|
readDelays() {
|
|
3313
|
+
const triggeredDurationSec = this.config.get("triggeredDurationSec");
|
|
3314
|
+
const stored = this.config.get("postTriggeredAction");
|
|
3167
3315
|
return {
|
|
3168
3316
|
exitDelaySec: this.config.get("exitDelaySec"),
|
|
3169
3317
|
entryDelaySec: this.config.get("entryDelaySec"),
|
|
3170
|
-
triggeredDurationSec
|
|
3318
|
+
triggeredDurationSec,
|
|
3319
|
+
postTriggeredAction: require_clip_model_registry.derivePostTriggeredAction(stored, triggeredDurationSec)
|
|
3171
3320
|
};
|
|
3172
3321
|
}
|
|
3173
3322
|
startTicking() {
|
|
@@ -5148,7 +5297,7 @@ function ncClassNoun(texts, className, count) {
|
|
|
5148
5297
|
count,
|
|
5149
5298
|
vars: { name: className }
|
|
5150
5299
|
});
|
|
5151
|
-
if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language);
|
|
5300
|
+
if (require_clip_model_registry.isEventKindLabelKey(normalised)) return require_clip_model_registry.eventKindLabel(normalised, texts.language).toLocaleLowerCase(texts.language);
|
|
5152
5301
|
return texts.text({
|
|
5153
5302
|
key: "class.other",
|
|
5154
5303
|
count,
|
|
@@ -5156,13 +5305,24 @@ function ncClassNoun(texts, className, count) {
|
|
|
5156
5305
|
});
|
|
5157
5306
|
}
|
|
5158
5307
|
/**
|
|
5159
|
-
* The
|
|
5160
|
-
*
|
|
5161
|
-
*
|
|
5308
|
+
* The ONE capitalisation the notification body gets: its first letter, after
|
|
5309
|
+
* every catalog fragment and placeholder has already been rendered lowercase.
|
|
5310
|
+
* Title, fragments and mid-sentence summaries are never passed through here.
|
|
5162
5311
|
*/
|
|
5163
|
-
function
|
|
5164
|
-
if (
|
|
5165
|
-
|
|
5312
|
+
function ncSentenceCaseBody(body) {
|
|
5313
|
+
if (body.length === 0) return body;
|
|
5314
|
+
const first = body[0];
|
|
5315
|
+
const upper = first.toLocaleUpperCase();
|
|
5316
|
+
if (upper === first) return body;
|
|
5317
|
+
return `${upper}${body.slice(1)}`;
|
|
5318
|
+
}
|
|
5319
|
+
/**
|
|
5320
|
+
* True when `label` is a taxonomy / classification id (species, COCO sub, …)
|
|
5321
|
+
* rather than a person name or plate. Those REPLACE the coarse class in the
|
|
5322
|
+
* subject — "dog" not "animal (dog)", "cane" not "animale (cane)".
|
|
5323
|
+
*/
|
|
5324
|
+
function isClassificationLabel(label) {
|
|
5325
|
+
return require_clip_model_registry.isEventKindLabelKey(label.trim().toLowerCase());
|
|
5166
5326
|
}
|
|
5167
5327
|
/** `person (John)` / `persona (John)` — the thing the notification is about. */
|
|
5168
5328
|
function ncSubjectText(texts, input) {
|
|
@@ -5171,11 +5331,15 @@ function ncSubjectText(texts, input) {
|
|
|
5171
5331
|
key: "detection.subject",
|
|
5172
5332
|
vars: { classNoun }
|
|
5173
5333
|
});
|
|
5334
|
+
if (input.labelKind !== "identity" && input.labelKind !== "plate" && isClassificationLabel(input.label)) return texts.text({
|
|
5335
|
+
key: "detection.subject",
|
|
5336
|
+
vars: { classNoun: ncClassNoun(texts, input.label, 1) }
|
|
5337
|
+
});
|
|
5174
5338
|
return texts.text({
|
|
5175
5339
|
key: subjectLabelKey(input.labelKind),
|
|
5176
5340
|
vars: {
|
|
5177
5341
|
classNoun,
|
|
5178
|
-
label:
|
|
5342
|
+
label: input.label
|
|
5179
5343
|
}
|
|
5180
5344
|
});
|
|
5181
5345
|
}
|
|
@@ -5765,7 +5929,7 @@ function isAdoptable(record, nowMs) {
|
|
|
5765
5929
|
* what `localDayMinute` does for the same input — one behaviour for a bad
|
|
5766
5930
|
* zone, not two.
|
|
5767
5931
|
*/
|
|
5768
|
-
function clockOf$
|
|
5932
|
+
function clockOf$7(atMs, timeZone) {
|
|
5769
5933
|
if (timeZone !== void 0) try {
|
|
5770
5934
|
return new Intl.DateTimeFormat("en-GB", {
|
|
5771
5935
|
timeZone,
|
|
@@ -5901,7 +6065,7 @@ var TimelapseRenderer = class {
|
|
|
5901
6065
|
includeAudio: false,
|
|
5902
6066
|
maxLifeMs: await this.ports.exportLifeMs(),
|
|
5903
6067
|
deleteAfterDownload: false,
|
|
5904
|
-
title: `${request.ruleName} ${clockOf$
|
|
6068
|
+
title: `${request.ruleName} ${clockOf$7(window.startMs, request.timezone)}→${clockOf$7(window.endMs, request.timezone)}`,
|
|
5905
6069
|
timelapse: {
|
|
5906
6070
|
everyMs,
|
|
5907
6071
|
outputFps: request.framerate,
|
|
@@ -6232,7 +6396,7 @@ var TimelapseRenderer = class {
|
|
|
6232
6396
|
...meta,
|
|
6233
6397
|
unservedWindowCount: verdict.unservedWindows.length,
|
|
6234
6398
|
unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
|
|
6235
|
-
at: clockOf$
|
|
6399
|
+
at: clockOf$7(request.window.startMs + w.fromSec * 1e3, request.timezone),
|
|
6236
6400
|
fromSec: Math.round(w.fromSec),
|
|
6237
6401
|
toSec: Math.round(w.toSec),
|
|
6238
6402
|
expectedDenseFrames: w.expectedDenseFrames,
|
|
@@ -8813,6 +8977,7 @@ function buildActionButtons(input) {
|
|
|
8813
8977
|
sequenceId: button.sequenceId,
|
|
8814
8978
|
actionId: button.id
|
|
8815
8979
|
}),
|
|
8980
|
+
mode: "background",
|
|
8816
8981
|
...button.icon !== void 0 ? { icon: button.icon } : {},
|
|
8817
8982
|
...button.destructive !== void 0 ? { destructive: button.destructive } : {}
|
|
8818
8983
|
});
|
|
@@ -8928,10 +9093,9 @@ var MAX_ARM_BUTTONS = 3;
|
|
|
8928
9093
|
* (`ALARM_DISARM_TTL_MS`). Both exist: the hint is for the person, the TTL is
|
|
8929
9094
|
* for the forwarded screenshot.
|
|
8930
9095
|
*
|
|
8931
|
-
*
|
|
8932
|
-
*
|
|
8933
|
-
*
|
|
8934
|
-
* confirmed.
|
|
9096
|
+
* Every alarm button is `background` — Zentik (and ntfy) fire a POST and leave
|
|
9097
|
+
* the lock screen alone. Opening a browser to arm or disarm costs more
|
|
9098
|
+
* attention than the notification did.
|
|
8935
9099
|
*
|
|
8936
9100
|
* `panel.availableModes` ARE the enabled modes: CamStack has no per-mode
|
|
8937
9101
|
* disable setting, so the Arm buttons offer exactly what the panel declares.
|
|
@@ -8965,14 +9129,14 @@ function buildAlarmButtons(input) {
|
|
|
8965
9129
|
}),
|
|
8966
9130
|
icon: "disarm",
|
|
8967
9131
|
destructive: true,
|
|
8968
|
-
mode: "
|
|
9132
|
+
mode: "background"
|
|
8969
9133
|
}];
|
|
8970
9134
|
}
|
|
8971
9135
|
/**
|
|
8972
9136
|
* The one Arm anyway a refusal notification carries — or none, when the row
|
|
8973
|
-
* names no refusal.
|
|
8974
|
-
*
|
|
8975
|
-
*
|
|
9137
|
+
* names no refusal. Destructive + background: it arms around an open door via
|
|
9138
|
+
* POST (Zentik BACKGROUND_CALL), same as disarm. Its TTL is the refusal's own
|
|
9139
|
+
* (`ARM_ANYWAY_TTL_MS`, priced by the minter).
|
|
8976
9140
|
*/
|
|
8977
9141
|
function armAnywayButton(refusalId, input) {
|
|
8978
9142
|
if (refusalId === void 0) return [];
|
|
@@ -8987,7 +9151,7 @@ function armAnywayButton(refusalId, input) {
|
|
|
8987
9151
|
}),
|
|
8988
9152
|
icon: "arm",
|
|
8989
9153
|
destructive: true,
|
|
8990
|
-
mode: "
|
|
9154
|
+
mode: "background"
|
|
8991
9155
|
}];
|
|
8992
9156
|
}
|
|
8993
9157
|
//#endregion
|
|
@@ -12909,6 +13073,10 @@ function alarmTextKeys(transition, body) {
|
|
|
12909
13073
|
titleKey,
|
|
12910
13074
|
bodyKey: `system.${kind}.body.${transition.sensor.event}`
|
|
12911
13075
|
};
|
|
13076
|
+
if (kind === "alarm-disarmed" && transition.autoCleared === true) return {
|
|
13077
|
+
titleKey,
|
|
13078
|
+
bodyKey: `system.${kind}.body.autoCleared`
|
|
13079
|
+
};
|
|
12912
13080
|
if (kind === "alarm-disarmed" && (transition.previous === "triggered" || transition.previous === "pending")) return {
|
|
12913
13081
|
titleKey,
|
|
12914
13082
|
bodyKey: `system.${kind}.body.defused`
|
|
@@ -14628,13 +14796,14 @@ var en_default = {
|
|
|
14628
14796
|
"system.alarm-arming.title.away": "Arming away — {{seconds}}s",
|
|
14629
14797
|
"system.alarm-arming.title.night": "Arming night — {{seconds}}s",
|
|
14630
14798
|
"system.alarm-arming.body": "Arming",
|
|
14631
|
-
"system.alarm-armed.title.home": "
|
|
14632
|
-
"system.alarm-armed.title.away": "
|
|
14633
|
-
"system.alarm-armed.title.night": "
|
|
14799
|
+
"system.alarm-armed.title.home": "Alarm armed: home",
|
|
14800
|
+
"system.alarm-armed.title.away": "Alarm armed: away",
|
|
14801
|
+
"system.alarm-armed.title.night": "Alarm armed: night",
|
|
14634
14802
|
"system.alarm-armed.body": "Alarm armed",
|
|
14635
14803
|
"system.alarm-disarmed.title": "Disarmed",
|
|
14636
14804
|
"system.alarm-disarmed.body": "Alarm disarmed",
|
|
14637
14805
|
"system.alarm-disarmed.body.defused": "Alarm disarmed during an alarm",
|
|
14806
|
+
"system.alarm-disarmed.body.autoCleared": "Alarm disarmed automatically after the siren",
|
|
14638
14807
|
"system.alarm-triggered.title": "ALARM",
|
|
14639
14808
|
"system.alarm-triggered.body": "Alarm triggered",
|
|
14640
14809
|
"system.alarm-triggered.body.rule": "Triggered by {{rule}} on {{deviceName}}",
|
|
@@ -14956,13 +15125,14 @@ var it_default = {
|
|
|
14956
15125
|
"system.alarm-arming.title.away": "Inserimento Fuori — {{seconds}}s",
|
|
14957
15126
|
"system.alarm-arming.title.night": "Inserimento Notte — {{seconds}}s",
|
|
14958
15127
|
"system.alarm-arming.body": "Inserimento in corso",
|
|
14959
|
-
"system.alarm-armed.title.home": "
|
|
14960
|
-
"system.alarm-armed.title.away": "
|
|
14961
|
-
"system.alarm-armed.title.night": "
|
|
15128
|
+
"system.alarm-armed.title.home": "Allarme inserito: Casa",
|
|
15129
|
+
"system.alarm-armed.title.away": "Allarme inserito: Fuori",
|
|
15130
|
+
"system.alarm-armed.title.night": "Allarme inserito: Notte",
|
|
14962
15131
|
"system.alarm-armed.body": "Allarme inserito",
|
|
14963
15132
|
"system.alarm-disarmed.title": "Disinserito",
|
|
14964
15133
|
"system.alarm-disarmed.body": "Allarme disinserito",
|
|
14965
15134
|
"system.alarm-disarmed.body.defused": "Allarme disinserito durante un allarme",
|
|
15135
|
+
"system.alarm-disarmed.body.autoCleared": "Allarme disinserito automaticamente dopo la sirena",
|
|
14966
15136
|
"system.alarm-triggered.title": "ALLARME",
|
|
14967
15137
|
"system.alarm-triggered.body": "Allarme scattato",
|
|
14968
15138
|
"system.alarm-triggered.body.rule": "Scattato da {{rule}} su {{deviceName}}",
|
|
@@ -23965,7 +24135,7 @@ var NcDispatcher = class {
|
|
|
23965
24135
|
...systemCount,
|
|
23966
24136
|
...systemEvent?.body !== void 0 ? { frozen: systemEvent.body } : {}
|
|
23967
24137
|
});
|
|
23968
|
-
return composeRuleAiBody({
|
|
24138
|
+
return ncSentenceCaseBody(composeRuleAiBody({
|
|
23969
24139
|
aiText,
|
|
23970
24140
|
body: entry.payload.synthetic === true ? this.renderText({
|
|
23971
24141
|
key: "test.prefix",
|
|
@@ -23973,7 +24143,7 @@ var NcDispatcher = class {
|
|
|
23973
24143
|
frozen: rendered
|
|
23974
24144
|
}) : rendered,
|
|
23975
24145
|
templateNamesAi
|
|
23976
|
-
});
|
|
24146
|
+
}));
|
|
23977
24147
|
};
|
|
23978
24148
|
const body = renderBody("");
|
|
23979
24149
|
const resolved = await this.resolveAttachments(entry);
|
|
@@ -25153,15 +25323,18 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
25153
25323
|
const systemEvent = subject.systemEvent;
|
|
25154
25324
|
const detection = systemEvent?.alarmSource?.className !== void 0 ? systemEvent.alarmSource : void 0;
|
|
25155
25325
|
const group = readOutboxGroup(entry.payload.group);
|
|
25326
|
+
const locale = texts.language;
|
|
25327
|
+
const cameraName = deviceName.toLocaleLowerCase(locale);
|
|
25328
|
+
const zonesLower = zoneLabels.map((z) => z.toLocaleLowerCase(locale));
|
|
25156
25329
|
return {
|
|
25157
|
-
camera:
|
|
25330
|
+
camera: cameraName,
|
|
25158
25331
|
class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
|
|
25159
25332
|
classLabel: (() => {
|
|
25160
25333
|
const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
|
|
25161
|
-
return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire,
|
|
25334
|
+
return wire.length === 0 ? "" : require_clip_model_registry.eventKindLabel(wire, locale).toLocaleLowerCase(locale);
|
|
25162
25335
|
})(),
|
|
25163
25336
|
label: detection?.label ?? subject.label ?? "",
|
|
25164
|
-
zones:
|
|
25337
|
+
zones: zonesLower.join(", "),
|
|
25165
25338
|
subject: detection?.className !== void 0 ? ncSubjectText(texts, {
|
|
25166
25339
|
className: detection.className,
|
|
25167
25340
|
...detection.label !== void 0 ? { label: detection.label } : {}
|
|
@@ -25171,9 +25344,9 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
|
|
|
25171
25344
|
...subject.labelKind !== void 0 ? { labelKind: subject.labelKind } : {}
|
|
25172
25345
|
}),
|
|
25173
25346
|
detectionSummary: group !== void 0 && group.members.length > 1 ? ncDetectionSummary(texts, classCounts(group.members)) : "",
|
|
25174
|
-
inZones: ncInZonesText(texts,
|
|
25175
|
-
scope: occupancy !== void 0 ? occupancy.zone ??
|
|
25176
|
-
zone: occupancy?.zone ?? zoneLabels[0] ?? "",
|
|
25347
|
+
inZones: ncInZonesText(texts, zonesLower),
|
|
25348
|
+
scope: occupancy !== void 0 ? occupancy.zone?.toLocaleLowerCase(locale) ?? cameraName : "",
|
|
25349
|
+
zone: (occupancy?.zone ?? zoneLabels[0] ?? "").toLocaleLowerCase(locale),
|
|
25177
25350
|
confidence: percentOf(detection?.confidence ?? subject.confidence),
|
|
25178
25351
|
time: ncClockText(texts.language, subject.timestamp, timeZone),
|
|
25179
25352
|
sounds: subject.audio?.labels.join(", ") ?? "",
|
|
@@ -29671,7 +29844,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
|
|
|
29671
29844
|
].join("");
|
|
29672
29845
|
}
|
|
29673
29846
|
/** `HH:MM` in the host timezone. */
|
|
29674
|
-
function clockOf$
|
|
29847
|
+
function clockOf$6(atMs) {
|
|
29675
29848
|
const d = new Date(atMs);
|
|
29676
29849
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
29677
29850
|
}
|
|
@@ -29709,7 +29882,7 @@ function buildRuleAiPrompt(input) {
|
|
|
29709
29882
|
if (shape === "detection-track") {
|
|
29710
29883
|
lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
|
|
29711
29884
|
for (const [i, frame] of input.frames.entries()) {
|
|
29712
|
-
const when = frame.atMs !== void 0 ? ` · ${clockOf$
|
|
29885
|
+
const when = frame.atMs !== void 0 ? ` · ${clockOf$6(frame.atMs)}` : "";
|
|
29713
29886
|
lines.push(`${String(i + 1)}.${when}`);
|
|
29714
29887
|
}
|
|
29715
29888
|
return lines.join("\n");
|
|
@@ -31480,7 +31653,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
|
|
|
31480
31653
|
*/
|
|
31481
31654
|
var SummaryAnswerSchema = require_clip_model_registry.object({ summary: require_clip_model_registry.string() });
|
|
31482
31655
|
/** `HH:MM` in the host timezone — the two numbers a frame label needs. */
|
|
31483
|
-
function clockOf$
|
|
31656
|
+
function clockOf$5(atMs) {
|
|
31484
31657
|
const d = new Date(atMs);
|
|
31485
31658
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
31486
31659
|
}
|
|
@@ -31503,7 +31676,7 @@ function buildSummaryAiPrompt(input, frames) {
|
|
|
31503
31676
|
const roster = frames.map((f, i) => {
|
|
31504
31677
|
const location = plainLabel(f.location ?? "");
|
|
31505
31678
|
const where = location.length > 0 ? ` (${location})` : "";
|
|
31506
|
-
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$
|
|
31679
|
+
return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$5(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
|
|
31507
31680
|
}).join("\n");
|
|
31508
31681
|
const described = /* @__PURE__ */ new Map();
|
|
31509
31682
|
for (const f of frames) {
|
|
@@ -32106,7 +32279,7 @@ var NO_DETECTIONS$1 = {
|
|
|
32106
32279
|
* has to check is a key somebody can get wrong. */
|
|
32107
32280
|
var BODY_SEPARATOR$1 = " · ";
|
|
32108
32281
|
/** `HH:MM` in the host timezone. */
|
|
32109
|
-
function clockOf$
|
|
32282
|
+
function clockOf$4(atMs) {
|
|
32110
32283
|
const d = new Date(atMs);
|
|
32111
32284
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
32112
32285
|
}
|
|
@@ -32120,8 +32293,8 @@ function clockOf$3(atMs) {
|
|
|
32120
32293
|
function summaryTemplateVars(input) {
|
|
32121
32294
|
return {
|
|
32122
32295
|
rule: input.ruleName,
|
|
32123
|
-
from: clockOf$
|
|
32124
|
-
to: clockOf$
|
|
32296
|
+
from: clockOf$4(input.window.startMs),
|
|
32297
|
+
to: clockOf$4(input.window.endMs),
|
|
32125
32298
|
time: ncClockText(input.texts.language, input.generatedAt),
|
|
32126
32299
|
events: String(input.tileCount),
|
|
32127
32300
|
cameras: String(input.cameraCount),
|
|
@@ -32216,11 +32389,11 @@ function summaryRecordId(ruleId, windowEndMs) {
|
|
|
32216
32389
|
/** The caption burned onto the mosaic's title band, with the same `{{var}}`
|
|
32217
32390
|
* vocabulary the title and body use. `''` is a real answer ("no caption"). */
|
|
32218
32391
|
function summaryCaptionText(input) {
|
|
32219
|
-
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$
|
|
32392
|
+
if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$4(input.window.startMs)}→${clockOf$4(input.window.endMs)}`;
|
|
32220
32393
|
return renderTemplate(input.captionText, {
|
|
32221
32394
|
rule: input.ruleName,
|
|
32222
|
-
from: clockOf$
|
|
32223
|
-
to: clockOf$
|
|
32395
|
+
from: clockOf$4(input.window.startMs),
|
|
32396
|
+
to: clockOf$4(input.window.endMs)
|
|
32224
32397
|
}) ?? "";
|
|
32225
32398
|
}
|
|
32226
32399
|
/**
|
|
@@ -33152,7 +33325,7 @@ var NcSummaryProducer = class {
|
|
|
33152
33325
|
purpose: "summary",
|
|
33153
33326
|
tiles: tiles.map(({ candidate, jpeg }) => ({
|
|
33154
33327
|
jpeg,
|
|
33155
|
-
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$
|
|
33328
|
+
...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$3(candidate.firstSeen)} · ${candidate.className}` } : {},
|
|
33156
33329
|
deviceId: candidate.deviceId,
|
|
33157
33330
|
sourceId: candidate.trackId
|
|
33158
33331
|
})),
|
|
@@ -33375,7 +33548,7 @@ var NcSummaryProducer = class {
|
|
|
33375
33548
|
}
|
|
33376
33549
|
};
|
|
33377
33550
|
/** `HH:MM` in the host timezone — the two numbers a tile label needs. */
|
|
33378
|
-
function clockOf$
|
|
33551
|
+
function clockOf$3(atMs) {
|
|
33379
33552
|
const d = new Date(atMs);
|
|
33380
33553
|
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
33381
33554
|
}
|
|
@@ -33875,6 +34048,99 @@ var NcCalendarEventsStore = class {
|
|
|
33875
34048
|
}
|
|
33876
34049
|
};
|
|
33877
34050
|
//#endregion
|
|
34051
|
+
//#region src/notification-center/calendar-events/calendar-events-body.ts
|
|
34052
|
+
/** Visual weight on the tomorrow header — Telegram/Zentik plain text. */
|
|
34053
|
+
var CALENDAR_EVENTS_TOMORROW_MARK = "★";
|
|
34054
|
+
/** Separates the tomorrow block from the rest of the horizon. */
|
|
34055
|
+
var CALENDAR_EVENTS_DAY_DELIMITER = "────────";
|
|
34056
|
+
var MAX_EVENT_LINES = 40;
|
|
34057
|
+
function pad2(n) {
|
|
34058
|
+
return String(n).padStart(2, "0");
|
|
34059
|
+
}
|
|
34060
|
+
/** Local calendar day key `YYYY-MM-DD`. */
|
|
34061
|
+
function localDayKey(atMs) {
|
|
34062
|
+
const d = new Date(atMs);
|
|
34063
|
+
return `${String(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
34064
|
+
}
|
|
34065
|
+
function startOfLocalDay(atMs) {
|
|
34066
|
+
const d = new Date(atMs);
|
|
34067
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
34068
|
+
}
|
|
34069
|
+
function addLocalDays(atMs, days) {
|
|
34070
|
+
const d = new Date(atMs);
|
|
34071
|
+
d.setDate(d.getDate() + days);
|
|
34072
|
+
return d.getTime();
|
|
34073
|
+
}
|
|
34074
|
+
function dayHeadingFor(dayStartMs, todayKey, tomorrowKey) {
|
|
34075
|
+
const key = localDayKey(dayStartMs);
|
|
34076
|
+
const label = new Date(dayStartMs).toLocaleDateString(void 0, {
|
|
34077
|
+
weekday: "short",
|
|
34078
|
+
month: "short",
|
|
34079
|
+
day: "numeric"
|
|
34080
|
+
});
|
|
34081
|
+
if (key === todayKey) return `Today — ${label}`;
|
|
34082
|
+
if (key === tomorrowKey) return `${CALENDAR_EVENTS_TOMORROW_MARK} Tomorrow — ${label}`;
|
|
34083
|
+
return label;
|
|
34084
|
+
}
|
|
34085
|
+
/** `HH:MM` local, or `YYYY-MM-DD HH:MM` when the end falls on another day. */
|
|
34086
|
+
function clockOf$2(atMs) {
|
|
34087
|
+
const d = new Date(atMs);
|
|
34088
|
+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
34089
|
+
}
|
|
34090
|
+
function formatEventLine(event) {
|
|
34091
|
+
const when = event.allDay === true ? "All day" : `${clockOf$2(event.startMs)}–${clockOf$2(event.endMs)}`;
|
|
34092
|
+
const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
|
|
34093
|
+
return `• ${when} ${event.title}${where}`;
|
|
34094
|
+
}
|
|
34095
|
+
function groupByLocalDay(events) {
|
|
34096
|
+
const map = /* @__PURE__ */ new Map();
|
|
34097
|
+
for (const ev of events) {
|
|
34098
|
+
const dayKey = localDayKey(ev.startMs);
|
|
34099
|
+
const existing = map.get(dayKey);
|
|
34100
|
+
if (existing !== void 0) {
|
|
34101
|
+
existing.events.push(ev);
|
|
34102
|
+
continue;
|
|
34103
|
+
}
|
|
34104
|
+
map.set(dayKey, {
|
|
34105
|
+
dayKey,
|
|
34106
|
+
dayStartMs: startOfLocalDay(ev.startMs),
|
|
34107
|
+
events: [ev]
|
|
34108
|
+
});
|
|
34109
|
+
}
|
|
34110
|
+
return [...map.values()].toSorted((a, b) => a.dayStartMs - b.dayStartMs);
|
|
34111
|
+
}
|
|
34112
|
+
/**
|
|
34113
|
+
* Build the digest body.
|
|
34114
|
+
*
|
|
34115
|
+
* @param nowMs — fire time; tomorrow is the local calendar day after this.
|
|
34116
|
+
*/
|
|
34117
|
+
function formatCalendarEventsBody(events, nowMs, horizonDays) {
|
|
34118
|
+
if (events.length === 0) return `No events in the next ${String(horizonDays)} day(s).`;
|
|
34119
|
+
const todayKey = localDayKey(nowMs);
|
|
34120
|
+
const tomorrowKey = localDayKey(addLocalDays(startOfLocalDay(nowMs), 1));
|
|
34121
|
+
const buckets = groupByLocalDay(events);
|
|
34122
|
+
const today = buckets.filter((b) => b.dayKey === todayKey);
|
|
34123
|
+
const tomorrow = buckets.filter((b) => b.dayKey === tomorrowKey);
|
|
34124
|
+
const later = buckets.filter((b) => b.dayKey !== todayKey && b.dayKey !== tomorrowKey);
|
|
34125
|
+
const lead = [...today, ...tomorrow];
|
|
34126
|
+
const lines = [];
|
|
34127
|
+
let emitted = 0;
|
|
34128
|
+
const pushBucket = (bucket) => {
|
|
34129
|
+
if (emitted >= MAX_EVENT_LINES) return;
|
|
34130
|
+
lines.push(dayHeadingFor(bucket.dayStartMs, todayKey, tomorrowKey));
|
|
34131
|
+
for (const ev of bucket.events) {
|
|
34132
|
+
if (emitted >= MAX_EVENT_LINES) break;
|
|
34133
|
+
lines.push(formatEventLine(ev));
|
|
34134
|
+
emitted += 1;
|
|
34135
|
+
}
|
|
34136
|
+
};
|
|
34137
|
+
for (const bucket of lead) pushBucket(bucket);
|
|
34138
|
+
if (lead.length > 0 && later.length > 0) lines.push(CALENDAR_EVENTS_DAY_DELIMITER);
|
|
34139
|
+
for (const bucket of later) pushBucket(bucket);
|
|
34140
|
+
if (events.length > MAX_EVENT_LINES) lines.push(`…and ${String(events.length - MAX_EVENT_LINES)} more`);
|
|
34141
|
+
return lines.join("\n");
|
|
34142
|
+
}
|
|
34143
|
+
//#endregion
|
|
33878
34144
|
//#region src/notification-center/calendar-events/calendar-events-delivery.ts
|
|
33879
34145
|
function calendarEventsRecordId(ruleId, windowEndMs) {
|
|
33880
34146
|
return `calendar-events:${ruleId}:${windowEndMs}`;
|
|
@@ -33884,16 +34150,8 @@ function clockOf$1(atMs) {
|
|
|
33884
34150
|
const d = new Date(atMs);
|
|
33885
34151
|
return `${`${String(d.getFullYear())}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`} ${`${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`}`;
|
|
33886
34152
|
}
|
|
33887
|
-
function formatEventLine(event) {
|
|
33888
|
-
const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
|
|
33889
|
-
const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
|
|
33890
|
-
return `• ${when} ${event.title}${where}`;
|
|
33891
|
-
}
|
|
33892
34153
|
function derivedBody$1(input) {
|
|
33893
|
-
|
|
33894
|
-
const lines = input.events.slice(0, 40).map(formatEventLine);
|
|
33895
|
-
const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
|
|
33896
|
-
return lines.join("\n") + extra;
|
|
34154
|
+
return formatCalendarEventsBody(input.events, input.eventWindow.startMs, input.horizonDays);
|
|
33897
34155
|
}
|
|
33898
34156
|
function templateVars(input) {
|
|
33899
34157
|
return {
|
|
@@ -39339,19 +39597,21 @@ var NotificationCenter = class {
|
|
|
39339
39597
|
}
|
|
39340
39598
|
/**
|
|
39341
39599
|
* Start photographing every camera that links `sensorId`, and return the
|
|
39342
|
-
* owners the sensor-level evaluation names — or `undefined` when there is
|
|
39343
|
-
*
|
|
39344
|
-
*
|
|
39345
|
-
*
|
|
39346
|
-
*
|
|
39347
|
-
*
|
|
39348
|
-
*
|
|
39349
|
-
*
|
|
39350
|
-
* never fires on a sensor).
|
|
39351
|
-
*
|
|
39352
|
-
*
|
|
39353
|
-
*
|
|
39354
|
-
*
|
|
39600
|
+
* owners the sensor-level evaluation names — or `undefined` when there is
|
|
39601
|
+
* nothing to photograph (`cameraIds` empty), nothing to show (every linked
|
|
39602
|
+
* camera muted), the event will be dropped as muted (the SENSOR is muted,
|
|
39603
|
+
* or there were row cameras and every one is muted — see the evaluation's
|
|
39604
|
+
* mute gate), or no enabled device-event rule that could fire on this
|
|
39605
|
+
* sensor KIND wants a picture (the gate is the RULE's media policy, scope
|
|
39606
|
+
* and kind conditions, read from the in-memory mirror — D49, as
|
|
39607
|
+
* `audioStillOwner`; an occupancy rule, which rides the same delivery,
|
|
39608
|
+
* never fires on a sensor). One linked camera is enough: link alone owns
|
|
39609
|
+
* notification media; tracked rows are a separate timeline question. Coarse
|
|
39610
|
+
* on purpose, in one direction only: a capture for a rule whose other
|
|
39611
|
+
* conditions then fail is one wasted snapshot, a missing capture is a
|
|
39612
|
+
* notification without its picture. The kind check is what keeps a chatty
|
|
39613
|
+
* multi-linked motion sensor from composing a mosaic on every edge for a
|
|
39614
|
+
* doorbell rule.
|
|
39355
39615
|
*
|
|
39356
39616
|
* A MUTED camera (D62) is never dialled: its tile is left out, said on that
|
|
39357
39617
|
* camera (once per window), and the primary is the first linked camera that
|
|
@@ -39364,7 +39624,7 @@ var NotificationCenter = class {
|
|
|
39364
39624
|
beginLinkedStills(request) {
|
|
39365
39625
|
const { sensorId, sensorKind, cameraIds, atMs, rowCameraIds, sensorRaised } = request;
|
|
39366
39626
|
const shelf = this.linkedStills;
|
|
39367
|
-
if (shelf === null || !this.evaluationActive || cameraIds.length
|
|
39627
|
+
if (shelf === null || !this.evaluationActive || cameraIds.length === 0) return void 0;
|
|
39368
39628
|
if (this.deviceMutes.isMuted(sensorId)) return void 0;
|
|
39369
39629
|
const rowShown = this.unmutedCameras(rowCameraIds);
|
|
39370
39630
|
if (rowCameraIds.length > 0 && rowShown.length === 0) return void 0;
|
|
@@ -41721,14 +41981,19 @@ var NotificationCenter = class {
|
|
|
41721
41981
|
}
|
|
41722
41982
|
sequenceActionsOf = (sequenceId) => this.sequences.get(sequenceId)?.actions;
|
|
41723
41983
|
/**
|
|
41724
|
-
* Run this rule's onTrigger / onTriggerOff bindings for this match
|
|
41984
|
+
* Run this rule's onTrigger / onTriggerOff bindings for this match, and —
|
|
41985
|
+
* when the rule is gated on `armed_*` — auto-trigger the CamStack
|
|
41986
|
+
* alarm-panel (never sirens).
|
|
41725
41987
|
*
|
|
41726
41988
|
* Called BEFORE the cooldown gate — see the call site for why. Everything
|
|
41727
41989
|
* here is fire-and-forget and nothing may throw into the evaluation.
|
|
41728
41990
|
*/
|
|
41729
41991
|
runRuleActions(rule, subject, kind, controls) {
|
|
41730
|
-
if (
|
|
41731
|
-
const
|
|
41992
|
+
if (controls.runActions === false) return;
|
|
41993
|
+
const triggerOff = this.isTriggerOffSubject(subject);
|
|
41994
|
+
if (!triggerOff) this.autoTriggerAlarmPanel(rule, subject, kind);
|
|
41995
|
+
if (this.actionRunner === null) return;
|
|
41996
|
+
const bindings = triggerOff ? rule.actions?.onTriggerOff : rule.actions?.onTrigger;
|
|
41732
41997
|
if (bindings === void 0 || bindings.length === 0) return;
|
|
41733
41998
|
this.actionRunner.runBindings({
|
|
41734
41999
|
ruleId: rule.id,
|
|
@@ -41740,6 +42005,41 @@ var NotificationCenter = class {
|
|
|
41740
42005
|
});
|
|
41741
42006
|
}
|
|
41742
42007
|
/**
|
|
42008
|
+
* A rule gated on `armed_*` fires the CamStack panel on match — no
|
|
42009
|
+
* `onTrigger` sequence required. Sirens are never actuated here.
|
|
42010
|
+
*/
|
|
42011
|
+
autoTriggerAlarmPanel(rule, subject, kind) {
|
|
42012
|
+
if (!require_clip_model_registry.ruleAutoTriggersAlarm(rule)) return;
|
|
42013
|
+
const panel = this.alarmPanel;
|
|
42014
|
+
if (panel === null) {
|
|
42015
|
+
this.logger.warn("alarm rule matched but no alarm panel is wired — panel not triggered", {
|
|
42016
|
+
tags: { deviceId: subject.deviceId },
|
|
42017
|
+
meta: {
|
|
42018
|
+
ruleId: rule.id,
|
|
42019
|
+
rule: rule.name
|
|
42020
|
+
}
|
|
42021
|
+
});
|
|
42022
|
+
return;
|
|
42023
|
+
}
|
|
42024
|
+
this.actuateWithAttribution({
|
|
42025
|
+
deviceId: panel.deviceId,
|
|
42026
|
+
cap: ALARM_PANEL_CAP,
|
|
42027
|
+
method: "trigger",
|
|
42028
|
+
args: {},
|
|
42029
|
+
origin: fireRefFor(rule, subject, kind)
|
|
42030
|
+
}).catch((err) => {
|
|
42031
|
+
this.logger.warn("alarm panel auto-trigger failed", {
|
|
42032
|
+
tags: { deviceId: subject.deviceId },
|
|
42033
|
+
meta: {
|
|
42034
|
+
ruleId: rule.id,
|
|
42035
|
+
rule: rule.name,
|
|
42036
|
+
panelId: panel.deviceId,
|
|
42037
|
+
error: err instanceof Error ? err.message : String(err)
|
|
42038
|
+
}
|
|
42039
|
+
});
|
|
42040
|
+
});
|
|
42041
|
+
}
|
|
42042
|
+
/**
|
|
41743
42043
|
* The muted device that drops this subject, or `undefined` when none does.
|
|
41744
42044
|
* A per-camera subject: its camera. A linked set: the sensor; else, when
|
|
41745
42045
|
* the change took rows and every row camera is muted, the sensor too — the
|
|
@@ -104137,8 +104437,22 @@ var NO_PUBLICATION = {
|
|
|
104137
104437
|
url: null,
|
|
104138
104438
|
artifactId: null
|
|
104139
104439
|
};
|
|
104140
|
-
|
|
104141
|
-
|
|
104440
|
+
/**
|
|
104441
|
+
* A tRPC-shaped router or procedure reached by DYNAMIC name off `ctx.api`.
|
|
104442
|
+
*
|
|
104443
|
+
* A rule names its cap and method as strings, so the generated client cannot be
|
|
104444
|
+
* indexed statically. The walk down `api → router → procedure → mutate|query`
|
|
104445
|
+
* is therefore guarded hop by hop rather than asserted: every hop that is not
|
|
104446
|
+
* what it must be raises the named error the rule author needs to see.
|
|
104447
|
+
*
|
|
104448
|
+
* Forked-addon `ctx.api` namespaces are tRPC v11 Proxies over a noop function
|
|
104449
|
+
* (`typeof === 'function'`). Treating only plain objects as routers made every
|
|
104450
|
+
* sequence step fail with `no router for capability "alarm-panel"` while the
|
|
104451
|
+
* same method answered fine over HTTP — the panel never left `armed_*`, so
|
|
104452
|
+
* HomeKit stayed quiet.
|
|
104453
|
+
*/
|
|
104454
|
+
function isCapNode(value) {
|
|
104455
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
104142
104456
|
}
|
|
104143
104457
|
function isCapCall(value) {
|
|
104144
104458
|
return typeof value === "function";
|
|
@@ -107257,10 +107571,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
107257
107571
|
},
|
|
107258
107572
|
actuate: async ({ deviceId, cap, method, args }) => {
|
|
107259
107573
|
const router = Reflect.get(api, require_clip_model_registry.kebabToCamel(cap));
|
|
107260
|
-
if (!
|
|
107261
|
-
const proc = router
|
|
107262
|
-
const
|
|
107263
|
-
|
|
107574
|
+
if (!isCapNode(router)) throw new Error(`rule action: no router for capability "${cap}"`);
|
|
107575
|
+
const proc = Reflect.get(router, method);
|
|
107576
|
+
const mutate = isCapNode(proc) ? Reflect.get(proc, "mutate") : void 0;
|
|
107577
|
+
const query = isCapNode(proc) ? Reflect.get(proc, "query") : void 0;
|
|
107578
|
+
const fn = isCapCall(mutate) ? mutate : isCapCall(query) ? query : void 0;
|
|
107579
|
+
if (fn === void 0) throw new Error(`rule action: "${cap}.${method}" is not a callable cap method`);
|
|
107264
107580
|
await fn({
|
|
107265
107581
|
deviceId,
|
|
107266
107582
|
...args
|
|
@@ -113513,7 +113829,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
113513
113829
|
});
|
|
113514
113830
|
return NO_SENSOR_NOTIFICATION_CAMERAS;
|
|
113515
113831
|
});
|
|
113516
|
-
const linkedSet = cameras.mediaCameraIds.length > 1;
|
|
113832
|
+
const linkedSet = cameras.mediaCameraIds.length > 1 || cameras.mediaCameraIds.length === 1 && cameras.rowCameraIds.length === 0;
|
|
113517
113833
|
const linkedStills = linkedSet ? this.startLinkedStills(data, timestamp, cameras) : void 0;
|
|
113518
113834
|
const persisted = [];
|
|
113519
113835
|
const unowned = [];
|