@camstack/addon-post-analysis 1.2.84 → 1.2.86
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-C_mhxTWT.js → dist-Crk9pYee.js} +86 -10
- package/dist/{dist-CJWHkPz8.mjs → dist-CzdBsvoR.mjs} +81 -11
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ZD03Wv9i.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-GLqrUCoW.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-BhW_ys0C.mjs → hostInit-DtD0Fd_e.mjs} +1 -1
- package/dist/pipeline-analytics/index.js +845 -86
- package/dist/pipeline-analytics/index.mjs +844 -86
- 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-Crk9pYee.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -1586,11 +1586,32 @@ var NC_ALARM_DECLARATION = {
|
|
|
1586
1586
|
* retention="pruned by NcArtifactStore's shelf bounds (14 d / 2000 rows / 2 GiB) — the index row falls with its bytes, on the producer node only (see prune(null) gate)"
|
|
1587
1587
|
*/
|
|
1588
1588
|
var NC_ARTIFACTS_COLLECTION = "notification-center:artifacts";
|
|
1589
|
-
|
|
1589
|
+
/**
|
|
1590
|
+
* What an artefact IS. The schema lives in the ADDON, not in `packages/types`,
|
|
1591
|
+
* for the same reason the summary rule's does: it ships on `camstack deploy`
|
|
1592
|
+
* and never waits for a framework train.
|
|
1593
|
+
*
|
|
1594
|
+
* TWO groups:
|
|
1595
|
+
* - **Gallery tiles** — what the operator browses (timelapse, gif, mp4, mosaic).
|
|
1596
|
+
* - **Posters** — companion stills (`timelapse-preview`, `event-still`). They
|
|
1597
|
+
* exist so a video/gif tile can name its thumbnail via `posterArtifactId`.
|
|
1598
|
+
* They are not a filter chip and they are omitted from an unfiltered list.
|
|
1599
|
+
*/
|
|
1600
|
+
var NC_GALLERY_ARTIFACT_KINDS = [
|
|
1590
1601
|
"timelapse",
|
|
1591
|
-
"
|
|
1602
|
+
"event-gif",
|
|
1603
|
+
"event-clip",
|
|
1592
1604
|
"summary-mosaic"
|
|
1593
|
-
]
|
|
1605
|
+
];
|
|
1606
|
+
var NC_POSTER_ARTIFACT_KINDS = ["timelapse-preview", "event-still"];
|
|
1607
|
+
var NC_ARTIFACT_KINDS = [...NC_GALLERY_ARTIFACT_KINDS, ...NC_POSTER_ARTIFACT_KINDS];
|
|
1608
|
+
var NcArtifactKindSchema = require_dist._enum(NC_ARTIFACT_KINDS);
|
|
1609
|
+
/** Kind a notification attachment is filed under in the gallery. */
|
|
1610
|
+
function artifactKindForMediaType(mediaType) {
|
|
1611
|
+
if (mediaType === "gif") return "event-gif";
|
|
1612
|
+
if (mediaType === "video") return "event-clip";
|
|
1613
|
+
return "event-still";
|
|
1614
|
+
}
|
|
1594
1615
|
/**
|
|
1595
1616
|
* How the viewer should DRAW it. Derived from the mime once, here, so no client
|
|
1596
1617
|
* has to carry a mime table — and so a GIF that degraded to a JPEG is drawn as
|
|
@@ -1949,7 +1970,7 @@ var NcArtifactIndex = class {
|
|
|
1949
1970
|
}
|
|
1950
1971
|
buildFilter(query, viewer) {
|
|
1951
1972
|
const whereIn = { ...this.visibility(viewer).whereIn };
|
|
1952
|
-
|
|
1973
|
+
whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
|
|
1953
1974
|
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
1954
1975
|
const whereBetween = {
|
|
1955
1976
|
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
@@ -5694,7 +5715,7 @@ var NcAudioStillShelf = class {
|
|
|
5694
5715
|
}
|
|
5695
5716
|
async fetch(captureId, deviceId, atMs) {
|
|
5696
5717
|
try {
|
|
5697
|
-
const shot = await withTimeout$
|
|
5718
|
+
const shot = await withTimeout$3(this.deps.getSnapshot(deviceId), SNAPSHOT_TIMEOUT_MS);
|
|
5698
5719
|
if (shot === null || shot.base64.length === 0) {
|
|
5699
5720
|
this.reportMiss(deviceId, "the camera returned no snapshot");
|
|
5700
5721
|
return;
|
|
@@ -5760,7 +5781,7 @@ function audioStillMedia(data, timestamp) {
|
|
|
5760
5781
|
];
|
|
5761
5782
|
}
|
|
5762
5783
|
/** Reject if `promise` does not settle within `ms`. */
|
|
5763
|
-
function withTimeout$
|
|
5784
|
+
function withTimeout$3(promise, ms) {
|
|
5764
5785
|
return new Promise((resolve, reject) => {
|
|
5765
5786
|
const timer = setTimeout(() => {
|
|
5766
5787
|
reject(/* @__PURE__ */ new Error(`snapshot cap timed out after ${String(ms)}ms`));
|
|
@@ -7769,10 +7790,24 @@ function matchesSystemEvent(condition, subject) {
|
|
|
7769
7790
|
if (!condition.kinds.includes(subject.kind)) return false;
|
|
7770
7791
|
if (condition.deviceIds !== void 0 && (subject.deviceId === void 0 || !condition.deviceIds.includes(subject.deviceId))) return false;
|
|
7771
7792
|
if (condition.deviceTypes !== void 0 && (subject.deviceType === void 0 || !condition.deviceTypes.includes(subject.deviceType))) return false;
|
|
7772
|
-
if (condition.nodeIds !== void 0
|
|
7773
|
-
|
|
7793
|
+
if (condition.nodeIds !== void 0) {
|
|
7794
|
+
if (!subjectNodes(subject).some((id) => condition.nodeIds.includes(id))) return false;
|
|
7795
|
+
}
|
|
7796
|
+
if (condition.packageNames !== void 0) {
|
|
7797
|
+
if (!subjectPackages(subject).some((name) => condition.packageNames.includes(name))) return false;
|
|
7798
|
+
}
|
|
7774
7799
|
return true;
|
|
7775
7800
|
}
|
|
7801
|
+
function subjectNodes(subject) {
|
|
7802
|
+
const ids = [...subject.nodeIds ?? []];
|
|
7803
|
+
if (subject.nodeId !== void 0 && !ids.includes(subject.nodeId)) ids.push(subject.nodeId);
|
|
7804
|
+
return ids;
|
|
7805
|
+
}
|
|
7806
|
+
function subjectPackages(subject) {
|
|
7807
|
+
const names = [...subject.packageNames ?? []];
|
|
7808
|
+
if (subject.packageName !== void 0 && !names.includes(subject.packageName)) names.push(subject.packageName);
|
|
7809
|
+
return names;
|
|
7810
|
+
}
|
|
7776
7811
|
function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
7777
7812
|
if (subject.kind === "system-event") {
|
|
7778
7813
|
if (rule.delivery !== "system-event") return fail("delivery");
|
|
@@ -8229,12 +8264,30 @@ var en_default = {
|
|
|
8229
8264
|
"one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
|
|
8230
8265
|
"other": "Camera {{deviceName}} has been analysed for {{minutes}} minutes with no detection at all"
|
|
8231
8266
|
},
|
|
8232
|
-
"system.addon-update-available.title":
|
|
8233
|
-
|
|
8267
|
+
"system.addon-update-available.title": {
|
|
8268
|
+
"one": "{{count}} addon update available",
|
|
8269
|
+
"other": "{{count}} addon updates available"
|
|
8270
|
+
},
|
|
8271
|
+
"system.addon-update-available.body": "{{list}}",
|
|
8234
8272
|
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8235
|
-
"system.server-update-available.title":
|
|
8273
|
+
"system.server-update-available.title": {
|
|
8274
|
+
"one": "Server update available on {{count}} node",
|
|
8275
|
+
"other": "Server update available on {{count}} nodes"
|
|
8276
|
+
},
|
|
8236
8277
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}})",
|
|
8237
8278
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8279
|
+
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on {{nodes}}",
|
|
8280
|
+
"system.addon-updated.title": {
|
|
8281
|
+
"one": "{{count}} addon updated",
|
|
8282
|
+
"other": "{{count}} addons updated"
|
|
8283
|
+
},
|
|
8284
|
+
"system.addon-updated.body": "{{list}}",
|
|
8285
|
+
"system.server-updated.title": "Server updated",
|
|
8286
|
+
"system.server-updated.body": "{{packageName}} on {{nodeId}} is now {{toVersion}} (was {{fromVersion}})",
|
|
8287
|
+
"system.server-updated.body.plain": "{{packageName}} on {{nodeId}} was updated",
|
|
8288
|
+
"system.export-completed.title": "Export ready",
|
|
8289
|
+
"system.export-completed.body": "The footage export is ready",
|
|
8290
|
+
"system.export-completed.body.titled": "{{title}} is ready",
|
|
8238
8291
|
"system.camera-online.title": "Device online",
|
|
8239
8292
|
"system.camera-online.body": "Device {{deviceName}} is online",
|
|
8240
8293
|
"system.camera-offline.title": "Device offline",
|
|
@@ -8423,12 +8476,30 @@ var it_default = {
|
|
|
8423
8476
|
"one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
|
|
8424
8477
|
"other": "La camera {{deviceName}} è analizzata da {{minutes}} minuti senza alcun rilevamento"
|
|
8425
8478
|
},
|
|
8426
|
-
"system.addon-update-available.title":
|
|
8427
|
-
|
|
8479
|
+
"system.addon-update-available.title": {
|
|
8480
|
+
"one": "{{count}} aggiornamento addon disponibile",
|
|
8481
|
+
"other": "{{count}} aggiornamenti addon disponibili"
|
|
8482
|
+
},
|
|
8483
|
+
"system.addon-update-available.body": "{{list}}",
|
|
8428
8484
|
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8429
|
-
"system.server-update-available.title":
|
|
8485
|
+
"system.server-update-available.title": {
|
|
8486
|
+
"one": "Aggiornamento server disponibile su {{count}} nodo",
|
|
8487
|
+
"other": "Aggiornamento server disponibile su {{count}} nodi"
|
|
8488
|
+
},
|
|
8430
8489
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}})",
|
|
8431
8490
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8491
|
+
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) su {{nodes}}",
|
|
8492
|
+
"system.addon-updated.title": {
|
|
8493
|
+
"one": "{{count}} addon aggiornato",
|
|
8494
|
+
"other": "{{count}} addon aggiornati"
|
|
8495
|
+
},
|
|
8496
|
+
"system.addon-updated.body": "{{list}}",
|
|
8497
|
+
"system.server-updated.title": "Server aggiornato",
|
|
8498
|
+
"system.server-updated.body": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}} (era {{fromVersion}})",
|
|
8499
|
+
"system.server-updated.body.plain": "{{packageName}} sul nodo {{nodeId}} è stato aggiornato",
|
|
8500
|
+
"system.export-completed.title": "Export pronto",
|
|
8501
|
+
"system.export-completed.body": "L'export del filmato è pronto",
|
|
8502
|
+
"system.export-completed.body.titled": "{{title}} è pronto",
|
|
8432
8503
|
"system.camera-online.title": "Dispositivo online",
|
|
8433
8504
|
"system.camera-online.body": "Il dispositivo {{deviceName}} è online",
|
|
8434
8505
|
"system.camera-offline.title": "Dispositivo offline",
|
|
@@ -9155,7 +9226,7 @@ var NcDispatcher = class {
|
|
|
9155
9226
|
vars: { body: rendered },
|
|
9156
9227
|
frozen: rendered
|
|
9157
9228
|
}) : rendered;
|
|
9158
|
-
const attachments = [...await this.withArtifactUrls(await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
|
|
9229
|
+
const attachments = [...await this.withArtifactUrls(entry, await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
|
|
9159
9230
|
const params = pickParams(entry.payload.params);
|
|
9160
9231
|
const actions = await this.mintActions(entry, target);
|
|
9161
9232
|
return {
|
|
@@ -9216,11 +9287,39 @@ var NcDispatcher = class {
|
|
|
9216
9287
|
}
|
|
9217
9288
|
});
|
|
9218
9289
|
}
|
|
9219
|
-
async withArtifactUrls(attachments) {
|
|
9290
|
+
async withArtifactUrls(entry, attachments) {
|
|
9291
|
+
const index = this.deps.indexArtifact;
|
|
9220
9292
|
const publish = this.deps.publishArtifact;
|
|
9221
|
-
if (publish === void 0) return [...attachments];
|
|
9293
|
+
if (index === void 0 && publish === void 0) return [...attachments];
|
|
9222
9294
|
const out = [];
|
|
9295
|
+
let posterId = null;
|
|
9296
|
+
const deviceId = entry.payload.subject.deviceId;
|
|
9297
|
+
const common = {
|
|
9298
|
+
deviceId: deviceId > 0 ? deviceId : null,
|
|
9299
|
+
deviceIds: deviceId > 0 ? [deviceId] : [],
|
|
9300
|
+
capturedAt: entry.payload.subject.timestamp,
|
|
9301
|
+
ruleId: entry.ruleId,
|
|
9302
|
+
ruleName: entry.payload.ruleName
|
|
9303
|
+
};
|
|
9223
9304
|
for (const att of attachments) {
|
|
9305
|
+
if (index !== void 0) {
|
|
9306
|
+
const kind = artifactKindForMediaType(att.mediaType);
|
|
9307
|
+
const meta = {
|
|
9308
|
+
...common,
|
|
9309
|
+
kind,
|
|
9310
|
+
...posterId !== null && kind !== "event-still" ? { posterArtifactId: posterId } : {}
|
|
9311
|
+
};
|
|
9312
|
+
const pub = await index(att.bytes, att.mime, meta).catch(() => ({
|
|
9313
|
+
url: null,
|
|
9314
|
+
artifactId: null
|
|
9315
|
+
}));
|
|
9316
|
+
if (kind === "event-still" && pub.artifactId !== null) posterId = pub.artifactId;
|
|
9317
|
+
out.push(pub.url === null ? { ...att } : {
|
|
9318
|
+
...att,
|
|
9319
|
+
url: pub.url
|
|
9320
|
+
});
|
|
9321
|
+
continue;
|
|
9322
|
+
}
|
|
9224
9323
|
const url = await publish(att.bytes, att.mime).catch(() => null);
|
|
9225
9324
|
out.push(url === null ? { ...att } : {
|
|
9226
9325
|
...att,
|
|
@@ -9834,6 +9933,16 @@ function stringField(data, key) {
|
|
|
9834
9933
|
const value = data[key];
|
|
9835
9934
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
9836
9935
|
}
|
|
9936
|
+
function stringList(data, key) {
|
|
9937
|
+
const raw = data[key];
|
|
9938
|
+
if (!Array.isArray(raw)) return [];
|
|
9939
|
+
const ids = [];
|
|
9940
|
+
for (const item of raw) {
|
|
9941
|
+
if (typeof item === "string" && item.length > 0 && !ids.includes(item)) ids.push(item);
|
|
9942
|
+
if (ids.length >= 20) break;
|
|
9943
|
+
}
|
|
9944
|
+
return ids;
|
|
9945
|
+
}
|
|
9837
9946
|
function numberField(data, key) {
|
|
9838
9947
|
const value = data[key];
|
|
9839
9948
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
@@ -9977,6 +10086,8 @@ function systemSubject(input) {
|
|
|
9977
10086
|
...input.deviceType !== void 0 ? { deviceType: input.deviceType } : {},
|
|
9978
10087
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
|
|
9979
10088
|
...input.packageName !== void 0 ? { packageName: input.packageName } : {},
|
|
10089
|
+
...input.nodeIds !== void 0 ? { nodeIds: input.nodeIds } : {},
|
|
10090
|
+
...input.packageNames !== void 0 ? { packageNames: input.packageNames } : {},
|
|
9980
10091
|
...input.textCount !== void 0 ? { textCount: input.textCount } : {},
|
|
9981
10092
|
...input.alarmSource !== void 0 ? { alarmSource: input.alarmSource } : {},
|
|
9982
10093
|
data
|
|
@@ -10153,34 +10264,189 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
10153
10264
|
...identity !== void 0 ? { deviceType: identity.type } : {}
|
|
10154
10265
|
});
|
|
10155
10266
|
}
|
|
10156
|
-
case "update.available":
|
|
10157
|
-
|
|
10158
|
-
const
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10267
|
+
case "update.available": return incomingFromUpdateAvailable(facts, data);
|
|
10268
|
+
case "addon.updated": {
|
|
10269
|
+
const update = parseAddonUpdateComplete(data);
|
|
10270
|
+
return update === null ? null : incomingFromAddonUpdates(facts, [update]);
|
|
10271
|
+
}
|
|
10272
|
+
case "system.restart-completed": return incomingFromServerUpdated(facts, data);
|
|
10273
|
+
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
10274
|
+
default: return null;
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
function incomingFromUpdateAvailable(facts, data) {
|
|
10278
|
+
const target = stringField(data, "target");
|
|
10279
|
+
if (target !== "addon" && target !== "server") return null;
|
|
10280
|
+
const packages = updatePackages(data);
|
|
10281
|
+
if (packages.length === 0) return null;
|
|
10282
|
+
const addon = target === "addon";
|
|
10283
|
+
const kind = addon ? "addon-update-available" : "server-update-available";
|
|
10284
|
+
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
|
|
10285
|
+
const packageNames = unique(packages.map((pkg) => pkg.packageName));
|
|
10286
|
+
const signature = packages.map((pkg) => `${pkg.nodeId ?? ""}:${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",");
|
|
10287
|
+
const list = packages.map(formatAvailableLine).join("\n");
|
|
10288
|
+
const nodes = nodeIds.join(", ");
|
|
10289
|
+
const head = packages[0];
|
|
10290
|
+
const oneNode = nodeIds.length === 1 ? nodeIds[0] : void 0;
|
|
10291
|
+
return systemSubject({
|
|
10292
|
+
facts,
|
|
10293
|
+
kind,
|
|
10294
|
+
subject: addon ? `packages:addon:${signature}` : `packages:server:${signature}`,
|
|
10295
|
+
titleKey: `system.${kind}.title`,
|
|
10296
|
+
bodyKey: !addon && oneNode !== void 0 ? `system.${kind}.body.onNode` : !addon && nodeIds.length > 1 ? `system.${kind}.body.onNodes` : `system.${kind}.body`,
|
|
10297
|
+
textParams: {
|
|
10298
|
+
list,
|
|
10299
|
+
packageName: head.packageName,
|
|
10300
|
+
currentVersion: head.currentVersion,
|
|
10301
|
+
latestVersion: head.latestVersion,
|
|
10302
|
+
nodes,
|
|
10303
|
+
count: String(addon ? packages.length : nodeIds.length > 0 ? nodeIds.length : 1),
|
|
10304
|
+
...oneNode !== void 0 ? { nodeId: oneNode } : {}
|
|
10305
|
+
},
|
|
10306
|
+
textCount: addon ? packages.length : nodeIds.length > 0 ? nodeIds.length : 1,
|
|
10307
|
+
packageName: head.packageName,
|
|
10308
|
+
packageNames,
|
|
10309
|
+
...oneNode !== void 0 ? { nodeId: oneNode } : {},
|
|
10310
|
+
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10311
|
+
});
|
|
10312
|
+
}
|
|
10313
|
+
/** Version-bearing `addon.updated` only — settings saves carry neither. */
|
|
10314
|
+
function parseAddonUpdateComplete(data) {
|
|
10315
|
+
const packageName = stringField(data, "packageName");
|
|
10316
|
+
const fromVersion = stringField(data, "fromVersion");
|
|
10317
|
+
const toVersion = stringField(data, "toVersion");
|
|
10318
|
+
if (packageName === void 0 || fromVersion === void 0 || toVersion === void 0) return null;
|
|
10319
|
+
const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
|
|
10320
|
+
return {
|
|
10321
|
+
packageName,
|
|
10322
|
+
fromVersion,
|
|
10323
|
+
toVersion,
|
|
10324
|
+
...nodeId !== void 0 ? { nodeId } : {}
|
|
10325
|
+
};
|
|
10326
|
+
}
|
|
10327
|
+
function incomingFromAddonUpdates(facts, updates) {
|
|
10328
|
+
if (updates.length === 0) return null;
|
|
10329
|
+
const packageNames = unique(updates.map((update) => update.packageName));
|
|
10330
|
+
const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
|
|
10331
|
+
const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
|
|
10332
|
+
const list = updates.map((update) => update.nodeId !== void 0 ? `${update.packageName} ${update.fromVersion} → ${update.toVersion} (${update.nodeId})` : `${update.packageName} ${update.fromVersion} → ${update.toVersion}`).join("\n");
|
|
10333
|
+
const head = updates[0];
|
|
10334
|
+
return systemSubject({
|
|
10335
|
+
facts,
|
|
10336
|
+
kind: "addon-updated",
|
|
10337
|
+
subject: `packages:addon-updated:${signature}`,
|
|
10338
|
+
titleKey: "system.addon-updated.title",
|
|
10339
|
+
bodyKey: "system.addon-updated.body",
|
|
10340
|
+
textParams: {
|
|
10341
|
+
list,
|
|
10342
|
+
count: String(packageNames.length)
|
|
10343
|
+
},
|
|
10344
|
+
textCount: packageNames.length,
|
|
10345
|
+
packageName: head.packageName,
|
|
10346
|
+
packageNames,
|
|
10347
|
+
...head.nodeId !== void 0 ? { nodeId: head.nodeId } : {},
|
|
10348
|
+
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10349
|
+
});
|
|
10350
|
+
}
|
|
10351
|
+
function incomingFromServerUpdated(facts, data) {
|
|
10352
|
+
const kind = stringField(data, "kind");
|
|
10353
|
+
const requestedBy = stringField(data, "requestedBy");
|
|
10354
|
+
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:"))) return null;
|
|
10355
|
+
const packageName = stringField(data, "packageName") ?? "@camstack/server";
|
|
10356
|
+
const fromVersion = stringField(data, "fromVersion");
|
|
10357
|
+
const toVersion = stringField(data, "toVersion") ?? versionFromRequestedBy(requestedBy);
|
|
10358
|
+
const nodeId = stringField(data, "nodeId") ?? "hub";
|
|
10359
|
+
const bodyKey = fromVersion !== void 0 && toVersion !== void 0 ? "system.server-updated.body" : "system.server-updated.body.plain";
|
|
10360
|
+
return systemSubject({
|
|
10361
|
+
facts,
|
|
10362
|
+
kind: "server-updated",
|
|
10363
|
+
subject: `server-updated:${nodeId}:${toVersion ?? "unknown"}`,
|
|
10364
|
+
titleKey: "system.server-updated.title",
|
|
10365
|
+
bodyKey,
|
|
10366
|
+
textParams: {
|
|
10367
|
+
packageName,
|
|
10368
|
+
nodeId,
|
|
10369
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
10370
|
+
...toVersion !== void 0 ? { toVersion } : {}
|
|
10371
|
+
},
|
|
10372
|
+
packageName,
|
|
10373
|
+
nodeId,
|
|
10374
|
+
nodeIds: [nodeId]
|
|
10375
|
+
});
|
|
10376
|
+
}
|
|
10377
|
+
function incomingFromExportCompleted(facts, data) {
|
|
10378
|
+
const exportId = stringField(data, "exportId");
|
|
10379
|
+
if (exportId === void 0) return null;
|
|
10380
|
+
const deviceId = numberField(data, "deviceId");
|
|
10381
|
+
const title = stringField(data, "title");
|
|
10382
|
+
const bytes = numberField(data, "fileBytes");
|
|
10383
|
+
const notifyTargetIds = stringList(data, "notifyTargetIds");
|
|
10384
|
+
const incoming = systemSubject({
|
|
10385
|
+
facts,
|
|
10386
|
+
kind: "export-completed",
|
|
10387
|
+
subject: `export:${exportId}`,
|
|
10388
|
+
titleKey: "system.export-completed.title",
|
|
10389
|
+
bodyKey: title !== void 0 ? "system.export-completed.body.titled" : "system.export-completed.body",
|
|
10390
|
+
textParams: {
|
|
10391
|
+
exportId,
|
|
10392
|
+
...title !== void 0 ? { title } : {},
|
|
10393
|
+
...bytes !== void 0 ? { bytes: String(bytes) } : {},
|
|
10394
|
+
...deviceId !== void 0 ? { deviceId: String(deviceId) } : {}
|
|
10395
|
+
},
|
|
10396
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
10397
|
+
});
|
|
10398
|
+
if (notifyTargetIds.length === 0) return incoming;
|
|
10399
|
+
return {
|
|
10400
|
+
...incoming,
|
|
10401
|
+
controls: {
|
|
10402
|
+
directTargetIds: notifyTargetIds,
|
|
10403
|
+
bypassMute: true
|
|
10404
|
+
}
|
|
10405
|
+
};
|
|
10406
|
+
}
|
|
10407
|
+
function updatePackages(data) {
|
|
10408
|
+
const raw = data["packages"];
|
|
10409
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
10410
|
+
const rows = [];
|
|
10411
|
+
for (const item of raw) {
|
|
10412
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
|
|
10413
|
+
const row = item;
|
|
10414
|
+
const packageName = stringField(row, "packageName");
|
|
10415
|
+
const currentVersion = stringField(row, "currentVersion");
|
|
10416
|
+
const latestVersion = stringField(row, "latestVersion");
|
|
10417
|
+
if (packageName === void 0 || currentVersion === void 0 || latestVersion === void 0) continue;
|
|
10418
|
+
const nodeId = stringField(row, "nodeId");
|
|
10419
|
+
rows.push({
|
|
10178
10420
|
packageName,
|
|
10421
|
+
currentVersion,
|
|
10422
|
+
latestVersion,
|
|
10179
10423
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10180
10424
|
});
|
|
10181
10425
|
}
|
|
10182
|
-
|
|
10426
|
+
if (rows.length > 0) return rows;
|
|
10183
10427
|
}
|
|
10428
|
+
const packageName = stringField(data, "packageName");
|
|
10429
|
+
const currentVersion = stringField(data, "currentVersion");
|
|
10430
|
+
const latestVersion = stringField(data, "latestVersion");
|
|
10431
|
+
if (packageName === void 0 || currentVersion === void 0 || latestVersion === void 0) return [];
|
|
10432
|
+
const nodeId = stringField(data, "nodeId");
|
|
10433
|
+
return [{
|
|
10434
|
+
packageName,
|
|
10435
|
+
currentVersion,
|
|
10436
|
+
latestVersion,
|
|
10437
|
+
...nodeId !== void 0 ? { nodeId } : {}
|
|
10438
|
+
}];
|
|
10439
|
+
}
|
|
10440
|
+
function formatAvailableLine(pkg) {
|
|
10441
|
+
const line = `${pkg.packageName} ${pkg.latestVersion} (current ${pkg.currentVersion})`;
|
|
10442
|
+
return pkg.nodeId !== void 0 ? `${line} on ${pkg.nodeId}` : line;
|
|
10443
|
+
}
|
|
10444
|
+
function versionFromRequestedBy(requestedBy) {
|
|
10445
|
+
if (requestedBy === void 0) return void 0;
|
|
10446
|
+
return /@([^@\s]+)$/.exec(requestedBy)?.[1];
|
|
10447
|
+
}
|
|
10448
|
+
function unique(values) {
|
|
10449
|
+
return [...new Set(values)];
|
|
10184
10450
|
}
|
|
10185
10451
|
/**
|
|
10186
10452
|
* The COMBINED row's two keys, or null when this transition is not one.
|
|
@@ -10896,6 +11162,67 @@ function recordToRow$2(subject, data) {
|
|
|
10896
11162
|
updatedAt
|
|
10897
11163
|
};
|
|
10898
11164
|
}
|
|
11165
|
+
function isDeviceLivenessKind(kind) {
|
|
11166
|
+
return kind === "device-online" || kind === "device-offline" || kind === "camera-online" || kind === "camera-offline";
|
|
11167
|
+
}
|
|
11168
|
+
function livenessStateOfKind(kind) {
|
|
11169
|
+
return kind.endsWith("-offline") ? "offline" : "online";
|
|
11170
|
+
}
|
|
11171
|
+
var DeviceLivenessHoldoff = class {
|
|
11172
|
+
flush;
|
|
11173
|
+
pending = /* @__PURE__ */ new Map();
|
|
11174
|
+
lastNotified = /* @__PURE__ */ new Map();
|
|
11175
|
+
timers = /* @__PURE__ */ new Map();
|
|
11176
|
+
delayMs;
|
|
11177
|
+
schedule;
|
|
11178
|
+
cancel;
|
|
11179
|
+
constructor(flush, options = {}) {
|
|
11180
|
+
this.flush = flush;
|
|
11181
|
+
this.delayMs = options.delayMs ?? 3e5;
|
|
11182
|
+
this.schedule = options.schedule ?? setTimeout;
|
|
11183
|
+
this.cancel = options.cancel ?? clearTimeout;
|
|
11184
|
+
}
|
|
11185
|
+
/** The seeded (or last delivered) state — a bounce back to this is silence. */
|
|
11186
|
+
remember(subject, state) {
|
|
11187
|
+
this.lastNotified.set(subject, state);
|
|
11188
|
+
}
|
|
11189
|
+
/** Directory-seeded rows never pass through {@link remember}; first flip does. */
|
|
11190
|
+
rememberIfAbsent(subject, state) {
|
|
11191
|
+
if (!this.lastNotified.has(subject)) this.lastNotified.set(subject, state);
|
|
11192
|
+
}
|
|
11193
|
+
/** A real ledger flip. Starts / resets the trailing timer. */
|
|
11194
|
+
arm(subject, incoming) {
|
|
11195
|
+
this.pending.set(subject, incoming);
|
|
11196
|
+
const existing = this.timers.get(subject);
|
|
11197
|
+
if (existing !== void 0) this.cancel(existing);
|
|
11198
|
+
if (this.delayMs <= 0) {
|
|
11199
|
+
this.timers.delete(subject);
|
|
11200
|
+
this.fire(subject);
|
|
11201
|
+
return;
|
|
11202
|
+
}
|
|
11203
|
+
const id = this.schedule(() => {
|
|
11204
|
+
this.timers.delete(subject);
|
|
11205
|
+
this.fire(subject);
|
|
11206
|
+
}, this.delayMs);
|
|
11207
|
+
this.timers.set(subject, id);
|
|
11208
|
+
}
|
|
11209
|
+
dispose() {
|
|
11210
|
+
for (const id of this.timers.values()) this.cancel(id);
|
|
11211
|
+
this.timers.clear();
|
|
11212
|
+
this.pending.clear();
|
|
11213
|
+
}
|
|
11214
|
+
fire(subject) {
|
|
11215
|
+
const incoming = this.pending.get(subject);
|
|
11216
|
+
this.pending.delete(subject);
|
|
11217
|
+
if (incoming === void 0) return;
|
|
11218
|
+
const kind = incoming.subject.systemEvent?.kind;
|
|
11219
|
+
if (kind === void 0) return;
|
|
11220
|
+
const state = livenessStateOfKind(kind);
|
|
11221
|
+
if (this.lastNotified.get(subject) === state) return;
|
|
11222
|
+
this.lastNotified.set(subject, state);
|
|
11223
|
+
this.flush(incoming);
|
|
11224
|
+
}
|
|
11225
|
+
};
|
|
10899
11226
|
//#endregion
|
|
10900
11227
|
//#region src/notification-center/occupancy-watcher.ts
|
|
10901
11228
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
@@ -17793,6 +18120,36 @@ var TimelapseStore = class {
|
|
|
17793
18120
|
return rule;
|
|
17794
18121
|
}
|
|
17795
18122
|
};
|
|
18123
|
+
var AddonUpdateCoalescer = class {
|
|
18124
|
+
flush;
|
|
18125
|
+
pending = /* @__PURE__ */ new Map();
|
|
18126
|
+
timer = null;
|
|
18127
|
+
delayMs;
|
|
18128
|
+
schedule;
|
|
18129
|
+
cancel;
|
|
18130
|
+
constructor(flush, options = {}) {
|
|
18131
|
+
this.flush = flush;
|
|
18132
|
+
this.delayMs = options.delayMs ?? 3e4;
|
|
18133
|
+
this.schedule = options.schedule ?? setTimeout;
|
|
18134
|
+
this.cancel = options.cancel ?? clearTimeout;
|
|
18135
|
+
}
|
|
18136
|
+
observe(update) {
|
|
18137
|
+
this.pending.set(update.packageName, update);
|
|
18138
|
+
if (this.timer !== null) this.cancel(this.timer);
|
|
18139
|
+
this.timer = this.schedule(() => {
|
|
18140
|
+
this.timer = null;
|
|
18141
|
+
const batch = [...this.pending.values()];
|
|
18142
|
+
this.pending.clear();
|
|
18143
|
+
if (batch.length > 0) this.flush(batch);
|
|
18144
|
+
}, this.delayMs);
|
|
18145
|
+
}
|
|
18146
|
+
dispose() {
|
|
18147
|
+
if (this.timer === null) return;
|
|
18148
|
+
this.cancel(this.timer);
|
|
18149
|
+
this.timer = null;
|
|
18150
|
+
this.pending.clear();
|
|
18151
|
+
}
|
|
18152
|
+
};
|
|
17796
18153
|
//#endregion
|
|
17797
18154
|
//#region src/notification-center/zone-owner-cache.ts
|
|
17798
18155
|
var NcZoneOwnershipCache = class {
|
|
@@ -17902,6 +18259,15 @@ var TIMELAPSE_LOOKAHEAD_MS = 9 * 864e5;
|
|
|
17902
18259
|
*/
|
|
17903
18260
|
var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
17904
18261
|
/**
|
|
18262
|
+
* Synthetic rule id for an export's on-screen picker.
|
|
18263
|
+
*
|
|
18264
|
+
* The operator named these targets on the export screen, not in a rule. The
|
|
18265
|
+
* row still lives in the outbox so history, retry and language rendering are
|
|
18266
|
+
* the same path as every other notification. A matching `export-completed`
|
|
18267
|
+
* rule that already reached a target is skipped so one export is one push.
|
|
18268
|
+
*/
|
|
18269
|
+
var NC_EXPORT_NOTIFY_RULE_ID = "nc-export-notify";
|
|
18270
|
+
/**
|
|
17905
18271
|
* Which alarm buttons a notification of this kind should carry, or `null` for
|
|
17906
18272
|
* every notification that is not about the panel.
|
|
17907
18273
|
*
|
|
@@ -18349,6 +18715,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18349
18715
|
* seeds, only a real flip notifies.
|
|
18350
18716
|
*/
|
|
18351
18717
|
liveness;
|
|
18718
|
+
/** Batches `addon.updated` (version-bearing) into one notification per 30 s. */
|
|
18719
|
+
addonUpdates;
|
|
18720
|
+
/** Trailing 5 min hold-off for camera liveness flaps (plugin restart). */
|
|
18721
|
+
livenessHoldoff;
|
|
18352
18722
|
/**
|
|
18353
18723
|
* Serialized evaluation chain. The persist hooks are fire-and-forget for
|
|
18354
18724
|
* the frame path, but two concurrent evaluations of the same device
|
|
@@ -18651,6 +19021,16 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18651
19021
|
store: deps.store,
|
|
18652
19022
|
logger: this.logger.child("liveness")
|
|
18653
19023
|
});
|
|
19024
|
+
this.addonUpdates = new AddonUpdateCoalescer((updates) => {
|
|
19025
|
+
const incoming = incomingFromAddonUpdates({
|
|
19026
|
+
eventId: `addon-updated:${updates.map((u) => u.packageName).join(",")}`,
|
|
19027
|
+
timestamp: this.now(),
|
|
19028
|
+
category: "addon.updated",
|
|
19029
|
+
data: { packages: updates }
|
|
19030
|
+
}, updates);
|
|
19031
|
+
if (incoming !== null) this.consumeEvent(incoming);
|
|
19032
|
+
});
|
|
19033
|
+
this.livenessHoldoff = new DeviceLivenessHoldoff((event) => this.consumeEvent(event), { delayMs: deps.livenessHoldoffMs ?? (process.env.VITEST ? 0 : 3e5) });
|
|
18654
19034
|
this.actionRunner = deps.actuate !== void 0 && deps.isCapDeviceScoped !== void 0 ? new NcRuleActionRunner({
|
|
18655
19035
|
logger: this.logger.child("actions"),
|
|
18656
19036
|
actuate: (input) => this.actuateWithAttribution(input),
|
|
@@ -18909,6 +19289,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18909
19289
|
this.timelapseScheduler?.stop();
|
|
18910
19290
|
this.summaryProducer?.stop();
|
|
18911
19291
|
this.audioStills?.clear();
|
|
19292
|
+
this.addonUpdates.dispose();
|
|
19293
|
+
this.livenessHoldoff.dispose();
|
|
18912
19294
|
this.evaluationActive = false;
|
|
18913
19295
|
}
|
|
18914
19296
|
/**
|
|
@@ -19383,6 +19765,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19383
19765
|
*/
|
|
19384
19766
|
onSystemEvent(event) {
|
|
19385
19767
|
if (!this.evaluationActive) return;
|
|
19768
|
+
if (event.category === "addon.updated") {
|
|
19769
|
+
const update = parseAddonUpdateComplete(event.data);
|
|
19770
|
+
if (update !== null) this.addonUpdates.observe(update);
|
|
19771
|
+
return;
|
|
19772
|
+
}
|
|
19386
19773
|
const incoming = incomingFromSystemEvent(event, this.deviceDirectory.lookup());
|
|
19387
19774
|
if (incoming === null) {
|
|
19388
19775
|
const dropped = discardedSystemEvent(event);
|
|
@@ -19398,11 +19785,22 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19398
19785
|
const system = incoming.subject.systemEvent;
|
|
19399
19786
|
const observation = system !== void 0 ? ledgeredLiveness(system) : null;
|
|
19400
19787
|
if (observation !== null) {
|
|
19788
|
+
const previous = this.liveness.stateOf(observation.subject);
|
|
19401
19789
|
const verdict = this.liveness.observe(observation, this.now());
|
|
19790
|
+
if (verdict === "seeded") {
|
|
19791
|
+
this.livenessHoldoff.remember(observation.subject, observation.state);
|
|
19792
|
+
this.reportLivenessDrop(incoming, verdict);
|
|
19793
|
+
return;
|
|
19794
|
+
}
|
|
19402
19795
|
if (verdict !== "flip") {
|
|
19403
19796
|
this.reportLivenessDrop(incoming, verdict);
|
|
19404
19797
|
return;
|
|
19405
19798
|
}
|
|
19799
|
+
if (previous !== void 0) this.livenessHoldoff.rememberIfAbsent(observation.subject, previous);
|
|
19800
|
+
if (isDeviceLivenessKind(observation.kind)) {
|
|
19801
|
+
this.livenessHoldoff.arm(observation.subject, incoming);
|
|
19802
|
+
return;
|
|
19803
|
+
}
|
|
19406
19804
|
} else if (system !== void 0 && (system.kind.startsWith("device-") || system.kind.startsWith("stream-"))) {
|
|
19407
19805
|
if (this.lastSystemLiveness.get(system.subject) === system.kind) return;
|
|
19408
19806
|
this.lastSystemLiveness.set(system.subject, system.kind);
|
|
@@ -19895,6 +20293,55 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19895
20293
|
});
|
|
19896
20294
|
}
|
|
19897
20295
|
/**
|
|
20296
|
+
* Deliver to targets the producer named (`controls.directTargetIds`).
|
|
20297
|
+
*
|
|
20298
|
+
* Skips ids a matching rule already reached so one export is one push.
|
|
20299
|
+
* Absent/empty is a no-op — every other producer omits the control.
|
|
20300
|
+
*/
|
|
20301
|
+
async enqueuePickerTargets(event, reached, outcomes) {
|
|
20302
|
+
const ids = event.controls?.directTargetIds;
|
|
20303
|
+
if (ids === void 0 || ids.length === 0) return;
|
|
20304
|
+
const targets = [...new Set(ids)].filter((id) => !reached.has(id));
|
|
20305
|
+
if (targets.length === 0) return;
|
|
20306
|
+
const subject = event.subject;
|
|
20307
|
+
const entries = targets.map((targetId) => ({
|
|
20308
|
+
ruleId: NC_EXPORT_NOTIFY_RULE_ID,
|
|
20309
|
+
targetId,
|
|
20310
|
+
deviceId: subject.deviceId,
|
|
20311
|
+
recordKind: "system-event",
|
|
20312
|
+
recordId: subject.recordId,
|
|
20313
|
+
payload: {
|
|
20314
|
+
ruleName: "export",
|
|
20315
|
+
delivery: "system-event",
|
|
20316
|
+
priority: 2,
|
|
20317
|
+
media: "none",
|
|
20318
|
+
subject: {
|
|
20319
|
+
deviceId: subject.deviceId,
|
|
20320
|
+
className: "export",
|
|
20321
|
+
zones: [],
|
|
20322
|
+
timestamp: subject.timestamp,
|
|
20323
|
+
...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
|
|
20324
|
+
}
|
|
20325
|
+
}
|
|
20326
|
+
}));
|
|
20327
|
+
const inserted = await this.outbox.enqueue(entries);
|
|
20328
|
+
outcomes.push({
|
|
20329
|
+
ruleId: NC_EXPORT_NOTIFY_RULE_ID,
|
|
20330
|
+
ruleName: "export",
|
|
20331
|
+
matched: true,
|
|
20332
|
+
cooldown: "n/a",
|
|
20333
|
+
enqueued: inserted,
|
|
20334
|
+
targetIds: entries.map((entry) => entry.targetId)
|
|
20335
|
+
});
|
|
20336
|
+
this.logger.info("export picker notification enqueued", {
|
|
20337
|
+
tags: { deviceId: subject.deviceId },
|
|
20338
|
+
meta: {
|
|
20339
|
+
targets: targets.length,
|
|
20340
|
+
inserted
|
|
20341
|
+
}
|
|
20342
|
+
});
|
|
20343
|
+
}
|
|
20344
|
+
/**
|
|
19898
20345
|
* The COMBINED notification's second half: withdraw what it replaced.
|
|
19899
20346
|
*
|
|
19900
20347
|
* Only for the targets the alarm actually reached, and only while their rows
|
|
@@ -20019,6 +20466,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20019
20466
|
}
|
|
20020
20467
|
});
|
|
20021
20468
|
if (kind === "track-end") await this.enqueueRecognitionUpgrades(subject, outcomes);
|
|
20469
|
+
await this.enqueuePickerTargets(event, /* @__PURE__ */ new Set(), outcomes);
|
|
20470
|
+
if (outcomes.some((outcome) => outcome.enqueued > 0)) return report({ candidates: 0 });
|
|
20022
20471
|
return report({ droppedBy: "no-candidate-rule" });
|
|
20023
20472
|
}
|
|
20024
20473
|
const now = this.now();
|
|
@@ -20215,6 +20664,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20215
20664
|
}
|
|
20216
20665
|
if (!anyMatched && rejections.length > 0) this.reportNoMatch(subject, kind, rejections, now);
|
|
20217
20666
|
if (kind === "track-end") await this.enqueueRecognitionUpgrades(subject, outcomes);
|
|
20667
|
+
const reached = new Set(outcomes.flatMap((outcome) => outcome.targetIds));
|
|
20668
|
+
await this.enqueuePickerTargets(event, reached, outcomes);
|
|
20218
20669
|
return report({ candidates: candidates.length });
|
|
20219
20670
|
}
|
|
20220
20671
|
/**
|
|
@@ -21434,6 +21885,9 @@ function systemEventSubject(kind, scope) {
|
|
|
21434
21885
|
if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
|
|
21435
21886
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
21436
21887
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
21888
|
+
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
21889
|
+
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
21890
|
+
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
21437
21891
|
return `alarm:${scope.deviceId ?? 0}`;
|
|
21438
21892
|
}
|
|
21439
21893
|
/**
|
|
@@ -21545,7 +21999,13 @@ function planRuleTest(input) {
|
|
|
21545
21999
|
profile: "low",
|
|
21546
22000
|
seconds: "30",
|
|
21547
22001
|
devices: "cuda:0",
|
|
21548
|
-
minutes: "15"
|
|
22002
|
+
minutes: "15",
|
|
22003
|
+
list: "@camstack/addon-pipeline 1.0.0 (current 0.0.0)",
|
|
22004
|
+
nodes: "hub",
|
|
22005
|
+
fromVersion: "0.0.0",
|
|
22006
|
+
toVersion: "0.0.1",
|
|
22007
|
+
title: "Export",
|
|
22008
|
+
count: "1"
|
|
21549
22009
|
},
|
|
21550
22010
|
...systemScope.deviceId !== void 0 ? { deviceId: systemScope.deviceId } : {},
|
|
21551
22011
|
...c.systemEvent?.deviceTypes?.[0] !== void 0 ? { deviceType: c.systemEvent.deviceTypes[0] } : {},
|
|
@@ -28414,7 +28874,123 @@ var OrphanAuditActionResultSchema = require_dist.object({
|
|
|
28414
28874
|
});
|
|
28415
28875
|
var orphanAuditActions = require_dist.defineCustomActions({ "retention.orphanAudit": require_dist.customAction(OrphanAuditActionInputSchema, OrphanAuditActionResultSchema, { auth: "admin" }) });
|
|
28416
28876
|
//#endregion
|
|
28877
|
+
//#region src/pipeline-analytics/videoclip-visits.ts
|
|
28878
|
+
/**
|
|
28879
|
+
* Videoclip visits — the grain a clip should have on an events-mode camera.
|
|
28880
|
+
*
|
|
28881
|
+
* Disk writes one fMP4 per ffmpeg roll (~8–12 s). The recorder calendar merges
|
|
28882
|
+
* only gaps ≤ 5 s, so a single pantry walk shows up as several availability
|
|
28883
|
+
* ranges with ~8 s GOP holes between them. Analytics then mints one 15 s clip
|
|
28884
|
+
* per motion heartbeat. A *visit* is the keep-window those fragments belong
|
|
28885
|
+
* to: cluster availability with {@link VISIT_MERGE_GAP_MS}, annotate with
|
|
28886
|
+
* overlapping events. No second copy of the media; the files stay the source
|
|
28887
|
+
* of truth.
|
|
28888
|
+
*/
|
|
28889
|
+
/** Synthetic event-id slot in `analytics:<start>:<end>:<id>` for a visit clip. */
|
|
28890
|
+
var VISIT_EVENT_ID = "visit";
|
|
28891
|
+
function clusterFootageVisits(ranges, gapMs = require_dist.VISIT_MERGE_GAP_MS) {
|
|
28892
|
+
if (ranges.length === 0) return [];
|
|
28893
|
+
const sorted = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
|
|
28894
|
+
const visits = [];
|
|
28895
|
+
let pieces = [sorted[0]];
|
|
28896
|
+
let endMs = sorted[0].endMs;
|
|
28897
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
28898
|
+
const next = sorted[i];
|
|
28899
|
+
if (next.startMs <= endMs + gapMs) {
|
|
28900
|
+
pieces.push(next);
|
|
28901
|
+
endMs = Math.max(endMs, next.endMs);
|
|
28902
|
+
continue;
|
|
28903
|
+
}
|
|
28904
|
+
visits.push(finishVisit(pieces, endMs));
|
|
28905
|
+
pieces = [next];
|
|
28906
|
+
endMs = next.endMs;
|
|
28907
|
+
}
|
|
28908
|
+
visits.push(finishVisit(pieces, endMs));
|
|
28909
|
+
return visits;
|
|
28910
|
+
}
|
|
28911
|
+
function finishVisit(pieces, endMs) {
|
|
28912
|
+
const holes = [];
|
|
28913
|
+
for (let i = 1; i < pieces.length; i++) {
|
|
28914
|
+
const prevEnd = pieces[i - 1].endMs;
|
|
28915
|
+
const nextStart = pieces[i].startMs;
|
|
28916
|
+
if (nextStart > prevEnd) holes.push({
|
|
28917
|
+
startMs: prevEnd,
|
|
28918
|
+
endMs: nextStart
|
|
28919
|
+
});
|
|
28920
|
+
}
|
|
28921
|
+
return {
|
|
28922
|
+
startMs: pieces[0].startMs,
|
|
28923
|
+
endMs,
|
|
28924
|
+
ranges: pieces,
|
|
28925
|
+
holes
|
|
28926
|
+
};
|
|
28927
|
+
}
|
|
28928
|
+
var KIND_ORDER = [
|
|
28929
|
+
"object",
|
|
28930
|
+
"motion",
|
|
28931
|
+
"audio"
|
|
28932
|
+
];
|
|
28933
|
+
function eventOverlapsVisit(event, visit, pad) {
|
|
28934
|
+
if (event.timestamp >= visit.startMs && event.timestamp <= visit.endMs) return true;
|
|
28935
|
+
const windowStart = event.timestamp - pad.preMs;
|
|
28936
|
+
const windowEnd = event.timestamp + pad.postMs;
|
|
28937
|
+
return windowStart < visit.endMs && windowEnd > visit.startMs;
|
|
28938
|
+
}
|
|
28939
|
+
function dominantKind(attached) {
|
|
28940
|
+
for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
|
|
28941
|
+
return "motion";
|
|
28942
|
+
}
|
|
28943
|
+
function visitsToClips(visits, events, pad) {
|
|
28944
|
+
const tagged = [
|
|
28945
|
+
...events.object.map((event) => ({
|
|
28946
|
+
kind: "object",
|
|
28947
|
+
event
|
|
28948
|
+
})),
|
|
28949
|
+
...events.motion.map((event) => ({
|
|
28950
|
+
kind: "motion",
|
|
28951
|
+
event
|
|
28952
|
+
})),
|
|
28953
|
+
...events.audio.map((event) => ({
|
|
28954
|
+
kind: "audio",
|
|
28955
|
+
event
|
|
28956
|
+
}))
|
|
28957
|
+
];
|
|
28958
|
+
return visits.map((visit) => {
|
|
28959
|
+
const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
|
|
28960
|
+
const kind = dominantKind(attached);
|
|
28961
|
+
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl) ?? attached.find((a) => a.event.mediaUrl);
|
|
28962
|
+
return {
|
|
28963
|
+
id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
|
|
28964
|
+
source: "analytics",
|
|
28965
|
+
kind,
|
|
28966
|
+
timeRange: {
|
|
28967
|
+
startMs: visit.startMs,
|
|
28968
|
+
endMs: visit.endMs
|
|
28969
|
+
},
|
|
28970
|
+
thumbnail: withThumb?.event.mediaUrl,
|
|
28971
|
+
eventIds: attached.map((a) => a.event.id),
|
|
28972
|
+
holes: [...visit.holes]
|
|
28973
|
+
};
|
|
28974
|
+
}).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
28975
|
+
}
|
|
28976
|
+
//#endregion
|
|
28417
28977
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
28978
|
+
/**
|
|
28979
|
+
* Default `videoclips` provider — composes recorded footage visits with the
|
|
28980
|
+
* analytics event log. A clip is a time-WINDOW over existing footage, never a
|
|
28981
|
+
* separate file: `getClipPlayback` resolves to
|
|
28982
|
+
* `recording.getPlaybackManifest(startMs, endMs)`.
|
|
28983
|
+
*
|
|
28984
|
+
* When availability is present the grain is a *visit* (events-mode keep-window
|
|
28985
|
+
* clustered at `VISIT_MERGE_GAP_MS`), not one clip per motion heartbeat. The
|
|
28986
|
+
* clip id still encodes the window (`analytics:<startMs>:<endMs>:visit`) so
|
|
28987
|
+
* playback is self-contained. Without availability (or when that read fails)
|
|
28988
|
+
* the provider falls back to per-event padded windows.
|
|
28989
|
+
*
|
|
28990
|
+
* Pure helpers (`eventsToClips` / `clusterFootageVisits` / `clipIdFor` /
|
|
28991
|
+
* `parseClipId`) are unit-tested in isolation; the provider factory wires
|
|
28992
|
+
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
28993
|
+
*/
|
|
28418
28994
|
var SOURCE = "analytics";
|
|
28419
28995
|
function clipIdFor(eventId, startMs, endMs) {
|
|
28420
28996
|
return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
|
|
@@ -28457,36 +29033,29 @@ function eventsToClips(events, pad) {
|
|
|
28457
29033
|
...events.audio.map((e) => make(e, "audio"))
|
|
28458
29034
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
28459
29035
|
}
|
|
28460
|
-
/** Keep only clips whose window overlaps at least one recorded range — a clip
|
|
28461
|
-
* is a window over footage, so a window with nothing behind it is unplayable
|
|
28462
|
-
* and must not be listed (a camera that never recorded showed 15 dead audio
|
|
28463
|
-
* "clips" in the viewer ribbon, every tap a 500). Pure. */
|
|
28464
|
-
function filterClipsByFootage(clips, ranges) {
|
|
28465
|
-
return clips.filter((c) => ranges.some((r) => c.timeRange.startMs < r.endMs && c.timeRange.endMs > r.startMs));
|
|
28466
|
-
}
|
|
28467
29036
|
function createVideoclipsProvider(deps) {
|
|
28468
29037
|
return {
|
|
28469
29038
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
28470
|
-
const
|
|
29039
|
+
const events = await deps.fetchEvents({
|
|
28471
29040
|
deviceId,
|
|
28472
29041
|
since,
|
|
28473
29042
|
until,
|
|
28474
|
-
limit
|
|
28475
|
-
})
|
|
28476
|
-
let
|
|
29043
|
+
...deps.fetchAvailability ? {} : { limit }
|
|
29044
|
+
});
|
|
29045
|
+
let clips = eventsToClips(events, deps.defaultPad);
|
|
28477
29046
|
if (deps.fetchAvailability) try {
|
|
28478
|
-
|
|
29047
|
+
clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
|
|
28479
29048
|
deviceId,
|
|
28480
29049
|
since,
|
|
28481
29050
|
until
|
|
28482
|
-
}));
|
|
29051
|
+
}), require_dist.VISIT_MERGE_GAP_MS), events, deps.defaultPad);
|
|
28483
29052
|
} catch (err) {
|
|
28484
29053
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
28485
29054
|
deviceId,
|
|
28486
29055
|
error: err instanceof Error ? err.message : String(err)
|
|
28487
29056
|
});
|
|
28488
29057
|
}
|
|
28489
|
-
return limit != null ?
|
|
29058
|
+
return limit != null ? clips.slice(0, limit) : clips;
|
|
28490
29059
|
},
|
|
28491
29060
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
28492
29061
|
const parsed = parseClipId(clipId);
|
|
@@ -29916,7 +30485,7 @@ var undecided = (candidate, reason) => ({
|
|
|
29916
30485
|
reason,
|
|
29917
30486
|
className: candidate.className
|
|
29918
30487
|
});
|
|
29919
|
-
function withTimeout$
|
|
30488
|
+
function withTimeout$2(promise, timeoutMs) {
|
|
29920
30489
|
return new Promise((resolve, reject) => {
|
|
29921
30490
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("confirmation-timeout")), timeoutMs);
|
|
29922
30491
|
promise.then((value) => {
|
|
@@ -29956,7 +30525,7 @@ async function confirmOne(candidate, config, deps, exhausted) {
|
|
|
29956
30525
|
const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
|
|
29957
30526
|
if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
|
|
29958
30527
|
try {
|
|
29959
|
-
return await withTimeout$
|
|
30528
|
+
return await withTimeout$2(runConfirmation(candidate, config, deps, exhausted), config.timeoutMs);
|
|
29960
30529
|
} catch {
|
|
29961
30530
|
return undecided(candidate, "timeout");
|
|
29962
30531
|
}
|
|
@@ -34203,7 +34772,8 @@ function isMediaOwnerKind(value) {
|
|
|
34203
34772
|
* write="one row per blob written through put / putReplacing — a track's
|
|
34204
34773
|
* keyFrame, thumbnail, firstFrame and lastFrame (UPSERTED on a deterministic
|
|
34205
34774
|
* id, so one per track+kind however many captures race), its periodic
|
|
34206
|
-
* snapshots, each event's crop and full frames,
|
|
34775
|
+
* snapshots, each event's crop and full frames, a motion visit's still
|
|
34776
|
+
* (`kind: snapshot` on ownerKind event), and the buffered face/plate
|
|
34207
34777
|
* crops. A row is also REOWNED here (not rewritten) when the operator enrols a
|
|
34208
34778
|
* face or a plate, which is what moves it out of retention's reach."
|
|
34209
34779
|
* retention="no age sweep of its own — MediaStore.evictBefore was deleted (D59)
|
|
@@ -35615,7 +36185,8 @@ async function rankKeyEvents(candidates, options, peakLookup) {
|
|
|
35615
36185
|
windowMs: t.lastSeen - t.firstSeen,
|
|
35616
36186
|
...t.markForTrain !== void 0 ? { markForTrain: t.markForTrain } : {},
|
|
35617
36187
|
...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
|
|
35618
|
-
...t.debug !== void 0 ? { debug: t.debug } : {}
|
|
36188
|
+
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
36189
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {}
|
|
35619
36190
|
});
|
|
35620
36191
|
}
|
|
35621
36192
|
scored.sort((a, b) => b.importance - a.importance);
|
|
@@ -37258,7 +37829,8 @@ async function applyTrackFlags(deps, input) {
|
|
|
37258
37829
|
}
|
|
37259
37830
|
const patch = {
|
|
37260
37831
|
...input.flags.markForTrain !== void 0 ? { markForTrain: input.flags.markForTrain } : {},
|
|
37261
|
-
...input.flags.debug !== void 0 ? { debug: input.flags.debug } : {}
|
|
37832
|
+
...input.flags.debug !== void 0 ? { debug: input.flags.debug } : {},
|
|
37833
|
+
...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {}
|
|
37262
37834
|
};
|
|
37263
37835
|
if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
|
|
37264
37836
|
const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
|
|
@@ -37266,6 +37838,7 @@ async function applyTrackFlags(deps, input) {
|
|
|
37266
37838
|
trackId: input.trackId,
|
|
37267
37839
|
markForTrain: retrainStatus === "staging",
|
|
37268
37840
|
debug: patch.debug ?? current.debug,
|
|
37841
|
+
favourited: patch.favourited ?? current.favourited,
|
|
37269
37842
|
retrainStatus
|
|
37270
37843
|
};
|
|
37271
37844
|
}
|
|
@@ -38506,11 +39079,14 @@ function tieredLabelColumnData(patch) {
|
|
|
38506
39079
|
* (MOTION_EVENT_HEARTBEAT_MS) while it stays on. The analyzer path and the
|
|
38507
39080
|
* onboard-firmware path share that throttle, so a camera cannot double-count.
|
|
38508
39081
|
* Measured at ~7,200 rows/day/camera."
|
|
38509
|
-
* retention="a TERMINAL age-keyed root — no trackId
|
|
38510
|
-
*
|
|
39082
|
+
* retention="a TERMINAL age-keyed root — no trackId. A motion row may own a
|
|
39083
|
+
* still (`ownerKind: 'event'`, `kind: 'snapshot'`) captured on the off→on
|
|
39084
|
+
* visit so events-mode recordings have a videoclip thumbnail. Deleted by
|
|
38511
39085
|
* EventStore.evictTracklessBefore on the DEVICE's one retention window
|
|
38512
|
-
* (follow-recordings horizon, or trackRetentionDays, default 7)
|
|
38513
|
-
*
|
|
39086
|
+
* (follow-recordings horizon, or trackRetentionDays, default 7), which
|
|
39087
|
+
* folds in MediaStore.deleteForEvents so the still leaves with the row.
|
|
39088
|
+
* 0 days = keep forever, and this table is the reason that setting is
|
|
39089
|
+
* dangerous."
|
|
38514
39090
|
*/
|
|
38515
39091
|
var MOTION_EVENTS_COLLECTION = "pipeline-analytics:motion-events";
|
|
38516
39092
|
/**
|
|
@@ -39239,9 +39815,10 @@ var EventStore = class {
|
|
|
39239
39815
|
* Object events now leave exactly one way: with their track, through
|
|
39240
39816
|
* {@link deleteByTracks} (D61).
|
|
39241
39817
|
*
|
|
39242
|
-
* Motion and audio carry no `trackId
|
|
39243
|
-
*
|
|
39244
|
-
*
|
|
39818
|
+
* Motion and audio carry no `trackId` and are referenced by nothing — they
|
|
39819
|
+
* are terminal retention ROOTS, so an age window on them cannot contradict
|
|
39820
|
+
* anybody's (`retention/retention-model.ts`). A motion visit still is
|
|
39821
|
+
* event-owned media of that row and is deleted here in lockstep.
|
|
39245
39822
|
*
|
|
39246
39823
|
* **ONE cutoff for both kinds** (D62). They used to take a window each
|
|
39247
39824
|
* (`retentionMotionDays` / `retentionAudioDays`), which is two clocks over
|
|
@@ -39258,9 +39835,17 @@ var EventStore = class {
|
|
|
39258
39835
|
...params.deviceId !== void 0 ? { where: { deviceId: params.deviceId } } : {},
|
|
39259
39836
|
whereBetween: { timestamp: [0, params.cutoffMs] }
|
|
39260
39837
|
});
|
|
39838
|
+
const motion = await batch(MOTION_EVENTS_COLLECTION);
|
|
39839
|
+
const audio = await batch(AUDIO_EVENTS_COLLECTION);
|
|
39840
|
+
const ids = [...motion, ...audio];
|
|
39841
|
+
if (ids.length > 0 && this.media !== void 0) try {
|
|
39842
|
+
await this.media.deleteForEvents(ids);
|
|
39843
|
+
} catch (err) {
|
|
39844
|
+
this.logger.warn("EventStore.evictTracklessBefore: event media delete failed", { meta: { error: String(err) } });
|
|
39845
|
+
}
|
|
39261
39846
|
return {
|
|
39262
|
-
motion
|
|
39263
|
-
audio
|
|
39847
|
+
motion,
|
|
39848
|
+
audio
|
|
39264
39849
|
};
|
|
39265
39850
|
}
|
|
39266
39851
|
/**
|
|
@@ -39285,23 +39870,32 @@ var EventStore = class {
|
|
|
39285
39870
|
* The SUBSET of `eventIds` that still exist — the orphan audit's liveness
|
|
39286
39871
|
* probe for event-owned media.
|
|
39287
39872
|
*
|
|
39873
|
+
* Motion and audio rows can own stills (`kind: 'snapshot'` on a motion
|
|
39874
|
+
* visit); probing only the object collection would classify those stills
|
|
39875
|
+
* as orphans the moment they were written.
|
|
39876
|
+
*
|
|
39288
39877
|
* THROWS on a query failure rather than answering partially. A probe that
|
|
39289
39878
|
* quietly under-reports here reads as "these events are gone", and the caller
|
|
39290
39879
|
* deletes live crops on the strength of it.
|
|
39291
39880
|
*/
|
|
39292
39881
|
async existingEventIds(eventIds) {
|
|
39293
39882
|
const found = /* @__PURE__ */ new Set();
|
|
39883
|
+
const collections = [
|
|
39884
|
+
OBJECT_EVENTS_COLLECTION,
|
|
39885
|
+
MOTION_EVENTS_COLLECTION,
|
|
39886
|
+
AUDIO_EVENTS_COLLECTION
|
|
39887
|
+
];
|
|
39294
39888
|
for (let i = 0; i < eventIds.length; i += 500) {
|
|
39295
39889
|
const chunk = eventIds.slice(i, i + 500);
|
|
39296
39890
|
if (chunk.length === 0) continue;
|
|
39297
|
-
const
|
|
39298
|
-
collection
|
|
39891
|
+
const pages = await Promise.all(collections.map((collection) => this.store.query.query({
|
|
39892
|
+
collection,
|
|
39299
39893
|
filter: {
|
|
39300
39894
|
whereIn: { id: [...chunk] },
|
|
39301
39895
|
limit: chunk.length
|
|
39302
39896
|
}
|
|
39303
|
-
});
|
|
39304
|
-
for (const row of rows) found.add(row.id);
|
|
39897
|
+
})));
|
|
39898
|
+
for (const rows of pages) for (const row of rows) found.add(row.id);
|
|
39305
39899
|
}
|
|
39306
39900
|
return found;
|
|
39307
39901
|
}
|
|
@@ -41309,6 +41903,10 @@ var TRACKS_COLUMNS = [
|
|
|
41309
41903
|
name: "debug",
|
|
41310
41904
|
type: "BOOLEAN"
|
|
41311
41905
|
},
|
|
41906
|
+
{
|
|
41907
|
+
name: "favourited",
|
|
41908
|
+
type: "BOOLEAN"
|
|
41909
|
+
},
|
|
41312
41910
|
{
|
|
41313
41911
|
name: "hasFace",
|
|
41314
41912
|
type: "BOOLEAN"
|
|
@@ -41402,6 +42000,7 @@ function cloneTrack(t) {
|
|
|
41402
42000
|
markForTrain: t.retrainStatus === "staging"
|
|
41403
42001
|
} : {},
|
|
41404
42002
|
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
42003
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {},
|
|
41405
42004
|
...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
|
|
41406
42005
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
41407
42006
|
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {}
|
|
@@ -41747,11 +42346,13 @@ var TrackStore = class {
|
|
|
41747
42346
|
const nextStatus = patch.markForTrain === void 0 ? void 0 : patch.markForTrain ? "staging" : "none";
|
|
41748
42347
|
if (nextStatus !== void 0) data["retrainStatus"] = nextStatus;
|
|
41749
42348
|
if (patch.debug !== void 0) data["debug"] = patch.debug;
|
|
42349
|
+
if (patch.favourited !== void 0) data["favourited"] = patch.favourited;
|
|
41750
42350
|
if (Object.keys(data).length === 0) return;
|
|
41751
42351
|
const active = this.active.get(trackId);
|
|
41752
42352
|
if (active) {
|
|
41753
42353
|
if (nextStatus !== void 0) active.retrainStatus = nextStatus;
|
|
41754
42354
|
if (patch.debug !== void 0) active.debug = patch.debug;
|
|
42355
|
+
if (patch.favourited !== void 0) active.favourited = patch.favourited;
|
|
41755
42356
|
}
|
|
41756
42357
|
try {
|
|
41757
42358
|
await this.store.update.mutate({
|
|
@@ -41892,13 +42493,15 @@ var TrackStore = class {
|
|
|
41892
42493
|
const active = this.active.get(trackId);
|
|
41893
42494
|
if (active) return {
|
|
41894
42495
|
retrainStatus: active.retrainStatus ?? "none",
|
|
41895
|
-
debug: active.debug === true
|
|
42496
|
+
debug: active.debug === true,
|
|
42497
|
+
favourited: active.favourited === true
|
|
41896
42498
|
};
|
|
41897
42499
|
const persisted = await this.getPersistedByTrackId(trackId);
|
|
41898
42500
|
if (!persisted) return null;
|
|
41899
42501
|
return {
|
|
41900
42502
|
retrainStatus: persisted.retrainStatus ?? "none",
|
|
41901
|
-
debug: persisted.debug === true
|
|
42503
|
+
debug: persisted.debug === true,
|
|
42504
|
+
favourited: persisted.favourited === true
|
|
41902
42505
|
};
|
|
41903
42506
|
}
|
|
41904
42507
|
/**
|
|
@@ -42033,6 +42636,7 @@ var TrackStore = class {
|
|
|
42033
42636
|
filter: {
|
|
42034
42637
|
where: { deviceId },
|
|
42035
42638
|
whereIn: { retrainStatus: ["none", "trained"] },
|
|
42639
|
+
whereNot: { favourited: true },
|
|
42036
42640
|
whereBetween: { lastSeen: [0, cutoffMs - 1] },
|
|
42037
42641
|
orderBy: {
|
|
42038
42642
|
field: "lastSeen",
|
|
@@ -42202,6 +42806,40 @@ var TrackStore = class {
|
|
|
42202
42806
|
}
|
|
42203
42807
|
}
|
|
42204
42808
|
/**
|
|
42809
|
+
* How many of this device's tracks are FAVOURITED — optionally only those
|
|
42810
|
+
* older than a cutoff, i.e. the rows a sweep just declined to touch because
|
|
42811
|
+
* of the boolean pin (independent of `retrainStatus`).
|
|
42812
|
+
*
|
|
42813
|
+
* Mirrors {@link countStaging}: the sweep log must name held work or a
|
|
42814
|
+
* favourited-only camera reads as "retention did nothing". Best-effort — a
|
|
42815
|
+
* failure reports 0 rather than disturbing the caller.
|
|
42816
|
+
*/
|
|
42817
|
+
async countFavourited(query) {
|
|
42818
|
+
const { deviceId, olderThanMs } = query;
|
|
42819
|
+
try {
|
|
42820
|
+
return await this.store.count.query({
|
|
42821
|
+
collection: TRACKS_COLLECTION,
|
|
42822
|
+
filter: {
|
|
42823
|
+
where: {
|
|
42824
|
+
deviceId,
|
|
42825
|
+
favourited: true
|
|
42826
|
+
},
|
|
42827
|
+
...olderThanMs !== void 0 ? { whereBetween: { lastSeen: [0, olderThanMs - 1] } } : {}
|
|
42828
|
+
}
|
|
42829
|
+
});
|
|
42830
|
+
} catch (err) {
|
|
42831
|
+
this.logger.warn("TrackStore.countFavourited failed", {
|
|
42832
|
+
tags: { deviceId },
|
|
42833
|
+
meta: {
|
|
42834
|
+
deviceId,
|
|
42835
|
+
olderThanMs,
|
|
42836
|
+
error: String(err)
|
|
42837
|
+
}
|
|
42838
|
+
});
|
|
42839
|
+
return 0;
|
|
42840
|
+
}
|
|
42841
|
+
}
|
|
42842
|
+
/**
|
|
42205
42843
|
* The SUBSET of `trackIds` that still have a persisted row — the orphan
|
|
42206
42844
|
* audit's liveness probe.
|
|
42207
42845
|
*
|
|
@@ -42574,6 +43212,7 @@ var TrackStore = class {
|
|
|
42574
43212
|
...t.audioLabels !== void 0 ? { audioLabels: [...t.audioLabels] } : {},
|
|
42575
43213
|
...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
|
|
42576
43214
|
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
43215
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {},
|
|
42577
43216
|
...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
|
|
42578
43217
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
42579
43218
|
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
@@ -42611,6 +43250,7 @@ var TrackStore = class {
|
|
|
42611
43250
|
const audioLabels = data["audioLabels"];
|
|
42612
43251
|
const retrainParsed = require_dist.RetrainStatusSchema.safeParse(data["retrainStatus"]);
|
|
42613
43252
|
const debug = data["debug"];
|
|
43253
|
+
const favourited = data["favourited"];
|
|
42614
43254
|
const hasFace = data["hasFace"];
|
|
42615
43255
|
/** Same read as `hasFace` — NULL / missing stays ABSENT, never `false`. */
|
|
42616
43256
|
const hasRider = data["hasRider"];
|
|
@@ -42650,6 +43290,7 @@ var TrackStore = class {
|
|
|
42650
43290
|
markForTrain: retrainParsed.data === "staging"
|
|
42651
43291
|
} : {},
|
|
42652
43292
|
...typeof debug === "boolean" ? { debug } : {},
|
|
43293
|
+
...typeof favourited === "boolean" ? { favourited } : {},
|
|
42653
43294
|
...typeof hasFace === "boolean" ? { hasFace } : {},
|
|
42654
43295
|
...typeof hasRider === "boolean" ? { hasRider } : {},
|
|
42655
43296
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
@@ -44672,7 +45313,10 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
44672
45313
|
require_dist.EventCategory.WorkerOffline,
|
|
44673
45314
|
require_dist.EventCategory.PipelineNodeInferenceUnavailable,
|
|
44674
45315
|
require_dist.EventCategory.PipelineDetectionBlind,
|
|
44675
|
-
require_dist.EventCategory.UpdateAvailable
|
|
45316
|
+
require_dist.EventCategory.UpdateAvailable,
|
|
45317
|
+
require_dist.EventCategory.AddonUpdated,
|
|
45318
|
+
require_dist.EventCategory.SystemRestartCompleted,
|
|
45319
|
+
require_dist.EventCategory.RecordingExportCompleted
|
|
44676
45320
|
];
|
|
44677
45321
|
/**
|
|
44678
45322
|
* Install the addon's bus subscriptions. Identical set + relative order to the
|
|
@@ -46109,16 +46753,16 @@ var DEFAULT_DEBOUNCE_MS = 1e4;
|
|
|
46109
46753
|
/** A hanging snapshot cap must not stall the sensor state-change path. */
|
|
46110
46754
|
var DEFAULT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
46111
46755
|
/** Distinguishes a snapshot-cap timeout from a fetch failure for reporting. */
|
|
46112
|
-
var SnapshotTimeoutError = class extends Error {
|
|
46756
|
+
var SnapshotTimeoutError$1 = class extends Error {
|
|
46113
46757
|
constructor(ms) {
|
|
46114
46758
|
super(`snapshot cap timed out after ${ms}ms`);
|
|
46115
46759
|
this.name = "SnapshotTimeoutError";
|
|
46116
46760
|
}
|
|
46117
46761
|
};
|
|
46118
46762
|
/** Reject with `SnapshotTimeoutError` if `promise` doesn't settle within `ms`. */
|
|
46119
|
-
function withTimeout(promise, ms) {
|
|
46763
|
+
function withTimeout$1(promise, ms) {
|
|
46120
46764
|
return new Promise((resolve, reject) => {
|
|
46121
|
-
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46765
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError$1(ms)), ms);
|
|
46122
46766
|
promise.then((value) => {
|
|
46123
46767
|
clearTimeout(timer);
|
|
46124
46768
|
resolve(value);
|
|
@@ -46173,11 +46817,11 @@ var SyntheticTrackMaterializer = class {
|
|
|
46173
46817
|
*/
|
|
46174
46818
|
async fetchSnapshot(cameraId, timeoutMs) {
|
|
46175
46819
|
try {
|
|
46176
|
-
const snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46820
|
+
const snap = await withTimeout$1(this.deps.snapshot.getSnapshot({ deviceId: cameraId }), timeoutMs);
|
|
46177
46821
|
if (snap === null) this.deps.onError?.("getSnapshot", /* @__PURE__ */ new Error("snapshot returned null"), cameraId);
|
|
46178
46822
|
return snap;
|
|
46179
46823
|
} catch (err) {
|
|
46180
|
-
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46824
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError$1 ? "snapshotTimeout" : "snapshotMedia", err, cameraId);
|
|
46181
46825
|
return null;
|
|
46182
46826
|
}
|
|
46183
46827
|
}
|
|
@@ -46243,6 +46887,66 @@ var SyntheticTrackMaterializer = class {
|
|
|
46243
46887
|
return track;
|
|
46244
46888
|
}
|
|
46245
46889
|
};
|
|
46890
|
+
var SnapshotTimeoutError = class extends Error {
|
|
46891
|
+
constructor(ms) {
|
|
46892
|
+
super(`snapshot cap timed out after ${ms}ms`);
|
|
46893
|
+
this.name = "SnapshotTimeoutError";
|
|
46894
|
+
}
|
|
46895
|
+
};
|
|
46896
|
+
function withTimeout(promise, ms) {
|
|
46897
|
+
return new Promise((resolve, reject) => {
|
|
46898
|
+
const timer = setTimeout(() => reject(new SnapshotTimeoutError(ms)), ms);
|
|
46899
|
+
promise.then((value) => {
|
|
46900
|
+
clearTimeout(timer);
|
|
46901
|
+
resolve(value);
|
|
46902
|
+
}, (err) => {
|
|
46903
|
+
clearTimeout(timer);
|
|
46904
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
46905
|
+
});
|
|
46906
|
+
});
|
|
46907
|
+
}
|
|
46908
|
+
var MotionEventSnapshotCapturer = class {
|
|
46909
|
+
deps;
|
|
46910
|
+
snapshotTimeoutMs;
|
|
46911
|
+
constructor(deps) {
|
|
46912
|
+
this.deps = deps;
|
|
46913
|
+
this.snapshotTimeoutMs = deps.snapshotTimeoutMs ?? 2500;
|
|
46914
|
+
}
|
|
46915
|
+
/**
|
|
46916
|
+
* Fetch a still and persist it as the motion event's snapshot. Never
|
|
46917
|
+
* throws — the durable record is the motion row, already inserted by
|
|
46918
|
+
* the caller.
|
|
46919
|
+
*/
|
|
46920
|
+
async capture(input) {
|
|
46921
|
+
let snap;
|
|
46922
|
+
try {
|
|
46923
|
+
snap = await withTimeout(this.deps.snapshot.getSnapshot({ deviceId: input.deviceId }), this.snapshotTimeoutMs);
|
|
46924
|
+
} catch (err) {
|
|
46925
|
+
this.deps.onError?.(err instanceof SnapshotTimeoutError ? "snapshotTimeout" : "snapshotFetch", err, input.deviceId);
|
|
46926
|
+
return;
|
|
46927
|
+
}
|
|
46928
|
+
if (snap === null) return;
|
|
46929
|
+
const raw = Buffer.from(snap.base64, "base64");
|
|
46930
|
+
let data = raw;
|
|
46931
|
+
try {
|
|
46932
|
+
data = await downscaleFullFrameJpeg(raw, 960, 540);
|
|
46933
|
+
} catch (err) {
|
|
46934
|
+
this.deps.onError?.("downscaleSnapshot", err, input.deviceId);
|
|
46935
|
+
}
|
|
46936
|
+
try {
|
|
46937
|
+
await this.deps.media.put({
|
|
46938
|
+
deviceId: input.deviceId,
|
|
46939
|
+
ownerKind: "event",
|
|
46940
|
+
ownerId: input.eventId,
|
|
46941
|
+
kind: "snapshot",
|
|
46942
|
+
timestamp: input.timestamp,
|
|
46943
|
+
data
|
|
46944
|
+
});
|
|
46945
|
+
} catch (err) {
|
|
46946
|
+
this.deps.onError?.("snapshotMedia", err, input.deviceId);
|
|
46947
|
+
}
|
|
46948
|
+
}
|
|
46949
|
+
};
|
|
46246
46950
|
var TrackingSettingsSchema = require_dist.object({
|
|
46247
46951
|
/**
|
|
46248
46952
|
* How much of a detection's box must lie inside a zone (0-1 fraction of the
|
|
@@ -49213,6 +49917,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
49213
49917
|
/** The ONE projector of non-spatial events into synthetic snapshot-carrying
|
|
49214
49918
|
* tracks — linked sensor/control state changes AND audio markers. */
|
|
49215
49919
|
syntheticTracks = null;
|
|
49920
|
+
/** Still for a motion row that has no track (events-mode recording without
|
|
49921
|
+
* a detection). Lands as event-owned `kind: 'snapshot'` so videoclip
|
|
49922
|
+
* thumbnails resolve through the existing event-media plane. */
|
|
49923
|
+
motionEventSnapshots = null;
|
|
49216
49924
|
/** Decides which audio frames deserve a synthetic marker (D62). */
|
|
49217
49925
|
audioMarkers = null;
|
|
49218
49926
|
faceStore = null;
|
|
@@ -50406,6 +51114,17 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50406
51114
|
} })
|
|
50407
51115
|
});
|
|
50408
51116
|
this.syntheticTracks = syntheticTracks;
|
|
51117
|
+
this.motionEventSnapshots = new MotionEventSnapshotCapturer({
|
|
51118
|
+
snapshot: { getSnapshot: (i) => api.snapshot?.getSnapshot.query(i) ?? Promise.resolve(null) },
|
|
51119
|
+
media: mediaStore,
|
|
51120
|
+
onError: (scope, err, deviceId) => logger.warn("motion event snapshot failed", {
|
|
51121
|
+
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
51122
|
+
meta: {
|
|
51123
|
+
scope,
|
|
51124
|
+
error: require_dist.errMsg(err)
|
|
51125
|
+
}
|
|
51126
|
+
})
|
|
51127
|
+
});
|
|
50409
51128
|
this.audioMarkers = new AudioMarkerProjector({
|
|
50410
51129
|
sink: syntheticTracks,
|
|
50411
51130
|
isMotionActive: (deviceId, atMs, quietMs) => this.isMotionActive(deviceId, atMs, quietMs),
|
|
@@ -50771,6 +51490,10 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
50771
51490
|
},
|
|
50772
51491
|
renderEventMedia: (req) => this.renderEventMedia(req),
|
|
50773
51492
|
publishArtifact: async (bytes, mime) => await this.ncArtifactPlane?.publish(bytes, mime) ?? null,
|
|
51493
|
+
indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
|
|
51494
|
+
url: null,
|
|
51495
|
+
artifactId: null
|
|
51496
|
+
},
|
|
50774
51497
|
listTargets: async () => {
|
|
50775
51498
|
return (await api.notificationOutput.listTargets.query({})).map((t) => {
|
|
50776
51499
|
const owner = t.config["ownerUserId"];
|
|
@@ -53472,7 +54195,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53472
54195
|
const last = this.lastMotionInsertByDevice.get(deviceId);
|
|
53473
54196
|
const heartbeatDue = !last || timestamp - last.atMs >= MOTION_EVENT_HEARTBEAT_MS;
|
|
53474
54197
|
const transitionedOn = detected && (last === void 0 || last.detected === false);
|
|
53475
|
-
if (!detected)
|
|
54198
|
+
if (!detected) {
|
|
54199
|
+
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
54200
|
+
detected: false,
|
|
54201
|
+
atMs: timestamp
|
|
54202
|
+
});
|
|
54203
|
+
return;
|
|
54204
|
+
}
|
|
53476
54205
|
if (!transitionedOn && !heartbeatDue) return;
|
|
53477
54206
|
const ev = {
|
|
53478
54207
|
id: (0, node_crypto.randomUUID)(),
|
|
@@ -53515,6 +54244,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53515
54244
|
timestamp: ev.timestamp
|
|
53516
54245
|
}
|
|
53517
54246
|
});
|
|
54247
|
+
if (transitionedOn) await this.motionEventSnapshots?.capture({
|
|
54248
|
+
deviceId,
|
|
54249
|
+
eventId: ev.id,
|
|
54250
|
+
timestamp: ev.timestamp
|
|
54251
|
+
});
|
|
53518
54252
|
}
|
|
53519
54253
|
/**
|
|
53520
54254
|
* Persist firmware-driven (onboard) motion events. Mirrors
|
|
@@ -53539,7 +54273,13 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53539
54273
|
const last = this.lastMotionInsertByDevice.get(deviceId);
|
|
53540
54274
|
const heartbeatDue = !last || timestamp - last.atMs >= MOTION_EVENT_HEARTBEAT_MS;
|
|
53541
54275
|
const transitionedOn = detected && (last === void 0 || last.detected === false);
|
|
53542
|
-
if (!detected)
|
|
54276
|
+
if (!detected) {
|
|
54277
|
+
if (last?.detected === true) this.lastMotionInsertByDevice.set(deviceId, {
|
|
54278
|
+
detected: false,
|
|
54279
|
+
atMs: timestamp
|
|
54280
|
+
});
|
|
54281
|
+
return;
|
|
54282
|
+
}
|
|
53543
54283
|
if (!transitionedOn && !heartbeatDue) return;
|
|
53544
54284
|
const regions = payload.regions ?? [];
|
|
53545
54285
|
const ev = {
|
|
@@ -53583,6 +54323,11 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53583
54323
|
timestamp: ev.timestamp
|
|
53584
54324
|
}
|
|
53585
54325
|
});
|
|
54326
|
+
if (transitionedOn) await this.motionEventSnapshots?.capture({
|
|
54327
|
+
deviceId,
|
|
54328
|
+
eventId: ev.id,
|
|
54329
|
+
timestamp: ev.timestamp
|
|
54330
|
+
});
|
|
53586
54331
|
}
|
|
53587
54332
|
async sweepExpiredTracks() {
|
|
53588
54333
|
if (this.shuttingDown || !this.trackStore) return;
|
|
@@ -53651,8 +54396,9 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
53651
54396
|
*
|
|
53652
54397
|
* 1. the track sweep, per device, cascading;
|
|
53653
54398
|
* 2. the TRACKLESS roots — motion, audio and sensor events, which carry no
|
|
53654
|
-
* trackId
|
|
53655
|
-
*
|
|
54399
|
+
* trackId and are referenced by nothing, so the cascade can never reach
|
|
54400
|
+
* them and a clock is the only thing that can. Motion visit stills leave
|
|
54401
|
+
* with their row inside `evictTracklessBefore`.
|
|
53656
54402
|
*
|
|
53657
54403
|
* That clock used to be three more settings (`retentionMotionDays` /
|
|
53658
54404
|
* `retentionObjectDays` / `retentionAudioDays`). It is now the device's own
|
|
@@ -55431,6 +56177,19 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
55431
56177
|
deleted: counts.tracks
|
|
55432
56178
|
}
|
|
55433
56179
|
});
|
|
56180
|
+
const favourited = await this.trackStore?.countFavourited({
|
|
56181
|
+
deviceId,
|
|
56182
|
+
olderThanMs: cutoffMs
|
|
56183
|
+
});
|
|
56184
|
+
if (favourited !== void 0 && favourited > 0) this.ctx.logger.info("track retention held favourited tracks", {
|
|
56185
|
+
tags: { deviceId },
|
|
56186
|
+
meta: {
|
|
56187
|
+
deviceId,
|
|
56188
|
+
favourited,
|
|
56189
|
+
cutoffMs,
|
|
56190
|
+
deleted: counts.tracks
|
|
56191
|
+
}
|
|
56192
|
+
});
|
|
55434
56193
|
if (counts.tracks > 0) this.eventsOpsLog?.append({
|
|
55435
56194
|
op: "prune",
|
|
55436
56195
|
reason: "retention",
|