@camstack/addon-post-analysis 1.2.24 → 1.2.25
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/{dist-MQmqxd7v.js → dist-Bi1fiPc0.js} +264 -5
- package/dist/{dist-h7dnARE_.mjs → dist-DsCSHZ9N.mjs} +247 -6
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DrRsUzaw.js → node-DFKBSXUx.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-arlk__yF.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-QDqT3XHa.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CXp0RfRy.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CHPq_p1Y.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-NxBfUaEi.mjs → hostInit-DvTbKmnz.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +968 -10
- package/dist/pipeline-analytics/index.mjs +967 -9
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-Bi1fiPc0.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_dist.__toESM(node_path);
|
|
@@ -1542,6 +1542,33 @@ function pointInPolygon$1(point, polygon) {
|
|
|
1542
1542
|
}
|
|
1543
1543
|
return inside;
|
|
1544
1544
|
}
|
|
1545
|
+
/** Parametric line-segment intersection. Returns intersection point or null. */
|
|
1546
|
+
function lineIntersection(a, b) {
|
|
1547
|
+
const dx1 = a.p2.x - a.p1.x;
|
|
1548
|
+
const dy1 = a.p2.y - a.p1.y;
|
|
1549
|
+
const dx2 = b.p2.x - b.p1.x;
|
|
1550
|
+
const dy2 = b.p2.y - b.p1.y;
|
|
1551
|
+
const denom = dx1 * dy2 - dy1 * dx2;
|
|
1552
|
+
if (Math.abs(denom) < 1e-10) return null;
|
|
1553
|
+
const t = ((b.p1.x - a.p1.x) * dy2 - (b.p1.y - a.p1.y) * dx2) / denom;
|
|
1554
|
+
const u = ((b.p1.x - a.p1.x) * dy1 - (b.p1.y - a.p1.y) * dx1) / denom;
|
|
1555
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null;
|
|
1556
|
+
return {
|
|
1557
|
+
x: a.p1.x + t * dx1,
|
|
1558
|
+
y: a.p1.y + t * dy1
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
/** Detect tripwire crossing and direction via cross product. */
|
|
1562
|
+
function tripwireCrossing(prev, curr, wire) {
|
|
1563
|
+
if (!lineIntersection({
|
|
1564
|
+
p1: prev,
|
|
1565
|
+
p2: curr
|
|
1566
|
+
}, wire)) return null;
|
|
1567
|
+
return {
|
|
1568
|
+
crossed: true,
|
|
1569
|
+
direction: (curr.x - prev.x) * (wire.p2.y - wire.p1.y) - (curr.y - prev.y) * (wire.p2.x - wire.p1.x) > 0 ? "right" : "left"
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1545
1572
|
/** Centroid of a bounding box { x, y, w, h } */
|
|
1546
1573
|
function bboxCentroid(bbox) {
|
|
1547
1574
|
return {
|
|
@@ -1556,6 +1583,95 @@ function normalizeToPixel(p, width, height) {
|
|
|
1556
1583
|
y: p.y * height
|
|
1557
1584
|
};
|
|
1558
1585
|
}
|
|
1586
|
+
var NONE = [];
|
|
1587
|
+
/**
|
|
1588
|
+
* Which side of the directed line the point is on.
|
|
1589
|
+
*
|
|
1590
|
+
* Sign of the 2D cross product of the line's own vector with (point − p1):
|
|
1591
|
+
* positive is the right-hand side of p1 → p2. Exactly zero means ON the line,
|
|
1592
|
+
* which is deliberately its own value: it is neither side, so it can neither
|
|
1593
|
+
* confirm nor cancel a crossing.
|
|
1594
|
+
*/
|
|
1595
|
+
function sideOf(point, wire) {
|
|
1596
|
+
const cross = (wire.p2.x - wire.p1.x) * (point.y - wire.p1.y) - (wire.p2.y - wire.p1.y) * (point.x - wire.p1.x);
|
|
1597
|
+
if (cross > 0) return 1;
|
|
1598
|
+
if (cross < 0) return -1;
|
|
1599
|
+
return 0;
|
|
1600
|
+
}
|
|
1601
|
+
var TripwireTracker = class {
|
|
1602
|
+
config;
|
|
1603
|
+
/** `trackId` → `wireId` → state. Keyed by track so `forget` is one delete. */
|
|
1604
|
+
tracks = /* @__PURE__ */ new Map();
|
|
1605
|
+
constructor(config = {}) {
|
|
1606
|
+
this.config = {
|
|
1607
|
+
confirmFrames: 2,
|
|
1608
|
+
...config
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Feed one frame's centroid for one track against one wire.
|
|
1613
|
+
*
|
|
1614
|
+
* Returns the crossings CONFIRMED on this frame — usually none. Coordinates
|
|
1615
|
+
* are whatever space the caller uses consistently; the zone geometry is
|
|
1616
|
+
* normalised (0–1), so the frame processor passes normalised centroids.
|
|
1617
|
+
*/
|
|
1618
|
+
observe(trackId, wireId, wire, centroid) {
|
|
1619
|
+
const wires = this.tracks.get(trackId) ?? /* @__PURE__ */ new Map();
|
|
1620
|
+
this.tracks.set(trackId, wires);
|
|
1621
|
+
const side = sideOf(centroid, wire);
|
|
1622
|
+
const state = wires.get(wireId);
|
|
1623
|
+
if (state === void 0) {
|
|
1624
|
+
wires.set(wireId, {
|
|
1625
|
+
last: centroid,
|
|
1626
|
+
confirmedSide: side,
|
|
1627
|
+
pendingSide: side,
|
|
1628
|
+
pendingFrames: 0,
|
|
1629
|
+
sawIntersection: false,
|
|
1630
|
+
pendingDirection: "in"
|
|
1631
|
+
});
|
|
1632
|
+
return NONE;
|
|
1633
|
+
}
|
|
1634
|
+
const crossedNow = tripwireCrossing(state.last, centroid, wire) !== null;
|
|
1635
|
+
state.last = centroid;
|
|
1636
|
+
if (side === 0) return NONE;
|
|
1637
|
+
if (side === state.confirmedSide) {
|
|
1638
|
+
state.pendingSide = side;
|
|
1639
|
+
state.pendingFrames = 0;
|
|
1640
|
+
state.sawIntersection = false;
|
|
1641
|
+
return NONE;
|
|
1642
|
+
}
|
|
1643
|
+
if (side !== state.pendingSide) {
|
|
1644
|
+
state.pendingSide = side;
|
|
1645
|
+
state.pendingFrames = 1;
|
|
1646
|
+
state.sawIntersection = crossedNow;
|
|
1647
|
+
state.pendingDirection = side > 0 ? "in" : "out";
|
|
1648
|
+
} else {
|
|
1649
|
+
state.pendingFrames += 1;
|
|
1650
|
+
state.sawIntersection = state.sawIntersection || crossedNow;
|
|
1651
|
+
}
|
|
1652
|
+
if (state.pendingFrames < this.config.confirmFrames) return NONE;
|
|
1653
|
+
if (!state.sawIntersection) {
|
|
1654
|
+
state.confirmedSide = side;
|
|
1655
|
+
state.pendingFrames = 0;
|
|
1656
|
+
return NONE;
|
|
1657
|
+
}
|
|
1658
|
+
state.confirmedSide = side;
|
|
1659
|
+
state.pendingFrames = 0;
|
|
1660
|
+
state.sawIntersection = false;
|
|
1661
|
+
return [{
|
|
1662
|
+
wireId,
|
|
1663
|
+
direction: state.pendingDirection
|
|
1664
|
+
}];
|
|
1665
|
+
}
|
|
1666
|
+
/** Drop a track. Called when the subject leaves, so state cannot accumulate. */
|
|
1667
|
+
forget(trackId) {
|
|
1668
|
+
this.tracks.delete(trackId);
|
|
1669
|
+
}
|
|
1670
|
+
/** Number of tracks held — a leak check for the caller's tests. */
|
|
1671
|
+
size() {
|
|
1672
|
+
return this.tracks.size;
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1559
1675
|
//#endregion
|
|
1560
1676
|
//#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
|
|
1561
1677
|
var DEFAULT_TRACKER_CONFIG = {
|
|
@@ -2244,10 +2360,11 @@ var DetectionEventEmitter = class {
|
|
|
2244
2360
|
const eventType = ZONE_TYPE_TO_EVENT[ze.type];
|
|
2245
2361
|
if (!eventType) continue;
|
|
2246
2362
|
if (!this.config.enabledTypes.includes(eventType)) continue;
|
|
2247
|
-
const
|
|
2363
|
+
const present = trackMap.get(ze.trackId);
|
|
2364
|
+
const track = present ?? this.lastKnownTracks.get(ze.trackId);
|
|
2248
2365
|
if (!track || track.trackAge < this.config.minTrackAge) continue;
|
|
2249
2366
|
const state = stateMap.get(ze.trackId);
|
|
2250
|
-
this.valuedTracks.add(ze.trackId);
|
|
2367
|
+
if (present) this.valuedTracks.add(ze.trackId);
|
|
2251
2368
|
events.push({
|
|
2252
2369
|
id: `evt-${++eventIdCounter}`,
|
|
2253
2370
|
type: eventType,
|
|
@@ -2594,6 +2711,19 @@ var FrameProcessor = class {
|
|
|
2594
2711
|
/** Zone crossings per track — the producer of `zone.enter` / `zone.exit`. */
|
|
2595
2712
|
zoneTransitions = new ZoneTransitionTracker();
|
|
2596
2713
|
/**
|
|
2714
|
+
* Tripwire crossings. Separate from `zoneTransitions` because a tripwire is
|
|
2715
|
+
* a DIRECTED line, not an area: membership is meaningless and the question is
|
|
2716
|
+
* which way the subject went over it.
|
|
2717
|
+
*/
|
|
2718
|
+
tripwires = new TripwireTracker();
|
|
2719
|
+
/**
|
|
2720
|
+
* Last detection seen per track. An edge exit fires for a track that is
|
|
2721
|
+
* ABSENT from the frame, so there is no current detection to attach — this is
|
|
2722
|
+
* the one the subject had when it was last seen. Cleared with the rest of the
|
|
2723
|
+
* per-track caches when the track leaves.
|
|
2724
|
+
*/
|
|
2725
|
+
lastDetectionByTrack = /* @__PURE__ */ new Map();
|
|
2726
|
+
/**
|
|
2597
2727
|
* Last zone membership seen per track, so an event for a track that is GONE
|
|
2598
2728
|
* this frame still carries the zones it was in. See the use site.
|
|
2599
2729
|
*/
|
|
@@ -2789,6 +2919,7 @@ var FrameProcessor = class {
|
|
|
2789
2919
|
}
|
|
2790
2920
|
this.lastZoneOverlaps = overlapsByTrack;
|
|
2791
2921
|
this.lastZoneRejections = rejectedByTrack;
|
|
2922
|
+
for (const td of trackedDetections) this.lastDetectionByTrack.set(td.trackId, td);
|
|
2792
2923
|
for (const [trackId, zoneIds] of zonesByTrack) this.lastZonesByTrack.set(trackId, zoneIds);
|
|
2793
2924
|
for (const state of objectStates) {
|
|
2794
2925
|
if (zonesByTrack.has(state.trackId)) continue;
|
|
@@ -2807,16 +2938,58 @@ var FrameProcessor = class {
|
|
|
2807
2938
|
timestamp
|
|
2808
2939
|
});
|
|
2809
2940
|
};
|
|
2941
|
+
for (const state of objectStates) {
|
|
2942
|
+
if (state.state !== "leaving") continue;
|
|
2943
|
+
const stillInside = this.zoneTransitions.forget(state.trackId);
|
|
2944
|
+
this.tripwires.forget(state.trackId);
|
|
2945
|
+
if (stillInside.length === 0) continue;
|
|
2946
|
+
const last = this.lastDetectionByTrack.get(state.trackId);
|
|
2947
|
+
if (last === void 0) continue;
|
|
2948
|
+
for (const zoneId of stillInside) pushZoneEvent("zone-exit", zoneId, last);
|
|
2949
|
+
}
|
|
2810
2950
|
for (const td of trackedDetections) {
|
|
2811
2951
|
const crossings = this.zoneTransitions.observe(td.trackId, zonesByTrack.get(td.trackId) ?? []);
|
|
2812
2952
|
for (const zoneId of crossings.entered) pushZoneEvent("zone-enter", zoneId, td);
|
|
2813
2953
|
for (const zoneId of crossings.exited) pushZoneEvent("zone-exit", zoneId, td);
|
|
2814
2954
|
}
|
|
2955
|
+
const wires = this.zones.filter((z) => z.kind === "tripwire" && z.polygon.length >= 2);
|
|
2956
|
+
if (wires.length > 0) for (const td of trackedDetections) {
|
|
2957
|
+
const centroid = bboxCentroid({
|
|
2958
|
+
x: td.bbox.x / frameWidth,
|
|
2959
|
+
y: td.bbox.y / frameHeight,
|
|
2960
|
+
w: td.bbox.w / frameWidth,
|
|
2961
|
+
h: td.bbox.h / frameHeight
|
|
2962
|
+
});
|
|
2963
|
+
for (const zone of wires) {
|
|
2964
|
+
const a = zone.polygon[0];
|
|
2965
|
+
const b = zone.polygon[1];
|
|
2966
|
+
if (a === void 0 || b === void 0) continue;
|
|
2967
|
+
const wire = {
|
|
2968
|
+
p1: {
|
|
2969
|
+
x: a.x,
|
|
2970
|
+
y: a.y
|
|
2971
|
+
},
|
|
2972
|
+
p2: {
|
|
2973
|
+
x: b.x,
|
|
2974
|
+
y: b.y
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
for (const cross of this.tripwires.observe(td.trackId, zone.id, wire, centroid)) zoneEvents.push({
|
|
2978
|
+
type: "tripwire-cross",
|
|
2979
|
+
zoneId: zone.id,
|
|
2980
|
+
zoneName: zone.name,
|
|
2981
|
+
trackId: td.trackId,
|
|
2982
|
+
detection: td,
|
|
2983
|
+
direction: cross.direction,
|
|
2984
|
+
timestamp
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2815
2988
|
const rawEvents = this.eventEmitter.emit(trackedDetections, objectStates, zoneEvents, [], String(this.deviceId));
|
|
2816
2989
|
for (const state of objectStates) {
|
|
2817
2990
|
if (state.state !== "leaving") continue;
|
|
2818
|
-
this.zoneTransitions.forget(state.trackId);
|
|
2819
2991
|
this.lastZonesByTrack.delete(state.trackId);
|
|
2992
|
+
this.lastDetectionByTrack.delete(state.trackId);
|
|
2820
2993
|
}
|
|
2821
2994
|
const tracked = trackedDetections.map((td) => {
|
|
2822
2995
|
const state = mapObjectStateToTrackState(objectStates.find((o) => o.trackId === td.trackId)?.state);
|
|
@@ -2856,9 +3029,9 @@ var FrameProcessor = class {
|
|
|
2856
3029
|
};
|
|
2857
3030
|
});
|
|
2858
3031
|
const crossingOf = (e) => {
|
|
2859
|
-
const direction = e.type === "zone.enter" ? "enter" : e.type === "zone.exit" ? "exit" : void 0;
|
|
2860
|
-
if (direction === void 0) return void 0;
|
|
2861
3032
|
const ze = e.zoneEvents[0];
|
|
3033
|
+
const direction = e.type === "zone.enter" ? "enter" : e.type === "zone.exit" ? "exit" : e.type === "tripwire.cross" ? ze?.direction === "out" ? "exit" : "enter" : void 0;
|
|
3034
|
+
if (direction === void 0) return void 0;
|
|
2862
3035
|
if (ze === void 0) return void 0;
|
|
2863
3036
|
return {
|
|
2864
3037
|
direction,
|
|
@@ -4950,6 +5123,29 @@ var NcDispatcher = class {
|
|
|
4950
5123
|
} });
|
|
4951
5124
|
return { ok: true };
|
|
4952
5125
|
}
|
|
5126
|
+
if (this.deps.snoozeGate !== void 0) {
|
|
5127
|
+
const snoozeId = await this.deps.snoozeGate({
|
|
5128
|
+
ruleId: entry.ruleId,
|
|
5129
|
+
ruleName: entry.payload.ruleName,
|
|
5130
|
+
targetId: target.id,
|
|
5131
|
+
deviceId: entry.deviceId,
|
|
5132
|
+
...target.ownerUserId !== void 0 ? { ownerUserId: target.ownerUserId } : {},
|
|
5133
|
+
at: this.now()
|
|
5134
|
+
});
|
|
5135
|
+
if (snoozeId !== null) {
|
|
5136
|
+
this.deps.logger.info("notification suppressed by snooze", {
|
|
5137
|
+
tags: { deviceId: entry.deviceId },
|
|
5138
|
+
meta: {
|
|
5139
|
+
snoozeId,
|
|
5140
|
+
ruleId: entry.ruleId,
|
|
5141
|
+
targetId: target.id,
|
|
5142
|
+
target: target.name,
|
|
5143
|
+
eventId: entry.recordId
|
|
5144
|
+
}
|
|
5145
|
+
});
|
|
5146
|
+
return { ok: true };
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
4953
5149
|
const notification = await this.buildNotification(entry);
|
|
4954
5150
|
try {
|
|
4955
5151
|
const result = await this.deps.send({
|
|
@@ -4982,6 +5178,81 @@ var NcDispatcher = class {
|
|
|
4982
5178
|
};
|
|
4983
5179
|
}
|
|
4984
5180
|
}
|
|
5181
|
+
/**
|
|
5182
|
+
* Deliver ONE end-of-window snooze digest to one target.
|
|
5183
|
+
*
|
|
5184
|
+
* Best-effort by design, and the return value says which it was rather than
|
|
5185
|
+
* throwing: the caller marks the window handled either way, because a digest
|
|
5186
|
+
* retried every drain tick for hours is worse than a digest missed once — and
|
|
5187
|
+
* a partially-delivered one must never be re-sent to the targets that already
|
|
5188
|
+
* have it. A failure is logged with the window id, so a missing summary is a
|
|
5189
|
+
* findable line rather than a silence inside a silence.
|
|
5190
|
+
*
|
|
5191
|
+
* The digest carries no media and no `tag`: it is not an event, and tagging
|
|
5192
|
+
* it with a rule id would let a phone collapse it into the very
|
|
5193
|
+
* notifications it is reporting on.
|
|
5194
|
+
*/
|
|
5195
|
+
async deliverDigest(input) {
|
|
5196
|
+
let target;
|
|
5197
|
+
try {
|
|
5198
|
+
target = await this.resolveTarget(input.targetId);
|
|
5199
|
+
} catch (err) {
|
|
5200
|
+
this.deps.logger.warn("snooze digest: target catalog unreachable", { meta: {
|
|
5201
|
+
snoozeId: input.snoozeId,
|
|
5202
|
+
targetId: input.targetId,
|
|
5203
|
+
error: String(err)
|
|
5204
|
+
} });
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5207
|
+
if (target === null || !target.enabled) {
|
|
5208
|
+
this.deps.logger.warn("snooze digest: target gone or disabled", { meta: {
|
|
5209
|
+
snoozeId: input.snoozeId,
|
|
5210
|
+
targetId: input.targetId
|
|
5211
|
+
} });
|
|
5212
|
+
return false;
|
|
5213
|
+
}
|
|
5214
|
+
try {
|
|
5215
|
+
const result = await this.deps.send({
|
|
5216
|
+
addonId: target.addonId,
|
|
5217
|
+
targetId: target.id,
|
|
5218
|
+
notification: {
|
|
5219
|
+
title: input.title,
|
|
5220
|
+
body: input.body,
|
|
5221
|
+
priority: 2
|
|
5222
|
+
}
|
|
5223
|
+
});
|
|
5224
|
+
if (!result.success) {
|
|
5225
|
+
this.deps.logger.warn("snooze digest send failed", { meta: {
|
|
5226
|
+
snoozeId: input.snoozeId,
|
|
5227
|
+
targetId: target.id,
|
|
5228
|
+
error: result.error
|
|
5229
|
+
} });
|
|
5230
|
+
return false;
|
|
5231
|
+
}
|
|
5232
|
+
this.deps.logger.info("snooze digest delivered", { meta: {
|
|
5233
|
+
snoozeId: input.snoozeId,
|
|
5234
|
+
target: target.name,
|
|
5235
|
+
hidden: input.total
|
|
5236
|
+
} });
|
|
5237
|
+
return true;
|
|
5238
|
+
} catch (err) {
|
|
5239
|
+
this.deps.logger.warn("snooze digest send threw", { meta: {
|
|
5240
|
+
snoozeId: input.snoozeId,
|
|
5241
|
+
targetId: target.id,
|
|
5242
|
+
error: String(err)
|
|
5243
|
+
} });
|
|
5244
|
+
return false;
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
/** Device display names for a set of ids, for the digest's lines. */
|
|
5248
|
+
async resolveDeviceNames(deviceIds) {
|
|
5249
|
+
const out = /* @__PURE__ */ new Map();
|
|
5250
|
+
for (const id of new Set(deviceIds)) {
|
|
5251
|
+
const name = await this.deps.getDeviceName(id).catch(() => null);
|
|
5252
|
+
if (name !== null) out.set(id, name);
|
|
5253
|
+
}
|
|
5254
|
+
return out;
|
|
5255
|
+
}
|
|
4985
5256
|
async resolveTarget(targetId) {
|
|
4986
5257
|
const cached = this.cachedTarget(targetId);
|
|
4987
5258
|
if (cached !== null) return cached;
|
|
@@ -5100,7 +5371,8 @@ var NcDispatcher = class {
|
|
|
5100
5371
|
format: want.format,
|
|
5101
5372
|
...entry.payload.mediaClipPreRollSec !== void 0 ? { preRollSec: entry.payload.mediaClipPreRollSec } : {},
|
|
5102
5373
|
...entry.payload.mediaClipPostRollSec !== void 0 ? { postRollSec: entry.payload.mediaClipPostRollSec } : {},
|
|
5103
|
-
...entry.payload.mediaProfile !== void 0 ? { profile: entry.payload.mediaProfile } : {}
|
|
5374
|
+
...entry.payload.mediaProfile !== void 0 ? { profile: entry.payload.mediaProfile } : {},
|
|
5375
|
+
...entry.payload.mediaClipSpeed !== void 0 ? { speed: entry.payload.mediaClipSpeed } : {}
|
|
5104
5376
|
});
|
|
5105
5377
|
if (rendered !== null && rendered.byteLength > 0) {
|
|
5106
5378
|
const bytes = new Uint8Array(rendered.byteLength);
|
|
@@ -6341,6 +6613,474 @@ var NcRuleStore = class {
|
|
|
6341
6613
|
}
|
|
6342
6614
|
};
|
|
6343
6615
|
//#endregion
|
|
6616
|
+
//#region src/notification-center/snooze-digest.ts
|
|
6617
|
+
/** `14:03` in the node's local zone — a digest is read by a person, at home. */
|
|
6618
|
+
function clock(ms) {
|
|
6619
|
+
const d = new Date(ms);
|
|
6620
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
6621
|
+
}
|
|
6622
|
+
function plural(n, one, many) {
|
|
6623
|
+
return n === 1 ? `1 ${one}` : `${n} ${many}`;
|
|
6624
|
+
}
|
|
6625
|
+
/**
|
|
6626
|
+
* What the window covered, in the words the operator chose it with — the rule's
|
|
6627
|
+
* NAME, the camera's name. "a rule was snoozed" tells the reader nothing they
|
|
6628
|
+
* could act on.
|
|
6629
|
+
*/
|
|
6630
|
+
function scopeLabel(snooze, rows, deviceNames) {
|
|
6631
|
+
switch (snooze.scope) {
|
|
6632
|
+
case "all": return "Everything";
|
|
6633
|
+
case "device": {
|
|
6634
|
+
const id = snooze.deviceId;
|
|
6635
|
+
if (id === void 0) return "A camera";
|
|
6636
|
+
return deviceNames.get(id) ?? `Camera ${id}`;
|
|
6637
|
+
}
|
|
6638
|
+
case "rule": {
|
|
6639
|
+
const named = rows.find((r) => r.ruleId === snooze.ruleId);
|
|
6640
|
+
return named !== void 0 ? `“${named.ruleName}”` : "A rule";
|
|
6641
|
+
}
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
/**
|
|
6645
|
+
* One digest for ONE target — the counters passed in are already the rows
|
|
6646
|
+
* belonging to it, so the message says what THAT destination missed rather
|
|
6647
|
+
* than what the household did.
|
|
6648
|
+
*
|
|
6649
|
+
* Returns null when the window hid nothing: "snooze ended, 0 notifications" is
|
|
6650
|
+
* a notification the operator did not need, and sending it for every quiet
|
|
6651
|
+
* window is how a summary becomes the noise it was meant to suppress.
|
|
6652
|
+
*/
|
|
6653
|
+
function buildSnoozeDigest(snooze, rows, deviceNames) {
|
|
6654
|
+
const total = rows.reduce((sum, r) => sum + r.count, 0);
|
|
6655
|
+
if (total === 0) return null;
|
|
6656
|
+
const from = clock(Math.min(...rows.map((r) => r.firstAt)));
|
|
6657
|
+
const to = clock(snooze.expiresAt);
|
|
6658
|
+
const title = `Snooze ended — ${plural(total, "notification", "notifications")} hidden`;
|
|
6659
|
+
const lines = rows.toSorted((a, b) => b.count - a.count || a.ruleName.localeCompare(b.ruleName)).map((r) => {
|
|
6660
|
+
const where = deviceNames.get(r.deviceId) ?? `camera ${r.deviceId}`;
|
|
6661
|
+
return `• ${r.ruleName} on ${where} — ${r.count}`;
|
|
6662
|
+
});
|
|
6663
|
+
return {
|
|
6664
|
+
title,
|
|
6665
|
+
body: [`${scopeLabel(snooze, rows, deviceNames)} was snoozed${snooze.global ? " for everyone" : ""}, ${from}–${to}.`, ...lines].join("\n"),
|
|
6666
|
+
total
|
|
6667
|
+
};
|
|
6668
|
+
}
|
|
6669
|
+
/** The distinct targets a window owes a digest to. */
|
|
6670
|
+
function digestTargets(rows) {
|
|
6671
|
+
return [...new Set(rows.map((r) => r.targetId))];
|
|
6672
|
+
}
|
|
6673
|
+
//#endregion
|
|
6674
|
+
//#region src/notification-center/snooze-policy.ts
|
|
6675
|
+
/** How broad a scope is, for choosing between overlapping snoozes. */
|
|
6676
|
+
var BREADTH = {
|
|
6677
|
+
rule: 1,
|
|
6678
|
+
device: 2,
|
|
6679
|
+
all: 3
|
|
6680
|
+
};
|
|
6681
|
+
function covers(entry, q) {
|
|
6682
|
+
if (q.nowMs < entry.startedAt) return false;
|
|
6683
|
+
if (q.nowMs >= entry.expiresAt) return false;
|
|
6684
|
+
if (!entry.global && entry.userId !== q.userId) return false;
|
|
6685
|
+
switch (entry.scope) {
|
|
6686
|
+
case "all": return true;
|
|
6687
|
+
case "rule": return entry.ruleId !== void 0 && entry.ruleId === q.ruleId;
|
|
6688
|
+
case "device": return entry.deviceId !== void 0 && entry.deviceId === q.deviceId;
|
|
6689
|
+
}
|
|
6690
|
+
}
|
|
6691
|
+
/**
|
|
6692
|
+
* The snooze that silences this notification, or null.
|
|
6693
|
+
*
|
|
6694
|
+
* The BROADEST match wins so one window produces one digest: an "all" snooze
|
|
6695
|
+
* overlapping a rule snooze should not leave the recipient two summaries for
|
|
6696
|
+
* the same silence.
|
|
6697
|
+
*/
|
|
6698
|
+
function activeSnoozeFor(entries, q) {
|
|
6699
|
+
let best = null;
|
|
6700
|
+
for (const e of entries) {
|
|
6701
|
+
if (!covers(e, q)) continue;
|
|
6702
|
+
if (best === null || BREADTH[e.scope] > BREADTH[best.scope]) best = e;
|
|
6703
|
+
}
|
|
6704
|
+
return best;
|
|
6705
|
+
}
|
|
6706
|
+
/**
|
|
6707
|
+
* May this caller silence EVERYONE, not just themselves?
|
|
6708
|
+
*
|
|
6709
|
+
* Global is the only part of a snooze that can affect a person who did not ask
|
|
6710
|
+
* for it, so it is never reachable by accident:
|
|
6711
|
+
*
|
|
6712
|
+
* - **rule scope** — the rule itself must opt in (`snoozeAllowGlobal`, default
|
|
6713
|
+
* off). That is the gardener case: this rule is known to be noisy for
|
|
6714
|
+
* everybody in a way that is legitimate to silence for everybody.
|
|
6715
|
+
* - **device or whole-feed scope** — there is no rule to carry the opt-in, and
|
|
6716
|
+
* "silence this camera for the household" is an administrative act. Admin
|
|
6717
|
+
* only.
|
|
6718
|
+
*
|
|
6719
|
+
* A personal snooze needs no permission at all: it silences only its author.
|
|
6720
|
+
*/
|
|
6721
|
+
function canSetGlobal(req) {
|
|
6722
|
+
if (req.isAdmin) return true;
|
|
6723
|
+
if (req.scope !== "rule") return false;
|
|
6724
|
+
return req.ruleAllowsGlobal === true;
|
|
6725
|
+
}
|
|
6726
|
+
/** Entries whose window has closed and whose digest has not been sent yet. */
|
|
6727
|
+
function dueSummaries(entries, nowMs, alreadySent) {
|
|
6728
|
+
return entries.filter((e) => e.summary && nowMs >= e.expiresAt && !alreadySent.has(e.id));
|
|
6729
|
+
}
|
|
6730
|
+
//#endregion
|
|
6731
|
+
//#region src/notification-center/snooze-store.ts
|
|
6732
|
+
/**
|
|
6733
|
+
* NcSnoozeStore — the durable half of snooze.
|
|
6734
|
+
*
|
|
6735
|
+
* Two collections, and the split matters:
|
|
6736
|
+
*
|
|
6737
|
+
* - **windows** (`notification-center:snoozes`) — small, mirrored into RAM,
|
|
6738
|
+
* read on the dispatcher's hot path with zero I/O. Refreshed on the drain
|
|
6739
|
+
* tick, so a window opened on the hub takes effect on the evaluating node
|
|
6740
|
+
* within one tick (D8: reconcile, never trust an event to arrive).
|
|
6741
|
+
* - **suppression counters** (`notification-center:snooze-suppressed`) — one
|
|
6742
|
+
* row per (window, target, rule, camera), holding a COUNT and the first/last
|
|
6743
|
+
* instant. Not one row per hidden notification: a snooze exists because
|
|
6744
|
+
* something is noisy, so the row-per-event version of this table is unbounded
|
|
6745
|
+
* exactly when it is used most. The digest reads "42 events, 14:03–17:58",
|
|
6746
|
+
* which is the aggregate anyway.
|
|
6747
|
+
*
|
|
6748
|
+
* Expiry is never a job here either. Nothing in this file ends a window; the
|
|
6749
|
+
* clock does, and {@link NcSnoozeStore.prune} only reclaims rows long after the
|
|
6750
|
+
* fact. A prune that never runs delays a digest and wastes a few KB — it can
|
|
6751
|
+
* never leave the operator silenced.
|
|
6752
|
+
*/
|
|
6753
|
+
var NC_SNOOZES_COLLECTION = "notification-center:snoozes";
|
|
6754
|
+
var NC_SNOOZE_SUPPRESSED_COLLECTION = "notification-center:snooze-suppressed";
|
|
6755
|
+
var NC_SNOOZES_COLUMNS = [
|
|
6756
|
+
{
|
|
6757
|
+
name: "id",
|
|
6758
|
+
type: "TEXT",
|
|
6759
|
+
primaryKey: true,
|
|
6760
|
+
notNull: true
|
|
6761
|
+
},
|
|
6762
|
+
{
|
|
6763
|
+
name: "userId",
|
|
6764
|
+
type: "TEXT",
|
|
6765
|
+
notNull: true
|
|
6766
|
+
},
|
|
6767
|
+
{
|
|
6768
|
+
name: "expiresAt",
|
|
6769
|
+
type: "INTEGER",
|
|
6770
|
+
notNull: true
|
|
6771
|
+
},
|
|
6772
|
+
(
|
|
6773
|
+
/** The FULL window (Zod-validated on read); the scalars are projections. */
|
|
6774
|
+
{
|
|
6775
|
+
name: "snooze",
|
|
6776
|
+
type: "JSON",
|
|
6777
|
+
notNull: true
|
|
6778
|
+
})
|
|
6779
|
+
];
|
|
6780
|
+
var NC_SNOOZES_INDEXES = [{
|
|
6781
|
+
name: "idx_nc_snoozes_expires",
|
|
6782
|
+
columns: ["expiresAt"]
|
|
6783
|
+
}];
|
|
6784
|
+
var NC_SNOOZE_SUPPRESSED_COLUMNS = [
|
|
6785
|
+
{
|
|
6786
|
+
name: "id",
|
|
6787
|
+
type: "TEXT",
|
|
6788
|
+
primaryKey: true,
|
|
6789
|
+
notNull: true
|
|
6790
|
+
},
|
|
6791
|
+
{
|
|
6792
|
+
name: "snoozeId",
|
|
6793
|
+
type: "TEXT",
|
|
6794
|
+
notNull: true
|
|
6795
|
+
},
|
|
6796
|
+
{
|
|
6797
|
+
name: "targetId",
|
|
6798
|
+
type: "TEXT",
|
|
6799
|
+
notNull: true
|
|
6800
|
+
},
|
|
6801
|
+
{
|
|
6802
|
+
name: "ruleId",
|
|
6803
|
+
type: "TEXT",
|
|
6804
|
+
notNull: true
|
|
6805
|
+
},
|
|
6806
|
+
{
|
|
6807
|
+
name: "ruleName",
|
|
6808
|
+
type: "TEXT",
|
|
6809
|
+
notNull: true
|
|
6810
|
+
},
|
|
6811
|
+
{
|
|
6812
|
+
name: "deviceId",
|
|
6813
|
+
type: "INTEGER",
|
|
6814
|
+
notNull: true
|
|
6815
|
+
},
|
|
6816
|
+
{
|
|
6817
|
+
name: "count",
|
|
6818
|
+
type: "INTEGER",
|
|
6819
|
+
notNull: true
|
|
6820
|
+
},
|
|
6821
|
+
{
|
|
6822
|
+
name: "firstAt",
|
|
6823
|
+
type: "INTEGER",
|
|
6824
|
+
notNull: true
|
|
6825
|
+
},
|
|
6826
|
+
{
|
|
6827
|
+
name: "lastAt",
|
|
6828
|
+
type: "INTEGER",
|
|
6829
|
+
notNull: true
|
|
6830
|
+
}
|
|
6831
|
+
];
|
|
6832
|
+
var NC_SNOOZE_SUPPRESSED_INDEXES = [{
|
|
6833
|
+
name: "idx_nc_snooze_suppressed_snooze",
|
|
6834
|
+
columns: ["snoozeId"]
|
|
6835
|
+
}];
|
|
6836
|
+
/** How long a closed window's rows survive, for a retried digest and for
|
|
6837
|
+
* "what did I miss" to still answer after a restart. */
|
|
6838
|
+
var PRUNE_HORIZON_MS = 6 * 36e5;
|
|
6839
|
+
/** Counter key — one row per (window, target, rule, camera). */
|
|
6840
|
+
function counterKey(s) {
|
|
6841
|
+
return `${s.snoozeId}|${s.targetId}|${s.ruleId}|${s.deviceId}`;
|
|
6842
|
+
}
|
|
6843
|
+
var NcSnoozeStore = class {
|
|
6844
|
+
byId = /* @__PURE__ */ new Map();
|
|
6845
|
+
store;
|
|
6846
|
+
logger;
|
|
6847
|
+
now;
|
|
6848
|
+
newId;
|
|
6849
|
+
constructor(deps) {
|
|
6850
|
+
this.store = deps.store;
|
|
6851
|
+
this.logger = deps.logger;
|
|
6852
|
+
this.now = deps.now ?? (() => Date.now());
|
|
6853
|
+
this.newId = deps.newId ?? (() => (0, node_crypto.randomUUID)());
|
|
6854
|
+
}
|
|
6855
|
+
static async declare(store) {
|
|
6856
|
+
await store.declareCollection.mutate({
|
|
6857
|
+
collection: NC_SNOOZES_COLLECTION,
|
|
6858
|
+
columns: [...NC_SNOOZES_COLUMNS],
|
|
6859
|
+
indexes: [...NC_SNOOZES_INDEXES]
|
|
6860
|
+
});
|
|
6861
|
+
await store.declareCollection.mutate({
|
|
6862
|
+
collection: NC_SNOOZE_SUPPRESSED_COLLECTION,
|
|
6863
|
+
columns: [...NC_SNOOZE_SUPPRESSED_COLUMNS],
|
|
6864
|
+
indexes: [...NC_SNOOZE_SUPPRESSED_INDEXES]
|
|
6865
|
+
});
|
|
6866
|
+
}
|
|
6867
|
+
/**
|
|
6868
|
+
* (Re)hydrate every window. Called at boot and on the drain tick — the table
|
|
6869
|
+
* holds active windows and recently closed ones, so it stays small enough to
|
|
6870
|
+
* read whole. A row whose JSON no longer validates is SKIPPED: a malformed
|
|
6871
|
+
* window must never be able to silence anything.
|
|
6872
|
+
*/
|
|
6873
|
+
async load() {
|
|
6874
|
+
try {
|
|
6875
|
+
const rows = await this.store.query.query({
|
|
6876
|
+
collection: NC_SNOOZES_COLLECTION,
|
|
6877
|
+
filter: { limit: 1e3 }
|
|
6878
|
+
});
|
|
6879
|
+
this.byId.clear();
|
|
6880
|
+
let skipped = 0;
|
|
6881
|
+
for (const row of rows) {
|
|
6882
|
+
const parsed = require_dist.NcSnoozeSchema.safeParse(row.data["snooze"]);
|
|
6883
|
+
if (!parsed.success) {
|
|
6884
|
+
skipped += 1;
|
|
6885
|
+
continue;
|
|
6886
|
+
}
|
|
6887
|
+
this.byId.set(parsed.data.id, parsed.data);
|
|
6888
|
+
}
|
|
6889
|
+
if (skipped > 0) this.logger.warn("snooze rows skipped as malformed — they silence NOTHING", { meta: { skipped } });
|
|
6890
|
+
} catch (err) {
|
|
6891
|
+
this.logger.warn("snooze load failed — keeping the windows already in memory", { meta: { error: String(err) } });
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
/** Every known window, newest first. */
|
|
6895
|
+
list() {
|
|
6896
|
+
return [...this.byId.values()].toSorted((a, b) => b.startedAt - a.startedAt);
|
|
6897
|
+
}
|
|
6898
|
+
/** The windows a caller may SEE: their own, plus the global ones that
|
|
6899
|
+
* silence them. Never another person's private silence. */
|
|
6900
|
+
listVisibleTo(userId, isAdmin) {
|
|
6901
|
+
if (isAdmin) return this.list();
|
|
6902
|
+
return this.list().filter((s) => s.userId === userId || s.global);
|
|
6903
|
+
}
|
|
6904
|
+
get(snoozeId) {
|
|
6905
|
+
return this.byId.get(snoozeId) ?? null;
|
|
6906
|
+
}
|
|
6907
|
+
/** Hot-path read — the window silencing this delivery, or null. */
|
|
6908
|
+
activeFor(query) {
|
|
6909
|
+
return activeSnoozeFor([...this.byId.values()], query);
|
|
6910
|
+
}
|
|
6911
|
+
/**
|
|
6912
|
+
* Open a window. Permission for `global` is the CALLER's problem (see
|
|
6913
|
+
* `canSetGlobal`) — this store persists what it is given, exactly like the
|
|
6914
|
+
* rule store leaves ownership to the bridge action.
|
|
6915
|
+
*/
|
|
6916
|
+
async create(input) {
|
|
6917
|
+
const startedAt = this.now();
|
|
6918
|
+
const snooze = require_dist.NcSnoozeSchema.parse({
|
|
6919
|
+
id: this.newId(),
|
|
6920
|
+
userId: input.userId,
|
|
6921
|
+
scope: input.scope,
|
|
6922
|
+
...input.ruleId !== void 0 ? { ruleId: input.ruleId } : {},
|
|
6923
|
+
...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
|
|
6924
|
+
startedAt,
|
|
6925
|
+
expiresAt: startedAt + input.durationMinutes * 6e4,
|
|
6926
|
+
global: input.global,
|
|
6927
|
+
summary: input.summary
|
|
6928
|
+
});
|
|
6929
|
+
await this.persist(snooze);
|
|
6930
|
+
this.byId.set(snooze.id, snooze);
|
|
6931
|
+
return snooze;
|
|
6932
|
+
}
|
|
6933
|
+
/**
|
|
6934
|
+
* End a window NOW by moving its expiry to this instant — never by deleting
|
|
6935
|
+
* it. The digest of what it hid is still owed to whoever opened it, and a
|
|
6936
|
+
* deleted row owes nothing.
|
|
6937
|
+
*/
|
|
6938
|
+
async cancel(snoozeId) {
|
|
6939
|
+
const existing = this.byId.get(snoozeId);
|
|
6940
|
+
if (existing === void 0) return null;
|
|
6941
|
+
const now = this.now();
|
|
6942
|
+
if (existing.expiresAt <= now) return existing;
|
|
6943
|
+
const ended = {
|
|
6944
|
+
...existing,
|
|
6945
|
+
expiresAt: now
|
|
6946
|
+
};
|
|
6947
|
+
await this.persist(ended);
|
|
6948
|
+
this.byId.set(ended.id, ended);
|
|
6949
|
+
return ended;
|
|
6950
|
+
}
|
|
6951
|
+
/**
|
|
6952
|
+
* Record that a window hid one notification. Read-modify-write on the
|
|
6953
|
+
* counter row; best-effort, because losing a line of a SUMMARY must never
|
|
6954
|
+
* cost the suppression decision that produced it.
|
|
6955
|
+
*/
|
|
6956
|
+
async recordSuppressed(s) {
|
|
6957
|
+
const key = counterKey(s);
|
|
6958
|
+
try {
|
|
6959
|
+
const existing = await this.readCounter(key);
|
|
6960
|
+
const next = {
|
|
6961
|
+
snoozeId: s.snoozeId,
|
|
6962
|
+
targetId: s.targetId,
|
|
6963
|
+
ruleId: s.ruleId,
|
|
6964
|
+
ruleName: s.ruleName,
|
|
6965
|
+
deviceId: s.deviceId,
|
|
6966
|
+
count: (existing?.count ?? 0) + 1,
|
|
6967
|
+
firstAt: existing?.firstAt ?? s.at,
|
|
6968
|
+
lastAt: s.at
|
|
6969
|
+
};
|
|
6970
|
+
await this.store.set.mutate({
|
|
6971
|
+
collection: NC_SNOOZE_SUPPRESSED_COLLECTION,
|
|
6972
|
+
key,
|
|
6973
|
+
value: { ...next }
|
|
6974
|
+
});
|
|
6975
|
+
} catch (err) {
|
|
6976
|
+
this.logger.warn("snooze suppression counter write failed (digest will under-report)", {
|
|
6977
|
+
tags: { deviceId: s.deviceId },
|
|
6978
|
+
meta: {
|
|
6979
|
+
snoozeId: s.snoozeId,
|
|
6980
|
+
ruleId: s.ruleId,
|
|
6981
|
+
error: String(err)
|
|
6982
|
+
}
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
/** Windows that have closed and still owe a digest. */
|
|
6987
|
+
dueDigests(nowMs) {
|
|
6988
|
+
const sent = new Set([...this.byId.values()].filter((s) => s.digestSentAt !== void 0).map((s) => s.id));
|
|
6989
|
+
return dueSummaries([...this.byId.values()], nowMs, sent);
|
|
6990
|
+
}
|
|
6991
|
+
/** What one window hid, grouped per target. */
|
|
6992
|
+
async suppressedFor(snoozeId) {
|
|
6993
|
+
try {
|
|
6994
|
+
const rows = await this.store.query.query({
|
|
6995
|
+
collection: NC_SNOOZE_SUPPRESSED_COLLECTION,
|
|
6996
|
+
filter: {
|
|
6997
|
+
where: { snoozeId },
|
|
6998
|
+
limit: 1e3
|
|
6999
|
+
}
|
|
7000
|
+
});
|
|
7001
|
+
const out = [];
|
|
7002
|
+
for (const row of rows) {
|
|
7003
|
+
const parsed = require_dist.NcSnoozeSuppressedSchema.safeParse(row.data);
|
|
7004
|
+
if (parsed.success) out.push(parsed.data);
|
|
7005
|
+
}
|
|
7006
|
+
return out;
|
|
7007
|
+
} catch (err) {
|
|
7008
|
+
this.logger.warn("snooze suppression read failed", { meta: {
|
|
7009
|
+
snoozeId,
|
|
7010
|
+
error: String(err)
|
|
7011
|
+
} });
|
|
7012
|
+
return [];
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
/**
|
|
7016
|
+
* Mark a digest as handled.
|
|
7017
|
+
*
|
|
7018
|
+
* Called after the ATTEMPT, not after a success: a digest that cannot be
|
|
7019
|
+
* delivered is not worth retrying every two seconds for six hours, and a
|
|
7020
|
+
* partially-sent one must not be re-sent to the targets that already got it.
|
|
7021
|
+
* The failure is logged with the window id — a missing digest is then a
|
|
7022
|
+
* findable line, not a silence inside a silence.
|
|
7023
|
+
*/
|
|
7024
|
+
async markDigestSent(snoozeId) {
|
|
7025
|
+
const existing = this.byId.get(snoozeId);
|
|
7026
|
+
if (existing === void 0) return;
|
|
7027
|
+
const marked = {
|
|
7028
|
+
...existing,
|
|
7029
|
+
digestSentAt: this.now()
|
|
7030
|
+
};
|
|
7031
|
+
await this.persist(marked);
|
|
7032
|
+
this.byId.set(marked.id, marked);
|
|
7033
|
+
}
|
|
7034
|
+
/**
|
|
7035
|
+
* Drop windows that closed more than {@link PRUNE_HORIZON_MS} ago, and their
|
|
7036
|
+
* counters. Only ever touches CLOSED windows — an active one cannot be
|
|
7037
|
+
* pruned into oblivion by a clock argument.
|
|
7038
|
+
*/
|
|
7039
|
+
async prune(nowMs) {
|
|
7040
|
+
const horizon = nowMs - PRUNE_HORIZON_MS;
|
|
7041
|
+
const stale = [...this.byId.values()].filter((s) => s.expiresAt < horizon);
|
|
7042
|
+
let dropped = 0;
|
|
7043
|
+
for (const s of stale) try {
|
|
7044
|
+
await this.store.deleteWhere.mutate({
|
|
7045
|
+
collection: NC_SNOOZE_SUPPRESSED_COLLECTION,
|
|
7046
|
+
filter: { where: { snoozeId: s.id } }
|
|
7047
|
+
});
|
|
7048
|
+
await this.store.delete.mutate({
|
|
7049
|
+
collection: NC_SNOOZES_COLLECTION,
|
|
7050
|
+
key: s.id
|
|
7051
|
+
});
|
|
7052
|
+
this.byId.delete(s.id);
|
|
7053
|
+
dropped += 1;
|
|
7054
|
+
} catch (err) {
|
|
7055
|
+
this.logger.warn("snooze prune failed", { meta: {
|
|
7056
|
+
snoozeId: s.id,
|
|
7057
|
+
error: String(err)
|
|
7058
|
+
} });
|
|
7059
|
+
}
|
|
7060
|
+
return dropped;
|
|
7061
|
+
}
|
|
7062
|
+
/** `get` returns the stored VALUE, not a `{ id, data }` row. */
|
|
7063
|
+
async readCounter(key) {
|
|
7064
|
+
const value = await this.store.get.query({
|
|
7065
|
+
collection: NC_SNOOZE_SUPPRESSED_COLLECTION,
|
|
7066
|
+
key
|
|
7067
|
+
});
|
|
7068
|
+
const parsed = require_dist.NcSnoozeSuppressedSchema.safeParse(value);
|
|
7069
|
+
return parsed.success ? parsed.data : null;
|
|
7070
|
+
}
|
|
7071
|
+
async persist(snooze) {
|
|
7072
|
+
await this.store.set.mutate({
|
|
7073
|
+
collection: NC_SNOOZES_COLLECTION,
|
|
7074
|
+
key: snooze.id,
|
|
7075
|
+
value: {
|
|
7076
|
+
userId: snooze.userId,
|
|
7077
|
+
expiresAt: snooze.expiresAt,
|
|
7078
|
+
snooze
|
|
7079
|
+
}
|
|
7080
|
+
});
|
|
7081
|
+
}
|
|
7082
|
+
};
|
|
7083
|
+
//#endregion
|
|
6344
7084
|
//#region src/notification-center/timelapse/timelapse-store.ts
|
|
6345
7085
|
var NC_TIMELAPSE_RULES_COLLECTION = "notification-center:timelapse-rules";
|
|
6346
7086
|
var NC_TIMELAPSE_RULES_COLUMNS = [
|
|
@@ -6702,6 +7442,9 @@ var NotificationCenter = class NotificationCenter {
|
|
|
6702
7442
|
* snapshots; watched keys are recomputed from the enabled occupancy rules. */
|
|
6703
7443
|
occupancyWatcher = new OccupancyWatcher();
|
|
6704
7444
|
occupancyStore;
|
|
7445
|
+
/** Snooze windows + what they hid. Mirrored into RAM; the dispatcher's gate
|
|
7446
|
+
* reads it with no I/O, the drain tick refreshes it. */
|
|
7447
|
+
snoozes;
|
|
6705
7448
|
/** True when ≥1 enabled `device-event` rule declares an occupancy condition —
|
|
6706
7449
|
* the watcher is idle (zero per-frame cost) otherwise. */
|
|
6707
7450
|
occupancyEnabled = false;
|
|
@@ -6734,10 +7477,17 @@ var NotificationCenter = class NotificationCenter {
|
|
|
6734
7477
|
store: deps.store,
|
|
6735
7478
|
logger: this.logger.child("occupancy")
|
|
6736
7479
|
});
|
|
7480
|
+
this.snoozes = new NcSnoozeStore({
|
|
7481
|
+
store: deps.store,
|
|
7482
|
+
logger: this.logger.child("snooze"),
|
|
7483
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
7484
|
+
});
|
|
6737
7485
|
this.dispatcher = new NcDispatcher({
|
|
6738
7486
|
...deps.dispatcher,
|
|
6739
7487
|
logger: this.logger.child("dispatch"),
|
|
6740
|
-
|
|
7488
|
+
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
7489
|
+
isRuleTargetDisabled: (ruleId, targetId) => this.rules.isRuleTargetDisabled(ruleId, targetId),
|
|
7490
|
+
snoozeGate: (input) => this.snoozeGate(input)
|
|
6741
7491
|
});
|
|
6742
7492
|
this.outbox = new NcOutbox({
|
|
6743
7493
|
store: deps.store,
|
|
@@ -6767,6 +7517,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
6767
7517
|
await NcRuleStore.declare(store);
|
|
6768
7518
|
await NcOutbox.declare(store);
|
|
6769
7519
|
await OccupancyStore.declare(store);
|
|
7520
|
+
await NcSnoozeStore.declare(store);
|
|
6770
7521
|
await TimelapseStore.declare(store);
|
|
6771
7522
|
}
|
|
6772
7523
|
/**
|
|
@@ -6777,6 +7528,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
6777
7528
|
*/
|
|
6778
7529
|
async start(opts) {
|
|
6779
7530
|
await this.rules.load();
|
|
7531
|
+
await this.snoozes.load();
|
|
6780
7532
|
this.refreshOccupancyWatch();
|
|
6781
7533
|
if (!opts.evaluation) return;
|
|
6782
7534
|
this.evaluationActive = true;
|
|
@@ -7005,6 +7757,12 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7005
7757
|
this.logger.debug("getHistory: skipping malformed outbox row", { meta: { id: entry.id } });
|
|
7006
7758
|
}
|
|
7007
7759
|
return { entries: mapped };
|
|
7760
|
+
},
|
|
7761
|
+
listSnoozes: async ({ caller }) => ({ snoozes: [...this.snoozes.listVisibleTo(caller.userId, caller.isAdmin)] }),
|
|
7762
|
+
createSnooze: async ({ snooze, caller }) => ({ snooze: await this.createSnooze(snooze, caller) }),
|
|
7763
|
+
cancelSnooze: async ({ snoozeId, caller }) => {
|
|
7764
|
+
await this.cancelSnooze(snoozeId, caller);
|
|
7765
|
+
return { success: true };
|
|
7008
7766
|
}
|
|
7009
7767
|
};
|
|
7010
7768
|
}
|
|
@@ -7135,6 +7893,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7135
7893
|
...rule.media.gif === true ? { mediaGif: true } : {},
|
|
7136
7894
|
...rule.media.clip === true ? { mediaClip: true } : {},
|
|
7137
7895
|
...rule.media.clipPreRollSec !== void 0 ? { mediaClipPreRollSec: rule.media.clipPreRollSec } : {},
|
|
7896
|
+
...rule.media?.clipSpeed !== void 0 ? { mediaClipSpeed: rule.media.clipSpeed } : {},
|
|
7138
7897
|
...rule.media.clipPostRollSec !== void 0 ? { mediaClipPostRollSec: rule.media.clipPostRollSec } : {},
|
|
7139
7898
|
...matchedOn !== void 0 && matchedOn.length > 0 ? { matchedOn } : {},
|
|
7140
7899
|
...target.params !== void 0 ? { params: target.params } : {},
|
|
@@ -7327,9 +8086,127 @@ var NotificationCenter = class NotificationCenter {
|
|
|
7327
8086
|
await this.outbox.setWatermark(now);
|
|
7328
8087
|
}
|
|
7329
8088
|
async drainTick() {
|
|
8089
|
+
await this.snoozes.load();
|
|
7330
8090
|
await this.outbox.drainOnce();
|
|
8091
|
+
await this.deliverDueDigests();
|
|
7331
8092
|
this.drainTicks += 1;
|
|
7332
|
-
if (this.drainTicks % WATERMARK_EVERY_TICKS === 0)
|
|
8093
|
+
if (this.drainTicks % WATERMARK_EVERY_TICKS === 0) {
|
|
8094
|
+
await this.outbox.setWatermark(this.now());
|
|
8095
|
+
await this.snoozes.prune(this.now());
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
/**
|
|
8099
|
+
* The dispatcher's gate: is this delivery silenced, and if so, count it.
|
|
8100
|
+
*
|
|
8101
|
+
* Returns the window's id (the dispatcher logs it) or null. Recording is
|
|
8102
|
+
* awaited rather than fired off so a suppression cannot be lost to a shutdown
|
|
8103
|
+
* between the decision and the row — an under-reported digest is the one
|
|
8104
|
+
* failure of this feature the operator cannot detect.
|
|
8105
|
+
*/
|
|
8106
|
+
async snoozeGate(input) {
|
|
8107
|
+
const active = this.snoozes.activeFor({
|
|
8108
|
+
nowMs: input.at,
|
|
8109
|
+
...input.ownerUserId !== void 0 ? { userId: input.ownerUserId } : {},
|
|
8110
|
+
ruleId: input.ruleId,
|
|
8111
|
+
deviceId: input.deviceId
|
|
8112
|
+
});
|
|
8113
|
+
if (active === null) return null;
|
|
8114
|
+
if (active.summary) await this.snoozes.recordSuppressed({
|
|
8115
|
+
snoozeId: active.id,
|
|
8116
|
+
targetId: input.targetId,
|
|
8117
|
+
ruleId: input.ruleId,
|
|
8118
|
+
ruleName: input.ruleName,
|
|
8119
|
+
deviceId: input.deviceId,
|
|
8120
|
+
at: input.at
|
|
8121
|
+
});
|
|
8122
|
+
return active.id;
|
|
8123
|
+
}
|
|
8124
|
+
/**
|
|
8125
|
+
* Send the summary owed by every window that has closed since the last tick.
|
|
8126
|
+
*
|
|
8127
|
+
* The digest goes exactly where the silenced notifications would have gone —
|
|
8128
|
+
* one per target that was actually suppressed — so it needs no routing of its
|
|
8129
|
+
* own and can never reach someone the snooze never touched.
|
|
8130
|
+
*/
|
|
8131
|
+
async deliverDueDigests() {
|
|
8132
|
+
const due = this.snoozes.dueDigests(this.now());
|
|
8133
|
+
for (const snooze of due) try {
|
|
8134
|
+
const rows = await this.snoozes.suppressedFor(snooze.id);
|
|
8135
|
+
const names = await this.dispatcher.resolveDeviceNames(rows.map((r) => r.deviceId));
|
|
8136
|
+
for (const targetId of digestTargets(rows)) {
|
|
8137
|
+
const digest = buildSnoozeDigest(snooze, rows.filter((r) => r.targetId === targetId), names);
|
|
8138
|
+
if (digest === null) continue;
|
|
8139
|
+
await this.dispatcher.deliverDigest({
|
|
8140
|
+
snoozeId: snooze.id,
|
|
8141
|
+
targetId,
|
|
8142
|
+
title: digest.title,
|
|
8143
|
+
body: digest.body,
|
|
8144
|
+
total: digest.total
|
|
8145
|
+
});
|
|
8146
|
+
}
|
|
8147
|
+
await this.snoozes.markDigestSent(snooze.id);
|
|
8148
|
+
} catch (err) {
|
|
8149
|
+
this.logger.warn("snooze digest tick failed", { meta: {
|
|
8150
|
+
snoozeId: snooze.id,
|
|
8151
|
+
error: String(err)
|
|
8152
|
+
} });
|
|
8153
|
+
}
|
|
8154
|
+
}
|
|
8155
|
+
/** The durable snooze store — the cap provider and the `nc.*` bridge serve
|
|
8156
|
+
* CRUD from it, the same write-through cache the gate reads. */
|
|
8157
|
+
get snoozeStore() {
|
|
8158
|
+
return this.snoozes;
|
|
8159
|
+
}
|
|
8160
|
+
/**
|
|
8161
|
+
* Open a window on behalf of a caller, with the permission check that makes
|
|
8162
|
+
* `global` safe. Lives here rather than in the store because it needs the
|
|
8163
|
+
* RULE (its `snoozeAllowGlobal` opt-in), and a permission decision that can
|
|
8164
|
+
* silence other people belongs where both halves are visible.
|
|
8165
|
+
*/
|
|
8166
|
+
async createSnooze(input, caller) {
|
|
8167
|
+
if (input.scope === "rule" && input.ruleId === void 0) throw new Error("a rule snooze needs a ruleId");
|
|
8168
|
+
if (input.scope === "device" && input.deviceId === void 0) throw new Error("a camera snooze needs a deviceId");
|
|
8169
|
+
const wantsGlobal = input.global === true;
|
|
8170
|
+
if (wantsGlobal) {
|
|
8171
|
+
const rule = input.ruleId !== void 0 ? this.rules.get(input.ruleId) : null;
|
|
8172
|
+
if (!canSetGlobal({
|
|
8173
|
+
scope: input.scope,
|
|
8174
|
+
...rule !== null ? { ruleAllowsGlobal: rule.snoozeAllowGlobal === true } : {},
|
|
8175
|
+
isAdmin: caller.isAdmin
|
|
8176
|
+
})) throw new Error("forbidden: this snooze may not be made global");
|
|
8177
|
+
}
|
|
8178
|
+
const snooze = await this.snoozes.create({
|
|
8179
|
+
userId: caller.userId,
|
|
8180
|
+
scope: input.scope,
|
|
8181
|
+
...input.ruleId !== void 0 ? { ruleId: input.ruleId } : {},
|
|
8182
|
+
...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
|
|
8183
|
+
durationMinutes: input.durationMinutes,
|
|
8184
|
+
global: wantsGlobal,
|
|
8185
|
+
summary: input.summary !== false
|
|
8186
|
+
});
|
|
8187
|
+
this.logger.info("snooze opened", {
|
|
8188
|
+
...snooze.deviceId !== void 0 ? { tags: { deviceId: snooze.deviceId } } : {},
|
|
8189
|
+
meta: {
|
|
8190
|
+
snoozeId: snooze.id,
|
|
8191
|
+
scope: snooze.scope,
|
|
8192
|
+
by: caller.userId,
|
|
8193
|
+
global: snooze.global,
|
|
8194
|
+
minutes: input.durationMinutes,
|
|
8195
|
+
...snooze.ruleId !== void 0 ? { ruleId: snooze.ruleId } : {}
|
|
8196
|
+
}
|
|
8197
|
+
});
|
|
8198
|
+
return snooze;
|
|
8199
|
+
}
|
|
8200
|
+
/** End a window early. A user may end their own; an admin may end any. */
|
|
8201
|
+
async cancelSnooze(snoozeId, caller) {
|
|
8202
|
+
const existing = this.snoozes.get(snoozeId);
|
|
8203
|
+
if (existing === null) throw new Error(`snooze not found: ${snoozeId}`);
|
|
8204
|
+
if (!caller.isAdmin && existing.userId !== caller.userId) throw new Error(`forbidden: snooze not owned: ${snoozeId}`);
|
|
8205
|
+
await this.snoozes.cancel(snoozeId);
|
|
8206
|
+
this.logger.info("snooze cancelled", { meta: {
|
|
8207
|
+
snoozeId,
|
|
8208
|
+
by: caller.userId
|
|
8209
|
+
} });
|
|
7333
8210
|
}
|
|
7334
8211
|
/** The "at least one addressee" invariant lives here, not in Zod: the
|
|
7335
8212
|
* schema allows empty `targets` (a users-only rule) and a cross-field
|
|
@@ -7446,6 +8323,21 @@ var ncActions = require_dist.defineCustomActions({
|
|
|
7446
8323
|
}), require_dist.object({ success: require_dist.literal(true) }), {
|
|
7447
8324
|
kind: "mutation",
|
|
7448
8325
|
caller: "required"
|
|
8326
|
+
}),
|
|
8327
|
+
/**
|
|
8328
|
+
* Snooze, from the phone — the surface that matters most for this feature,
|
|
8329
|
+
* because "silence this until morning" is a thing you press in bed, not at a
|
|
8330
|
+
* desk. Scoped to the caller: their own windows plus the global ones that
|
|
8331
|
+
* silence them.
|
|
8332
|
+
*/
|
|
8333
|
+
"nc.listSnoozes": require_dist.customAction(require_dist.object({}), require_dist.object({ snoozes: require_dist.array(require_dist.NcSnoozeSchema) }), { caller: "required" }),
|
|
8334
|
+
"nc.createSnooze": require_dist.customAction(require_dist.object({ snooze: require_dist.NcSnoozeInputSchema }), require_dist.object({ snooze: require_dist.NcSnoozeSchema }), {
|
|
8335
|
+
kind: "mutation",
|
|
8336
|
+
caller: "required"
|
|
8337
|
+
}),
|
|
8338
|
+
"nc.cancelSnooze": require_dist.customAction(require_dist.object({ snoozeId: require_dist.string() }), require_dist.object({ success: require_dist.literal(true) }), {
|
|
8339
|
+
kind: "mutation",
|
|
8340
|
+
caller: "required"
|
|
7449
8341
|
})
|
|
7450
8342
|
});
|
|
7451
8343
|
/** Fail-closed caller resolution — an absent forwarded caller is NEVER admin. */
|
|
@@ -7538,6 +8430,28 @@ function makeNcActionHandlers(deps) {
|
|
|
7538
8430
|
} });
|
|
7539
8431
|
return { success: true };
|
|
7540
8432
|
},
|
|
8433
|
+
"nc.listSnoozes": async (_input, caller) => {
|
|
8434
|
+
const c = requireCaller(caller);
|
|
8435
|
+
return { snoozes: [...deps.snooze.list(c.userId, c.isAdmin)] };
|
|
8436
|
+
},
|
|
8437
|
+
"nc.createSnooze": async (input, caller) => {
|
|
8438
|
+
const c = requireCaller(caller);
|
|
8439
|
+
const parsed = require_dist.NcSnoozeInputSchema.parse(input.snooze);
|
|
8440
|
+
if (parsed.ruleId !== void 0) assertRuleVisible(parsed.ruleId, c);
|
|
8441
|
+
const snooze = await deps.snooze.create(parsed, c);
|
|
8442
|
+
deps.logger.info("nc snooze opened", { meta: {
|
|
8443
|
+
snoozeId: snooze.id,
|
|
8444
|
+
scope: snooze.scope,
|
|
8445
|
+
by: c.userId,
|
|
8446
|
+
global: snooze.global
|
|
8447
|
+
} });
|
|
8448
|
+
return { snooze };
|
|
8449
|
+
},
|
|
8450
|
+
"nc.cancelSnooze": async (input, caller) => {
|
|
8451
|
+
const c = requireCaller(caller);
|
|
8452
|
+
await deps.snooze.cancel(input.snoozeId, c);
|
|
8453
|
+
return { success: true };
|
|
8454
|
+
},
|
|
7541
8455
|
"nc.setRuleTargetEnabled": async (input, caller) => {
|
|
7542
8456
|
const c = requireCaller(caller);
|
|
7543
8457
|
assertRuleVisible(input.ruleId, c);
|
|
@@ -11866,6 +12780,37 @@ var OpsLogStore = class {
|
|
|
11866
12780
|
}
|
|
11867
12781
|
return out;
|
|
11868
12782
|
}
|
|
12783
|
+
/**
|
|
12784
|
+
* Delete audit entries older than `cutoffMs`. Returns how many went.
|
|
12785
|
+
*
|
|
12786
|
+
* The audit log is the one collection that records everyone else's
|
|
12787
|
+
* retention and had none of its own — it grows monotonically for the life of
|
|
12788
|
+
* the node (11,519 rows on the operator's hub before this existed).
|
|
12789
|
+
*
|
|
12790
|
+
* ONE statement, not a select-then-delete drain: `deleteWhere` is exactly
|
|
12791
|
+
* the bulk path that removed the N+1 loops from every other retention path.
|
|
12792
|
+
* `whereBetween` on `at` is inclusive, so `[0, cutoffMs - 1]` selects
|
|
12793
|
+
* strictly-older rows — the same boundary `EventStore.pruneBefore` uses.
|
|
12794
|
+
*
|
|
12795
|
+
* BEST-EFFORT, like `append`: a failed audit trim must never fail the
|
|
12796
|
+
* retention sweep that called it. It warns rather than throwing, because a
|
|
12797
|
+
* trim that silently does nothing is how a table gets to 11,519 rows.
|
|
12798
|
+
*/
|
|
12799
|
+
async pruneBefore(cutoffMs) {
|
|
12800
|
+
try {
|
|
12801
|
+
const { deleted } = await this.store.deleteWhere.mutate({
|
|
12802
|
+
collection: OPS_LOG_COLLECTION,
|
|
12803
|
+
filter: { whereBetween: { at: [0, cutoffMs - 1] } }
|
|
12804
|
+
});
|
|
12805
|
+
return deleted;
|
|
12806
|
+
} catch (err) {
|
|
12807
|
+
this.logger.warn("ops-log: prune failed — the audit log will keep growing", { meta: {
|
|
12808
|
+
cutoffMs,
|
|
12809
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12810
|
+
} });
|
|
12811
|
+
return 0;
|
|
12812
|
+
}
|
|
12813
|
+
}
|
|
11869
12814
|
};
|
|
11870
12815
|
/** SQLite stores nullable columns as `null`; the row schema uses `.nullable()`
|
|
11871
12816
|
* for deviceId/detail (accepts null) but drop any stray null on required
|
|
@@ -20341,6 +21286,12 @@ var NcArtifactSecretSchema = require_dist.string();
|
|
|
20341
21286
|
var EmbeddingEnabledSchema = require_dist.boolean();
|
|
20342
21287
|
var SILENCE_FLOOR_DBFS = -55;
|
|
20343
21288
|
var RETENTION_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
21289
|
+
/**
|
|
21290
|
+
* How long the events ops-log is kept. 30 days: long enough to answer "what
|
|
21291
|
+
* deleted my footage last month", short enough that the audit never becomes
|
|
21292
|
+
* the largest thing the node stores.
|
|
21293
|
+
*/
|
|
21294
|
+
var OPS_LOG_RETENTION_MS = 720 * 60 * 6e4;
|
|
20344
21295
|
/** Throttle for the "overlay synthesis failed" warn — one line / minute / device. */
|
|
20345
21296
|
var OVERLAY_SYNTHESIS_WARN_THROTTLE_MS = 6e4;
|
|
20346
21297
|
var AUDIO_EVENT_HEARTBEAT_MS = 5e3;
|
|
@@ -20854,6 +21805,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
20854
21805
|
return makeNcActionHandlers({
|
|
20855
21806
|
ruleStore: center.ruleStore,
|
|
20856
21807
|
logger: this.ctx.logger.child("nc-actions"),
|
|
21808
|
+
snooze: {
|
|
21809
|
+
list: (userId, isAdmin) => center.snoozeStore.listVisibleTo(userId, isAdmin),
|
|
21810
|
+
create: (input, caller) => center.createSnooze(input, caller),
|
|
21811
|
+
cancel: (snoozeId, caller) => center.cancelSnooze(snoozeId, caller)
|
|
21812
|
+
},
|
|
20857
21813
|
listCallerTargetIds: async (userId) => {
|
|
20858
21814
|
return (await api.notificationOutput.listTargets.query({})).filter((t) => t.config["ownerUserId"] === userId).map((t) => t.id);
|
|
20859
21815
|
}
|
|
@@ -20885,7 +21841,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
20885
21841
|
let storage = this.ctx.kernel.storage;
|
|
20886
21842
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
20887
21843
|
if (mediaRoot) {
|
|
20888
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
21844
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-DFKBSXUx.js"));
|
|
20889
21845
|
storage = new FilesystemStorageProvider(mediaRoot, { eventMedia: mediaRoot });
|
|
20890
21846
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
20891
21847
|
}
|
|
@@ -21266,6 +22222,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21266
22222
|
postRollSec: req.postRollSec ?? NC_FOOTAGE_POST_ROLL_SEC,
|
|
21267
22223
|
maxWidth: NC_FOOTAGE_MAX_WIDTH,
|
|
21268
22224
|
fps: NC_FOOTAGE_FPS,
|
|
22225
|
+
...req.speed !== void 0 ? { speed: req.speed } : {},
|
|
21269
22226
|
...req.profile === "high" || req.profile === "mid" || req.profile === "low" ? { profile: req.profile } : {}
|
|
21270
22227
|
});
|
|
21271
22228
|
const buf = Buffer.from(res.base64, "base64");
|
|
@@ -21423,6 +22380,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
21423
22380
|
this.sweepRetention();
|
|
21424
22381
|
this.runTrackRetentionSweep();
|
|
21425
22382
|
this.stationaryRegistry?.sweep(Date.now());
|
|
22383
|
+
this.eventsOpsLog?.pruneBefore(Date.now() - OPS_LOG_RETENTION_MS);
|
|
21426
22384
|
}, RETENTION_SWEEP_INTERVAL_MS);
|
|
21427
22385
|
}
|
|
21428
22386
|
/** Cap provider registrations returned from `onInitialize` — the widgets
|