@camstack/addon-post-analysis 1.2.83 → 1.2.85
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-Owxednhs.js → dist-Crk9pYee.js} +94 -11
- package/dist/{dist-DFUffKyJ.mjs → dist-CzdBsvoR.mjs} +89 -12
- 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-DJ0Laqx0.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-GLqrUCoW.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-CBUX_Xu3.mjs → hostInit-DtD0Fd_e.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +698 -59
- package/dist/pipeline-analytics/index.mjs +697 -59
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { At as
|
|
1
|
+
import { $ as faceGalleryCapability, At as record, B as VISIT_MERGE_GAP_MS, C as NcSnoozeSchema, Ct as array, D as OpsLogEntrySchema, Dt as number, E as NcTaxonomySchema, Et as literal, F as TIMELAPSE_DENSE_FLOOR_SEC, G as audioModeOf, H as alarmPanelCapability, I as TimelapseRuleInputSchema, J as customAction, K as buildEventKindDescriptor, L as TimelapseRulePatchSchema, M as SCENE_DEFAULT_UNCOVERED_POLICY, Mt as unknown, N as SCENE_DIVERGED, Nt as EventCategory, O as RECORDING_EXPORT_MAX_READ_BYTES, Ot as object, P as SceneMonitorSchema, Q as encodeVectorBase64, R as TimelapseRuleSchema, S as NcSnoozeInputSchema, St as _enum, T as NcSystemEventKindSchema, Tt as discriminatedUnion, U as assertTimelapseCadences, V as addonWidgetsSourceCapability, W as audioMetricsCapability, X as deriveRecordingMode, Y as defineCustomActions, _ as NcRuleInputSchema, _t as createEvent, at as pipelineAnalyticsCapability, b as NcRuleTargetSchema, bt as nodePin, c as EVENT_PAD_MS, ct as readTimelapseGeneratedAt, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as vectorDimFromBase64, f as NC_CONDITION_CATALOG, ft as videoclipsCapability, g as NcConditionDescriptorSchema, gt as DeviceType, h as NC_TAXONOMY, ht as BaseAddon, i as DETECTION_MACRO_CLASSES, it as notificationRulesCapability, j as SCENE_DEFAULT_ANCHOR_THRESHOLD, jt as string, k as RetrainStatusSchema, kt as partialRecord, l as LabelAttributionSchema, lt as sceneMonitorCapability, mt as errMsg, n as DEFAULT_EVENT_COLOR, nt as isScheduleActive, o as DeclaredDevices, ot as plateGalleryCapability, p as NC_DEFAULT_SNOOZE_MINUTES, pt as zoneAnalyticsCapability, q as cosineSimilarity$1, rt as kebabToCamel, s as EVENT_KIND_BY_CAP, st as readDeviceStateFrom, t as BaseDevice, tt as isDetectionMacroClass, u as MACRO_LABELS, ut as subKindsOf, v as NcRulePatchSchema, vt as hydrateSchema, w as NcSnoozeSuppressedSchema, wt as boolean, x as NcScheduleSchema, xt as sleep, y as NcRuleSchema, yt as isDeviceScopedCap, z as TrackSourceSchema } from "../dist-CzdBsvoR.mjs";
|
|
2
2
|
import { t as __exportAll } from "../embedding-encoder/index.mjs";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import { promises } from "node:fs";
|
|
@@ -1580,11 +1580,31 @@ var NC_ALARM_DECLARATION = {
|
|
|
1580
1580
|
* 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)"
|
|
1581
1581
|
*/
|
|
1582
1582
|
var NC_ARTIFACTS_COLLECTION = "notification-center:artifacts";
|
|
1583
|
-
|
|
1583
|
+
/**
|
|
1584
|
+
* What an artefact IS. The schema lives in the ADDON, not in `packages/types`,
|
|
1585
|
+
* for the same reason the summary rule's does: it ships on `camstack deploy`
|
|
1586
|
+
* and never waits for a framework train.
|
|
1587
|
+
*
|
|
1588
|
+
* TWO groups:
|
|
1589
|
+
* - **Gallery tiles** — what the operator browses (timelapse, gif, mp4, mosaic).
|
|
1590
|
+
* - **Posters** — companion stills (`timelapse-preview`, `event-still`). They
|
|
1591
|
+
* exist so a video/gif tile can name its thumbnail via `posterArtifactId`.
|
|
1592
|
+
* They are not a filter chip and they are omitted from an unfiltered list.
|
|
1593
|
+
*/
|
|
1594
|
+
var NC_GALLERY_ARTIFACT_KINDS = [
|
|
1584
1595
|
"timelapse",
|
|
1585
|
-
"
|
|
1596
|
+
"event-gif",
|
|
1597
|
+
"event-clip",
|
|
1586
1598
|
"summary-mosaic"
|
|
1587
|
-
]
|
|
1599
|
+
];
|
|
1600
|
+
var NC_POSTER_ARTIFACT_KINDS = ["timelapse-preview", "event-still"];
|
|
1601
|
+
var NcArtifactKindSchema = _enum([...NC_GALLERY_ARTIFACT_KINDS, ...NC_POSTER_ARTIFACT_KINDS]);
|
|
1602
|
+
/** Kind a notification attachment is filed under in the gallery. */
|
|
1603
|
+
function artifactKindForMediaType(mediaType) {
|
|
1604
|
+
if (mediaType === "gif") return "event-gif";
|
|
1605
|
+
if (mediaType === "video") return "event-clip";
|
|
1606
|
+
return "event-still";
|
|
1607
|
+
}
|
|
1588
1608
|
/**
|
|
1589
1609
|
* How the viewer should DRAW it. Derived from the mime once, here, so no client
|
|
1590
1610
|
* has to carry a mime table — and so a GIF that degraded to a JPEG is drawn as
|
|
@@ -1943,7 +1963,7 @@ var NcArtifactIndex = class {
|
|
|
1943
1963
|
}
|
|
1944
1964
|
buildFilter(query, viewer) {
|
|
1945
1965
|
const whereIn = { ...this.visibility(viewer).whereIn };
|
|
1946
|
-
|
|
1966
|
+
whereIn["kind"] = query.kinds !== void 0 && query.kinds.length > 0 ? [...query.kinds] : [...NC_GALLERY_ARTIFACT_KINDS];
|
|
1947
1967
|
if (query.deviceIds !== void 0 && query.deviceIds.length > 0) whereIn["deviceId"] = [...query.deviceIds];
|
|
1948
1968
|
const whereBetween = {
|
|
1949
1969
|
capturedAt: [query.fromMs ?? TIME_MIN, query.toMs ?? TIME_MAX],
|
|
@@ -7763,10 +7783,24 @@ function matchesSystemEvent(condition, subject) {
|
|
|
7763
7783
|
if (!condition.kinds.includes(subject.kind)) return false;
|
|
7764
7784
|
if (condition.deviceIds !== void 0 && (subject.deviceId === void 0 || !condition.deviceIds.includes(subject.deviceId))) return false;
|
|
7765
7785
|
if (condition.deviceTypes !== void 0 && (subject.deviceType === void 0 || !condition.deviceTypes.includes(subject.deviceType))) return false;
|
|
7766
|
-
if (condition.nodeIds !== void 0
|
|
7767
|
-
|
|
7786
|
+
if (condition.nodeIds !== void 0) {
|
|
7787
|
+
if (!subjectNodes(subject).some((id) => condition.nodeIds.includes(id))) return false;
|
|
7788
|
+
}
|
|
7789
|
+
if (condition.packageNames !== void 0) {
|
|
7790
|
+
if (!subjectPackages(subject).some((name) => condition.packageNames.includes(name))) return false;
|
|
7791
|
+
}
|
|
7768
7792
|
return true;
|
|
7769
7793
|
}
|
|
7794
|
+
function subjectNodes(subject) {
|
|
7795
|
+
const ids = [...subject.nodeIds ?? []];
|
|
7796
|
+
if (subject.nodeId !== void 0 && !ids.includes(subject.nodeId)) ids.push(subject.nodeId);
|
|
7797
|
+
return ids;
|
|
7798
|
+
}
|
|
7799
|
+
function subjectPackages(subject) {
|
|
7800
|
+
const names = [...subject.packageNames ?? []];
|
|
7801
|
+
if (subject.packageName !== void 0 && !names.includes(subject.packageName)) names.push(subject.packageName);
|
|
7802
|
+
return names;
|
|
7803
|
+
}
|
|
7770
7804
|
function evaluateRule(rule, subject, deviceState, zoneOwners, sceneStates) {
|
|
7771
7805
|
if (subject.kind === "system-event") {
|
|
7772
7806
|
if (rule.delivery !== "system-event") return fail("delivery");
|
|
@@ -8223,12 +8257,30 @@ var en_default = {
|
|
|
8223
8257
|
"one": "Camera {{deviceName}} has been analysed for {{minutes}} minute with no detection at all",
|
|
8224
8258
|
"other": "Camera {{deviceName}} has been analysed for {{minutes}} minutes with no detection at all"
|
|
8225
8259
|
},
|
|
8226
|
-
"system.addon-update-available.title":
|
|
8227
|
-
|
|
8260
|
+
"system.addon-update-available.title": {
|
|
8261
|
+
"one": "{{count}} addon update available",
|
|
8262
|
+
"other": "{{count}} addon updates available"
|
|
8263
|
+
},
|
|
8264
|
+
"system.addon-update-available.body": "{{list}}",
|
|
8228
8265
|
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8229
|
-
"system.server-update-available.title":
|
|
8266
|
+
"system.server-update-available.title": {
|
|
8267
|
+
"one": "Server update available on {{count}} node",
|
|
8268
|
+
"other": "Server update available on {{count}} nodes"
|
|
8269
|
+
},
|
|
8230
8270
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}})",
|
|
8231
8271
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on node {{nodeId}}",
|
|
8272
|
+
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} is available (current {{currentVersion}}) on {{nodes}}",
|
|
8273
|
+
"system.addon-updated.title": {
|
|
8274
|
+
"one": "{{count}} addon updated",
|
|
8275
|
+
"other": "{{count}} addons updated"
|
|
8276
|
+
},
|
|
8277
|
+
"system.addon-updated.body": "{{list}}",
|
|
8278
|
+
"system.server-updated.title": "Server updated",
|
|
8279
|
+
"system.server-updated.body": "{{packageName}} on {{nodeId}} is now {{toVersion}} (was {{fromVersion}})",
|
|
8280
|
+
"system.server-updated.body.plain": "{{packageName}} on {{nodeId}} was updated",
|
|
8281
|
+
"system.export-completed.title": "Export ready",
|
|
8282
|
+
"system.export-completed.body": "The footage export is ready",
|
|
8283
|
+
"system.export-completed.body.titled": "{{title}} is ready",
|
|
8232
8284
|
"system.camera-online.title": "Device online",
|
|
8233
8285
|
"system.camera-online.body": "Device {{deviceName}} is online",
|
|
8234
8286
|
"system.camera-offline.title": "Device offline",
|
|
@@ -8417,12 +8469,30 @@ var it_default = {
|
|
|
8417
8469
|
"one": "La camera {{deviceName}} è analizzata da {{minutes}} minuto senza alcun rilevamento",
|
|
8418
8470
|
"other": "La camera {{deviceName}} è analizzata da {{minutes}} minuti senza alcun rilevamento"
|
|
8419
8471
|
},
|
|
8420
|
-
"system.addon-update-available.title":
|
|
8421
|
-
|
|
8472
|
+
"system.addon-update-available.title": {
|
|
8473
|
+
"one": "{{count}} aggiornamento addon disponibile",
|
|
8474
|
+
"other": "{{count}} aggiornamenti addon disponibili"
|
|
8475
|
+
},
|
|
8476
|
+
"system.addon-update-available.body": "{{list}}",
|
|
8422
8477
|
"system.addon-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8423
|
-
"system.server-update-available.title":
|
|
8478
|
+
"system.server-update-available.title": {
|
|
8479
|
+
"one": "Aggiornamento server disponibile su {{count}} nodo",
|
|
8480
|
+
"other": "Aggiornamento server disponibile su {{count}} nodi"
|
|
8481
|
+
},
|
|
8424
8482
|
"system.server-update-available.body": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}})",
|
|
8425
8483
|
"system.server-update-available.body.onNode": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) sul nodo {{nodeId}}",
|
|
8484
|
+
"system.server-update-available.body.onNodes": "{{packageName}} {{latestVersion}} è disponibile (attuale {{currentVersion}}) su {{nodes}}",
|
|
8485
|
+
"system.addon-updated.title": {
|
|
8486
|
+
"one": "{{count}} addon aggiornato",
|
|
8487
|
+
"other": "{{count}} addon aggiornati"
|
|
8488
|
+
},
|
|
8489
|
+
"system.addon-updated.body": "{{list}}",
|
|
8490
|
+
"system.server-updated.title": "Server aggiornato",
|
|
8491
|
+
"system.server-updated.body": "{{packageName}} sul nodo {{nodeId}} è ora {{toVersion}} (era {{fromVersion}})",
|
|
8492
|
+
"system.server-updated.body.plain": "{{packageName}} sul nodo {{nodeId}} è stato aggiornato",
|
|
8493
|
+
"system.export-completed.title": "Export pronto",
|
|
8494
|
+
"system.export-completed.body": "L'export del filmato è pronto",
|
|
8495
|
+
"system.export-completed.body.titled": "{{title}} è pronto",
|
|
8426
8496
|
"system.camera-online.title": "Dispositivo online",
|
|
8427
8497
|
"system.camera-online.body": "Il dispositivo {{deviceName}} è online",
|
|
8428
8498
|
"system.camera-offline.title": "Dispositivo offline",
|
|
@@ -9149,7 +9219,7 @@ var NcDispatcher = class {
|
|
|
9149
9219
|
vars: { body: rendered },
|
|
9150
9220
|
frozen: rendered
|
|
9151
9221
|
}) : rendered;
|
|
9152
|
-
const attachments = [...await this.withArtifactUrls(await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
|
|
9222
|
+
const attachments = [...await this.withArtifactUrls(entry, await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
|
|
9153
9223
|
const params = pickParams(entry.payload.params);
|
|
9154
9224
|
const actions = await this.mintActions(entry, target);
|
|
9155
9225
|
return {
|
|
@@ -9210,11 +9280,39 @@ var NcDispatcher = class {
|
|
|
9210
9280
|
}
|
|
9211
9281
|
});
|
|
9212
9282
|
}
|
|
9213
|
-
async withArtifactUrls(attachments) {
|
|
9283
|
+
async withArtifactUrls(entry, attachments) {
|
|
9284
|
+
const index = this.deps.indexArtifact;
|
|
9214
9285
|
const publish = this.deps.publishArtifact;
|
|
9215
|
-
if (publish === void 0) return [...attachments];
|
|
9286
|
+
if (index === void 0 && publish === void 0) return [...attachments];
|
|
9216
9287
|
const out = [];
|
|
9288
|
+
let posterId = null;
|
|
9289
|
+
const deviceId = entry.payload.subject.deviceId;
|
|
9290
|
+
const common = {
|
|
9291
|
+
deviceId: deviceId > 0 ? deviceId : null,
|
|
9292
|
+
deviceIds: deviceId > 0 ? [deviceId] : [],
|
|
9293
|
+
capturedAt: entry.payload.subject.timestamp,
|
|
9294
|
+
ruleId: entry.ruleId,
|
|
9295
|
+
ruleName: entry.payload.ruleName
|
|
9296
|
+
};
|
|
9217
9297
|
for (const att of attachments) {
|
|
9298
|
+
if (index !== void 0) {
|
|
9299
|
+
const kind = artifactKindForMediaType(att.mediaType);
|
|
9300
|
+
const meta = {
|
|
9301
|
+
...common,
|
|
9302
|
+
kind,
|
|
9303
|
+
...posterId !== null && kind !== "event-still" ? { posterArtifactId: posterId } : {}
|
|
9304
|
+
};
|
|
9305
|
+
const pub = await index(att.bytes, att.mime, meta).catch(() => ({
|
|
9306
|
+
url: null,
|
|
9307
|
+
artifactId: null
|
|
9308
|
+
}));
|
|
9309
|
+
if (kind === "event-still" && pub.artifactId !== null) posterId = pub.artifactId;
|
|
9310
|
+
out.push(pub.url === null ? { ...att } : {
|
|
9311
|
+
...att,
|
|
9312
|
+
url: pub.url
|
|
9313
|
+
});
|
|
9314
|
+
continue;
|
|
9315
|
+
}
|
|
9218
9316
|
const url = await publish(att.bytes, att.mime).catch(() => null);
|
|
9219
9317
|
out.push(url === null ? { ...att } : {
|
|
9220
9318
|
...att,
|
|
@@ -9828,6 +9926,16 @@ function stringField(data, key) {
|
|
|
9828
9926
|
const value = data[key];
|
|
9829
9927
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
9830
9928
|
}
|
|
9929
|
+
function stringList(data, key) {
|
|
9930
|
+
const raw = data[key];
|
|
9931
|
+
if (!Array.isArray(raw)) return [];
|
|
9932
|
+
const ids = [];
|
|
9933
|
+
for (const item of raw) {
|
|
9934
|
+
if (typeof item === "string" && item.length > 0 && !ids.includes(item)) ids.push(item);
|
|
9935
|
+
if (ids.length >= 20) break;
|
|
9936
|
+
}
|
|
9937
|
+
return ids;
|
|
9938
|
+
}
|
|
9831
9939
|
function numberField(data, key) {
|
|
9832
9940
|
const value = data[key];
|
|
9833
9941
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
@@ -9971,6 +10079,8 @@ function systemSubject(input) {
|
|
|
9971
10079
|
...input.deviceType !== void 0 ? { deviceType: input.deviceType } : {},
|
|
9972
10080
|
...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
|
|
9973
10081
|
...input.packageName !== void 0 ? { packageName: input.packageName } : {},
|
|
10082
|
+
...input.nodeIds !== void 0 ? { nodeIds: input.nodeIds } : {},
|
|
10083
|
+
...input.packageNames !== void 0 ? { packageNames: input.packageNames } : {},
|
|
9974
10084
|
...input.textCount !== void 0 ? { textCount: input.textCount } : {},
|
|
9975
10085
|
...input.alarmSource !== void 0 ? { alarmSource: input.alarmSource } : {},
|
|
9976
10086
|
data
|
|
@@ -10147,34 +10257,189 @@ function incomingFromSystemEvent(event, lookup) {
|
|
|
10147
10257
|
...identity !== void 0 ? { deviceType: identity.type } : {}
|
|
10148
10258
|
});
|
|
10149
10259
|
}
|
|
10150
|
-
case "update.available":
|
|
10151
|
-
|
|
10152
|
-
const
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10260
|
+
case "update.available": return incomingFromUpdateAvailable(facts, data);
|
|
10261
|
+
case "addon.updated": {
|
|
10262
|
+
const update = parseAddonUpdateComplete(data);
|
|
10263
|
+
return update === null ? null : incomingFromAddonUpdates(facts, [update]);
|
|
10264
|
+
}
|
|
10265
|
+
case "system.restart-completed": return incomingFromServerUpdated(facts, data);
|
|
10266
|
+
case "recording.export.completed": return incomingFromExportCompleted(facts, data);
|
|
10267
|
+
default: return null;
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10270
|
+
function incomingFromUpdateAvailable(facts, data) {
|
|
10271
|
+
const target = stringField(data, "target");
|
|
10272
|
+
if (target !== "addon" && target !== "server") return null;
|
|
10273
|
+
const packages = updatePackages(data);
|
|
10274
|
+
if (packages.length === 0) return null;
|
|
10275
|
+
const addon = target === "addon";
|
|
10276
|
+
const kind = addon ? "addon-update-available" : "server-update-available";
|
|
10277
|
+
const nodeIds = unique(packages.map((pkg) => pkg.nodeId).filter((id) => id !== void 0));
|
|
10278
|
+
const packageNames = unique(packages.map((pkg) => pkg.packageName));
|
|
10279
|
+
const signature = packages.map((pkg) => `${pkg.nodeId ?? ""}:${pkg.packageName}@${pkg.latestVersion}`).toSorted().join(",");
|
|
10280
|
+
const list = packages.map(formatAvailableLine).join("\n");
|
|
10281
|
+
const nodes = nodeIds.join(", ");
|
|
10282
|
+
const head = packages[0];
|
|
10283
|
+
const oneNode = nodeIds.length === 1 ? nodeIds[0] : void 0;
|
|
10284
|
+
return systemSubject({
|
|
10285
|
+
facts,
|
|
10286
|
+
kind,
|
|
10287
|
+
subject: addon ? `packages:addon:${signature}` : `packages:server:${signature}`,
|
|
10288
|
+
titleKey: `system.${kind}.title`,
|
|
10289
|
+
bodyKey: !addon && oneNode !== void 0 ? `system.${kind}.body.onNode` : !addon && nodeIds.length > 1 ? `system.${kind}.body.onNodes` : `system.${kind}.body`,
|
|
10290
|
+
textParams: {
|
|
10291
|
+
list,
|
|
10292
|
+
packageName: head.packageName,
|
|
10293
|
+
currentVersion: head.currentVersion,
|
|
10294
|
+
latestVersion: head.latestVersion,
|
|
10295
|
+
nodes,
|
|
10296
|
+
count: String(addon ? packages.length : nodeIds.length > 0 ? nodeIds.length : 1),
|
|
10297
|
+
...oneNode !== void 0 ? { nodeId: oneNode } : {}
|
|
10298
|
+
},
|
|
10299
|
+
textCount: addon ? packages.length : nodeIds.length > 0 ? nodeIds.length : 1,
|
|
10300
|
+
packageName: head.packageName,
|
|
10301
|
+
packageNames,
|
|
10302
|
+
...oneNode !== void 0 ? { nodeId: oneNode } : {},
|
|
10303
|
+
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10304
|
+
});
|
|
10305
|
+
}
|
|
10306
|
+
/** Version-bearing `addon.updated` only — settings saves carry neither. */
|
|
10307
|
+
function parseAddonUpdateComplete(data) {
|
|
10308
|
+
const packageName = stringField(data, "packageName");
|
|
10309
|
+
const fromVersion = stringField(data, "fromVersion");
|
|
10310
|
+
const toVersion = stringField(data, "toVersion");
|
|
10311
|
+
if (packageName === void 0 || fromVersion === void 0 || toVersion === void 0) return null;
|
|
10312
|
+
const nodeId = stringField(data, "agent") ?? stringField(data, "nodeId");
|
|
10313
|
+
return {
|
|
10314
|
+
packageName,
|
|
10315
|
+
fromVersion,
|
|
10316
|
+
toVersion,
|
|
10317
|
+
...nodeId !== void 0 ? { nodeId } : {}
|
|
10318
|
+
};
|
|
10319
|
+
}
|
|
10320
|
+
function incomingFromAddonUpdates(facts, updates) {
|
|
10321
|
+
if (updates.length === 0) return null;
|
|
10322
|
+
const packageNames = unique(updates.map((update) => update.packageName));
|
|
10323
|
+
const nodeIds = unique(updates.map((update) => update.nodeId).filter((id) => id !== void 0));
|
|
10324
|
+
const signature = updates.map((update) => `${update.nodeId ?? ""}:${update.packageName}@${update.toVersion}`).toSorted().join(",");
|
|
10325
|
+
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");
|
|
10326
|
+
const head = updates[0];
|
|
10327
|
+
return systemSubject({
|
|
10328
|
+
facts,
|
|
10329
|
+
kind: "addon-updated",
|
|
10330
|
+
subject: `packages:addon-updated:${signature}`,
|
|
10331
|
+
titleKey: "system.addon-updated.title",
|
|
10332
|
+
bodyKey: "system.addon-updated.body",
|
|
10333
|
+
textParams: {
|
|
10334
|
+
list,
|
|
10335
|
+
count: String(packageNames.length)
|
|
10336
|
+
},
|
|
10337
|
+
textCount: packageNames.length,
|
|
10338
|
+
packageName: head.packageName,
|
|
10339
|
+
packageNames,
|
|
10340
|
+
...head.nodeId !== void 0 ? { nodeId: head.nodeId } : {},
|
|
10341
|
+
...nodeIds.length > 0 ? { nodeIds } : {}
|
|
10342
|
+
});
|
|
10343
|
+
}
|
|
10344
|
+
function incomingFromServerUpdated(facts, data) {
|
|
10345
|
+
const kind = stringField(data, "kind");
|
|
10346
|
+
const requestedBy = stringField(data, "requestedBy");
|
|
10347
|
+
if (!(kind === "framework-update" || kind === "framework-bulk-update" || requestedBy !== void 0 && requestedBy.startsWith("server-update:"))) return null;
|
|
10348
|
+
const packageName = stringField(data, "packageName") ?? "@camstack/server";
|
|
10349
|
+
const fromVersion = stringField(data, "fromVersion");
|
|
10350
|
+
const toVersion = stringField(data, "toVersion") ?? versionFromRequestedBy(requestedBy);
|
|
10351
|
+
const nodeId = stringField(data, "nodeId") ?? "hub";
|
|
10352
|
+
const bodyKey = fromVersion !== void 0 && toVersion !== void 0 ? "system.server-updated.body" : "system.server-updated.body.plain";
|
|
10353
|
+
return systemSubject({
|
|
10354
|
+
facts,
|
|
10355
|
+
kind: "server-updated",
|
|
10356
|
+
subject: `server-updated:${nodeId}:${toVersion ?? "unknown"}`,
|
|
10357
|
+
titleKey: "system.server-updated.title",
|
|
10358
|
+
bodyKey,
|
|
10359
|
+
textParams: {
|
|
10360
|
+
packageName,
|
|
10361
|
+
nodeId,
|
|
10362
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
10363
|
+
...toVersion !== void 0 ? { toVersion } : {}
|
|
10364
|
+
},
|
|
10365
|
+
packageName,
|
|
10366
|
+
nodeId,
|
|
10367
|
+
nodeIds: [nodeId]
|
|
10368
|
+
});
|
|
10369
|
+
}
|
|
10370
|
+
function incomingFromExportCompleted(facts, data) {
|
|
10371
|
+
const exportId = stringField(data, "exportId");
|
|
10372
|
+
if (exportId === void 0) return null;
|
|
10373
|
+
const deviceId = numberField(data, "deviceId");
|
|
10374
|
+
const title = stringField(data, "title");
|
|
10375
|
+
const bytes = numberField(data, "fileBytes");
|
|
10376
|
+
const notifyTargetIds = stringList(data, "notifyTargetIds");
|
|
10377
|
+
const incoming = systemSubject({
|
|
10378
|
+
facts,
|
|
10379
|
+
kind: "export-completed",
|
|
10380
|
+
subject: `export:${exportId}`,
|
|
10381
|
+
titleKey: "system.export-completed.title",
|
|
10382
|
+
bodyKey: title !== void 0 ? "system.export-completed.body.titled" : "system.export-completed.body",
|
|
10383
|
+
textParams: {
|
|
10384
|
+
exportId,
|
|
10385
|
+
...title !== void 0 ? { title } : {},
|
|
10386
|
+
...bytes !== void 0 ? { bytes: String(bytes) } : {},
|
|
10387
|
+
...deviceId !== void 0 ? { deviceId: String(deviceId) } : {}
|
|
10388
|
+
},
|
|
10389
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
10390
|
+
});
|
|
10391
|
+
if (notifyTargetIds.length === 0) return incoming;
|
|
10392
|
+
return {
|
|
10393
|
+
...incoming,
|
|
10394
|
+
controls: {
|
|
10395
|
+
directTargetIds: notifyTargetIds,
|
|
10396
|
+
bypassMute: true
|
|
10397
|
+
}
|
|
10398
|
+
};
|
|
10399
|
+
}
|
|
10400
|
+
function updatePackages(data) {
|
|
10401
|
+
const raw = data["packages"];
|
|
10402
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
10403
|
+
const rows = [];
|
|
10404
|
+
for (const item of raw) {
|
|
10405
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) continue;
|
|
10406
|
+
const row = item;
|
|
10407
|
+
const packageName = stringField(row, "packageName");
|
|
10408
|
+
const currentVersion = stringField(row, "currentVersion");
|
|
10409
|
+
const latestVersion = stringField(row, "latestVersion");
|
|
10410
|
+
if (packageName === void 0 || currentVersion === void 0 || latestVersion === void 0) continue;
|
|
10411
|
+
const nodeId = stringField(row, "nodeId");
|
|
10412
|
+
rows.push({
|
|
10172
10413
|
packageName,
|
|
10414
|
+
currentVersion,
|
|
10415
|
+
latestVersion,
|
|
10173
10416
|
...nodeId !== void 0 ? { nodeId } : {}
|
|
10174
10417
|
});
|
|
10175
10418
|
}
|
|
10176
|
-
|
|
10419
|
+
if (rows.length > 0) return rows;
|
|
10177
10420
|
}
|
|
10421
|
+
const packageName = stringField(data, "packageName");
|
|
10422
|
+
const currentVersion = stringField(data, "currentVersion");
|
|
10423
|
+
const latestVersion = stringField(data, "latestVersion");
|
|
10424
|
+
if (packageName === void 0 || currentVersion === void 0 || latestVersion === void 0) return [];
|
|
10425
|
+
const nodeId = stringField(data, "nodeId");
|
|
10426
|
+
return [{
|
|
10427
|
+
packageName,
|
|
10428
|
+
currentVersion,
|
|
10429
|
+
latestVersion,
|
|
10430
|
+
...nodeId !== void 0 ? { nodeId } : {}
|
|
10431
|
+
}];
|
|
10432
|
+
}
|
|
10433
|
+
function formatAvailableLine(pkg) {
|
|
10434
|
+
const line = `${pkg.packageName} ${pkg.latestVersion} (current ${pkg.currentVersion})`;
|
|
10435
|
+
return pkg.nodeId !== void 0 ? `${line} on ${pkg.nodeId}` : line;
|
|
10436
|
+
}
|
|
10437
|
+
function versionFromRequestedBy(requestedBy) {
|
|
10438
|
+
if (requestedBy === void 0) return void 0;
|
|
10439
|
+
return /@([^@\s]+)$/.exec(requestedBy)?.[1];
|
|
10440
|
+
}
|
|
10441
|
+
function unique(values) {
|
|
10442
|
+
return [...new Set(values)];
|
|
10178
10443
|
}
|
|
10179
10444
|
/**
|
|
10180
10445
|
* The COMBINED row's two keys, or null when this transition is not one.
|
|
@@ -10890,6 +11155,67 @@ function recordToRow$2(subject, data) {
|
|
|
10890
11155
|
updatedAt
|
|
10891
11156
|
};
|
|
10892
11157
|
}
|
|
11158
|
+
function isDeviceLivenessKind(kind) {
|
|
11159
|
+
return kind === "device-online" || kind === "device-offline" || kind === "camera-online" || kind === "camera-offline";
|
|
11160
|
+
}
|
|
11161
|
+
function livenessStateOfKind(kind) {
|
|
11162
|
+
return kind.endsWith("-offline") ? "offline" : "online";
|
|
11163
|
+
}
|
|
11164
|
+
var DeviceLivenessHoldoff = class {
|
|
11165
|
+
flush;
|
|
11166
|
+
pending = /* @__PURE__ */ new Map();
|
|
11167
|
+
lastNotified = /* @__PURE__ */ new Map();
|
|
11168
|
+
timers = /* @__PURE__ */ new Map();
|
|
11169
|
+
delayMs;
|
|
11170
|
+
schedule;
|
|
11171
|
+
cancel;
|
|
11172
|
+
constructor(flush, options = {}) {
|
|
11173
|
+
this.flush = flush;
|
|
11174
|
+
this.delayMs = options.delayMs ?? 3e5;
|
|
11175
|
+
this.schedule = options.schedule ?? setTimeout;
|
|
11176
|
+
this.cancel = options.cancel ?? clearTimeout;
|
|
11177
|
+
}
|
|
11178
|
+
/** The seeded (or last delivered) state — a bounce back to this is silence. */
|
|
11179
|
+
remember(subject, state) {
|
|
11180
|
+
this.lastNotified.set(subject, state);
|
|
11181
|
+
}
|
|
11182
|
+
/** Directory-seeded rows never pass through {@link remember}; first flip does. */
|
|
11183
|
+
rememberIfAbsent(subject, state) {
|
|
11184
|
+
if (!this.lastNotified.has(subject)) this.lastNotified.set(subject, state);
|
|
11185
|
+
}
|
|
11186
|
+
/** A real ledger flip. Starts / resets the trailing timer. */
|
|
11187
|
+
arm(subject, incoming) {
|
|
11188
|
+
this.pending.set(subject, incoming);
|
|
11189
|
+
const existing = this.timers.get(subject);
|
|
11190
|
+
if (existing !== void 0) this.cancel(existing);
|
|
11191
|
+
if (this.delayMs <= 0) {
|
|
11192
|
+
this.timers.delete(subject);
|
|
11193
|
+
this.fire(subject);
|
|
11194
|
+
return;
|
|
11195
|
+
}
|
|
11196
|
+
const id = this.schedule(() => {
|
|
11197
|
+
this.timers.delete(subject);
|
|
11198
|
+
this.fire(subject);
|
|
11199
|
+
}, this.delayMs);
|
|
11200
|
+
this.timers.set(subject, id);
|
|
11201
|
+
}
|
|
11202
|
+
dispose() {
|
|
11203
|
+
for (const id of this.timers.values()) this.cancel(id);
|
|
11204
|
+
this.timers.clear();
|
|
11205
|
+
this.pending.clear();
|
|
11206
|
+
}
|
|
11207
|
+
fire(subject) {
|
|
11208
|
+
const incoming = this.pending.get(subject);
|
|
11209
|
+
this.pending.delete(subject);
|
|
11210
|
+
if (incoming === void 0) return;
|
|
11211
|
+
const kind = incoming.subject.systemEvent?.kind;
|
|
11212
|
+
if (kind === void 0) return;
|
|
11213
|
+
const state = livenessStateOfKind(kind);
|
|
11214
|
+
if (this.lastNotified.get(subject) === state) return;
|
|
11215
|
+
this.lastNotified.set(subject, state);
|
|
11216
|
+
this.flush(incoming);
|
|
11217
|
+
}
|
|
11218
|
+
};
|
|
10893
11219
|
//#endregion
|
|
10894
11220
|
//#region src/notification-center/occupancy-watcher.ts
|
|
10895
11221
|
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
@@ -17772,6 +18098,36 @@ var TimelapseStore = class {
|
|
|
17772
18098
|
return rule;
|
|
17773
18099
|
}
|
|
17774
18100
|
};
|
|
18101
|
+
var AddonUpdateCoalescer = class {
|
|
18102
|
+
flush;
|
|
18103
|
+
pending = /* @__PURE__ */ new Map();
|
|
18104
|
+
timer = null;
|
|
18105
|
+
delayMs;
|
|
18106
|
+
schedule;
|
|
18107
|
+
cancel;
|
|
18108
|
+
constructor(flush, options = {}) {
|
|
18109
|
+
this.flush = flush;
|
|
18110
|
+
this.delayMs = options.delayMs ?? 3e4;
|
|
18111
|
+
this.schedule = options.schedule ?? setTimeout;
|
|
18112
|
+
this.cancel = options.cancel ?? clearTimeout;
|
|
18113
|
+
}
|
|
18114
|
+
observe(update) {
|
|
18115
|
+
this.pending.set(update.packageName, update);
|
|
18116
|
+
if (this.timer !== null) this.cancel(this.timer);
|
|
18117
|
+
this.timer = this.schedule(() => {
|
|
18118
|
+
this.timer = null;
|
|
18119
|
+
const batch = [...this.pending.values()];
|
|
18120
|
+
this.pending.clear();
|
|
18121
|
+
if (batch.length > 0) this.flush(batch);
|
|
18122
|
+
}, this.delayMs);
|
|
18123
|
+
}
|
|
18124
|
+
dispose() {
|
|
18125
|
+
if (this.timer === null) return;
|
|
18126
|
+
this.cancel(this.timer);
|
|
18127
|
+
this.timer = null;
|
|
18128
|
+
this.pending.clear();
|
|
18129
|
+
}
|
|
18130
|
+
};
|
|
17775
18131
|
//#endregion
|
|
17776
18132
|
//#region src/notification-center/zone-owner-cache.ts
|
|
17777
18133
|
var NcZoneOwnershipCache = class {
|
|
@@ -17881,6 +18237,15 @@ var TIMELAPSE_LOOKAHEAD_MS = 9 * 864e5;
|
|
|
17881
18237
|
*/
|
|
17882
18238
|
var NC_ALARM_ANNOUNCE_RULE_ID = "nc-alarm-announce";
|
|
17883
18239
|
/**
|
|
18240
|
+
* Synthetic rule id for an export's on-screen picker.
|
|
18241
|
+
*
|
|
18242
|
+
* The operator named these targets on the export screen, not in a rule. The
|
|
18243
|
+
* row still lives in the outbox so history, retry and language rendering are
|
|
18244
|
+
* the same path as every other notification. A matching `export-completed`
|
|
18245
|
+
* rule that already reached a target is skipped so one export is one push.
|
|
18246
|
+
*/
|
|
18247
|
+
var NC_EXPORT_NOTIFY_RULE_ID = "nc-export-notify";
|
|
18248
|
+
/**
|
|
17884
18249
|
* Which alarm buttons a notification of this kind should carry, or `null` for
|
|
17885
18250
|
* every notification that is not about the panel.
|
|
17886
18251
|
*
|
|
@@ -18328,6 +18693,10 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18328
18693
|
* seeds, only a real flip notifies.
|
|
18329
18694
|
*/
|
|
18330
18695
|
liveness;
|
|
18696
|
+
/** Batches `addon.updated` (version-bearing) into one notification per 30 s. */
|
|
18697
|
+
addonUpdates;
|
|
18698
|
+
/** Trailing 5 min hold-off for camera liveness flaps (plugin restart). */
|
|
18699
|
+
livenessHoldoff;
|
|
18331
18700
|
/**
|
|
18332
18701
|
* Serialized evaluation chain. The persist hooks are fire-and-forget for
|
|
18333
18702
|
* the frame path, but two concurrent evaluations of the same device
|
|
@@ -18630,6 +18999,16 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18630
18999
|
store: deps.store,
|
|
18631
19000
|
logger: this.logger.child("liveness")
|
|
18632
19001
|
});
|
|
19002
|
+
this.addonUpdates = new AddonUpdateCoalescer((updates) => {
|
|
19003
|
+
const incoming = incomingFromAddonUpdates({
|
|
19004
|
+
eventId: `addon-updated:${updates.map((u) => u.packageName).join(",")}`,
|
|
19005
|
+
timestamp: this.now(),
|
|
19006
|
+
category: "addon.updated",
|
|
19007
|
+
data: { packages: updates }
|
|
19008
|
+
}, updates);
|
|
19009
|
+
if (incoming !== null) this.consumeEvent(incoming);
|
|
19010
|
+
});
|
|
19011
|
+
this.livenessHoldoff = new DeviceLivenessHoldoff((event) => this.consumeEvent(event), { delayMs: deps.livenessHoldoffMs ?? (process.env.VITEST ? 0 : 3e5) });
|
|
18633
19012
|
this.actionRunner = deps.actuate !== void 0 && deps.isCapDeviceScoped !== void 0 ? new NcRuleActionRunner({
|
|
18634
19013
|
logger: this.logger.child("actions"),
|
|
18635
19014
|
actuate: (input) => this.actuateWithAttribution(input),
|
|
@@ -18888,6 +19267,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
18888
19267
|
this.timelapseScheduler?.stop();
|
|
18889
19268
|
this.summaryProducer?.stop();
|
|
18890
19269
|
this.audioStills?.clear();
|
|
19270
|
+
this.addonUpdates.dispose();
|
|
19271
|
+
this.livenessHoldoff.dispose();
|
|
18891
19272
|
this.evaluationActive = false;
|
|
18892
19273
|
}
|
|
18893
19274
|
/**
|
|
@@ -19362,6 +19743,11 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19362
19743
|
*/
|
|
19363
19744
|
onSystemEvent(event) {
|
|
19364
19745
|
if (!this.evaluationActive) return;
|
|
19746
|
+
if (event.category === "addon.updated") {
|
|
19747
|
+
const update = parseAddonUpdateComplete(event.data);
|
|
19748
|
+
if (update !== null) this.addonUpdates.observe(update);
|
|
19749
|
+
return;
|
|
19750
|
+
}
|
|
19365
19751
|
const incoming = incomingFromSystemEvent(event, this.deviceDirectory.lookup());
|
|
19366
19752
|
if (incoming === null) {
|
|
19367
19753
|
const dropped = discardedSystemEvent(event);
|
|
@@ -19377,11 +19763,22 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19377
19763
|
const system = incoming.subject.systemEvent;
|
|
19378
19764
|
const observation = system !== void 0 ? ledgeredLiveness(system) : null;
|
|
19379
19765
|
if (observation !== null) {
|
|
19766
|
+
const previous = this.liveness.stateOf(observation.subject);
|
|
19380
19767
|
const verdict = this.liveness.observe(observation, this.now());
|
|
19768
|
+
if (verdict === "seeded") {
|
|
19769
|
+
this.livenessHoldoff.remember(observation.subject, observation.state);
|
|
19770
|
+
this.reportLivenessDrop(incoming, verdict);
|
|
19771
|
+
return;
|
|
19772
|
+
}
|
|
19381
19773
|
if (verdict !== "flip") {
|
|
19382
19774
|
this.reportLivenessDrop(incoming, verdict);
|
|
19383
19775
|
return;
|
|
19384
19776
|
}
|
|
19777
|
+
if (previous !== void 0) this.livenessHoldoff.rememberIfAbsent(observation.subject, previous);
|
|
19778
|
+
if (isDeviceLivenessKind(observation.kind)) {
|
|
19779
|
+
this.livenessHoldoff.arm(observation.subject, incoming);
|
|
19780
|
+
return;
|
|
19781
|
+
}
|
|
19385
19782
|
} else if (system !== void 0 && (system.kind.startsWith("device-") || system.kind.startsWith("stream-"))) {
|
|
19386
19783
|
if (this.lastSystemLiveness.get(system.subject) === system.kind) return;
|
|
19387
19784
|
this.lastSystemLiveness.set(system.subject, system.kind);
|
|
@@ -19874,6 +20271,55 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19874
20271
|
});
|
|
19875
20272
|
}
|
|
19876
20273
|
/**
|
|
20274
|
+
* Deliver to targets the producer named (`controls.directTargetIds`).
|
|
20275
|
+
*
|
|
20276
|
+
* Skips ids a matching rule already reached so one export is one push.
|
|
20277
|
+
* Absent/empty is a no-op — every other producer omits the control.
|
|
20278
|
+
*/
|
|
20279
|
+
async enqueuePickerTargets(event, reached, outcomes) {
|
|
20280
|
+
const ids = event.controls?.directTargetIds;
|
|
20281
|
+
if (ids === void 0 || ids.length === 0) return;
|
|
20282
|
+
const targets = [...new Set(ids)].filter((id) => !reached.has(id));
|
|
20283
|
+
if (targets.length === 0) return;
|
|
20284
|
+
const subject = event.subject;
|
|
20285
|
+
const entries = targets.map((targetId) => ({
|
|
20286
|
+
ruleId: NC_EXPORT_NOTIFY_RULE_ID,
|
|
20287
|
+
targetId,
|
|
20288
|
+
deviceId: subject.deviceId,
|
|
20289
|
+
recordKind: "system-event",
|
|
20290
|
+
recordId: subject.recordId,
|
|
20291
|
+
payload: {
|
|
20292
|
+
ruleName: "export",
|
|
20293
|
+
delivery: "system-event",
|
|
20294
|
+
priority: 2,
|
|
20295
|
+
media: "none",
|
|
20296
|
+
subject: {
|
|
20297
|
+
deviceId: subject.deviceId,
|
|
20298
|
+
className: "export",
|
|
20299
|
+
zones: [],
|
|
20300
|
+
timestamp: subject.timestamp,
|
|
20301
|
+
...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
|
|
20302
|
+
}
|
|
20303
|
+
}
|
|
20304
|
+
}));
|
|
20305
|
+
const inserted = await this.outbox.enqueue(entries);
|
|
20306
|
+
outcomes.push({
|
|
20307
|
+
ruleId: NC_EXPORT_NOTIFY_RULE_ID,
|
|
20308
|
+
ruleName: "export",
|
|
20309
|
+
matched: true,
|
|
20310
|
+
cooldown: "n/a",
|
|
20311
|
+
enqueued: inserted,
|
|
20312
|
+
targetIds: entries.map((entry) => entry.targetId)
|
|
20313
|
+
});
|
|
20314
|
+
this.logger.info("export picker notification enqueued", {
|
|
20315
|
+
tags: { deviceId: subject.deviceId },
|
|
20316
|
+
meta: {
|
|
20317
|
+
targets: targets.length,
|
|
20318
|
+
inserted
|
|
20319
|
+
}
|
|
20320
|
+
});
|
|
20321
|
+
}
|
|
20322
|
+
/**
|
|
19877
20323
|
* The COMBINED notification's second half: withdraw what it replaced.
|
|
19878
20324
|
*
|
|
19879
20325
|
* Only for the targets the alarm actually reached, and only while their rows
|
|
@@ -19998,6 +20444,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
19998
20444
|
}
|
|
19999
20445
|
});
|
|
20000
20446
|
if (kind === "track-end") await this.enqueueRecognitionUpgrades(subject, outcomes);
|
|
20447
|
+
await this.enqueuePickerTargets(event, /* @__PURE__ */ new Set(), outcomes);
|
|
20448
|
+
if (outcomes.some((outcome) => outcome.enqueued > 0)) return report({ candidates: 0 });
|
|
20001
20449
|
return report({ droppedBy: "no-candidate-rule" });
|
|
20002
20450
|
}
|
|
20003
20451
|
const now = this.now();
|
|
@@ -20194,6 +20642,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
20194
20642
|
}
|
|
20195
20643
|
if (!anyMatched && rejections.length > 0) this.reportNoMatch(subject, kind, rejections, now);
|
|
20196
20644
|
if (kind === "track-end") await this.enqueueRecognitionUpgrades(subject, outcomes);
|
|
20645
|
+
const reached = new Set(outcomes.flatMap((outcome) => outcome.targetIds));
|
|
20646
|
+
await this.enqueuePickerTargets(event, reached, outcomes);
|
|
20197
20647
|
return report({ candidates: candidates.length });
|
|
20198
20648
|
}
|
|
20199
20649
|
/**
|
|
@@ -21413,6 +21863,9 @@ function systemEventSubject(kind, scope) {
|
|
|
21413
21863
|
if (kind === "node-inference-unavailable") return `node-inference:${scope.nodeId ?? "hub"}`;
|
|
21414
21864
|
if (kind === "detection-blind") return `detection-blind:${scope.deviceId ?? 0}`;
|
|
21415
21865
|
if (kind === "addon-update-available" || kind === "server-update-available") return `package:${scope.packageName ?? "@camstack/server"}`;
|
|
21866
|
+
if (kind === "addon-updated") return `packages:addon-updated:${scope.packageName ?? "pkg"}`;
|
|
21867
|
+
if (kind === "server-updated") return `server-updated:${scope.nodeId ?? "hub"}`;
|
|
21868
|
+
if (kind === "export-completed") return `export:${scope.deviceId ?? 0}`;
|
|
21416
21869
|
return `alarm:${scope.deviceId ?? 0}`;
|
|
21417
21870
|
}
|
|
21418
21871
|
/**
|
|
@@ -21524,7 +21977,13 @@ function planRuleTest(input) {
|
|
|
21524
21977
|
profile: "low",
|
|
21525
21978
|
seconds: "30",
|
|
21526
21979
|
devices: "cuda:0",
|
|
21527
|
-
minutes: "15"
|
|
21980
|
+
minutes: "15",
|
|
21981
|
+
list: "@camstack/addon-pipeline 1.0.0 (current 0.0.0)",
|
|
21982
|
+
nodes: "hub",
|
|
21983
|
+
fromVersion: "0.0.0",
|
|
21984
|
+
toVersion: "0.0.1",
|
|
21985
|
+
title: "Export",
|
|
21986
|
+
count: "1"
|
|
21528
21987
|
},
|
|
21529
21988
|
...systemScope.deviceId !== void 0 ? { deviceId: systemScope.deviceId } : {},
|
|
21530
21989
|
...c.systemEvent?.deviceTypes?.[0] !== void 0 ? { deviceType: c.systemEvent.deviceTypes[0] } : {},
|
|
@@ -28356,7 +28815,123 @@ var orphanAuditActions = defineCustomActions({ "retention.orphanAudit": customAc
|
|
|
28356
28815
|
durationMs: number()
|
|
28357
28816
|
}), { auth: "admin" }) });
|
|
28358
28817
|
//#endregion
|
|
28818
|
+
//#region src/pipeline-analytics/videoclip-visits.ts
|
|
28819
|
+
/**
|
|
28820
|
+
* Videoclip visits — the grain a clip should have on an events-mode camera.
|
|
28821
|
+
*
|
|
28822
|
+
* Disk writes one fMP4 per ffmpeg roll (~8–12 s). The recorder calendar merges
|
|
28823
|
+
* only gaps ≤ 5 s, so a single pantry walk shows up as several availability
|
|
28824
|
+
* ranges with ~8 s GOP holes between them. Analytics then mints one 15 s clip
|
|
28825
|
+
* per motion heartbeat. A *visit* is the keep-window those fragments belong
|
|
28826
|
+
* to: cluster availability with {@link VISIT_MERGE_GAP_MS}, annotate with
|
|
28827
|
+
* overlapping events. No second copy of the media; the files stay the source
|
|
28828
|
+
* of truth.
|
|
28829
|
+
*/
|
|
28830
|
+
/** Synthetic event-id slot in `analytics:<start>:<end>:<id>` for a visit clip. */
|
|
28831
|
+
var VISIT_EVENT_ID = "visit";
|
|
28832
|
+
function clusterFootageVisits(ranges, gapMs = VISIT_MERGE_GAP_MS) {
|
|
28833
|
+
if (ranges.length === 0) return [];
|
|
28834
|
+
const sorted = [...ranges].toSorted((a, b) => a.startMs - b.startMs);
|
|
28835
|
+
const visits = [];
|
|
28836
|
+
let pieces = [sorted[0]];
|
|
28837
|
+
let endMs = sorted[0].endMs;
|
|
28838
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
28839
|
+
const next = sorted[i];
|
|
28840
|
+
if (next.startMs <= endMs + gapMs) {
|
|
28841
|
+
pieces.push(next);
|
|
28842
|
+
endMs = Math.max(endMs, next.endMs);
|
|
28843
|
+
continue;
|
|
28844
|
+
}
|
|
28845
|
+
visits.push(finishVisit(pieces, endMs));
|
|
28846
|
+
pieces = [next];
|
|
28847
|
+
endMs = next.endMs;
|
|
28848
|
+
}
|
|
28849
|
+
visits.push(finishVisit(pieces, endMs));
|
|
28850
|
+
return visits;
|
|
28851
|
+
}
|
|
28852
|
+
function finishVisit(pieces, endMs) {
|
|
28853
|
+
const holes = [];
|
|
28854
|
+
for (let i = 1; i < pieces.length; i++) {
|
|
28855
|
+
const prevEnd = pieces[i - 1].endMs;
|
|
28856
|
+
const nextStart = pieces[i].startMs;
|
|
28857
|
+
if (nextStart > prevEnd) holes.push({
|
|
28858
|
+
startMs: prevEnd,
|
|
28859
|
+
endMs: nextStart
|
|
28860
|
+
});
|
|
28861
|
+
}
|
|
28862
|
+
return {
|
|
28863
|
+
startMs: pieces[0].startMs,
|
|
28864
|
+
endMs,
|
|
28865
|
+
ranges: pieces,
|
|
28866
|
+
holes
|
|
28867
|
+
};
|
|
28868
|
+
}
|
|
28869
|
+
var KIND_ORDER = [
|
|
28870
|
+
"object",
|
|
28871
|
+
"motion",
|
|
28872
|
+
"audio"
|
|
28873
|
+
];
|
|
28874
|
+
function eventOverlapsVisit(event, visit, pad) {
|
|
28875
|
+
if (event.timestamp >= visit.startMs && event.timestamp <= visit.endMs) return true;
|
|
28876
|
+
const windowStart = event.timestamp - pad.preMs;
|
|
28877
|
+
const windowEnd = event.timestamp + pad.postMs;
|
|
28878
|
+
return windowStart < visit.endMs && windowEnd > visit.startMs;
|
|
28879
|
+
}
|
|
28880
|
+
function dominantKind(attached) {
|
|
28881
|
+
for (const kind of KIND_ORDER) if (attached.some((a) => a.kind === kind)) return kind;
|
|
28882
|
+
return "motion";
|
|
28883
|
+
}
|
|
28884
|
+
function visitsToClips(visits, events, pad) {
|
|
28885
|
+
const tagged = [
|
|
28886
|
+
...events.object.map((event) => ({
|
|
28887
|
+
kind: "object",
|
|
28888
|
+
event
|
|
28889
|
+
})),
|
|
28890
|
+
...events.motion.map((event) => ({
|
|
28891
|
+
kind: "motion",
|
|
28892
|
+
event
|
|
28893
|
+
})),
|
|
28894
|
+
...events.audio.map((event) => ({
|
|
28895
|
+
kind: "audio",
|
|
28896
|
+
event
|
|
28897
|
+
}))
|
|
28898
|
+
];
|
|
28899
|
+
return visits.map((visit) => {
|
|
28900
|
+
const attached = tagged.filter((t) => eventOverlapsVisit(t.event, visit, pad));
|
|
28901
|
+
const kind = dominantKind(attached);
|
|
28902
|
+
const withThumb = attached.find((a) => a.kind === kind && a.event.mediaUrl) ?? attached.find((a) => a.event.mediaUrl);
|
|
28903
|
+
return {
|
|
28904
|
+
id: `analytics:${visit.startMs}:${visit.endMs}:${VISIT_EVENT_ID}`,
|
|
28905
|
+
source: "analytics",
|
|
28906
|
+
kind,
|
|
28907
|
+
timeRange: {
|
|
28908
|
+
startMs: visit.startMs,
|
|
28909
|
+
endMs: visit.endMs
|
|
28910
|
+
},
|
|
28911
|
+
thumbnail: withThumb?.event.mediaUrl,
|
|
28912
|
+
eventIds: attached.map((a) => a.event.id),
|
|
28913
|
+
holes: [...visit.holes]
|
|
28914
|
+
};
|
|
28915
|
+
}).toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
28916
|
+
}
|
|
28917
|
+
//#endregion
|
|
28359
28918
|
//#region src/pipeline-analytics/videoclips-provider.ts
|
|
28919
|
+
/**
|
|
28920
|
+
* Default `videoclips` provider — composes recorded footage visits with the
|
|
28921
|
+
* analytics event log. A clip is a time-WINDOW over existing footage, never a
|
|
28922
|
+
* separate file: `getClipPlayback` resolves to
|
|
28923
|
+
* `recording.getPlaybackManifest(startMs, endMs)`.
|
|
28924
|
+
*
|
|
28925
|
+
* When availability is present the grain is a *visit* (events-mode keep-window
|
|
28926
|
+
* clustered at `VISIT_MERGE_GAP_MS`), not one clip per motion heartbeat. The
|
|
28927
|
+
* clip id still encodes the window (`analytics:<startMs>:<endMs>:visit`) so
|
|
28928
|
+
* playback is self-contained. Without availability (or when that read fails)
|
|
28929
|
+
* the provider falls back to per-event padded windows.
|
|
28930
|
+
*
|
|
28931
|
+
* Pure helpers (`eventsToClips` / `clusterFootageVisits` / `clipIdFor` /
|
|
28932
|
+
* `parseClipId`) are unit-tested in isolation; the provider factory wires
|
|
28933
|
+
* injected fetch/resolve deps so the compose logic is testable without I/O.
|
|
28934
|
+
*/
|
|
28360
28935
|
var SOURCE = "analytics";
|
|
28361
28936
|
function clipIdFor(eventId, startMs, endMs) {
|
|
28362
28937
|
return `${SOURCE}:${startMs}:${endMs}:${eventId}`;
|
|
@@ -28399,36 +28974,29 @@ function eventsToClips(events, pad) {
|
|
|
28399
28974
|
...events.audio.map((e) => make(e, "audio"))
|
|
28400
28975
|
].toSorted((a, b) => b.timeRange.startMs - a.timeRange.startMs);
|
|
28401
28976
|
}
|
|
28402
|
-
/** Keep only clips whose window overlaps at least one recorded range — a clip
|
|
28403
|
-
* is a window over footage, so a window with nothing behind it is unplayable
|
|
28404
|
-
* and must not be listed (a camera that never recorded showed 15 dead audio
|
|
28405
|
-
* "clips" in the viewer ribbon, every tap a 500). Pure. */
|
|
28406
|
-
function filterClipsByFootage(clips, ranges) {
|
|
28407
|
-
return clips.filter((c) => ranges.some((r) => c.timeRange.startMs < r.endMs && c.timeRange.endMs > r.startMs));
|
|
28408
|
-
}
|
|
28409
28977
|
function createVideoclipsProvider(deps) {
|
|
28410
28978
|
return {
|
|
28411
28979
|
listClips: async ({ deviceId, since, until, limit }) => {
|
|
28412
|
-
const
|
|
28980
|
+
const events = await deps.fetchEvents({
|
|
28413
28981
|
deviceId,
|
|
28414
28982
|
since,
|
|
28415
28983
|
until,
|
|
28416
|
-
limit
|
|
28417
|
-
})
|
|
28418
|
-
let
|
|
28984
|
+
...deps.fetchAvailability ? {} : { limit }
|
|
28985
|
+
});
|
|
28986
|
+
let clips = eventsToClips(events, deps.defaultPad);
|
|
28419
28987
|
if (deps.fetchAvailability) try {
|
|
28420
|
-
|
|
28988
|
+
clips = visitsToClips(clusterFootageVisits(await deps.fetchAvailability({
|
|
28421
28989
|
deviceId,
|
|
28422
28990
|
since,
|
|
28423
28991
|
until
|
|
28424
|
-
}));
|
|
28992
|
+
}), VISIT_MERGE_GAP_MS), events, deps.defaultPad);
|
|
28425
28993
|
} catch (err) {
|
|
28426
28994
|
deps.logWarn?.("videoclips: availability read failed — listing unfiltered", {
|
|
28427
28995
|
deviceId,
|
|
28428
28996
|
error: err instanceof Error ? err.message : String(err)
|
|
28429
28997
|
});
|
|
28430
28998
|
}
|
|
28431
|
-
return limit != null ?
|
|
28999
|
+
return limit != null ? clips.slice(0, limit) : clips;
|
|
28432
29000
|
},
|
|
28433
29001
|
getClipPlayback: async ({ deviceId, clipId }) => {
|
|
28434
29002
|
const parsed = parseClipId(clipId);
|
|
@@ -35557,7 +36125,8 @@ async function rankKeyEvents(candidates, options, peakLookup) {
|
|
|
35557
36125
|
windowMs: t.lastSeen - t.firstSeen,
|
|
35558
36126
|
...t.markForTrain !== void 0 ? { markForTrain: t.markForTrain } : {},
|
|
35559
36127
|
...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
|
|
35560
|
-
...t.debug !== void 0 ? { debug: t.debug } : {}
|
|
36128
|
+
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
36129
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {}
|
|
35561
36130
|
});
|
|
35562
36131
|
}
|
|
35563
36132
|
scored.sort((a, b) => b.importance - a.importance);
|
|
@@ -37200,7 +37769,8 @@ async function applyTrackFlags(deps, input) {
|
|
|
37200
37769
|
}
|
|
37201
37770
|
const patch = {
|
|
37202
37771
|
...input.flags.markForTrain !== void 0 ? { markForTrain: input.flags.markForTrain } : {},
|
|
37203
|
-
...input.flags.debug !== void 0 ? { debug: input.flags.debug } : {}
|
|
37772
|
+
...input.flags.debug !== void 0 ? { debug: input.flags.debug } : {},
|
|
37773
|
+
...input.flags.favourited !== void 0 ? { favourited: input.flags.favourited } : {}
|
|
37204
37774
|
};
|
|
37205
37775
|
if (Object.keys(patch).length > 0) await store.setFlags(input.trackId, patch);
|
|
37206
37776
|
const retrainStatus = patch.markForTrain === void 0 ? current.retrainStatus : patch.markForTrain ? "staging" : "none";
|
|
@@ -37208,6 +37778,7 @@ async function applyTrackFlags(deps, input) {
|
|
|
37208
37778
|
trackId: input.trackId,
|
|
37209
37779
|
markForTrain: retrainStatus === "staging",
|
|
37210
37780
|
debug: patch.debug ?? current.debug,
|
|
37781
|
+
favourited: patch.favourited ?? current.favourited,
|
|
37211
37782
|
retrainStatus
|
|
37212
37783
|
};
|
|
37213
37784
|
}
|
|
@@ -41251,6 +41822,10 @@ var TRACKS_COLUMNS = [
|
|
|
41251
41822
|
name: "debug",
|
|
41252
41823
|
type: "BOOLEAN"
|
|
41253
41824
|
},
|
|
41825
|
+
{
|
|
41826
|
+
name: "favourited",
|
|
41827
|
+
type: "BOOLEAN"
|
|
41828
|
+
},
|
|
41254
41829
|
{
|
|
41255
41830
|
name: "hasFace",
|
|
41256
41831
|
type: "BOOLEAN"
|
|
@@ -41344,6 +41919,7 @@ function cloneTrack(t) {
|
|
|
41344
41919
|
markForTrain: t.retrainStatus === "staging"
|
|
41345
41920
|
} : {},
|
|
41346
41921
|
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
41922
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {},
|
|
41347
41923
|
...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
|
|
41348
41924
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
41349
41925
|
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {}
|
|
@@ -41689,11 +42265,13 @@ var TrackStore = class {
|
|
|
41689
42265
|
const nextStatus = patch.markForTrain === void 0 ? void 0 : patch.markForTrain ? "staging" : "none";
|
|
41690
42266
|
if (nextStatus !== void 0) data["retrainStatus"] = nextStatus;
|
|
41691
42267
|
if (patch.debug !== void 0) data["debug"] = patch.debug;
|
|
42268
|
+
if (patch.favourited !== void 0) data["favourited"] = patch.favourited;
|
|
41692
42269
|
if (Object.keys(data).length === 0) return;
|
|
41693
42270
|
const active = this.active.get(trackId);
|
|
41694
42271
|
if (active) {
|
|
41695
42272
|
if (nextStatus !== void 0) active.retrainStatus = nextStatus;
|
|
41696
42273
|
if (patch.debug !== void 0) active.debug = patch.debug;
|
|
42274
|
+
if (patch.favourited !== void 0) active.favourited = patch.favourited;
|
|
41697
42275
|
}
|
|
41698
42276
|
try {
|
|
41699
42277
|
await this.store.update.mutate({
|
|
@@ -41834,13 +42412,15 @@ var TrackStore = class {
|
|
|
41834
42412
|
const active = this.active.get(trackId);
|
|
41835
42413
|
if (active) return {
|
|
41836
42414
|
retrainStatus: active.retrainStatus ?? "none",
|
|
41837
|
-
debug: active.debug === true
|
|
42415
|
+
debug: active.debug === true,
|
|
42416
|
+
favourited: active.favourited === true
|
|
41838
42417
|
};
|
|
41839
42418
|
const persisted = await this.getPersistedByTrackId(trackId);
|
|
41840
42419
|
if (!persisted) return null;
|
|
41841
42420
|
return {
|
|
41842
42421
|
retrainStatus: persisted.retrainStatus ?? "none",
|
|
41843
|
-
debug: persisted.debug === true
|
|
42422
|
+
debug: persisted.debug === true,
|
|
42423
|
+
favourited: persisted.favourited === true
|
|
41844
42424
|
};
|
|
41845
42425
|
}
|
|
41846
42426
|
/**
|
|
@@ -41975,6 +42555,7 @@ var TrackStore = class {
|
|
|
41975
42555
|
filter: {
|
|
41976
42556
|
where: { deviceId },
|
|
41977
42557
|
whereIn: { retrainStatus: ["none", "trained"] },
|
|
42558
|
+
whereNot: { favourited: true },
|
|
41978
42559
|
whereBetween: { lastSeen: [0, cutoffMs - 1] },
|
|
41979
42560
|
orderBy: {
|
|
41980
42561
|
field: "lastSeen",
|
|
@@ -42144,6 +42725,40 @@ var TrackStore = class {
|
|
|
42144
42725
|
}
|
|
42145
42726
|
}
|
|
42146
42727
|
/**
|
|
42728
|
+
* How many of this device's tracks are FAVOURITED — optionally only those
|
|
42729
|
+
* older than a cutoff, i.e. the rows a sweep just declined to touch because
|
|
42730
|
+
* of the boolean pin (independent of `retrainStatus`).
|
|
42731
|
+
*
|
|
42732
|
+
* Mirrors {@link countStaging}: the sweep log must name held work or a
|
|
42733
|
+
* favourited-only camera reads as "retention did nothing". Best-effort — a
|
|
42734
|
+
* failure reports 0 rather than disturbing the caller.
|
|
42735
|
+
*/
|
|
42736
|
+
async countFavourited(query) {
|
|
42737
|
+
const { deviceId, olderThanMs } = query;
|
|
42738
|
+
try {
|
|
42739
|
+
return await this.store.count.query({
|
|
42740
|
+
collection: TRACKS_COLLECTION,
|
|
42741
|
+
filter: {
|
|
42742
|
+
where: {
|
|
42743
|
+
deviceId,
|
|
42744
|
+
favourited: true
|
|
42745
|
+
},
|
|
42746
|
+
...olderThanMs !== void 0 ? { whereBetween: { lastSeen: [0, olderThanMs - 1] } } : {}
|
|
42747
|
+
}
|
|
42748
|
+
});
|
|
42749
|
+
} catch (err) {
|
|
42750
|
+
this.logger.warn("TrackStore.countFavourited failed", {
|
|
42751
|
+
tags: { deviceId },
|
|
42752
|
+
meta: {
|
|
42753
|
+
deviceId,
|
|
42754
|
+
olderThanMs,
|
|
42755
|
+
error: String(err)
|
|
42756
|
+
}
|
|
42757
|
+
});
|
|
42758
|
+
return 0;
|
|
42759
|
+
}
|
|
42760
|
+
}
|
|
42761
|
+
/**
|
|
42147
42762
|
* The SUBSET of `trackIds` that still have a persisted row — the orphan
|
|
42148
42763
|
* audit's liveness probe.
|
|
42149
42764
|
*
|
|
@@ -42516,6 +43131,7 @@ var TrackStore = class {
|
|
|
42516
43131
|
...t.audioLabels !== void 0 ? { audioLabels: [...t.audioLabels] } : {},
|
|
42517
43132
|
...t.retrainStatus !== void 0 ? { retrainStatus: t.retrainStatus } : {},
|
|
42518
43133
|
...t.debug !== void 0 ? { debug: t.debug } : {},
|
|
43134
|
+
...t.favourited !== void 0 ? { favourited: t.favourited } : {},
|
|
42519
43135
|
...t.hasFace !== void 0 ? { hasFace: t.hasFace } : {},
|
|
42520
43136
|
...t.hasRider !== void 0 ? { hasRider: t.hasRider } : {},
|
|
42521
43137
|
...t.closedReason !== void 0 ? { closedReason: t.closedReason } : {},
|
|
@@ -42553,6 +43169,7 @@ var TrackStore = class {
|
|
|
42553
43169
|
const audioLabels = data["audioLabels"];
|
|
42554
43170
|
const retrainParsed = RetrainStatusSchema.safeParse(data["retrainStatus"]);
|
|
42555
43171
|
const debug = data["debug"];
|
|
43172
|
+
const favourited = data["favourited"];
|
|
42556
43173
|
const hasFace = data["hasFace"];
|
|
42557
43174
|
/** Same read as `hasFace` — NULL / missing stays ABSENT, never `false`. */
|
|
42558
43175
|
const hasRider = data["hasRider"];
|
|
@@ -42592,6 +43209,7 @@ var TrackStore = class {
|
|
|
42592
43209
|
markForTrain: retrainParsed.data === "staging"
|
|
42593
43210
|
} : {},
|
|
42594
43211
|
...typeof debug === "boolean" ? { debug } : {},
|
|
43212
|
+
...typeof favourited === "boolean" ? { favourited } : {},
|
|
42595
43213
|
...typeof hasFace === "boolean" ? { hasFace } : {},
|
|
42596
43214
|
...typeof hasRider === "boolean" ? { hasRider } : {},
|
|
42597
43215
|
...closedReason !== void 0 ? { closedReason } : {},
|
|
@@ -44614,7 +45232,10 @@ var NC_SYSTEM_EVENT_CATEGORIES = [
|
|
|
44614
45232
|
EventCategory.WorkerOffline,
|
|
44615
45233
|
EventCategory.PipelineNodeInferenceUnavailable,
|
|
44616
45234
|
EventCategory.PipelineDetectionBlind,
|
|
44617
|
-
EventCategory.UpdateAvailable
|
|
45235
|
+
EventCategory.UpdateAvailable,
|
|
45236
|
+
EventCategory.AddonUpdated,
|
|
45237
|
+
EventCategory.SystemRestartCompleted,
|
|
45238
|
+
EventCategory.RecordingExportCompleted
|
|
44618
45239
|
];
|
|
44619
45240
|
/**
|
|
44620
45241
|
* Install the addon's bus subscriptions. Identical set + relative order to the
|
|
@@ -50713,6 +51334,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
50713
51334
|
},
|
|
50714
51335
|
renderEventMedia: (req) => this.renderEventMedia(req),
|
|
50715
51336
|
publishArtifact: async (bytes, mime) => await this.ncArtifactPlane?.publish(bytes, mime) ?? null,
|
|
51337
|
+
indexArtifact: async (bytes, mime, meta) => await this.ncArtifactPlane?.publishIndexed(bytes, mime, meta) ?? {
|
|
51338
|
+
url: null,
|
|
51339
|
+
artifactId: null
|
|
51340
|
+
},
|
|
50716
51341
|
listTargets: async () => {
|
|
50717
51342
|
return (await api.notificationOutput.listTargets.query({})).map((t) => {
|
|
50718
51343
|
const owner = t.config["ownerUserId"];
|
|
@@ -55373,6 +55998,19 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
|
|
|
55373
55998
|
deleted: counts.tracks
|
|
55374
55999
|
}
|
|
55375
56000
|
});
|
|
56001
|
+
const favourited = await this.trackStore?.countFavourited({
|
|
56002
|
+
deviceId,
|
|
56003
|
+
olderThanMs: cutoffMs
|
|
56004
|
+
});
|
|
56005
|
+
if (favourited !== void 0 && favourited > 0) this.ctx.logger.info("track retention held favourited tracks", {
|
|
56006
|
+
tags: { deviceId },
|
|
56007
|
+
meta: {
|
|
56008
|
+
deviceId,
|
|
56009
|
+
favourited,
|
|
56010
|
+
cutoffMs,
|
|
56011
|
+
deleted: counts.tracks
|
|
56012
|
+
}
|
|
56013
|
+
});
|
|
55376
56014
|
if (counts.tracks > 0) this.eventsOpsLog?.append({
|
|
55377
56015
|
op: "prune",
|
|
55378
56016
|
reason: "retention",
|