@camstack/addon-post-analysis 1.2.70 → 1.2.71

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 readDeviceStateFrom, A as TimelapseRuleInputSchema, B as customAction, C as NcSnoozeSchema, Ct as unknown, D as RECORDING_EXPORT_MAX_READ_BYTES, E as OpsLogEntrySchema, F as alarmPanelCapability, G as faceGalleryCapability, H as deriveRecordingMode, I as assertTimelapseCadences, J as isScheduleActive, L as audioMetricsCapability, M as TimelapseRuleSchema, N as TrackSourceSchema, O as RetrainStatusSchema, P as addonWidgetsSourceCapability, Q as plateGalleryCapability, R as buildEventKindDescriptor, S as NcSnoozeInputSchema, St as string, T as NcTaxonomySchema, V as defineCustomActions, W as encodeVectorBase64, X as notificationRulesCapability, Y as kebabToCamel, Z as pipelineAnalyticsCapability, _ as NcRuleInputSchema, _t as literal, at as errMsg, b as NcRuleTargetSchema, bt as partialRecord, c as EVENT_PAD_MS, ct as createEvent, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as nodePin, et as readTimelapseGeneratedAt, f as NC_CONDITION_CATALOG, ft as sleep, g as NcConditionDescriptorSchema, gt as discriminatedUnion, h as NC_TAXONOMY, ht as boolean, i as DETECTION_MACRO_CLASSES, it as zoneAnalyticsCapability, j as TimelapseRulePatchSchema, k as TIMELAPSE_DENSE_FLOOR_SEC, l as LabelAttributionSchema, lt as hydrateSchema, mt as array, n as DEFAULT_EVENT_COLOR, nt as vectorDimFromBase64, o as DeclaredDevices, ot as BaseAddon, p as NC_DEFAULT_SNOOZE_MINUTES, pt as _enum, q as isDetectionMacroClass, rt as videoclipsCapability, s as EVENT_KIND_BY_CAP, st as DeviceType, t as BaseDevice, tt as subKindsOf, u as MACRO_LABELS, ut as isDeviceScopedCap, v as NcRulePatchSchema, vt as number, w as NcSnoozeSuppressedSchema, wt as EventCategory, x as NcScheduleSchema, xt as record, y as NcRuleSchema, yt as object, z as cosineSimilarity } from "../dist-6T-F0YJO.mjs";
1
+ import { $ as readDeviceStateFrom, A as TimelapseRuleInputSchema, B as customAction, C as NcSnoozeSchema, Ct as unknown, D as RECORDING_EXPORT_MAX_READ_BYTES, E as OpsLogEntrySchema, F as alarmPanelCapability, G as faceGalleryCapability, H as deriveRecordingMode, I as assertTimelapseCadences, J as isScheduleActive, L as audioMetricsCapability, M as TimelapseRuleSchema, N as TrackSourceSchema, O as RetrainStatusSchema, P as addonWidgetsSourceCapability, Q as plateGalleryCapability, R as buildEventKindDescriptor, S as NcSnoozeInputSchema, St as string, T as NcTaxonomySchema, V as defineCustomActions, W as encodeVectorBase64, X as notificationRulesCapability, Y as kebabToCamel, Z as pipelineAnalyticsCapability, _ as NcRuleInputSchema, _t as literal, at as errMsg, b as NcRuleTargetSchema, bt as partialRecord, c as EVENT_PAD_MS, ct as createEvent, d as NC_ALARM_SYSTEM_EVENT_KINDS, dt as nodePin, et as readTimelapseGeneratedAt, f as NC_CONDITION_CATALOG, ft as sleep, g as NcConditionDescriptorSchema, gt as discriminatedUnion, h as NC_TAXONOMY, ht as boolean, i as DETECTION_MACRO_CLASSES, it as zoneAnalyticsCapability, j as TimelapseRulePatchSchema, k as TIMELAPSE_DENSE_FLOOR_SEC, l as LabelAttributionSchema, lt as hydrateSchema, mt as array, n as DEFAULT_EVENT_COLOR, nt as vectorDimFromBase64, o as DeclaredDevices, ot as BaseAddon, p as NC_DEFAULT_SNOOZE_MINUTES, pt as _enum, q as isDetectionMacroClass, rt as videoclipsCapability, s as EVENT_KIND_BY_CAP, st as DeviceType, t as BaseDevice, tt as subKindsOf, u as MACRO_LABELS, ut as isDeviceScopedCap, v as NcRulePatchSchema, vt as number, w as NcSnoozeSuppressedSchema, wt as EventCategory, x as NcScheduleSchema, xt as record, y as NcRuleSchema, yt as object, z as cosineSimilarity } from "../dist-B--dE3Ry.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";
@@ -549,7 +549,14 @@ function alarmCoverageGap(coverage) {
549
549
  return "all-skipped";
550
550
  }
