@camstack/addon-post-analysis 1.2.660 → 1.2.662

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.
Files changed (16) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/{clip-model-registry-DAKPBZfa.js → clip-model-registry-C3N1NpST.js} +274 -36
  3. package/dist/{clip-model-registry-D9BbIQ-h.mjs → clip-model-registry-ELaSS9jq.mjs} +269 -37
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CvrKdp_8.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BvOlyiL9.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D4zkNcgf.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-vaatUWY6.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit--CbMNXQy.mjs → hostInit-BjZchMMh.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +506 -172
  12. package/dist/pipeline-analytics/index.mjs +506 -172
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDKliSzv.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BwWOFFOc.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-DAKPBZfa.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-C3N1NpST.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -8888,11 +8888,25 @@ function cleanSourcePreference(ownerKind) {
8888
8888
  * The ONE placeholder grammar. `renderTemplate` substitutes what it matches and
8889
8889
  * {@link templatePlaceholders} lists it; a second regex here would be a legend
8890
8890
  * that disagrees with the renderer about what a variable is, which is the
8891
- * failure mode where an editor advertises `{{ camera }}` as unsupported and the
8891
+ * failure mode where an editor advertises `{{ device }}` as unsupported and the
8892
8892
  * notification prints the camera anyway.
8893
8893
  */
8894
8894
  var PLACEHOLDER_PATTERN = /\{\{\s*(\w+)\s*\}\}/g;
8895
8895
  /**
8896
+ * Names that were renamed and still resolve at render time, mapped to the name
8897
+ * that replaced them. `{{camera}}` became `{{device}}` (a rule on an appliance
8898
+ * names that device, not a camera); a template written by an older client still
8899
+ * prints the right name. An alias is NOT declared in the catalog and no picker
8900
+ * offers it: the stored rules are rewritten by the `pipeline-analytics` template
8901
+ * migration, and this table only covers a client that has not updated yet.
8902
+ */
8903
+ var NC_DEPRECATED_TEMPLATE_VARS = { camera: "device" };
8904
+ /** The deprecated names a template still uses, in order of first appearance. */
8905
+ function deprecatedPlaceholders(template) {
8906
+ if (template === void 0) return [];
8907
+ return templatePlaceholders(template).filter((name) => name in NC_DEPRECATED_TEMPLATE_VARS);
8908
+ }
8909
+ /**
8896
8910
  * The variable names a template names, de-duplicated, in order of first
8897
8911
  * appearance.
8898
8912
  *
@@ -8918,19 +8932,35 @@ function templatePlaceholders(template) {
8918
8932
  */
8919
8933
  function renderTemplate(template, vars) {
8920
8934
  if (template === void 0 || template.trim().length === 0) return null;
8921
- return template.replace(PLACEHOLDER_PATTERN, (_m, name) => vars[name] ?? "");
8935
+ return template.replace(PLACEHOLDER_PATTERN, (_m, name) => {
8936
+ const current = vars[name];
8937
+ if (current !== void 0) return current;
8938
+ const renamed = NC_DEPRECATED_TEMPLATE_VARS[name];
8939
+ return (renamed !== void 0 ? vars[renamed] : void 0) ?? "";
8940
+ });
8922
8941
  }
8923
8942
  //#endregion
8924
8943
  //#region src/notification-center/texts/en.json
8925
8944
  var en_default = {
8926
8945
  "detection.title": "{{rule}}",
8927
- "detection.body": "{{subject}} on {{camera}}{{inZones}}",
8928
- "detection.body.trackEnd": "{{subject}} on {{camera}}{{inZones}} — visit ended",
8946
+ "detection.body": "{{subject}} on {{device}}{{inZones}}",
8947
+ "detection.body.trackEnd": "{{subject}} on {{device}}{{inZones}} — visit ended",
8929
8948
  "detection.subject": "{{classNoun}}",
8930
8949
  "detection.subject.labelled": "{{classNoun}} ({{label}})",
8931
8950
  "detection.subject.identity": "{{label}}",
8932
8951
  "detection.subject.plate": "{{classNoun}} {{label}}",
8933
8952
  "detection.inZones": " in {{zones}}",
8953
+ "appliance.finished.title": "{{device}} finished",
8954
+ "appliance.finished.body": "{{device}} finished{{afterDuration}}{{usedEnergy}}",
8955
+ "appliance.finished.duration": " after {{duration}}",
8956
+ "appliance.finished.energy": " and used {{energy}}",
8957
+ "appliance.duration.minutes": {
8958
+ "one": "{{minutes}} minute",
8959
+ "other": "{{minutes}} minutes"
8960
+ },
8961
+ "appliance.duration.hours": "{{hours}} h",
8962
+ "appliance.duration.hoursMinutes": "{{hours}} h {{minutes}} min",
8963
+ "appliance.energy.value": "{{value}} kWh",
8934
8964
  "class.person": {
8935
8965
  "one": "person",
8936
8966
  "other": "people"
@@ -8997,11 +9027,11 @@ var en_default = {
8997
9027
  "other": "{{count}} other {{classNoun}}"
8998
9028
  },
8999
9029
  "package.delivered.title": "Parcel delivered",
9000
- "package.delivered.body": "Delivered at {{camera}}{{inZones}}",
9030
+ "package.delivered.body": "Delivered at {{device}}{{inZones}}",
9001
9031
  "package.delivered.possible.title": "Possible parcel",
9002
- "package.delivered.possible.body": "Possible parcel at {{camera}}{{inZones}}",
9032
+ "package.delivered.possible.body": "Possible parcel at {{device}}{{inZones}}",
9003
9033
  "package.picked-up.title": "Parcel collected",
9004
- "package.picked-up.body": "Collected from {{camera}}{{inZones}}",
9034
+ "package.picked-up.body": "Collected from {{device}}{{inZones}}",
9005
9035
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
9006
9036
  "occupancy.op.occupied": "occupied",
9007
9037
  "occupancy.op.free": "free",
@@ -9137,7 +9167,7 @@ var en_default = {
9137
9167
  "system.camera-enabled.title": "Device switched on",
9138
9168
  "system.camera-enabled.body": "Device {{deviceName}} was switched back on",
9139
9169
  "test.prefix": "[TEST] {{body}}",
9140
- "timelapse.title": "Timelapse — {{camera}}",
9170
+ "timelapse.title": "Timelapse — {{device}}",
9141
9171
  "timelapse.body.window": "{{rule}} — {{from}}→{{to}}",
9142
9172
  "timelapse.body.tooManyEvents": "too many events for dense sampling: video at a uniform cadence",
9143
9173
  "timelapse.body.denseRanges": {
@@ -9151,7 +9181,7 @@ var en_default = {
9151
9181
  },
9152
9182
  "snooze.digest.header": "{{scope}} was snoozed, {{from}}–{{to}}.",
9153
9183
  "snooze.digest.header.global": "{{scope}} was snoozed for everyone, {{from}}–{{to}}.",
9154
- "snooze.digest.line": "• {{rule}} on {{camera}} — {{count}}",
9184
+ "snooze.digest.line": "• {{rule}} on {{device}} — {{count}}",
9155
9185
  "snooze.scope.all": "Everything",
9156
9186
  "snooze.scope.anyDevice": "A camera",
9157
9187
  "snooze.scope.anyRule": "A rule",
@@ -9330,13 +9360,24 @@ var en_default = {
9330
9360
  //#region src/notification-center/texts/it.json
9331
9361
  var it_default = {
9332
9362
  "detection.title": "{{rule}}",
9333
- "detection.body": "{{subject}} su {{camera}}{{inZones}}",
9334
- "detection.body.trackEnd": "{{subject}} su {{camera}}{{inZones}} — visita terminata",
9363
+ "detection.body": "{{subject}} su {{device}}{{inZones}}",
9364
+ "detection.body.trackEnd": "{{subject}} su {{device}}{{inZones}} — visita terminata",
9335
9365
  "detection.subject": "{{classNoun}}",
9336
9366
  "detection.subject.labelled": "{{classNoun}} ({{label}})",
9337
9367
  "detection.subject.identity": "{{label}}",
9338
9368
  "detection.subject.plate": "{{classNoun}} {{label}}",
9339
9369
  "detection.inZones": " in {{zones}}",
9370
+ "appliance.finished.title": "{{device}} ha terminato",
9371
+ "appliance.finished.body": "{{device}} ha terminato{{afterDuration}}{{usedEnergy}}",
9372
+ "appliance.finished.duration": " dopo {{duration}}",
9373
+ "appliance.finished.energy": " e ha consumato {{energy}}",
9374
+ "appliance.duration.minutes": {
9375
+ "one": "{{minutes}} minuto",
9376
+ "other": "{{minutes}} minuti"
9377
+ },
9378
+ "appliance.duration.hours": "{{hours}} h",
9379
+ "appliance.duration.hoursMinutes": "{{hours}} h {{minutes}} min",
9380
+ "appliance.energy.value": "{{value}} kWh",
9340
9381
  "class.person": {
9341
9382
  "one": "persona",
9342
9383
  "other": "persone"
@@ -9403,11 +9444,11 @@ var it_default = {
9403
9444
  "other": "{{count}} altri {{classNoun}}"
9404
9445
  },
9405
9446
  "package.delivered.title": "Pacco consegnato",
9406
- "package.delivered.body": "Consegnato su {{camera}}{{inZones}}",
9447
+ "package.delivered.body": "Consegnato su {{device}}{{inZones}}",
9407
9448
  "package.delivered.possible.title": "Possibile pacco",
9408
- "package.delivered.possible.body": "Possibile pacco su {{camera}}{{inZones}}",
9449
+ "package.delivered.possible.body": "Possibile pacco su {{device}}{{inZones}}",
9409
9450
  "package.picked-up.title": "Pacco ritirato",
9410
- "package.picked-up.body": "Ritirato da {{camera}}{{inZones}}",
9451
+ "package.picked-up.body": "Ritirato da {{device}}{{inZones}}",
9411
9452
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
9412
9453
  "occupancy.op.occupied": "occupata",
9413
9454
  "occupancy.op.free": "libera",
@@ -9543,7 +9584,7 @@ var it_default = {
9543
9584
  "system.camera-enabled.title": "Dispositivo riacceso",
9544
9585
  "system.camera-enabled.body": "Il dispositivo {{deviceName}} è stato riacceso",
9545
9586
  "test.prefix": "[PROVA] {{body}}",
9546
- "timelapse.title": "Sequenza — {{camera}}",
9587
+ "timelapse.title": "Sequenza — {{device}}",
9547
9588
  "timelapse.body.window": "{{rule}} — {{from}}→{{to}}",
9548
9589
  "timelapse.body.tooManyEvents": "troppi eventi per il campionamento denso: video a cadenza uniforme",
9549
9590
  "timelapse.body.denseRanges": {
@@ -9557,7 +9598,7 @@ var it_default = {
9557
9598
  },
9558
9599
  "snooze.digest.header": "{{scope}} in pausa, {{from}}–{{to}}.",
9559
9600
  "snooze.digest.header.global": "{{scope}} in pausa per tutti, {{from}}–{{to}}.",
9560
- "snooze.digest.line": "• {{rule}} su {{camera}} — {{count}}",
9601
+ "snooze.digest.line": "• {{rule}} su {{device}} — {{count}}",
9561
9602
  "snooze.scope.all": "Tutto",
9562
9603
  "snooze.scope.anyDevice": "Una camera",
9563
9604
  "snooze.scope.anyRule": "Una regola",
@@ -9781,7 +9822,7 @@ var it_default = {
9781
9822
  * as plain strings. {@link renderTemplate} never re-scans a replacement, so
9782
9823
  * there is exactly one level of composition and it cannot grow: a fragment that
9783
9824
  * names another composite renders it empty, and a value that happens to look
9784
- * like `{{camera}}` is printed, not substituted.
9825
+ * like `{{device}}` is printed, not substituted.
9785
9826
  */
9786
9827
  /** The languages the hub ships texts for. */
9787
9828
  var NC_LANGUAGES = ["en", "it"];
@@ -56230,6 +56271,258 @@ var ZoneCatalogueCache = class {
56230
56271
  }
56231
56272
  };
56232
56273
  //#endregion
56274
+ //#region src/notification-center/record-kinds.ts
56275
+ /**
56276
+ * Record kinds owned by the pipeline-analytics addon (records plan phase 2,
56277
+ * steps 18-24): the notification-center config tables.
56278
+ *
56279
+ * The notification-center module is hosted inside the pipeline-analytics addon.
56280
+ * That addon declares no `execution.group`, so the UDS owner stamp is the addon
56281
+ * id: `pipeline-analytics`. Declared hub-only (agents have no `ctx.records`).
56282
+ * One deploy covers all 7 kinds.
56283
+ */
56284
+ /**
56285
+ * The hub language and operator-authored per-key overrides.
56286
+ * One row, id `texts`. Losing it degrades to the shipped English catalog.
56287
+ */
56288
+ var TextsBodySchema = require_clip_model_registry.object({
56289
+ language: NcLanguageSchema,
56290
+ overrides: require_clip_model_registry.partialRecord(NcLanguageSchema, require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.string()))
56291
+ });
56292
+ /**
56293
+ * A shared action sequence (without id, which is the record key).
56294
+ * Extends the wire shape with `createdAt` / `updatedAt` (stored by the legacy table).
56295
+ */
56296
+ var SequenceBodySchema = require_clip_model_registry.NcSharedSequenceSchema.omit({ id: true }).extend({
56297
+ createdAt: require_clip_model_registry.number(),
56298
+ updatedAt: require_clip_model_registry.number()
56299
+ });
56300
+ /**
56301
+ * The AI section of one ordinary rule. Record id = the rule id.
56302
+ */
56303
+ var RuleAiBodySchema = require_clip_model_registry.object({
56304
+ ai: NcRuleAiSchema,
56305
+ updatedAt: require_clip_model_registry.number()
56306
+ });
56307
+ /** A calendar-events rule (without `id`). */
56308
+ var CalendarEventsRuleBodySchema = NcCalendarEventsRuleSchema.omit({ id: true });
56309
+ /** A multi-camera digest rule (without `id`). */
56310
+ var SummaryRuleBodySchema = NcSummaryRuleSchema.omit({ id: true });
56311
+ /** A timelapse rule (without `id`). */
56312
+ var TimelapseRuleBodySchema = require_clip_model_registry.TimelapseRuleSchema.omit({ id: true });
56313
+ /** A notification rule (without `id`). */
56314
+ var RuleBodySchema = require_clip_model_registry.NcRuleSchema.omit({ id: true });
56315
+ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
56316
+ /**
56317
+ * The hub language + per-key text overrides. One row (id `texts`).
56318
+ */
56319
+ texts: {
56320
+ schema: TextsBodySchema,
56321
+ class: "config",
56322
+ writeTrigger: "an operator changes the hub language (setLanguage) or replaces the per-key override set (setOverrides)",
56323
+ device: "none",
56324
+ at: "none",
56325
+ retention: { none: "one row (id `texts`); losing it costs the chosen language and household wording, never a notification — every read degrades to the shipped English catalog" }
56326
+ },
56327
+ /**
56328
+ * Shared action sequences — named, ordered runs of steps with their own throttle.
56329
+ * One row per sequence, id = sequence UUID.
56330
+ */
56331
+ sequences: {
56332
+ schema: SequenceBodySchema,
56333
+ class: "config",
56334
+ writeTrigger: "an admin creates/edits/deletes a shared sequence via notificationRules.createSequence / updateSequence / deleteSequence, or the one-shot boot migration that materialises legacy inline sequences",
56335
+ device: "none",
56336
+ at: "none",
56337
+ retention: { none: "a row goes only when an admin deletes it; bounded by the sequence set a human is willing to author" }
56338
+ },
56339
+ /**
56340
+ * Per-rule AI section: whether and how a rule narrates itself.
56341
+ * Record id = the rule id it belongs to.
56342
+ */
56343
+ "rule-ai": {
56344
+ schema: RuleAiBodySchema,
56345
+ class: "config",
56346
+ writeTrigger: "an operator switches the AI section of a notification rule on or off, or edits its prompt / word cap / language",
56347
+ device: "none",
56348
+ at: "none",
56349
+ retention: { none: "a row goes when the operator clears the section or deletes the rule; bounded by the rule set a human is willing to author" }
56350
+ },
56351
+ /**
56352
+ * Calendar-events rules. One row per rule, id = rule UUID.
56353
+ */
56354
+ "calendar-events-rules": {
56355
+ schema: CalendarEventsRuleBodySchema,
56356
+ class: "config",
56357
+ writeTrigger: "an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated",
56358
+ device: "none",
56359
+ at: "none",
56360
+ indexes: [{
56361
+ name: "enabled",
56362
+ fields: ["enabled"]
56363
+ }],
56364
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
56365
+ },
56366
+ /**
56367
+ * Multi-camera digest rules. One row per rule, id = rule UUID.
56368
+ */
56369
+ "summary-rules": {
56370
+ schema: SummaryRuleBodySchema,
56371
+ class: "config",
56372
+ writeTrigger: "an operator creates or edits a digest rule, or the producer stamps a delivered window via markGenerated",
56373
+ device: "none",
56374
+ at: "none",
56375
+ indexes: [{
56376
+ name: "enabled",
56377
+ fields: ["enabled"]
56378
+ }],
56379
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
56380
+ },
56381
+ /**
56382
+ * Timelapse rules. One row per rule, id = rule UUID.
56383
+ * Three rows on prod (2026-10-18).
56384
+ */
56385
+ "timelapse-rules": {
56386
+ schema: TimelapseRuleBodySchema,
56387
+ class: "config",
56388
+ writeTrigger: "an operator creates or edits a timelapse rule, or the scheduler stamps a successful per-camera generation via markGenerated",
56389
+ device: "none",
56390
+ at: "none",
56391
+ indexes: [{
56392
+ name: "enabled",
56393
+ fields: ["enabled"]
56394
+ }],
56395
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
56396
+ },
56397
+ /**
56398
+ * Notification rules — the main rule set. One row per rule, id = rule UUID.
56399
+ * 18 rows on prod (2026-10-18); most coupled kind in this owner.
56400
+ */
56401
+ rules: {
56402
+ schema: RuleBodySchema,
56403
+ class: "config",
56404
+ writeTrigger: "an operator creates, edits, enables/disables a rule or opts a target out of it; nothing writes on the evaluation path",
56405
+ device: "none",
56406
+ at: "none",
56407
+ indexes: [{
56408
+ name: "delivery",
56409
+ fields: ["delivery"]
56410
+ }],
56411
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
56412
+ }
56413
+ });
56414
+ //#endregion
56415
+ //#region src/migrations/device-template-var.ts
56416
+ /** `{{camera}}` and `{{ camera }}`: the whitespace the renderer allows is kept as written. */
56417
+ var OLD_VARIABLE = /\{\{(\s*)camera(\s*)\}\}/g;
56418
+ /** The template text with the variable renamed; the same string when it names none. */
56419
+ function renameDeviceVariable(text) {
56420
+ return text.replace(OLD_VARIABLE, "{{$1device$2}}");
56421
+ }
56422
+ /** A `{ title, body }` template with both fields renamed; `null` when neither changed. */
56423
+ function renamedTemplate(template) {
56424
+ if (template === void 0) return null;
56425
+ const title = template.title === void 0 ? void 0 : renameDeviceVariable(template.title);
56426
+ const body = template.body === void 0 ? void 0 : renameDeviceVariable(template.body);
56427
+ if (title === template.title && body === template.body) return null;
56428
+ return {
56429
+ ...template,
56430
+ ...title !== void 0 ? { title } : {},
56431
+ ...body !== void 0 ? { body } : {}
56432
+ };
56433
+ }
56434
+ function renamedRule(data) {
56435
+ const template = renamedTemplate(data.template);
56436
+ return template === null ? null : {
56437
+ ...data,
56438
+ template
56439
+ };
56440
+ }
56441
+ function renamedTimelapseRule(data) {
56442
+ const template = renamedTemplate(data.template);
56443
+ const previewText = data.previewText === void 0 ? void 0 : renameDeviceVariable(data.previewText);
56444
+ const previewChanged = previewText !== data.previewText;
56445
+ if (template === null && !previewChanged) return null;
56446
+ return {
56447
+ ...data,
56448
+ ...template !== null ? { template } : {},
56449
+ ...previewChanged && previewText !== void 0 ? { previewText } : {}
56450
+ };
56451
+ }
56452
+ function renamedTexts(data) {
56453
+ let changed = false;
56454
+ const overrides = Object.fromEntries(Object.entries(data.overrides).map(([language, table]) => [language, Object.fromEntries(Object.entries(table).map(([key, text]) => {
56455
+ const next = renameDeviceVariable(text);
56456
+ if (next !== text) changed = true;
56457
+ return [key, next];
56458
+ }))]));
56459
+ return changed ? {
56460
+ ...data,
56461
+ overrides
56462
+ } : null;
56463
+ }
56464
+ var PAGE$1 = 500;
56465
+ /** Every row of a kind, page by page. */
56466
+ async function allRows(kind) {
56467
+ const rows = [];
56468
+ let cursor;
56469
+ do {
56470
+ const page = await kind.query({
56471
+ limit: PAGE$1,
56472
+ ...cursor !== void 0 ? { cursor } : {}
56473
+ });
56474
+ rows.push(...page.records);
56475
+ cursor = page.nextCursor ?? void 0;
56476
+ } while (cursor !== void 0);
56477
+ return rows;
56478
+ }
56479
+ /** Rewrite the rows of one kind that carry the old name; returns how many were written. */
56480
+ async function rewriteKind(kind, rewrite) {
56481
+ let written = 0;
56482
+ for (const row of await allRows(kind)) {
56483
+ const next = rewrite(row.data);
56484
+ if (next === null) continue;
56485
+ await kind.put({
56486
+ id: row.id,
56487
+ data: next
56488
+ }, { ifRev: row.rev });
56489
+ written += 1;
56490
+ }
56491
+ return written;
56492
+ }
56493
+ async function migrateDeviceTemplateVariable(ports) {
56494
+ const rules = await rewriteKind(ports.rules, renamedRule);
56495
+ const timelapseRules = await rewriteKind(ports.timelapseRules, renamedTimelapseRule);
56496
+ const texts = await rewriteKind(ports.texts, renamedTexts);
56497
+ return {
56498
+ outcome: rules + timelapseRules + texts === 0 ? "already-done" : "done",
56499
+ summary: {
56500
+ rules,
56501
+ timelapseRules,
56502
+ texts
56503
+ }
56504
+ };
56505
+ }
56506
+ //#endregion
56507
+ //#region src/migrations/index.ts
56508
+ /** The `pipeline-analytics` catalogue (D987): offered to the engine at the hub's boot. */
56509
+ var ADDON_MIGRATIONS = [{
56510
+ meta: {
56511
+ id: "pipeline-analytics/2026-11-01-template-camera-to-device",
56512
+ owner: "pipeline-analytics",
56513
+ level: "addon-state",
56514
+ seq: 1,
56515
+ dependsOn: [],
56516
+ store: "settings",
56517
+ scope: "cluster",
56518
+ onFailure: "tolerate",
56519
+ bodyKind: "procedure",
56520
+ description: "Rename the {{camera}} template variable to {{device}} in every stored notification rule, timelapse rule and text override",
56521
+ fixtureMark: "the rule name and the trigger conditions the migration does not know"
56522
+ },
56523
+ procedure: migrateDeviceTemplateVariable
56524
+ }];
56525
+ //#endregion
56233
56526
  //#region src/pipeline-analytics/clip-space-plan.ts
56234
56527
  /**
56235
56528
  * Which CLIP feature space a search reads and a rebuild fills — decided in one
@@ -67128,7 +67421,7 @@ var CaptureScheduler = class {
67128
67421
  for (let i = lane.queue.length - 1; i >= 0; i -= 1) {
67129
67422
  const entry = lane.queue[i];
67130
67423
  if (entry === void 0) continue;
67131
- if (entry.staleAfterMs === void 0 || nowMs - entry.enqueuedAt <= entry.staleAfterMs) continue;
67424
+ if (entry.staleAfterMs === void 0 || nowMs - entry.enqueuedAt < entry.staleAfterMs) continue;
67132
67425
  lane.queue.splice(i, 1);
67133
67426
  this.bump(deviceId, "droppedStale");
67134
67427
  this.noteDropped(deviceId, entry, "stale");
@@ -67204,7 +67497,7 @@ var CaptureScheduler = class {
67204
67497
  while (lane.inFlight < this.budget && lane.queue.length > 0) {
67205
67498
  const entry = lane.queue.shift();
67206
67499
  if (entry === void 0) return;
67207
- if (entry.staleAfterMs !== void 0 && this.now() - entry.enqueuedAt > entry.staleAfterMs) {
67500
+ if (entry.staleAfterMs !== void 0 && this.now() - entry.enqueuedAt >= entry.staleAfterMs) {
67208
67501
  this.bump(deviceId, "droppedStale");
67209
67502
  this.noteDropped(deviceId, entry, "stale");
67210
67503
  entry.drop();
@@ -68432,6 +68725,109 @@ function createAnalyticsWidgetsProvider() {
68432
68725
  ] };
68433
68726
  }
68434
68727
  //#endregion
68728
+ //#region src/notification-center/appliance-cycle.ts
68729
+ var NC_UNKNOWN_CYCLE_FACTS = {
68730
+ durationMs: null,
68731
+ energyKwh: null
68732
+ };
68733
+ var NC_EMPTY_CYCLE_VARS = {
68734
+ duration: "",
68735
+ energy: "",
68736
+ afterDuration: "",
68737
+ usedEnergy: ""
68738
+ };
68739
+ /** `52 minutes` / `1 h 12 min` / `2 h`; `''` below one minute (never "0 minutes"). */
68740
+ function durationText(texts, durationMs) {
68741
+ if (durationMs === null || !Number.isFinite(durationMs)) return "";
68742
+ const totalMinutes = Math.round(durationMs / 6e4);
68743
+ if (totalMinutes < 1) return "";
68744
+ if (totalMinutes < 60) return texts.text({
68745
+ key: "appliance.duration.minutes",
68746
+ count: totalMinutes,
68747
+ vars: { minutes: `${totalMinutes}` }
68748
+ });
68749
+ const hours = Math.floor(totalMinutes / 60);
68750
+ const minutes = totalMinutes % 60;
68751
+ return minutes === 0 ? texts.text({
68752
+ key: "appliance.duration.hours",
68753
+ vars: { hours: `${hours}` }
68754
+ }) : texts.text({
68755
+ key: "appliance.duration.hoursMinutes",
68756
+ vars: {
68757
+ hours: `${hours}`,
68758
+ minutes: `${minutes}`
68759
+ }
68760
+ });
68761
+ }
68762
+ /** `0,84 kWh` (it) / `0.84 kWh` (en); `''` when unmeasured. */
68763
+ function energyText(texts, energyKwh) {
68764
+ if (energyKwh === null || !Number.isFinite(energyKwh) || energyKwh < 0) return "";
68765
+ const value = energyKwh.toLocaleString(texts.language, {
68766
+ minimumFractionDigits: 2,
68767
+ maximumFractionDigits: 2
68768
+ });
68769
+ return texts.text({
68770
+ key: "appliance.energy.value",
68771
+ vars: { value }
68772
+ });
68773
+ }
68774
+ /** The four variables of a finished cycle, fragments rendered from scalars only. */
68775
+ function ncApplianceCycleVars(texts, facts) {
68776
+ const duration = durationText(texts, facts.durationMs);
68777
+ const energy = energyText(texts, facts.energyKwh);
68778
+ return {
68779
+ duration,
68780
+ energy,
68781
+ afterDuration: duration === "" ? "" : texts.text({
68782
+ key: "appliance.finished.duration",
68783
+ vars: { duration }
68784
+ }),
68785
+ usedEnergy: energy === "" ? "" : texts.text({
68786
+ key: "appliance.finished.energy",
68787
+ vars: { energy }
68788
+ })
68789
+ };
68790
+ }
68791
+ /**
68792
+ * The frozen reference a FINISHED appliance cycle row carries (`{}` for every
68793
+ * other row): kind `appliance-cycle`, edge `cleared`. The appliance is the
68794
+ * sensor itself — a linked camera is only where the row was attributed.
68795
+ */
68796
+ function applianceCycleOf(subject) {
68797
+ if (subject.sensorKind !== "appliance-cycle" || subject.sensorRaised !== false) return {};
68798
+ return { applianceCycle: { deviceId: subject.linkedSensorId ?? subject.sourceDeviceId ?? subject.deviceId } };
68799
+ }
68800
+ //#endregion
68801
+ //#region src/notification-center/appliance-cycle-runs.ts
68802
+ var CYCLE_FACTS_WAIT_MS = 3e3;
68803
+ var CYCLE_FACTS_POLL_MS = 250;
68804
+ /** A closed run belongs to this finish when it ended within this much of it. */
68805
+ var CYCLE_RUN_MATCH_MS = 10 * 6e4;
68806
+ function factsOfClosed(run, finishedAtMs) {
68807
+ if (run.endedAt === null || Math.abs(run.endedAt - finishedAtMs) > CYCLE_RUN_MATCH_MS) return NC_UNKNOWN_CYCLE_FACTS;
68808
+ return {
68809
+ durationMs: run.durationMs,
68810
+ energyKwh: run.detail.energyKwh
68811
+ };
68812
+ }
68813
+ function applianceCycleFactsFromRuns(ports) {
68814
+ return async ({ deviceId, finishedAtMs }) => {
68815
+ let waited = 0;
68816
+ for (;;) {
68817
+ const run = await ports.newestRun(deviceId);
68818
+ if (run === null) return NC_UNKNOWN_CYCLE_FACTS;
68819
+ if (run.state === "closed") return factsOfClosed(run, finishedAtMs);
68820
+ if (run.startedAt > finishedAtMs) return NC_UNKNOWN_CYCLE_FACTS;
68821
+ if (waited >= CYCLE_FACTS_WAIT_MS) return {
68822
+ durationMs: finishedAtMs - run.startedAt,
68823
+ energyKwh: null
68824
+ };
68825
+ await ports.sleep(CYCLE_FACTS_POLL_MS);
68826
+ waited += CYCLE_FACTS_POLL_MS;
68827
+ }
68828
+ };
68829
+ }
68830
+ //#endregion
68435
68831
  //#region src/pipeline-analytics/runtime/wire-subscriptions.ts
68436
68832
  /**
68437
68833
  * Every category the Notification Center's system-event intake can turn into a
@@ -70393,6 +70789,7 @@ Object.freeze({
70393
70789
  "page": 1,
70394
70790
  "pageSize": 50
70395
70791
  } },
70792
+ "device-runs": { "listRuns": { "limit": 50 } },
70396
70793
  "llm": { "upsertProfile": { "profile": { "__nested__": {
70397
70794
  "timeoutMs": 6e4,
70398
70795
  "connectTimeoutMs": 1e4,
@@ -82396,147 +82793,6 @@ var NcDeviceMuteStore = class {
82396
82793
  }
82397
82794
  };
82398
82795
  //#endregion
82399
- //#region src/notification-center/record-kinds.ts
82400
- /**
82401
- * Record kinds owned by the pipeline-analytics addon (records plan phase 2,
82402
- * steps 18-24): the notification-center config tables.
82403
- *
82404
- * The notification-center module is hosted inside the pipeline-analytics addon.
82405
- * That addon declares no `execution.group`, so the UDS owner stamp is the addon
82406
- * id: `pipeline-analytics`. Declared hub-only (agents have no `ctx.records`).
82407
- * One deploy covers all 7 kinds.
82408
- */
82409
- /**
82410
- * The hub language and operator-authored per-key overrides.
82411
- * One row, id `texts`. Losing it degrades to the shipped English catalog.
82412
- */
82413
- var TextsBodySchema = require_clip_model_registry.object({
82414
- language: NcLanguageSchema,
82415
- overrides: require_clip_model_registry.partialRecord(NcLanguageSchema, require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.string()))
82416
- });
82417
- /**
82418
- * A shared action sequence (without id, which is the record key).
82419
- * Extends the wire shape with `createdAt` / `updatedAt` (stored by the legacy table).
82420
- */
82421
- var SequenceBodySchema = require_clip_model_registry.NcSharedSequenceSchema.omit({ id: true }).extend({
82422
- createdAt: require_clip_model_registry.number(),
82423
- updatedAt: require_clip_model_registry.number()
82424
- });
82425
- /**
82426
- * The AI section of one ordinary rule. Record id = the rule id.
82427
- */
82428
- var RuleAiBodySchema = require_clip_model_registry.object({
82429
- ai: NcRuleAiSchema,
82430
- updatedAt: require_clip_model_registry.number()
82431
- });
82432
- /** A calendar-events rule (without `id`). */
82433
- var CalendarEventsRuleBodySchema = NcCalendarEventsRuleSchema.omit({ id: true });
82434
- /** A multi-camera digest rule (without `id`). */
82435
- var SummaryRuleBodySchema = NcSummaryRuleSchema.omit({ id: true });
82436
- /** A timelapse rule (without `id`). */
82437
- var TimelapseRuleBodySchema = require_clip_model_registry.TimelapseRuleSchema.omit({ id: true });
82438
- /** A notification rule (without `id`). */
82439
- var RuleBodySchema = require_clip_model_registry.NcRuleSchema.omit({ id: true });
82440
- var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
82441
- /**
82442
- * The hub language + per-key text overrides. One row (id `texts`).
82443
- */
82444
- texts: {
82445
- schema: TextsBodySchema,
82446
- class: "config",
82447
- writeTrigger: "an operator changes the hub language (setLanguage) or replaces the per-key override set (setOverrides)",
82448
- device: "none",
82449
- at: "none",
82450
- retention: { none: "one row (id `texts`); losing it costs the chosen language and household wording, never a notification — every read degrades to the shipped English catalog" }
82451
- },
82452
- /**
82453
- * Shared action sequences — named, ordered runs of steps with their own throttle.
82454
- * One row per sequence, id = sequence UUID.
82455
- */
82456
- sequences: {
82457
- schema: SequenceBodySchema,
82458
- class: "config",
82459
- writeTrigger: "an admin creates/edits/deletes a shared sequence via notificationRules.createSequence / updateSequence / deleteSequence, or the one-shot boot migration that materialises legacy inline sequences",
82460
- device: "none",
82461
- at: "none",
82462
- retention: { none: "a row goes only when an admin deletes it; bounded by the sequence set a human is willing to author" }
82463
- },
82464
- /**
82465
- * Per-rule AI section: whether and how a rule narrates itself.
82466
- * Record id = the rule id it belongs to.
82467
- */
82468
- "rule-ai": {
82469
- schema: RuleAiBodySchema,
82470
- class: "config",
82471
- writeTrigger: "an operator switches the AI section of a notification rule on or off, or edits its prompt / word cap / language",
82472
- device: "none",
82473
- at: "none",
82474
- retention: { none: "a row goes when the operator clears the section or deletes the rule; bounded by the rule set a human is willing to author" }
82475
- },
82476
- /**
82477
- * Calendar-events rules. One row per rule, id = rule UUID.
82478
- */
82479
- "calendar-events-rules": {
82480
- schema: CalendarEventsRuleBodySchema,
82481
- class: "config",
82482
- writeTrigger: "an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated",
82483
- device: "none",
82484
- at: "none",
82485
- indexes: [{
82486
- name: "enabled",
82487
- fields: ["enabled"]
82488
- }],
82489
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
82490
- },
82491
- /**
82492
- * Multi-camera digest rules. One row per rule, id = rule UUID.
82493
- */
82494
- "summary-rules": {
82495
- schema: SummaryRuleBodySchema,
82496
- class: "config",
82497
- writeTrigger: "an operator creates or edits a digest rule, or the producer stamps a delivered window via markGenerated",
82498
- device: "none",
82499
- at: "none",
82500
- indexes: [{
82501
- name: "enabled",
82502
- fields: ["enabled"]
82503
- }],
82504
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
82505
- },
82506
- /**
82507
- * Timelapse rules. One row per rule, id = rule UUID.
82508
- * Three rows on prod (2026-10-18).
82509
- */
82510
- "timelapse-rules": {
82511
- schema: TimelapseRuleBodySchema,
82512
- class: "config",
82513
- writeTrigger: "an operator creates or edits a timelapse rule, or the scheduler stamps a successful per-camera generation via markGenerated",
82514
- device: "none",
82515
- at: "none",
82516
- indexes: [{
82517
- name: "enabled",
82518
- fields: ["enabled"]
82519
- }],
82520
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
82521
- },
82522
- /**
82523
- * Notification rules — the main rule set. One row per rule, id = rule UUID.
82524
- * 18 rows on prod (2026-10-18); most coupled kind in this owner.
82525
- */
82526
- rules: {
82527
- schema: RuleBodySchema,
82528
- class: "config",
82529
- writeTrigger: "an operator creates, edits, enables/disables a rule or opts a target out of it; nothing writes on the evaluation path",
82530
- device: "none",
82531
- at: "none",
82532
- indexes: [{
82533
- name: "delivery",
82534
- fields: ["delivery"]
82535
- }],
82536
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
82537
- }
82538
- });
82539
- //#endregion
82540
82796
  //#region src/notification-center/sequence-store.ts
82541
82797
  /**
82542
82798
  * Shared action sequences for the notification centre.
@@ -87621,6 +87877,56 @@ var NcDispatcher = class {
87621
87877
  renderText(input) {
87622
87878
  return renderRowText(this.texts, input);
87623
87879
  }
87880
+ /** Rules already warned about a deprecated variable in this process — once each. */
87881
+ deprecatedVarWarned = /* @__PURE__ */ new Set();
87882
+ /**
87883
+ * A rule template written by an older client may still say `{{camera}}`. It
87884
+ * renders (the alias), and the rule is named once so the operator can edit it.
87885
+ */
87886
+ warnDeprecatedVars(entry) {
87887
+ if (this.deprecatedVarWarned.has(entry.ruleId)) return;
87888
+ const template = entry.payload.template;
87889
+ const used = [...new Set([...deprecatedPlaceholders(template?.title), ...deprecatedPlaceholders(template?.body)])];
87890
+ if (used.length === 0) return;
87891
+ this.deprecatedVarWarned.add(entry.ruleId);
87892
+ this.deps.logger.warn("rule template uses a renamed variable — it still renders, edit it to the new name", {
87893
+ tags: { ruleId: entry.ruleId },
87894
+ meta: {
87895
+ variables: used,
87896
+ replacement: "device"
87897
+ }
87898
+ });
87899
+ }
87900
+ /**
87901
+ * The facts of a finished appliance cycle; unknown on every other row and on
87902
+ * any failure (a part that cannot be read is omitted, never a blocked send).
87903
+ */
87904
+ async applianceCycleFacts(entry) {
87905
+ const subject = entry.payload.subject;
87906
+ const resolve = this.deps.resolveApplianceCycleFacts;
87907
+ const deviceId = subject.applianceCycle?.deviceId;
87908
+ if (resolve === void 0 || deviceId === void 0) return NC_UNKNOWN_CYCLE_FACTS;
87909
+ try {
87910
+ const facts = await resolve({
87911
+ deviceId,
87912
+ finishedAtMs: subject.timestamp
87913
+ });
87914
+ if (facts.durationMs === null || facts.energyKwh === null) this.deps.logger.debug("appliance cycle part omitted — unknown", {
87915
+ tags: { deviceId },
87916
+ meta: {
87917
+ duration: facts.durationMs === null ? "unknown" : "known",
87918
+ energy: facts.energyKwh === null ? "unknown" : "known"
87919
+ }
87920
+ });
87921
+ return facts;
87922
+ } catch (err) {
87923
+ this.deps.logger.debug("appliance cycle facts failed — parts omitted", {
87924
+ tags: { deviceId },
87925
+ meta: { error: err instanceof Error ? err.message : String(err) }
87926
+ });
87927
+ return NC_UNKNOWN_CYCLE_FACTS;
87928
+ }
87929
+ }
87624
87930
  /**
87625
87931
  * Builds the notification AND returns the artefact ids its media resolve
87626
87932
  * indexed — see {@link withArtifactUrls}. A tuple rather than a field on
@@ -87633,7 +87939,9 @@ var NcDispatcher = class {
87633
87939
  const deviceName = await notificationDeviceName(entry, this.deps.getDeviceName);
87634
87940
  const alarmSource = systemEvent?.alarmSource;
87635
87941
  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) : [];
87636
- const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels);
87942
+ const cycleFacts = await this.applianceCycleFacts(entry);
87943
+ const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels, void 0, cycleFacts);
87944
+ this.warnDeprecatedVars(entry);
87637
87945
  const systemCount = systemEvent?.textCount !== void 0 ? { count: systemEvent.textCount } : {};
87638
87946
  const title = renderRowTitle(this.texts, entry, vars);
87639
87947
  const authoredBody = entry.payload.template?.body;
@@ -88748,15 +89056,15 @@ async function resolveZoneLabels(getZoneNames, deviceId, zoneIds) {
88748
89056
  function percentOf(confidence) {
88749
89057
  return confidence !== void 0 ? `${Math.round(confidence * 100)}%` : "";
88750
89058
  }
88751
- /** What `{{camera}}` renders on a system row that names no device. */
89059
+ /** What `{{device}}` renders on a system row that names no device. */
88752
89060
  var NC_DEVICELESS_CAMERA_NAME = "system";
88753
89061
  /**
88754
- * The name `{{camera}}` renders for a row — the ONE derivation, shared by the
89062
+ * The name `{{device}}` renders for a row — the ONE derivation, shared by the
88755
89063
  * dispatcher and the template-variable parity spec.
88756
89064
  *
88757
89065
  * A system row that names no device (a node, a backup, an update) renders the
88758
89066
  * filler {@link NC_DEVICELESS_CAMERA_NAME}: there is no camera to name, which
88759
- * is why the catalog offers `{{camera}}` on a system rule only for the kinds
89067
+ * is why the catalog offers `{{device}}` on a system rule only for the kinds
88760
89068
  * that carry a device (template-vars.ts). Everything else asks the directory,
88761
89069
  * and falls back to `camera <id>` when it cannot answer.
88762
89070
  */
@@ -88799,7 +89107,7 @@ function systemEventParamsForTemplate(params) {
88799
89107
  const { form: _form, attention: _attention, ...rest } = params;
88800
89108
  return rest;
88801
89109
  }
88802
- function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
89110
+ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone, cycle) {
88803
89111
  const subject = entry.payload.subject;
88804
89112
  const occupancy = subject.occupancy;
88805
89113
  const systemEvent = subject.systemEvent;
@@ -88809,7 +89117,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
88809
89117
  const cameraName = deviceName.toLocaleLowerCase(locale);
88810
89118
  const zonesLower = zoneLabels.map((z) => z.toLocaleLowerCase(locale));
88811
89119
  return {
88812
- camera: cameraName,
89120
+ device: cameraName,
88813
89121
  class: detection?.className ?? (systemEvent !== void 0 ? "" : subject.className),
88814
89122
  classLabel: (() => {
88815
89123
  const wire = detection?.className ?? (systemEvent !== void 0 ? "" : subject.className);
@@ -88835,6 +89143,7 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone) {
88835
89143
  hitPercent: subject.audio?.hitPercent !== void 0 ? `${Math.round(subject.audio.hitPercent)}` : "",
88836
89144
  db: subject.audio?.peakDbfs !== void 0 ? `${Math.round(subject.audio.peakDbfs)}` : "",
88837
89145
  eventType: subject.eventType ?? "",
89146
+ ...cycle !== void 0 ? ncApplianceCycleVars(texts, cycle) : NC_EMPTY_CYCLE_VARS,
88838
89147
  rule: entry.payload.ruleName,
88839
89148
  count: occupancy !== void 0 ? `${occupancy.count}` : group !== void 0 && group.members.length > 1 ? `${group.members.length}` : "",
88840
89149
  capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
@@ -88861,6 +89170,7 @@ function bodyKeyFor(entry) {
88861
89170
  const subject = entry.payload.subject;
88862
89171
  if (subject.systemEvent !== void 0) return subject.systemEvent.bodyKey ?? "";
88863
89172
  if (subject.occupancy !== void 0) return "occupancy.body";
89173
+ if (subject.applianceCycle !== void 0) return "appliance.finished.body";
88864
89174
  if (subject.packagePhase === "delivered" && subject.packagePossible === true) return "package.delivered.possible.body";
88865
89175
  if (subject.packagePhase !== void 0) return `package.${subject.packagePhase}.body`;
88866
89176
  return entry.recordKind === "track-end" ? "detection.body.trackEnd" : "detection.body";
@@ -88898,7 +89208,7 @@ function renderRowTitle(texts, entry, vars) {
88898
89208
  const systemEvent = entry.payload.subject.systemEvent;
88899
89209
  const intrusion = entry.payload.alarmIntrusion;
88900
89210
  const count = systemEvent?.textCount;
88901
- return renderRowText(texts, {
89211
+ const rendered = renderRowText(texts, {
88902
89212
  template: intrusion !== void 0 ? void 0 : entry.payload.template?.title,
88903
89213
  key: titleKeyFor(entry),
88904
89214
  vars: intrusion !== void 0 ? {
@@ -88908,11 +89218,13 @@ function renderRowTitle(texts, entry, vars) {
88908
89218
  ...count !== void 0 ? { count } : {},
88909
89219
  frozen: systemEvent?.title ?? entry.payload.ruleName
88910
89220
  });
89221
+ return titleKeyFor(entry) === "appliance.finished.title" && entry.payload.template?.title === void 0 ? ncSentenceCaseBody(rendered) : rendered;
88911
89222
  }
88912
89223
  /** The default TITLE key — same shape, same pre-catalog reasoning. */
88913
89224
  function titleKeyFor(entry) {
88914
89225
  const systemEvent = entry.payload.subject.systemEvent;
88915
89226
  if (systemEvent !== void 0) return systemEvent.titleKey ?? "";
89227
+ if (entry.payload.subject.applianceCycle !== void 0) return "appliance.finished.title";
88916
89228
  if (entry.payload.alarmIntrusion !== void 0) return "alarm.intrusion.title";
88917
89229
  const packagePhase = entry.payload.subject.packagePhase;
88918
89230
  if (packagePhase === "delivered" && entry.payload.subject.packagePossible === true) return "package.delivered.possible.title";
@@ -96673,7 +96985,7 @@ function clockOf(atMs) {
96673
96985
  * catalog, the same way `buildTemplateVars` is asserted for rule rows. */
96674
96986
  function timelapseTemplateVars(input) {
96675
96987
  return {
96676
- camera: input.deviceName ?? `camera ${input.deviceId}`,
96988
+ device: input.deviceName ?? `camera ${input.deviceId}`,
96677
96989
  rule: input.ruleName,
96678
96990
  from: clockOf(input.window.startMs),
96679
96991
  to: clockOf(input.window.endMs),
@@ -96739,7 +97051,7 @@ function paramsFor(target, clickUrl) {
96739
97051
  */
96740
97052
  function timelapsePreviewText(input) {
96741
97053
  const vars = {
96742
- camera: input.deviceName ?? `camera ${input.deviceId}`,
97054
+ device: input.deviceName ?? `camera ${input.deviceId}`,
96743
97055
  ...input.ruleName !== void 0 ? { rule: input.ruleName } : {},
96744
97056
  ...input.window !== void 0 ? {
96745
97057
  from: clockOf(input.window.startMs),
@@ -101984,6 +102296,7 @@ function buildEntries(host, rule, subject, kind, matchedOn, userTargets = [], or
101984
102296
  ...subject.audioWindow.peakDbfs !== void 0 ? { peakDbfs: subject.audioWindow.peakDbfs } : {}
101985
102297
  } } : {},
101986
102298
  ...subject.eventType !== void 0 ? { eventType: subject.eventType } : {},
102299
+ ...applianceCycleOf(subject),
101987
102300
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
101988
102301
  }
101989
102302
  };
@@ -104271,7 +104584,7 @@ function buildSnoozeDigest(texts, snooze, rows, deviceNames) {
104271
104584
  key: "snooze.digest.line",
104272
104585
  vars: {
104273
104586
  rule: r.ruleName,
104274
- camera: deviceNames.get(r.deviceId) ?? `camera ${r.deviceId}`,
104587
+ device: deviceNames.get(r.deviceId) ?? `camera ${r.deviceId}`,
104275
104588
  count: `${r.count}`
104276
104589
  }
104277
104590
  }));
@@ -109721,6 +110034,7 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
109721
110034
  }));
109722
110035
  },
109723
110036
  getDeviceName,
110037
+ resolveApplianceCycleFacts: this.deps.resolveApplianceCycleFacts,
109724
110038
  getDeviceLocation,
109725
110039
  buildActions: (input) => this.notificationCenter?.mintButtons(input) ?? Promise.resolve([])
109726
110040
  },
@@ -122906,6 +123220,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
122906
123220
  return out;
122907
123221
  };
122908
123222
  await step("declareCollections", () => this.declareCollections(api));
123223
+ if (this.localOwnNodeId() === "hub") await step("migrations", () => this.runTemplateMigrations());
122909
123224
  this.armAlarmPanelDeclaration(api);
122910
123225
  const logger = this.ctx.logger;
122911
123226
  const storage = await step("resolveMediaStorage", () => this.resolveMediaStorage(logger));
@@ -123256,6 +123571,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
123256
123571
  });
123257
123572
  return this.collectionsBootInstance;
123258
123573
  }
123574
+ /** The addon's migration catalogue (D987), bound to the notification kinds. */
123575
+ async runTemplateMigrations() {
123576
+ const records = this.requireRecords();
123577
+ const ports = {
123578
+ rules: records.kind(ncRecordKinds["rules"]),
123579
+ timelapseRules: records.kind(ncRecordKinds["timelapse-rules"]),
123580
+ texts: records.kind(ncRecordKinds.texts)
123581
+ };
123582
+ await this.runMigrations(require_clip_model_registry.defineAddonMigrations(ADDON_MIGRATIONS, ports));
123583
+ }
123259
123584
  /** `declare-collections.ts` — declareCollections. */
123260
123585
  async declareCollections(api) {
123261
123586
  return this.collectionsBoot.declareCollections(api);
@@ -123581,7 +123906,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
123581
123906
  buildTimelapsePorts: (api, stores) => this.buildTimelapsePorts(api, stores),
123582
123907
  buildSummaryPorts: (api, stores) => this.buildSummaryPorts(api, stores),
123583
123908
  wireAlarmPanel: (center) => this.wireAlarmPanel(center),
123584
- serveNcActionPlane: (center) => this.serveNcActionPlane(center)
123909
+ serveNcActionPlane: (center) => this.serveNcActionPlane(center),
123910
+ resolveApplianceCycleFacts: applianceCycleFactsFromRuns({
123911
+ newestRun: async (deviceId) => {
123912
+ return (await this.ctx.api.deviceRuns.listRuns.query({
123913
+ deviceId,
123914
+ limit: 1
123915
+ })).runs[0] ?? null;
123916
+ },
123917
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
123918
+ })
123585
123919
  });
123586
123920
  return this.notificationCenterBootInstance;
123587
123921
  }