@camstack/addon-post-analysis 1.2.56 → 1.2.59

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.
@@ -1,4 +1,4 @@
1
- import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-CAC3R7XW.mjs";
1
+ import { $ as array, B as subKindsOf, C as addonWidgetsSourceCapability, D as cosineSimilarity, E as buildEventKindDescriptor, F as kebabToCamel, G as BaseAddon, H as videoclipsCapability, I as notificationRulesCapability, J as hydrateSchema, K as DeviceType, L as pipelineAnalyticsCapability, M as faceGalleryCapability, O as customAction, P as isScheduleActive, Q as _enum, R as plateGalleryCapability, S as TrackSourceSchema, T as audioMetricsCapability, U as zoneAnalyticsCapability, V as vectorDimFromBase64, W as errMsg, X as nodePin, Y as isDeviceScopedCap, Z as sleep, _ as NcTaxonomySchema, a as EVENT_PAD_MS, at as string, b as TimelapseRuleInputSchema, c as NC_CONDITION_CATALOG, d as NcRuleInputSchema, et as boolean, f as NcRulePatchSchema, g as NcSnoozeSuppressedSchema, h as NcSnoozeSchema, i as EVENT_KIND_BY_CAP, it as record, j as encodeVectorBase64, k as defineCustomActions, l as NC_TAXONOMY, m as NcSnoozeInputSchema, n as DEFAULT_EVENT_COLOR, nt as number, o as LabelAttributionSchema, ot as unknown, p as NcRuleSchema, q as createEvent, r as DeclaredDevices, rt as object, s as MACRO_LABELS, st as EventCategory, t as BaseDevice, tt as literal, u as NcConditionDescriptorSchema, v as OpsLogEntrySchema, w as alarmPanelCapability, x as TimelapseRuleSchema, y as RetrainStatusSchema, z as readDeviceStateFrom } from "../dist-DqEv8qMO.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";
@@ -2042,6 +2042,7 @@ function fail(condition) {
2042
2042
  */
2043
2043
  function presentConditionIds(c) {
2044
2044
  const ids = [];
2045
+ if (c.systemEvent !== void 0) ids.push("systemEvent");
2045
2046
  if (c.devices !== void 0 && c.devices.length > 0) ids.push("devices");
2046
2047
  if (c.source !== void 0 && c.source !== "any") ids.push("source");
2047
2048
  if (c.classes !== void 0 && c.classes.length > 0) ids.push("classes");
@@ -2135,19 +2136,34 @@ function referencesAudioClass(classes) {
2135
2136
  if (classes === void 0) return false;
2136
2137
  return classes.some((c) => c.trim().toLowerCase().startsWith("audio-"));
2137
2138
  }
2139
+ function matchesSystemEvent(condition, subject) {
2140
+ if (!condition.kinds.includes(subject.kind)) return false;
2141
+ if (condition.deviceIds !== void 0 && (subject.deviceId === void 0 || !condition.deviceIds.includes(subject.deviceId))) return false;
2142
+ if (condition.nodeIds !== void 0 && (subject.nodeId === void 0 || !condition.nodeIds.includes(subject.nodeId))) return false;
2143
+ if (condition.packageNames !== void 0 && (subject.packageName === void 0 || !condition.packageNames.includes(subject.packageName))) return false;
2144
+ return true;
2145
+ }
2138
2146
  function evaluateRule(rule, subject, deviceState) {
2139
- if (subject.kind === "occupancy-event") {
2147
+ if (subject.kind === "system-event") {
2148
+ if (rule.delivery !== "system-event") return fail("delivery");
2149
+ const condition = rule.conditions.systemEvent;
2150
+ if (condition === void 0 || subject.systemEvent === void 0 || !matchesSystemEvent(condition, subject.systemEvent)) return fail("systemEvent");
2151
+ if (rule.conditions.occupancy !== void 0) return fail("occupancy");
2152
+ } else if (subject.kind === "occupancy-event") {
2140
2153
  if (rule.delivery !== "device-event") return fail("delivery");
2141
2154
  const occ = rule.conditions.occupancy;
2142
2155
  if (occ === void 0) return fail("occupancy");
2143
2156
  if (subject.occupancy === void 0 || !matchesOccupancy(occ, subject.occupancy)) return fail("occupancy");
2157
+ if (rule.conditions.systemEvent !== void 0) return fail("systemEvent");
2144
2158
  } else if (subject.kind === "audio-event") {
2145
2159
  if (rule.delivery !== "immediate") return fail("delivery");
2146
2160
  if (!referencesAudioClass(rule.conditions.classes)) return fail("classes");
2147
2161
  if (rule.conditions.occupancy !== void 0) return fail("occupancy");
2162
+ if (rule.conditions.systemEvent !== void 0) return fail("systemEvent");
2148
2163
  } else {
2149
2164
  if ((rule.delivery === "immediate" ? "object-event" : rule.delivery) !== subject.kind) return fail("delivery");
2150
2165
  if (rule.conditions.occupancy !== void 0) return fail("occupancy");
2166
+ if (rule.conditions.systemEvent !== void 0) return fail("systemEvent");
2151
2167
  }
2152
2168
  const c = rule.conditions;
2153
2169
  if (c.deviceState !== void 0) {
@@ -2307,7 +2323,8 @@ function matchesDeviceScope(scope, subject) {
2307
2323
  function cooldownKey(rule, subject) {
2308
2324
  const first = subject.classNames[0];
2309
2325
  const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
2310
- return rule.throttle.scope === "rule" ? `r:${rule.id}${classKey}` : `r:${rule.id}:d:${subject.deviceId}${classKey}`;
2326
+ const scopeRef = subject.systemEvent !== void 0 ? `s:${subject.systemEvent.subject}` : `d:${subject.deviceId}`;
2327
+ return rule.throttle.scope === "rule" ? `r:${rule.id}${classKey}` : `r:${rule.id}:${scopeRef}${classKey}`;
2311
2328
  }
2312
2329
  /** True when the rule fired within its cooldown window before `now`. */
2313
2330
  function isCoolingDown(rule, lastFiredAt, now) {
@@ -2704,10 +2721,12 @@ var NcDispatcher = class {
2704
2721
  }
2705
2722
  async buildNotification(entry, target) {
2706
2723
  const subject = entry.payload.subject;
2707
- const deviceName = await this.deps.getDeviceName(subject.deviceId).catch(() => null) ?? `camera ${subject.deviceId}`;
2708
- const zoneLabels = await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones);
2724
+ const systemEvent = subject.systemEvent;
2725
+ const displayDeviceId = systemEvent?.deviceId ?? subject.deviceId;
2726
+ const deviceName = systemEvent !== void 0 && systemEvent.deviceId === void 0 ? "system" : await this.deps.getDeviceName(displayDeviceId).catch(() => null) ?? `camera ${displayDeviceId}`;
2727
+ const zoneLabels = systemEvent !== void 0 ? [] : await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones);
2709
2728
  const vars = buildTemplateVars(entry, deviceName, zoneLabels);
2710
- const title = renderTemplate(entry.payload.template?.title, vars) ?? entry.payload.ruleName;
2729
+ const title = renderTemplate(entry.payload.template?.title, vars) ?? systemEvent?.title ?? entry.payload.ruleName;
2711
2730
  const body = renderTemplate(entry.payload.template?.body, vars) ?? defaultBody(entry, deviceName, zoneLabels);
2712
2731
  const attachments = await this.withArtifactUrls(await this.resolveAttachments(entry));
2713
2732
  const params = pickParams(entry.payload.params);
@@ -2718,8 +2737,13 @@ var NcDispatcher = class {
2718
2737
  priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
2719
2738
  ...params,
2720
2739
  tag: entry.ruleId,
2721
- deviceId: subject.deviceId,
2740
+ ...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
2722
2741
  ...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
2742
+ ...systemEvent !== void 0 ? { metadata: {
2743
+ systemEventKind: systemEvent.kind,
2744
+ subject: systemEvent.subject,
2745
+ ...systemEvent.data
2746
+ } } : {},
2723
2747
  ...attachments.length > 0 ? { attachments } : {},
2724
2748
  ...actions.length > 0 ? { actions } : {}
2725
2749
  };
@@ -2946,6 +2970,7 @@ async function resolveZoneLabels(getZoneNames, deviceId, zoneIds) {
2946
2970
  function buildTemplateVars(entry, deviceName, zoneLabels) {
2947
2971
  const subject = entry.payload.subject;
2948
2972
  const occupancy = subject.occupancy;
2973
+ const systemEvent = subject.systemEvent;
2949
2974
  return {
2950
2975
  camera: deviceName,
2951
2976
  class: subject.className,
@@ -2957,7 +2982,13 @@ function buildTemplateVars(entry, deviceName, zoneLabels) {
2957
2982
  rule: entry.payload.ruleName,
2958
2983
  count: occupancy !== void 0 ? `${occupancy.count}` : "",
2959
2984
  capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
2960
- op: occupancy !== void 0 ? occupancyOpWord(occupancy.occupied) : ""
2985
+ op: occupancy !== void 0 ? occupancyOpWord(occupancy.occupied) : "",
2986
+ eventKind: systemEvent?.kind ?? "",
2987
+ subject: systemEvent?.subject ?? "",
2988
+ node: systemEvent?.nodeId ?? "",
2989
+ package: systemEvent?.packageName ?? "",
2990
+ currentVersion: typeof systemEvent?.data["currentVersion"] === "string" ? systemEvent.data["currentVersion"] : "",
2991
+ latestVersion: typeof systemEvent?.data["latestVersion"] === "string" ? systemEvent.data["latestVersion"] : ""
2961
2992
  };
2962
2993
  }
2963
2994
  /** The human-readable edge polarity used by `{{op}}` and the default body. */
@@ -2971,6 +3002,7 @@ function renderTemplate(template, vars) {
2971
3002
  }
2972
3003
  function defaultBody(entry, deviceName, zoneLabels) {
2973
3004
  const subject = entry.payload.subject;
3005
+ if (subject.systemEvent !== void 0) return subject.systemEvent.body;
2974
3006
  const occupancy = subject.occupancy;
2975
3007
  if (occupancy !== void 0) return `${occupancy.zone ?? deviceName} ${occupancyOpWord(occupancy.occupied)} (${occupancy.count}/${occupancy.capacity})`;
2976
3008
  const label = subject.label !== void 0 ? ` (${subject.label})` : "";
@@ -3102,6 +3134,146 @@ function incomingFromOccupancyEdge(edge) {
3102
3134
  })
3103
3135
  };
3104
3136
  }
3137
+ function stringField(data, key) {
3138
+ const value = data[key];
3139
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3140
+ }
3141
+ function numberField(data, key) {
3142
+ const value = data[key];
3143
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3144
+ }
3145
+ function recordField(data, key) {
3146
+ const value = data[key];
3147
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3148
+ }
3149
+ function systemSubject(input) {
3150
+ const data = {
3151
+ ...input.event.data,
3152
+ category: input.event.category
3153
+ };
3154
+ const deviceId = input.deviceId ?? 0;
3155
+ const tags = input.deviceId !== void 0 ? { deviceId: input.deviceId } : {};
3156
+ return {
3157
+ kind: "system-event",
3158
+ origin: "system",
3159
+ subject: {
3160
+ kind: "system-event",
3161
+ recordId: input.event.id,
3162
+ deviceId,
3163
+ timestamp: input.event.timestamp.getTime(),
3164
+ classNames: [],
3165
+ zones: [],
3166
+ systemEvent: {
3167
+ kind: input.kind,
3168
+ subject: input.subject,
3169
+ title: input.title,
3170
+ body: input.body,
3171
+ ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
3172
+ ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {},
3173
+ ...input.packageName !== void 0 ? { packageName: input.packageName } : {},
3174
+ data
3175
+ }
3176
+ },
3177
+ log: () => ({
3178
+ tags,
3179
+ meta: {
3180
+ eventId: input.event.id,
3181
+ category: input.event.category,
3182
+ kind: input.kind,
3183
+ subject: input.subject
3184
+ }
3185
+ })
3186
+ };
3187
+ }
3188
+ /**
3189
+ * Normalize the infrastructure categories consumed by Notification Center.
3190
+ * Malformed payloads are rejected instead of guessing an identity.
3191
+ */
3192
+ function incomingFromSystemEvent(event) {
3193
+ const data = event.data;
3194
+ switch (event.category) {
3195
+ case "device.state-changed": {
3196
+ const deviceId = numberField(data, "deviceId");
3197
+ const capName = stringField(data, "capName");
3198
+ const online = recordField(data, "slice")?.["online"];
3199
+ if (deviceId === void 0 || capName !== "device-status" || typeof online !== "boolean") return null;
3200
+ return systemSubject({
3201
+ event,
3202
+ kind: online ? "camera-online" : "camera-offline",
3203
+ subject: `camera:${deviceId}`,
3204
+ title: online ? "Camera online" : "Camera offline",
3205
+ body: `Camera ${deviceId} is ${online ? "online" : "offline"}`,
3206
+ deviceId
3207
+ });
3208
+ }
3209
+ case "device.online":
3210
+ case "device.offline": {
3211
+ const deviceId = numberField(data, "deviceId");
3212
+ if (deviceId === void 0) return null;
3213
+ const online = event.category === "device.online";
3214
+ return systemSubject({
3215
+ event,
3216
+ kind: online ? "camera-online" : "camera-offline",
3217
+ subject: `camera:${deviceId}`,
3218
+ title: online ? "Camera online" : "Camera offline",
3219
+ body: `Camera ${deviceId} is ${online ? "online" : "offline"}`,
3220
+ deviceId
3221
+ });
3222
+ }
3223
+ case "stream.online":
3224
+ case "stream.offline": {
3225
+ const deviceId = numberField(data, "deviceId");
3226
+ const camStreamId = stringField(data, "camStreamId");
3227
+ if (deviceId === void 0 || camStreamId === void 0) return null;
3228
+ const online = event.category === "stream.online";
3229
+ const profile = stringField(data, "profile");
3230
+ return systemSubject({
3231
+ event,
3232
+ kind: online ? "stream-online" : "stream-offline",
3233
+ subject: `stream:${camStreamId}`,
3234
+ title: online ? "Stream online" : "Stream offline",
3235
+ body: `${profile !== void 0 ? `${profile} stream` : "Stream"} for camera ${deviceId} is ${online ? "online" : "offline"}`,
3236
+ deviceId
3237
+ });
3238
+ }
3239
+ case "agent.online":
3240
+ case "agent.offline":
3241
+ case "worker.online":
3242
+ case "worker.offline": {
3243
+ const agent = event.category.startsWith("agent.");
3244
+ const nodeId = stringField(data, agent ? "agentId" : "workerId");
3245
+ if (nodeId === void 0) return null;
3246
+ const online = event.category.endsWith(".online");
3247
+ return systemSubject({
3248
+ event,
3249
+ kind: online ? "node-online" : "node-offline",
3250
+ subject: `node:${nodeId}`,
3251
+ title: online ? "Node online" : "Node offline",
3252
+ body: `${agent ? "Agent" : "Worker"} ${nodeId} is ${online ? "online" : "offline"}`,
3253
+ nodeId
3254
+ });
3255
+ }
3256
+ case "update.available": {
3257
+ const target = stringField(data, "target");
3258
+ const packageName = stringField(data, "packageName");
3259
+ const currentVersion = stringField(data, "currentVersion");
3260
+ const latestVersion = stringField(data, "latestVersion");
3261
+ if (target !== "addon" && target !== "server" || packageName === void 0 || currentVersion === void 0 || latestVersion === void 0) return null;
3262
+ const addon = target === "addon";
3263
+ const nodeId = stringField(data, "nodeId");
3264
+ return systemSubject({
3265
+ event,
3266
+ kind: addon ? "addon-update-available" : "server-update-available",
3267
+ subject: addon ? `package:${nodeId ?? "hub"}:${packageName}` : `package:${packageName}`,
3268
+ title: addon ? "Addon update available" : "Server update available",
3269
+ body: `${packageName} ${latestVersion} is available (current ${currentVersion})${nodeId !== void 0 ? ` on node ${nodeId}` : ""}`,
3270
+ packageName,
3271
+ ...nodeId !== void 0 ? { nodeId } : {}
3272
+ });
3273
+ }
3274
+ default: return null;
3275
+ }
3276
+ }
3105
3277
  /** Re-stamp a live-producer envelope as a boot-reconcile replay of the same
3106
3278
  * record — same subject, honest origin in the log. */
3107
3279
  function asReconcile(event) {
@@ -3945,7 +4117,8 @@ var OUTBOX_RECORD_KINDS = new Set([
3945
4117
  "track-end",
3946
4118
  "device-event",
3947
4119
  "package-event",
3948
- "audio-event"
4120
+ "audio-event",
4121
+ "system-event"
3949
4122
  ]);
3950
4123
  function isOutboxRecordKind(x) {
3951
4124
  return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
@@ -5453,7 +5626,8 @@ function outboxEntryToHistory(entry) {
5453
5626
  ...s.label !== void 0 ? { label: s.label } : {},
5454
5627
  ...s.confidence !== void 0 ? { confidence: s.confidence } : {},
5455
5628
  zones: [...s.zones],
5456
- timestamp: s.timestamp
5629
+ timestamp: s.timestamp,
5630
+ ...s.systemEvent !== void 0 ? { systemEvent: s.systemEvent } : {}
5457
5631
  }
5458
5632
  };
5459
5633
  }
@@ -5502,6 +5676,9 @@ var NotificationCenter = class NotificationCenter {
5502
5676
  /** Same rate-limit pair for the per-camera mute drop — see `reportMutedDrop`. */
5503
5677
  lastMutedReportAt = /* @__PURE__ */ new Map();
5504
5678
  mutedSuppressed = /* @__PURE__ */ new Map();
5679
+ /** Dedupes the explicit liveness category and device-status fallback when
5680
+ * both describe the same edge. */
5681
+ lastSystemLiveness = /* @__PURE__ */ new Map();
5505
5682
  /**
5506
5683
  * Serialized evaluation chain. The persist hooks are fire-and-forget for
5507
5684
  * the frame path, but two concurrent evaluations of the same device
@@ -5979,6 +6156,17 @@ var NotificationCenter = class NotificationCenter {
5979
6156
  onOccupancyEdge(edge) {
5980
6157
  this.consumeEvent(incomingFromOccupancyEdge(edge));
5981
6158
  }
6159
+ /** Event-bus infrastructure/update intake. Invalid payloads are ignored. */
6160
+ onSystemEvent(event) {
6161
+ const incoming = incomingFromSystemEvent(event);
6162
+ if (incoming === null) return;
6163
+ const system = incoming.subject.systemEvent;
6164
+ if (system !== void 0 && (system.kind.startsWith("camera-") || system.kind.startsWith("stream-") || system.kind.startsWith("node-"))) {
6165
+ if (this.lastSystemLiveness.get(system.subject) === system.kind) return;
6166
+ this.lastSystemLiveness.set(system.subject, system.kind);
6167
+ }
6168
+ this.consumeEvent(incoming);
6169
+ }
5982
6170
  buildProvider() {
5983
6171
  return {
5984
6172
  listRules: async () => ({ rules: [...this.rules.list()] }),
@@ -6371,6 +6559,7 @@ var NotificationCenter = class NotificationCenter {
6371
6559
  buildEntries(rule, subject, kind, matchedOn, userTargets = [], origin = "pipeline") {
6372
6560
  const hasEventMedia = kind === "object-event" || kind === "package-event";
6373
6561
  const isTrackScoped = kind === "object-event" || kind === "track-end";
6562
+ const isSystemEvent = kind === "system-event";
6374
6563
  const direct = new Set(rule.targets.map((t) => t.targetId));
6375
6564
  return [...rule.targets, ...userTargets.filter((t) => !direct.has(t.targetId))].filter((target) => !rule.disabledTargetIds.includes(target.targetId)).map((target) => {
6376
6565
  const payload = {
@@ -6379,15 +6568,15 @@ var NotificationCenter = class NotificationCenter {
6379
6568
  ...origin === "synthetic" ? { synthetic: true } : {},
6380
6569
  priority: rule.priority,
6381
6570
  ...rule.template !== void 0 ? { template: rule.template } : {},
6382
- media: rule.media.attach,
6383
- ...rule.media.zoneCrop === true && rule.conditions.zones !== void 0 ? { mediaZoneIds: [...rule.conditions.zones.ids] } : {},
6384
- ...rule.media.frame !== void 0 ? { mediaFrame: rule.media.frame } : {},
6385
- ...rule.media.profile !== void 0 ? { mediaProfile: rule.media.profile } : {},
6386
- ...rule.media.gif === true ? { mediaGif: true } : {},
6387
- ...rule.media.clip === true ? { mediaClip: true } : {},
6388
- ...rule.media.clipPreRollSec !== void 0 ? { mediaClipPreRollSec: rule.media.clipPreRollSec } : {},
6389
- ...rule.media?.clipSpeed !== void 0 ? { mediaClipSpeed: rule.media.clipSpeed } : {},
6390
- ...rule.media.clipPostRollSec !== void 0 ? { mediaClipPostRollSec: rule.media.clipPostRollSec } : {},
6571
+ media: isSystemEvent ? "none" : rule.media.attach,
6572
+ ...!isSystemEvent && rule.media.zoneCrop === true && rule.conditions.zones !== void 0 ? { mediaZoneIds: [...rule.conditions.zones.ids] } : {},
6573
+ ...!isSystemEvent && rule.media.frame !== void 0 ? { mediaFrame: rule.media.frame } : {},
6574
+ ...!isSystemEvent && rule.media.profile !== void 0 ? { mediaProfile: rule.media.profile } : {},
6575
+ ...!isSystemEvent && rule.media.gif === true ? { mediaGif: true } : {},
6576
+ ...!isSystemEvent && rule.media.clip === true ? { mediaClip: true } : {},
6577
+ ...!isSystemEvent && rule.media.clipPreRollSec !== void 0 ? { mediaClipPreRollSec: rule.media.clipPreRollSec } : {},
6578
+ ...!isSystemEvent && rule.media?.clipSpeed !== void 0 ? { mediaClipSpeed: rule.media.clipSpeed } : {},
6579
+ ...!isSystemEvent && rule.media.clipPostRollSec !== void 0 ? { mediaClipPostRollSec: rule.media.clipPostRollSec } : {},
6391
6580
  ...matchedOn !== void 0 && matchedOn.length > 0 ? { matchedOn } : {},
6392
6581
  ...target.params !== void 0 ? { params: target.params } : {},
6393
6582
  subject: {
@@ -6399,7 +6588,8 @@ var NotificationCenter = class NotificationCenter {
6399
6588
  ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
6400
6589
  zones: subject.zones,
6401
6590
  timestamp: subject.timestamp,
6402
- ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {}
6591
+ ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
6592
+ ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
6403
6593
  }
6404
6594
  };
6405
6595
  return {
@@ -6476,7 +6666,7 @@ var NotificationCenter = class NotificationCenter {
6476
6666
  for (const entry of entries) {
6477
6667
  const rule = this.rules.get(entry.ruleId);
6478
6668
  if (rule === null) continue;
6479
- const key = rule.throttle.scope === "rule" ? `r:${rule.id}` : `r:${rule.id}:d:${entry.deviceId}`;
6669
+ const key = rule.throttle.scope === "rule" ? `r:${rule.id}` : entry.payload.subject.systemEvent !== void 0 ? `r:${rule.id}:s:${entry.payload.subject.systemEvent.subject}` : `r:${rule.id}:d:${entry.deviceId}`;
6480
6670
  const prev = this.lastFiredAt.get(key) ?? 0;
6481
6671
  if (entry.createdAt > prev) this.lastFiredAt.set(key, entry.createdAt);
6482
6672
  }
@@ -12531,26 +12721,6 @@ function wideCentralSquareLayout(bbox, frame) {
12531
12721
  frameOriginX
12532
12722
  };
12533
12723
  }
12534
- //#endregion
12535
- //#region src/shared/frame/subject-crop-variants.ts
12536
- /**
12537
- * Best-shot subject-crop variants (best-crop fast-load, 2026-07-21; 16:9
12538
- * central-square reframe, 2026-07-22).
12539
- *
12540
- * ONE native fetch (the 16:9 central-square WINDOW — see
12541
- * {@link wide-central-square-crop}) backs both persisted variants:
12542
- * - `thumbnail` — the native uncapped 16:9 window JPEG (subject in the
12543
- * central square, lateral scene context, never upscaled).
12544
- * - `thumbnailSmall` — the SAME window downscaled to {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH}
12545
- * long side (854×480), the reel / lists / grid fast-load
12546
- * representative.
12547
- *
12548
- * The small variant is DERIVED from the already-composed native window — never a
12549
- * second native round-trip and NEVER upscaled: if the window is already ≤ 854 on
12550
- * its long side it is stored AS-IS (byte-identical). The window is composed from
12551
- * the in-frame slab plus lateral letterbox padding for the (rare) part of the
12552
- * 16:9 window that falls outside the frame at a horizontal edge.
12553
- */
12554
12724
  /** Gaussian sigma for the blurred-scene letterbox fill. */
12555
12725
  var LETTERBOX_BLUR_SIGMA = 25;
12556
12726
  /** Brightness factor for the blurred fill — dimmed so the real slab reads as
@@ -12596,15 +12766,12 @@ async function composeWideCentralSquareThumbnail(slab, layout) {
12596
12766
  }
12597
12767
  /**
12598
12768
  * Derive the `thumbnailSmall` JPEG from the composed native 16:9 window.
12599
- * Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (854) on the long
12600
- * side (aspect preserved → ~854×480). If the window's long side is already ≤ the
12601
- * cap, the ORIGINAL buffer is returned unchanged (store as-is, never upscale).
12769
+ * Downscales to at most {@link WIDE_THUMBNAIL_SMALL_MAX_WIDTH} (480) on the long
12770
+ * side (aspect preserved → 480×270) and re-encodes at representative quality.
12771
+ * `withoutEnlargement` preserves smaller sources.
12602
12772
  */
12603
12773
  async function deriveThumbnailSmall(nativeWideJpeg) {
12604
- const meta = await sharp(nativeWideJpeg).metadata();
12605
- const longSide = Math.max(meta.width ?? 0, meta.height ?? 0);
12606
- if (longSide > 0 && longSide <= 854) return nativeWideJpeg;
12607
- return sharp(nativeWideJpeg).resize(854, null, { withoutEnlargement: true }).jpeg({ quality: 88 }).toBuffer();
12774
+ return sharp(nativeWideJpeg).resize(480, null, { withoutEnlargement: true }).jpeg({ quality: 75 }).toBuffer();
12608
12775
  }
12609
12776
  //#endregion
12610
12777
  //#region src/shared/frame/keyframe-wide-thumbnail.ts
@@ -14858,6 +15025,7 @@ function createEventMediaHandler(deps) {
14858
15025
  }
14859
15026
  const variant = parseEventMediaVariant(query);
14860
15027
  const preferKind = parseEventMediaKind(query);
15028
+ const resolveStartedAt = performance.now();
14861
15029
  let media = null;
14862
15030
  try {
14863
15031
  media = await deps.getMedia(eventId, variant ?? void 0, preferKind ?? void 0);
@@ -14865,21 +15033,24 @@ function createEventMediaHandler(deps) {
14865
15033
  const body = "Internal server error";
14866
15034
  res.writeHead(500, {
14867
15035
  "content-type": "text/plain",
14868
- "content-length": String(Buffer.byteLength(body))
15036
+ "content-length": String(Buffer.byteLength(body)),
15037
+ "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`
14869
15038
  });
14870
15039
  if (req.method !== "HEAD") res.end(body);
14871
15040
  else res.end();
14872
15041
  return;
14873
15042
  }
14874
15043
  if (media === null) {
14875
- res.writeHead(404).end();
15044
+ res.writeHead(404, { "server-timing": `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}` }).end();
14876
15045
  return;
14877
15046
  }
14878
15047
  const etag = "\"" + media.key + "\"";
15048
+ const serverTiming = `resolve;dur=${(performance.now() - resolveStartedAt).toFixed(1)}`;
14879
15049
  if (req.headers["if-none-match"] === etag) {
14880
15050
  res.writeHead(304, {
14881
15051
  etag,
14882
- "cache-control": CACHE_CONTROL
15052
+ "cache-control": CACHE_CONTROL,
15053
+ "server-timing": serverTiming
14883
15054
  }).end();
14884
15055
  return;
14885
15056
  }
@@ -14887,7 +15058,8 @@ function createEventMediaHandler(deps) {
14887
15058
  "content-type": "image/jpeg",
14888
15059
  "cache-control": CACHE_CONTROL,
14889
15060
  etag,
14890
- "content-length": String(media.bytes.byteLength)
15061
+ "content-length": String(media.bytes.byteLength),
15062
+ "server-timing": serverTiming
14891
15063
  });
14892
15064
  if (req.method === "HEAD") res.end();
14893
15065
  else res.end(Buffer.from(media.bytes));
@@ -26522,6 +26694,18 @@ function wireSubscriptions(deps) {
26522
26694
  if (nodes === null) return;
26523
26695
  deps.onClusterTopology(nodes);
26524
26696
  }));
26697
+ unsubs.push(deps.eventBus.subscribe({ category: [
26698
+ EventCategory.DeviceOnline,
26699
+ EventCategory.DeviceOffline,
26700
+ EventCategory.DeviceStateChanged,
26701
+ EventCategory.StreamOnline,
26702
+ EventCategory.StreamOffline,
26703
+ EventCategory.AgentOnline,
26704
+ EventCategory.AgentOffline,
26705
+ EventCategory.WorkerOnline,
26706
+ EventCategory.WorkerOffline,
26707
+ EventCategory.UpdateAvailable
26708
+ ] }, (ev) => deps.onSystemEvent(ev)));
26525
26709
  }
26526
26710
  unsubs.push(deps.eventBus.subscribe({ category: EventCategory.DeviceBindingsChanged }, (ev) => {
26527
26711
  deps.onBindingsChanged(ev.data);
@@ -27614,9 +27798,9 @@ var EventMediaDispatcher = class {
27614
27798
  * the runner's retained native surface with NO `maxWidth` (uncapped TRUE
27615
27799
  * native). It is composed into the full 16:9 window (lateral letterbox only for
27616
27800
  * the part outside the frame) → the `thumbnail`. The `thumbnailSmall` is
27617
- * DERIVED by downscaling that SAME window to the 854 long-side cap (854×480) —
27801
+ * DERIVED by downscaling that SAME window to the 480 long-side cap (480×270) —
27618
27802
  * never a second `getNativeCropJpeg`, never upscaled ({@link deriveThumbnailSmall}
27619
- * returns the window as-is when it is already ≤ 854).
27803
+ * never upscales a smaller window).
27620
27804
  *
27621
27805
  * Both variants share the SAME framing + central-square containment guarantee:
27622
27806
  * wide consumers use the 16:9 image as-is (subject centered, no cut); square
@@ -30875,6 +31059,7 @@ var MOTION_EVENT_HEARTBEAT_MS = 5e3;
30875
31059
  var CLEAN_MEDIA_KINDS = [
30876
31060
  "crop",
30877
31061
  "thumbnail",
31062
+ "thumbnailSmall",
30878
31063
  "fullFrame",
30879
31064
  "keyFrame",
30880
31065
  "keyFrameSmall"
@@ -31574,7 +31759,8 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
31574
31759
  onSensorStateChanged: (data, timestampMs) => this.handleSensorStateChanged(data, timestampMs),
31575
31760
  onBindingsChanged: (data) => this.handleBindingsChangedEvent(data),
31576
31761
  onDeviceUnregistered: (deviceId) => this.handleDeviceUnregisteredEvent(deviceId),
31577
- onClusterTopology: (nodes) => this.nodeLiveness.applySnapshot(nodes, Date.now())
31762
+ onClusterTopology: (nodes) => this.nodeLiveness.applySnapshot(nodes, Date.now()),
31763
+ onSystemEvent: (event) => this.notificationCenter?.onSystemEvent(event)
31578
31764
  });
31579
31765
  if (this.isPostProcessingNode) await this.startEmbeddingDispatcher(logger, transport);
31580
31766
  this.startSweepTimers();
@@ -36213,6 +36399,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
36213
36399
  * failure the full blob is served (a thumb must never 500 / blank a tile).
36214
36400
  */
36215
36401
  async applyThumbVariant(media, variant) {
36402
+ if (media.key.includes(":thumbnailSmall") && media.bytes.byteLength <= 64 * 1024) return media;
36216
36403
  try {
36217
36404
  return {
36218
36405
  bytes: await makeSquareThumb(media.bytes, variant.size),
@@ -36231,7 +36418,13 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
36231
36418
  const eventFiles = await (this.mediaStore?.listByOwner("event", id) ?? Promise.resolve([]));
36232
36419
  if (preferKind !== void 0 && preferKind.length > 0) {
36233
36420
  const trackFiles = await (this.mediaStore?.listByOwner("track", id) ?? Promise.resolve([]));
36234
- const chosen = resolveCropForcedMedia([...eventFiles, ...trackFiles], (files) => pickCleanMedia(files, preferKind));
36421
+ const ownerTrackId = trackFiles.length === 0 ? await (this.eventStore?.getTrackIdForEvent(id) ?? Promise.resolve(null)) : null;
36422
+ const owningTrackFiles = ownerTrackId === null ? [] : await (this.mediaStore?.listByOwner("track", ownerTrackId) ?? Promise.resolve([]));
36423
+ const chosen = resolveCropForcedMedia([
36424
+ ...eventFiles,
36425
+ ...trackFiles,
36426
+ ...owningTrackFiles
36427
+ ], (files) => pickCleanMedia(files, preferKind));
36235
36428
  if (!chosen) return null;
36236
36429
  return {
36237
36430
  bytes: Buffer.from(chosen.base64, "base64"),
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ABX4r2Sy.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-ChhzqbFf.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.56",
3
+ "version": "1.2.59",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",