551
551
  /**
552
- * "Away armed — 3 cameras: Front door, Garage, Gate."
552
+ * "Attivi: Cancello, Garage — Esclusi: Porta retro."
553
+ *
554
+ * A LIST, not a sentence. The mode is in the title and the body is one
555
+ * `Label: names` line per device group, because this is read on a lock screen
556
+ * and the question is always "which one" — the prose version ("3 dispositivi
557
+ * attivi: …. Esclusi: ….") made the operator parse a paragraph to find a
558
+ * window. Each variant still owns its own catalog key, so the Texts tab can
559
+ * re-word "nothing is armed" without touching "these are armed".
553
560
  *
554
561
  * Names, not ids: this is read by a person standing at a door. An id that has
555
562
  * no name falls back to a NAMED, translated placeholder rather than being
@@ -628,7 +635,7 @@ function buildArmAnnouncement(coverage, deviceName, bypassed = []) {
628
635
  };
629
636
  }
630
637
  /**
631
- * "Home armed — with the veranda window open, excluded."
638
+ * "Attivi: 2 — Bypassati: Finestra veranda."
632
639
  *
633
640
  * The bypassed sensors are named and the armed ones are COUNTED, not listed:
634
641
  * this sentence's subject is the exception, and burying it under thirteen
@@ -651,13 +658,13 @@ function armedWithBypass(coverage, bypassed, deviceName) {
651
658
  };
652
659
  }
653
660
  /**
654
- * "The alarm was NOT armed: the veranda window is open."
661
+ * "Aperti: Finestra veranda, Porta retro" + how to arm anyway.
655
662
  *
656
663
  * The one alarm sentence about something that did not happen, so it has to
657
664
  * carry both halves an operator needs to act: WHICH sensors are open, and that
658
665
  * pressing arm again arms without them. Naming them is the whole message — a
659
666
  * refusal that says only "cannot arm" is a panel the operator will conclude is
660
- * broken.
667
+ * broken — so the names are the FIRST line and the retry hint is the second.
661
668
  */
662
669
  function buildArmRefusedAnnouncement(mode, names, retrySeconds) {
663
670
  return {
@@ -672,7 +679,7 @@ function buildArmRefusedAnnouncement(mode, names, retrySeconds) {
672
679
  };
673
680
  }
674
681
  /**
675
- * The same coverage, said 30 seconds earlier: "Armed in 30 seconds."
682
+ * The EXCEPTIONS to the same coverage, said 30 seconds earlier.
676
683
  *
677
684
  * The exit-delay sentence is deliberately SHORTER than the arm one — it is read
678
685
  * on the way out of a door — and it carries exactly one thing the arm sentence
@@ -680,8 +687,15 @@ function buildArmRefusedAnnouncement(mode, names, retrySeconds) {
680
687
  * A mode that arms nothing is worth interrupting somebody for at second 0 and
681
688
  * merely worth recording at second 30.
682
689
  *
683
- * Pluralises on the SECONDS, not on the devices: this sentence's only number is
684
- * the countdown, and Italian needs "1 secondo" / "30 secondi".
690
+ * **It never repeats the device list.** The countdown lives in the TITLE
691
+ * (`system.alarm-arming.title.<mode>` renders `{{seconds}}`), and the armed
692
+ * list arrives thirty seconds later in the arm announcement — printing it
693
+ * twice is the verbosity that made the operator stop reading both. So the body
694
+ * is what is WRONG: what was bypassed, what is excluded, or that nothing is.
695
+ *
696
+ * `seconds` stays in the params because the title asks for it, and `textCount`
697
+ * stays with it: a row enqueued before this deploy still pluralises on the
698
+ * countdown it was written for.
685
699
  */
686
700
  function buildArmingAnnouncement(coverage, seconds, deviceName, bypassed = []) {
687
701
  const textParams = {
@@ -696,6 +710,7 @@ function buildArmingAnnouncement(coverage, seconds, deviceName, bypassed = []) {
696
710
  },
697
711
  textCount: seconds
698
712
  };
713
+ const skippedNames = coverage.skippedDevices.map((s) => deviceName(s.deviceId)).join(", ");
699
714
  const gap = alarmCoverageGap(coverage);
700
715
  if (gap === "uncovered") return {
701
716
  bodyKey: "alarm.arming.body.uncovered",
@@ -706,10 +721,18 @@ function buildArmingAnnouncement(coverage, seconds, deviceName, bypassed = []) {
706
721
  bodyKey: "alarm.arming.body.allSkipped",
707
722
  textParams: {
708
723
  ...textParams,
709
- names: coverage.skippedDevices.map((s) => deviceName(s.deviceId)).join(", ")
724
+ names: skippedNames
710
725
  },
711
726
  textCount: seconds
712
727
  };
728
+ if (coverage.skippedDevices.length > 0) return {
729
+ bodyKey: "alarm.arming.body.skipped",
730
+ textParams: {
731
+ ...textParams,
732
+ names: skippedNames
733
+ },
734
+ textCount: coverage.skippedDevices.length
735
+ };
713
736
  return {
714
737
  bodyKey: "alarm.arming.body",
715
738
  textParams,
@@ -4336,69 +4359,38 @@ var en_default = {
4336
4359
  "snooze.scope.anyDevice": "A camera",
4337
4360
  "snooze.scope.anyRule": "A rule",
4338
4361
  "snooze.scope.rule": "“{{rule}}”",
4339
- "alarm.arm.body.allDevices": {
4340
- "one": "Every device is armed ({{rules}} rule).",
4341
- "other": "Every device is armed ({{rules}} rules)."
4342
- },
4343
- "alarm.arm.body.noDevices": {
4344
- "one": "{{rules}} rule, no device restriction.",
4345
- "other": "{{rules}} rules, no device restriction."
4346
- },
4347
- "alarm.arm.body.devices": {
4348
- "one": "{{devices}} device armed: {{names}}.",
4349
- "other": "{{devices}} devices armed: {{names}}."
4350
- },
4351
- "alarm.arm.body.devicesSkipped": {
4352
- "one": "{{devices}} device armed: {{names}}. Excluded: {{skippedNames}}.",
4353
- "other": "{{devices}} devices armed: {{names}}. Excluded: {{skippedNames}}."
4354
- },
4355
- "alarm.arm.body.uncovered": "Nothing is armed — no enabled rule is gated on this mode.",
4356
- "alarm.arm.body.allSkipped": {
4357
- "one": "Nothing is armed: the {{devices}} device this mode covers is excluded ({{names}}).",
4358
- "other": "Nothing is armed: all {{devices}} devices this mode covers are excluded ({{names}})."
4359
- },
4360
- "alarm.arming.body": {
4361
- "one": "Armed in {{seconds}} second.",
4362
- "other": "Armed in {{seconds}} seconds."
4363
- },
4364
- "alarm.arming.body.uncovered": {
4365
- "one": "Armed in {{seconds}} second — but no enabled rule is gated on this mode, so it will protect nothing.",
4366
- "other": "Armed in {{seconds}} seconds — but no enabled rule is gated on this mode, so it will protect nothing."
4367
- },
4368
- "alarm.arming.body.allSkipped": {
4369
- "one": "Armed in {{seconds}} second — but every device this mode covers is excluded ({{names}}), so it will protect nothing.",
4370
- "other": "Armed in {{seconds}} seconds — but every device this mode covers is excluded ({{names}}), so it will protect nothing."
4371
- },
4372
- "alarm.arm.body.bypassed": {
4373
- "one": "{{devices}} device armed — open and excluded: {{bypassedNames}}.",
4374
- "other": "{{devices}} devices armed — open and excluded: {{bypassedNames}}."
4375
- },
4376
- "alarm.arming.body.bypassed": {
4377
- "one": "Armed in {{seconds}} second, excluding what is open: {{bypassedNames}}.",
4378
- "other": "Armed in {{seconds}} seconds, excluding what is open: {{bypassedNames}}."
4379
- },
4380
- "alarm.armRefused.body": {
4381
- "one": "{{names}} is open. Press arm again within {{seconds}}s to arm without it.",
4382
- "other": "These are open: {{names}}. Press arm again within {{seconds}}s to arm without them."
4383
- },
4362
+ "alarm.arm.body.allDevices": "Active: all devices",
4363
+ "alarm.arm.body.noDevices": "Active: no device restriction",
4364
+ "alarm.arm.body.devices": "Active: {{names}}",
4365
+ "alarm.arm.body.devicesSkipped": "Active: {{names}}\nExcluded: {{skippedNames}}",
4366
+ "alarm.arm.body.uncovered": "Active: none — no rule uses this mode",
4367
+ "alarm.arm.body.allSkipped": "Active: none\nExcluded: {{names}}",
4368
+ "alarm.arming.body": "No exclusions",
4369
+ "alarm.arming.body.uncovered": "Active: none — no rule uses this mode",
4370
+ "alarm.arming.body.allSkipped": "Active: none\nExcluded: {{names}}",
4371
+ "alarm.arming.body.skipped": "Excluded: {{names}}",
4372
+ "alarm.arm.body.bypassed": "Active: {{devices}}\nBypassed: {{bypassedNames}}",
4373
+ "alarm.arming.body.bypassed": "Bypassed: {{bypassedNames}}",
4374
+ "alarm.armRefused.body": "Open: {{names}}\nRepeat within {{seconds}}s to arm with bypass.",
4384
4375
  "alarm.device.unnamed": "Device {{id}}",
4385
- "system.alarm-arm-refused.title.home": "Home NOT armed",
4386
- "system.alarm-arm-refused.title.away": "Away NOT armed",
4387
- "system.alarm-arm-refused.title.night": "Night NOT armed",
4388
- "system.alarm-arm-refused.body": "The alarm could not be armed — something it covers is open.",
4389
- "system.alarm-arming.title.home": "Arming home",
4390
- "system.alarm-arming.title.away": "Arming away",
4391
- "system.alarm-arming.title.night": "Arming night",
4392
- "system.alarm-arming.body": "The alarm is arming",
4393
- "system.alarm-armed.title.home": "Home armed",
4394
- "system.alarm-armed.title.away": "Away armed",
4395
- "system.alarm-armed.title.night": "Night armed",
4396
- "system.alarm-armed.body": "The alarm was armed",
4397
- "system.alarm-disarmed.title": "Alarm disarmed",
4398
- "system.alarm-disarmed.body": "The alarm was disarmed",
4376
+ "alarm.combined.title": "Alarm: {{deviceName}}",
4377
+ "system.alarm-arm-refused.title.home": "Arming home refused",
4378
+ "system.alarm-arm-refused.title.away": "Arming away refused",
4379
+ "system.alarm-arm-refused.title.night": "Arming night refused",
4380
+ "system.alarm-arm-refused.body": "Arming refused",
4381
+ "system.alarm-arming.title.home": "Arming home — {{seconds}}s",
4382
+ "system.alarm-arming.title.away": "Arming away — {{seconds}}s",
4383
+ "system.alarm-arming.title.night": "Arming night — {{seconds}}s",
4384
+ "system.alarm-arming.body": "Arming",
4385
+ "system.alarm-armed.title.home": "Armed: home",
4386
+ "system.alarm-armed.title.away": "Armed: away",
4387
+ "system.alarm-armed.title.night": "Armed: night",
4388
+ "system.alarm-armed.body": "Alarm armed",
4389
+ "system.alarm-disarmed.title": "Disarmed",
4390
+ "system.alarm-disarmed.body": "Alarm disarmed",
4399
4391
  "system.alarm-triggered.title": "ALARM",
4400
- "system.alarm-triggered.body": "The alarm was triggered",
4401
- "system.alarm-triggered.body.rule": "The alarm was triggered by {{rule}} on {{deviceName}}",
4392
+ "system.alarm-triggered.body": "Alarm triggered",
4393
+ "system.alarm-triggered.body.rule": "Triggered by {{rule}} on {{deviceName}}",
4402
4394
  "button.snooze.minutes": "Snooze {{minutes}} min",
4403
4395
  "button.snooze.hours": {
4404
4396
  "one": "Snooze {{hours}} hour",
@@ -4530,69 +4522,38 @@ var it_default = {
4530
4522
  "snooze.scope.anyDevice": "Una camera",
4531
4523
  "snooze.scope.anyRule": "Una regola",
4532
4524
  "snooze.scope.rule": "“{{rule}}”",
4533
- "alarm.arm.body.allDevices": {
4534
- "one": "Ogni dispositivo è attivo ({{rules}} regola).",
4535
- "other": "Ogni dispositivo è attivo ({{rules}} regole)."
4536
- },
4537
- "alarm.arm.body.noDevices": {
4538
- "one": "{{rules}} regola, nessuna restrizione sui dispositivi.",
4539
- "other": "{{rules}} regole, nessuna restrizione sui dispositivi."
4540
- },
4541
- "alarm.arm.body.devices": {
4542
- "one": "{{devices}} dispositivo attivo: {{names}}.",
4543
- "other": "{{devices}} dispositivi attivi: {{names}}."
4544
- },
4545
- "alarm.arm.body.devicesSkipped": {
4546
- "one": "{{devices}} dispositivo attivo: {{names}}. Esclusi: {{skippedNames}}.",
4547
- "other": "{{devices}} dispositivi attivi: {{names}}. Esclusi: {{skippedNames}}."
4548
- },
4549
- "alarm.arm.body.uncovered": "Non è attivo nulla — nessuna regola abilitata è legata a questa modalità.",
4550
- "alarm.arm.body.allSkipped": {
4551
- "one": "Non è attivo nulla: l'unico dispositivo ({{devices}}) coperto da questa modalità è escluso ({{names}}).",
4552
- "other": "Non è attivo nulla: tutti i {{devices}} dispositivi coperti da questa modalità sono esclusi ({{names}})."
4553
- },
4554
- "alarm.arming.body": {
4555
- "one": "Attivo tra {{seconds}} secondo.",
4556
- "other": "Attivo tra {{seconds}} secondi."
4557
- },
4558
- "alarm.arming.body.uncovered": {
4559
- "one": "Attivo tra {{seconds}} secondo — ma nessuna regola abilitata è legata a questa modalità: non proteggerà nulla.",
4560
- "other": "Attivo tra {{seconds}} secondi — ma nessuna regola abilitata è legata a questa modalità: non proteggerà nulla."
4561
- },
4562
- "alarm.arming.body.allSkipped": {
4563
- "one": "Attivo tra {{seconds}} secondo — ma ogni dispositivo coperto da questa modalità è escluso ({{names}}): non proteggerà nulla.",
4564
- "other": "Attivo tra {{seconds}} secondi — ma ogni dispositivo coperto da questa modalità è escluso ({{names}}): non proteggerà nulla."
4565
- },
4566
- "alarm.arm.body.bypassed": {
4567
- "one": "{{devices}} dispositivo attivo — aperto ed escluso: {{bypassedNames}}.",
4568
- "other": "{{devices}} dispositivi attivi — aperti ed esclusi: {{bypassedNames}}."
4569
- },
4570
- "alarm.arming.body.bypassed": {
4571
- "one": "Attivo tra {{seconds}} secondo, escludendo ciò che è aperto: {{bypassedNames}}.",
4572
- "other": "Attivo tra {{seconds}} secondi, escludendo ciò che è aperto: {{bypassedNames}}."
4573
- },
4574
- "alarm.armRefused.body": {
4575
- "one": "{{names}} è aperto. Premi di nuovo entro {{seconds}}s per inserire senza di esso.",
4576
- "other": "Sono aperti: {{names}}. Premi di nuovo entro {{seconds}}s per inserire senza di essi."
4577
- },
4525
+ "alarm.arm.body.allDevices": "Attivi: tutti i dispositivi",
4526
+ "alarm.arm.body.noDevices": "Attivi: nessuna restrizione",
4527
+ "alarm.arm.body.devices": "Attivi: {{names}}",
4528
+ "alarm.arm.body.devicesSkipped": "Attivi: {{names}}\nEsclusi: {{skippedNames}}",
4529
+ "alarm.arm.body.uncovered": "Attivi: nessuno — nessuna regola usa questa modalità",
4530
+ "alarm.arm.body.allSkipped": "Attivi: nessuno\nEsclusi: {{names}}",
4531
+ "alarm.arming.body": "Nessuna esclusione",
4532
+ "alarm.arming.body.uncovered": "Attivi: nessuno — nessuna regola usa questa modalità",
4533
+ "alarm.arming.body.allSkipped": "Attivi: nessuno\nEsclusi: {{names}}",
4534
+ "alarm.arming.body.skipped": "Esclusi: {{names}}",
4535
+ "alarm.arm.body.bypassed": "Attivi: {{devices}}\nBypassati: {{bypassedNames}}",
4536
+ "alarm.arming.body.bypassed": "Bypassati: {{bypassedNames}}",
4537
+ "alarm.armRefused.body": "Aperti: {{names}}\nRipeti entro {{seconds}}s per inserire con bypass.",
4578
4538
  "alarm.device.unnamed": "Dispositivo {{id}}",
4539
+ "alarm.combined.title": "Allarme: {{deviceName}}",
4579
4540
  "system.alarm-arm-refused.title.home": "Inserimento Casa rifiutato",
4580
4541
  "system.alarm-arm-refused.title.away": "Inserimento Fuori rifiutato",
4581
4542
  "system.alarm-arm-refused.title.night": "Inserimento Notte rifiutato",
4582
- "system.alarm-arm-refused.body": "L'allarme non è stato inserito — qualcosa che copre è aperto.",
4583
- "system.alarm-arming.title.home": "Inserimento Casa",
4584
- "system.alarm-arming.title.away": "Inserimento Fuori",
4585
- "system.alarm-arming.title.night": "Inserimento Notte",
4586
- "system.alarm-arming.body": "L'allarme si sta inserendo",
4587
- "system.alarm-armed.title.home": "Casa attivato",
4588
- "system.alarm-armed.title.away": "Fuori attivato",
4589
- "system.alarm-armed.title.night": "Notte attivato",
4590
- "system.alarm-armed.body": "L'allarme è stato inserito",
4591
- "system.alarm-disarmed.title": "Allarme disattivato",
4592
- "system.alarm-disarmed.body": "L'allarme è stato disattivato",
4543
+ "system.alarm-arm-refused.body": "Inserimento rifiutato",
4544
+ "system.alarm-arming.title.home": "Inserimento Casa — {{seconds}}s",
4545
+ "system.alarm-arming.title.away": "Inserimento Fuori — {{seconds}}s",
4546
+ "system.alarm-arming.title.night": "Inserimento Notte — {{seconds}}s",
4547
+ "system.alarm-arming.body": "Inserimento in corso",
4548
+ "system.alarm-armed.title.home": "Inserito: Casa",
4549
+ "system.alarm-armed.title.away": "Inserito: Fuori",
4550
+ "system.alarm-armed.title.night": "Inserito: Notte",
4551
+ "system.alarm-armed.body": "Allarme inserito",
4552
+ "system.alarm-disarmed.title": "Disinserito",
4553
+ "system.alarm-disarmed.body": "Allarme disinserito",
4593
4554
  "system.alarm-triggered.title": "ALLARME",
4594
- "system.alarm-triggered.body": "L'allarme è scattato",
4595
- "system.alarm-triggered.body.rule": "L'allarme è scattato per {{rule}} su {{deviceName}}",
4555
+ "system.alarm-triggered.body": "Allarme scattato",
4556
+ "system.alarm-triggered.body.rule": "Scattato da {{rule}} su {{deviceName}}",
4596
4557
  "button.snooze.minutes": "Silenzia {{minutes}} min",
4597
4558
  "button.snooze.hours": {
4598
4559
  "one": "Silenzia {{hours}} ora",
@@ -5311,7 +5272,8 @@ var NcDispatcher = class {
5311
5272
  const systemEvent = subject.systemEvent;
5312
5273
  const displayDeviceId = systemEvent?.deviceId ?? subject.deviceId;
5313
5274
  const deviceName = systemEvent !== void 0 && systemEvent.deviceId === void 0 ? "system" : await this.deps.getDeviceName(displayDeviceId).catch(() => null) ?? `camera ${displayDeviceId}`;
5314
- const zoneLabels = systemEvent !== void 0 ? [] : await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones);
5275
+ const alarmSource = systemEvent?.alarmSource;
5276
+ const zoneLabels = systemEvent === void 0 ? await resolveZoneLabels(this.deps.getZoneNames, subject.deviceId, subject.zones) : alarmSource?.zones !== void 0 && alarmSource.zones.length > 0 ? await resolveZoneLabels(this.deps.getZoneNames, alarmSource.deviceId, alarmSource.zones) : [];
5315
5277
  const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels);
5316
5278
  const systemCount = systemEvent?.textCount !== void 0 ? { count: systemEvent.textCount } : {};
5317
5279
  const title = this.renderText({
@@ -5654,6 +5616,10 @@ async function resolveZoneLabels(getZoneNames, deviceId, zoneIds) {
5654
5616
  return [...zoneIds];
5655
5617
  }
5656
5618
  }
5619
+ /** `{{confidence}}` as a whole percent, or empty when nothing measured one. */
5620
+ function percentOf(confidence) {
5621
+ return confidence !== void 0 ? `${Math.round(confidence * 100)}%` : "";
5622
+ }
5657
5623
  /**
5658
5624
  * The variable map ONE render pass sees: scalars, then the composites built
5659
5625
  * from them, then the params a system-event row froze at intake.
@@ -5669,19 +5635,23 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels) {
5669
5635
  const subject = entry.payload.subject;
5670
5636
  const occupancy = subject.occupancy;
5671
5637
  const systemEvent = subject.systemEvent;
5638
+ const detection = systemEvent?.alarmSource?.className !== void 0 ? systemEvent.alarmSource : void 0;
5672
5639
  return {
5673
5640
  camera: deviceName,
5674
- class: subject.className,
5675
- label: subject.label ?? "",
5641
+ class: detection?.className ?? subject.className,
5642
+ label: detection?.label ?? subject.label ?? "",
5676
5643
  zones: zoneLabels.join(", "),
5677
- subject: systemEvent?.subject ?? ncSubjectText(texts, {
5644
+ subject: detection?.className !== void 0 ? ncSubjectText(texts, {
5645
+ className: detection.className,
5646
+ ...detection.label !== void 0 ? { label: detection.label } : {}
5647
+ }) : systemEvent?.subject ?? ncSubjectText(texts, {
5678
5648
  className: subject.className,
5679
5649
  ...subject.label !== void 0 ? { label: subject.label } : {}
5680
5650
  }),
5681
5651
  inZones: ncInZonesText(texts, zoneLabels),
5682
5652
  scope: occupancy?.zone ?? deviceName,
5683
5653
  zone: occupancy?.zone ?? zoneLabels[0] ?? "",
5684
- confidence: subject.confidence !== void 0 ? `${Math.round(subject.confidence * 100)}%` : "",
5654
+ confidence: percentOf(detection?.confidence ?? subject.confidence),
5685
5655
  time: new Date(subject.timestamp).toLocaleTimeString(),
5686
5656
  rule: entry.payload.ruleName,
5687
5657
  count: occupancy !== void 0 ? `${occupancy.count}` : "",
@@ -6221,6 +6191,35 @@ function incomingFromSystemEvent(event, lookup) {
6221
6191
  }
6222
6192
  }
6223
6193
  /**
6194
+ * The COMBINED row's two keys, or null when this transition is not one.
6195
+ *
6196
+ * A rule that trips the alarm produces ONE notification, and the operator has
6197
+ * to be able to read it: the title is the severity marker over the camera
6198
+ * (`alarm.combined.title`, shipped as `Alarm: {{deviceName}}` and re-wordable
6199
+ * per rule or in the Texts tab), and the body is `detection.body` — the exact
6200
+ * sentence that rule's ordinary notification would have carried, with its own
6201
+ * class, identity and zones.
6202
+ *
6203
+ * The alarm used to write its own prose here ("The alarm was triggered by
6204
+ * Person at gate on Ingresso"), which made the one notification that matters
6205
+ * the only one in the system that did not look like the rule's others: no
6206
+ * identity, no zone, a different subject. The prefix carries the severity; the
6207
+ * familiar line carries the meaning.
6208
+ *
6209
+ * **Gated on the detection vocabulary, not on the source.** `detection.body`
6210
+ * renders `{{subject}}`, and a fire ref with no class composes it from the
6211
+ * system event's dedup slot — "alarm:42 su Ingresso". A source without a class
6212
+ * therefore keeps the alarm's own sentence, which is also what every row
6213
+ * enqueued before this deploy carries.
6214
+ */
6215
+ function combinedAlarmText(source) {
6216
+ if (source?.className === void 0) return null;
6217
+ return {
6218
+ titleKey: "alarm.combined.title",
6219
+ bodyKey: "detection.body"
6220
+ };
6221
+ }
6222
+ /**
6224
6223
  * The panel's own transition, as a system-event subject.
6225
6224
  *
6226
6225
  * IN-PROCESS, not off the bus. The alarm and the Notification Center live in
@@ -6243,6 +6242,7 @@ function incomingFromAlarmTransition(input) {
6243
6242
  const deviceId = source?.deviceId ?? panelDeviceId;
6244
6243
  const identity = input.lookup(deviceId);
6245
6244
  const kind = transition.kind;
6245
+ const combined = combinedAlarmText(source);
6246
6246
  return systemSubject({
6247
6247
  facts: {
6248
6248
  eventId: `alarm:${kind}:${String(transition.at)}`,
@@ -6258,12 +6258,13 @@ function incomingFromAlarmTransition(input) {
6258
6258
  },
6259
6259
  kind,
6260
6260
  subject: `alarm:${String(panelDeviceId)}`,
6261
- titleKey: transition.mode !== void 0 ? `system.${kind}.title.${transition.mode}` : `system.${kind}.title`,
6262
- bodyKey: input.body?.bodyKey ?? (source !== void 0 ? `system.${kind}.body.rule` : `system.${kind}.body`),
6261
+ titleKey: combined !== null ? combined.titleKey : transition.mode !== void 0 ? `system.${kind}.title.${transition.mode}` : `system.${kind}.title`,
6262
+ bodyKey: combined?.bodyKey ?? input.body?.bodyKey ?? (source !== void 0 ? `system.${kind}.body.rule` : `system.${kind}.body`),
6263
6263
  textParams: {
6264
6264
  ...deviceTextParams(deviceId, identity),
6265
6265
  ...transition.mode !== void 0 ? { mode: transition.mode } : {},
6266
6266
  ...source !== void 0 ? { rule: source.ruleName } : {},
6267
+ ...kind === "alarm-arming" ? { seconds: `${transition.seconds ?? 0}` } : {},
6267
6268
  ...input.body?.textParams
6268
6269
  },
6269
6270
  deviceId,
@@ -12570,6 +12571,7 @@ function frozenOccupancy(rule, occ) {
12570
12571
  */
12571
12572
  function fireRefFor(rule, subject, kind) {
12572
12573
  const hasEventMedia = kind === "object-event" || kind === "package-event";
12574
+ const className = subject.classNames[0];
12573
12575
  return {
12574
12576
  ruleId: rule.id,
12575
12577
  ruleName: rule.name,
@@ -12578,7 +12580,11 @@ function fireRefFor(rule, subject, kind) {
12578
12580
  recordId: subject.recordId,
12579
12581
  ...subject.trackId !== void 0 ? { trackId: subject.trackId } : {},
12580
12582
  ...hasEventMedia ? { eventId: subject.recordId } : {},
12581
- timestamp: subject.timestamp
12583
+ timestamp: subject.timestamp,
12584
+ ...className !== void 0 ? { className } : {},
12585
+ ...subject.label !== void 0 ? { label: subject.label } : {},
12586
+ zones: notificationZones(rule, subject),
12587
+ ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {}
12582
12588
  };
12583
12589
  }
12584
12590
  function notificationZones(rule, subject) {
@@ -36056,6 +36062,138 @@ var BindingCache = class {
36056
36062
  }
36057
36063
  };
36058
36064
  //#endregion
36065
+ //#region src/pipeline-analytics/runtime/detection-coverage-cache.ts
36066
+ /**
36067
+ * DetectionCoverageCache — fleet-seeded, event-invalidated mirror of WHICH
36068
+ * devices carry a WRAPPED `detection-pipeline` binding. This is the alarm's
36069
+ * coverage authority ("Away armed, 12 cameras" is false if three of them are
36070
+ * offline or no longer wrapped), read by the notification centre's device
36071
+ * directory (`listDeviceIdentities`).
36072
+ *
36073
+ * WHY a mirror, not a per-tick sweep. The directory refreshes on the NC's 30 s
36074
+ * rule-reload tick. Answering "who still has detection?" with a fleet-wide
36075
+ * `getAllBindings` on EVERY tick cost 903 ms / 373 KB / ~505 `getBindings:
36076
+ * default wrappers skipped` log lines on the live hub (524 devices), forever —
36077
+ * ~3% of hub-main's main thread plus a permanent log-spam baseline. Same defect
36078
+ * and fix as `SystemMirror.refreshBinding` (639073e8): ONE fleet read to warm,
36079
+ * then per-device updates driven by `DeviceBindingsChanged` events. A wrapper
36080
+ * toggle emits that event carrying the device's id and `capName`
36081
+ * (`detection-pipeline`), so a single `getBindings(deviceId)` — 21 ms / 613 B —
36082
+ * keeps the mirror current without ever touching the fleet again.
36083
+ *
36084
+ * ── Destructive-direction discipline (D49) ──────────────────────────────────
36085
+ *
36086
+ * `detectionActive: false` reports a camera as UNCOVERED / switched-off (D62) —
36087
+ * the destructive answer, and the one an operator reads as "my alarm is not
36088
+ * watching that door". A transient `getBindings` blip must never flip a
36089
+ * known-covered camera to uncovered:
36090
+ * • a failed refresh KEEPS the previous value (a read that throws changes
36091
+ * nothing), and
36092
+ * • a single read that would flip a known `true → false` is confirmed by a
36093
+ * concordant SECOND read before it lands — an empty binding list is not
36094
+ * proof the wrapper is gone.
36095
+ * A COLD mirror (seed not yet landed, or a seed that failed) answers `undefined`
36096
+ * for every id — and unknown never reports a camera as excluded from coverage.
36097
+ */
36098
+ var DetectionCoverageCache = class {
36099
+ api;
36100
+ logger;
36101
+ /** deviceId → "has a WRAPPED detection-pipeline binding". Absent = unknown. */
36102
+ state = /* @__PURE__ */ new Map();
36103
+ warm = false;
36104
+ seeding = null;
36105
+ constructor(deps) {
36106
+ this.api = deps.api;
36107
+ this.logger = deps.logger;
36108
+ }
36109
+ /** Has the ONE fleet seed ever landed? A cold mirror knows nothing. */
36110
+ isWarm() {
36111
+ return this.warm;
36112
+ }
36113
+ /** Do we have an answer for this id at all? (`false` ⇒ absent ⇒ unknown.) */
36114
+ has(deviceId) {
36115
+ return this.state.has(deviceId);
36116
+ }
36117
+ /** The coverage answer, or `undefined` when this id is unknown. No I/O. */
36118
+ get(deviceId) {
36119
+ return this.state.get(deviceId);
36120
+ }
36121
+ /**
36122
+ * Seed the whole fleet ONCE with a single `getAllBindings`. Idempotent: a
36123
+ * warm cache is a no-op, and concurrent callers coalesce onto one read. A
36124
+ * failed seed leaves the mirror COLD so the next tick retries — during that
36125
+ * window every id answers unknown, which never reports a camera as excluded.
36126
+ */
36127
+ async ensureWarm() {
36128
+ if (this.warm) return;
36129
+ if (this.seeding) return this.seeding;
36130
+ this.seeding = this.seed();
36131
+ try {
36132
+ await this.seeding;
36133
+ } finally {
36134
+ this.seeding = null;
36135
+ }
36136
+ }
36137
+ async seed() {
36138
+ try {
36139
+ const rows = await this.api.deviceManager.getAllBindings.query({});
36140
+ for (const row of rows) {
36141
+ if (this.state.has(row.deviceId)) continue;
36142
+ this.state.set(row.deviceId, hasWrappedDetection(row.entries));
36143
+ }
36144
+ this.warm = true;
36145
+ this.logger.info("detection-coverage mirror warm", { meta: { devices: this.state.size } });
36146
+ } catch (err) {
36147
+ this.logger.warn("detection-coverage seed failed — coverage unknown until it lands", { meta: { error: String(err) } });
36148
+ }
36149
+ }
36150
+ /**
36151
+ * A `DeviceBindingsChanged` for the detection cap re-reads THAT ONE device
36152
+ * (never the fleet). Fire-and-forget from the synchronous event handler.
36153
+ */
36154
+ onBindingsChanged(event) {
36155
+ if (event.capName !== "detection-pipeline") return Promise.resolve();
36156
+ return this.refreshDevice(event.deviceId);
36157
+ }
36158
+ /** Drop a device the fleet no longer has (`device.unregistered`). */
36159
+ forget(deviceId) {
36160
+ this.state.delete(deviceId);
36161
+ }
36162
+ /** Return to cold — used on addon shutdown. */
36163
+ clearAll() {
36164
+ this.state.clear();
36165
+ this.warm = false;
36166
+ this.seeding = null;
36167
+ }
36168
+ async refreshDevice(deviceId) {
36169
+ try {
36170
+ const active = await this.readWrappedDetection(deviceId);
36171
+ const prev = this.state.get(deviceId);
36172
+ if (!active && prev === true) {
36173
+ if (await this.readWrappedDetection(deviceId)) {
36174
+ this.state.set(deviceId, true);
36175
+ return;
36176
+ }
36177
+ }
36178
+ this.state.set(deviceId, active);
36179
+ } catch (err) {
36180
+ this.logger.debug("detection-coverage refresh failed — keeping the previous value", {
36181
+ tags: { deviceId },
36182
+ meta: { error: String(err) }
36183
+ });
36184
+ }
36185
+ }
36186
+ async readWrappedDetection(deviceId) {
36187
+ return hasWrappedDetection((await this.api.deviceManager.getBindings.query({ deviceId })).entries);
36188
+ }
36189
+ };
36190
+ /** The one authority: a WRAPPED `detection-pipeline` binding is present. A
36191
+ * native binding of the same cap is NOT coverage (D62: absence of the WRAPPED
36192
+ * binding is how a deactivated wrapper is represented). */
36193
+ function hasWrappedDetection(entries) {
36194
+ return entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped");
36195
+ }
36196
+ //#endregion
36059
36197
  //#region src/pipeline-analytics/runtime/capture-scheduler.ts
36060
36198
  /**
36061
36199
  * Kind-derived default priorities (0 = highest). Explicit `priority` wins.
@@ -41438,6 +41576,12 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
41438
41576
  * path). Null on non-post-processing nodes or when disabled. */
41439
41577
  embeddingDispatcher = null;
41440
41578
  bindingCache = null;
41579
+ /** Alarm-coverage mirror the NC device directory reads — WHICH devices carry
41580
+ * a wrapped `detection-pipeline` binding. Fleet-seeded ONCE, then per-device
41581
+ * updates off `DeviceBindingsChanged`, so the 30 s rule-reload tick stops
41582
+ * sweeping `getAllBindings` over the whole fleet. See
41583
+ * `runtime/detection-coverage-cache.ts`. */
41584
+ detectionCoverage = null;
41441
41585
  /** Per-device throttle for the inactive-binding frame-drop warn (5 min). */
41442
41586
  inactiveBindingDropWarnAt = /* @__PURE__ */ new Map();
41443
41587
  zoneAnalytics = null;
@@ -42621,6 +42765,10 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
42621
42765
  api,
42622
42766
  logger: logger.child("BindingCache")
42623
42767
  });
42768
+ this.detectionCoverage = new DetectionCoverageCache({
42769
+ api,
42770
+ logger: logger.child("DetectionCoverage")
42771
+ });
42624
42772
  const zoneAnalytics = new ZoneAnalyticsProvider({
42625
42773
  logger: logger.child("ZoneAnalytics"),
42626
42774
  onOccupancySample: (deviceId, count, at, zoneId) => this.lts?.note("occupancy", deviceId, count, at, zoneId ?? ""),
@@ -42702,17 +42850,15 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
42702
42850
  },
42703
42851
  listDeviceIdentities: async () => {
42704
42852
  const all = await api.deviceManager.listAll.query({ projection: "slim" });
42705
- const detection = await api.deviceManager.getAllBindings.query({}).then((rows) => new Map(rows.map((row) => [row.deviceId, row.entries.some((e) => e.capName === "detection-pipeline" && e.kind === "wrapped")]))).catch((err) => {
42706
- this.ctx.logger.debug("bindings read failed — alarm coverage cannot report skips", { meta: { error: String(err) } });
42707
- return /* @__PURE__ */ new Map();
42708
- });
42853
+ const coverage = this.detectionCoverage;
42854
+ if (coverage) await coverage.ensureWarm();
42709
42855
  return all.map((d) => ({
42710
42856
  deviceId: d.id,
42711
42857
  type: d.type,
42712
42858
  name: d.name,
42713
42859
  isCamera: d.isCamera,
42714
42860
  online: d.online && !d.disabled,
42715
- ...detection.has(d.id) ? { detectionActive: detection.get(d.id) === true } : {}
42861
+ ...coverage?.has(d.id) ? { detectionActive: coverage.get(d.id) === true } : {}
42716
42862
  }));
42717
42863
  },
42718
42864
  readDeviceZoneIds: async (ids) => {
@@ -43420,6 +43566,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
43420
43566
  * `runtime/wire-subscriptions.ts`. */
43421
43567
  handleBindingsChangedEvent(data) {
43422
43568
  this.bindingCache?.onBindingsChanged(data);
43569
+ this.detectionCoverage?.onBindingsChanged(data);
43423
43570
  if (data.capName === "pipeline-analytics" && data.reason === "wrapper-deactivated") {
43424
43571
  this.trackStore?.clearDevice(data.deviceId);
43425
43572
  this.residents.closeDevice(data.deviceId);
@@ -43445,6 +43592,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
43445
43592
  this.audioMarkers?.forgetDevice(deviceId);
43446
43593
  this.settingsCaches.clearDevice(deviceId);
43447
43594
  this.bindingCache?.invalidate(deviceId);
43595
+ this.detectionCoverage?.forget(deviceId);
43448
43596
  this.zoneAnalytics?.forgetDevice(deviceId);
43449
43597
  this.audioMetrics?.forgetDevice(deviceId);
43450
43598
  this.releaseProxy(deviceId);
@@ -43650,6 +43798,7 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
43650
43798
  this.trackStore?.clearAll();
43651
43799
  this.stationaryRegistry = null;
43652
43800
  this.bindingCache?.clearAll();
43801
+ this.detectionCoverage?.clearAll();
43653
43802
  await this.eventMediaDataPlane?.dispose();
43654
43803
  this.eventMediaDataPlane = null;
43655
43804
  this.eventMediaBaseUrl = null;
@@ -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-Cxzt8LHT.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-3nHxWRIp.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.70",
3
+ "version": "1.2.71",
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",