@camstack/addon-post-analysis 1.2.320 → 1.2.322

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 (15) hide show
  1. package/dist/{clip-model-registry-C_aKxHpy.js → clip-model-registry-BFbP9U7z.js} +427 -0
  2. package/dist/{clip-model-registry-C-IEAIQQ.mjs → clip-model-registry-CwK2SLQD.mjs} +374 -1
  3. package/dist/embedding-encoder/index.js +1 -1
  4. package/dist/embedding-encoder/index.mjs +1 -1
  5. package/dist/pipeline-analytics/_stub.js +2 -2
  6. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D553qWb_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dmt5eh93.mjs} +2 -2
  7. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mzYK6V9S.mjs +26 -0
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CuOcvcK7.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-ByDyL2zi.mjs → hostInit-DtKO_7UT.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +916 -50
  11. package/dist/pipeline-analytics/index.mjs +916 -50
  12. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  13. package/package.json +1 -1
  14. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-7e_3hsiC.mjs +0 -26
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C1V_UEyk.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-C_aKxHpy.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-BFbP9U7z.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");
@@ -1948,6 +1948,21 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1948
1948
  exitDelaySec: require_clip_model_registry.number().optional(),
1949
1949
  entryDelaySec: require_clip_model_registry.number().optional(),
1950
1950
  triggeredDurationSec: require_clip_model_registry.number().optional()
1951
+ })).default([]),
1952
+ /**
1953
+ * Active alarm configuration profile id. Empty / absent on a blob older than
1954
+ * profiles → seeded to Default on first read via {@link ensureAlarmProfiles}.
1955
+ */
1956
+ activeProfileId: require_clip_model_registry.string().default(require_clip_model_registry.NC_ALARM_DEFAULT_PROFILE_ID),
1957
+ /**
1958
+ * Named settings blobs. Loose on purpose — each profile's settings are
1959
+ * re-validated through {@link settingsBlobFromPersisted} so a corrupt entry
1960
+ * drops rather than taking down the panel.
1961
+ */
1962
+ profiles: require_clip_model_registry.array(require_clip_model_registry.object({
1963
+ id: require_clip_model_registry.string(),
1964
+ name: require_clip_model_registry.string(),
1965
+ settings: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.unknown())
1951
1966
  })).default([])
1952
1967
  });
1953
1968
  /**
@@ -1970,6 +1985,106 @@ function modeDeviceListFromPersisted(raw) {
1970
1985
  return lists;
1971
1986
  }
1972
1987
  /**
1988
+ * Rebuild a trusted {@link NcAlarmSettings} from a persisted profile blob.
1989
+ * Unknown / corrupt fields fall back to system-safe empties via the flat-key
1990
+ * readers the panel already uses for the live settings.
1991
+ */
1992
+ function settingsBlobFromPersisted(raw) {
1993
+ const exit = typeof raw["exitDelaySec"] === "number" ? raw["exitDelaySec"] : DEFAULT_DELAYS.exitDelaySec;
1994
+ const entry = typeof raw["entryDelaySec"] === "number" ? raw["entryDelaySec"] : DEFAULT_DELAYS.entryDelaySec;
1995
+ const triggered = typeof raw["triggeredDurationSec"] === "number" ? raw["triggeredDurationSec"] : DEFAULT_DELAYS.triggeredDurationSec;
1996
+ const announceTargets = Array.isArray(raw["announceTargets"]) ? raw["announceTargets"].filter((t) => typeof t === "string") : [];
1997
+ const lists = (key) => {
1998
+ const v = raw[key];
1999
+ if (!Array.isArray(v)) return [];
2000
+ const rows = [];
2001
+ for (const item of v) {
2002
+ if (typeof item !== "object" || item === null) continue;
2003
+ const rec = item;
2004
+ if (typeof rec["mode"] !== "string" || !Array.isArray(rec["deviceIds"])) continue;
2005
+ rows.push({
2006
+ mode: rec["mode"],
2007
+ deviceIds: rec["deviceIds"].filter((id) => typeof id === "number")
2008
+ });
2009
+ }
2010
+ return modeDeviceListFromPersisted(rows);
2011
+ };
2012
+ const modeDelaysRaw = Array.isArray(raw["modeDelays"]) ? raw["modeDelays"] : [];
2013
+ const modeDelaysLoose = [];
2014
+ for (const item of modeDelaysRaw) {
2015
+ if (typeof item !== "object" || item === null) continue;
2016
+ const rec = item;
2017
+ if (typeof rec["mode"] !== "string") continue;
2018
+ const entry = { mode: rec["mode"] };
2019
+ if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
2020
+ if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
2021
+ if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
2022
+ modeDelaysLoose.push(entry);
2023
+ }
2024
+ return {
2025
+ exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
2026
+ entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
2027
+ triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
2028
+ announceTargets,
2029
+ nonBlocking: lists("nonBlocking"),
2030
+ blocking: lists("blocking"),
2031
+ secureOnArmOff: lists("secureOnArmOff"),
2032
+ modeDelays: modeDelaysFromPersisted(modeDelaysLoose).entries
2033
+ };
2034
+ }
2035
+ function settingsToPersisted(settings) {
2036
+ return {
2037
+ exitDelaySec: settings.exitDelaySec,
2038
+ entryDelaySec: settings.entryDelaySec,
2039
+ triggeredDurationSec: settings.triggeredDurationSec,
2040
+ announceTargets: [...settings.announceTargets],
2041
+ nonBlocking: settings.nonBlocking.map((l) => ({
2042
+ mode: l.mode,
2043
+ deviceIds: [...l.deviceIds]
2044
+ })),
2045
+ blocking: settings.blocking.map((l) => ({
2046
+ mode: l.mode,
2047
+ deviceIds: [...l.deviceIds]
2048
+ })),
2049
+ secureOnArmOff: settings.secureOnArmOff.map((l) => ({
2050
+ mode: l.mode,
2051
+ deviceIds: [...l.deviceIds]
2052
+ })),
2053
+ modeDelays: settings.modeDelays.map((e) => ({ ...e }))
2054
+ };
2055
+ }
2056
+ function profilesStateFromPersisted(activeId, rows, current) {
2057
+ const profiles = rows.filter((r) => r.id.length > 0 && r.name.trim().length > 0).map((r) => ({
2058
+ id: r.id,
2059
+ name: r.name,
2060
+ settings: settingsBlobFromPersisted(r.settings)
2061
+ }));
2062
+ return require_clip_model_registry.ensureAlarmProfiles(profiles.length > 0 ? {
2063
+ activeId,
2064
+ profiles
2065
+ } : null, current);
2066
+ }
2067
+ function profilesToPersisted(state) {
2068
+ return {
2069
+ activeProfileId: state.activeId,
2070
+ profiles: state.profiles.map((p) => ({
2071
+ id: p.id,
2072
+ name: p.name,
2073
+ settings: settingsToPersisted(p.settings)
2074
+ }))
2075
+ };
2076
+ }
2077
+ function profilesViewOf(state) {
2078
+ return {
2079
+ activeId: state.activeId,
2080
+ items: state.profiles.map((p) => ({
2081
+ id: p.id,
2082
+ name: p.name,
2083
+ isDefault: require_clip_model_registry.isDefaultAlarmProfileId(p.id)
2084
+ }))
2085
+ };
2086
+ }
2087
+ /**
1973
2088
  * The log line for a trigger the state machine ignored, per state. `trigger`
1974
2089
  * ignores four: disarmed, arming (the exit delay), and pending / triggered
1975
2090
  * (already counting down or going off — a second sensor must not restart it).
@@ -2217,7 +2332,134 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2217
2332
  changed: membershipChanged
2218
2333
  }
2219
2334
  });
2220
- return this.settings();
2335
+ const settled = this.settings();
2336
+ await this.persistProfilesState(require_clip_model_registry.syncActiveAlarmProfileSettings(this.readProfilesState(), settled));
2337
+ return settled;
2338
+ }
2339
+ /** Named profiles + which one is active — for {@link NcAlarmConfig.profiles}. */
2340
+ profilesView() {
2341
+ return profilesViewOf(this.readProfilesState());
2342
+ }
2343
+ /**
2344
+ * Create a named profile (system defaults, or clone). Does not activate.
2345
+ * `newId` is assigned by the caller (usually `randomUUID`).
2346
+ */
2347
+ async createProfile(input) {
2348
+ const result = require_clip_model_registry.createAlarmProfile(this.readProfilesState(), {
2349
+ name: input.name,
2350
+ id: input.newId,
2351
+ source: input.source
2352
+ });
2353
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2354
+ await this.persistProfilesState(result.state);
2355
+ this.ctx.logger.info("alarm profile created", {
2356
+ tags: { deviceId: this.id },
2357
+ meta: {
2358
+ profileId: input.newId,
2359
+ name: input.name.trim(),
2360
+ source: input.source.kind
2361
+ }
2362
+ });
2363
+ return profilesViewOf(result.state);
2364
+ }
2365
+ async renameProfile(id, name) {
2366
+ const result = require_clip_model_registry.renameAlarmProfile(this.readProfilesState(), {
2367
+ id,
2368
+ name
2369
+ });
2370
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2371
+ await this.persistProfilesState(result.state);
2372
+ this.ctx.logger.info("alarm profile renamed", {
2373
+ tags: { deviceId: this.id },
2374
+ meta: {
2375
+ profileId: id,
2376
+ name: name.trim()
2377
+ }
2378
+ });
2379
+ return profilesViewOf(result.state);
2380
+ }
2381
+ /**
2382
+ * Delete a non-default profile. If it was active, Default becomes active and
2383
+ * its settings are applied to the flat keys (arm path).
2384
+ */
2385
+ async deleteProfile(id) {
2386
+ const before = this.readProfilesState();
2387
+ const result = require_clip_model_registry.deleteAlarmProfile(before, id);
2388
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2389
+ const switched = before.activeId === id;
2390
+ await this.persistProfilesState(result.state);
2391
+ if (switched) {
2392
+ const def = result.state.profiles.find((p) => p.id === require_clip_model_registry.NC_ALARM_DEFAULT_PROFILE_ID);
2393
+ if (def !== void 0) await this.replaceFlatSettings(def.settings);
2394
+ }
2395
+ this.ctx.logger.info("alarm profile deleted", {
2396
+ tags: { deviceId: this.id },
2397
+ meta: {
2398
+ profileId: id,
2399
+ activatedDefault: switched
2400
+ }
2401
+ });
2402
+ return profilesViewOf(result.state);
2403
+ }
2404
+ /**
2405
+ * Make a profile active and apply its blob onto the flat settings the arm
2406
+ * path reads.
2407
+ */
2408
+ async activateProfile(id) {
2409
+ const result = require_clip_model_registry.activateAlarmProfile(require_clip_model_registry.syncActiveAlarmProfileSettings(this.readProfilesState(), this.settings()), id);
2410
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2411
+ const next = result.state.profiles.find((p) => p.id === id);
2412
+ if (next === void 0) throw this.profilesRefuseError("not-found", id);
2413
+ await this.persistProfilesState(result.state);
2414
+ await this.replaceFlatSettings(next.settings);
2415
+ this.ctx.logger.info("alarm profile activated", {
2416
+ tags: { deviceId: this.id },
2417
+ meta: {
2418
+ profileId: id,
2419
+ name: next.name
2420
+ }
2421
+ });
2422
+ return profilesViewOf(result.state);
2423
+ }
2424
+ readProfilesState() {
2425
+ return profilesStateFromPersisted(this.config.get("activeProfileId"), this.config.get("profiles"), this.settings());
2426
+ }
2427
+ async persistProfilesState(state) {
2428
+ const persisted = profilesToPersisted(state);
2429
+ await Promise.all([this.config.set("activeProfileId", persisted.activeProfileId), this.config.set("profiles", persisted.profiles)]);
2430
+ }
2431
+ /** Write a full settings blob onto the flat keys the arm path reads. */
2432
+ async replaceFlatSettings(settings) {
2433
+ const blob = require_clip_model_registry.cloneAlarmSettings(settings);
2434
+ await Promise.all([
2435
+ this.config.set("exitDelaySec", blob.exitDelaySec),
2436
+ this.config.set("entryDelaySec", blob.entryDelaySec),
2437
+ this.config.set("triggeredDurationSec", blob.triggeredDurationSec),
2438
+ this.config.set("announceTargets", [...blob.announceTargets]),
2439
+ this.config.set("nonBlocking", blob.nonBlocking.map((l) => ({
2440
+ mode: l.mode,
2441
+ deviceIds: [...l.deviceIds]
2442
+ }))),
2443
+ this.config.set("blocking", blob.blocking.map((l) => ({
2444
+ mode: l.mode,
2445
+ deviceIds: [...l.deviceIds]
2446
+ }))),
2447
+ this.config.set("secureOnArmOff", blob.secureOnArmOff.map((l) => ({
2448
+ mode: l.mode,
2449
+ deviceIds: [...l.deviceIds]
2450
+ }))),
2451
+ this.config.set("modeDelays", blob.modeDelays.map((e) => ({ ...e })))
2452
+ ]);
2453
+ }
2454
+ profilesRefuseError(reason, detail) {
2455
+ switch (reason) {
2456
+ case "name-empty": return /* @__PURE__ */ new Error("alarm profile name must not be empty");
2457
+ case "name-taken": return /* @__PURE__ */ new Error(`an alarm profile named '${detail ?? ""}' already exists`);
2458
+ case "cannot-delete-default": return /* @__PURE__ */ new Error("the Default alarm profile cannot be deleted");
2459
+ case "clone-source-missing": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found to clone`);
2460
+ case "not-found": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found`);
2461
+ default: return /* @__PURE__ */ new Error(`alarm profile operation refused (${reason})`);
2462
+ }
2221
2463
  }
2222
2464
  /**
2223
2465
  * Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
@@ -2950,6 +3192,11 @@ function ncAlarmPanelPort(panel) {
2950
3192
  deviceId: panel.id,
2951
3193
  settings: () => panel.settings(),
2952
3194
  applySettings: (patch) => panel.applySettings(patch),
3195
+ profilesView: () => panel.profilesView(),
3196
+ createProfile: (input) => panel.createProfile(input),
3197
+ renameProfile: (id, name) => panel.renameProfile(id, name),
3198
+ deleteProfile: (id) => panel.deleteProfile(id),
3199
+ activateProfile: (id) => panel.activateProfile(id),
2953
3200
  availableModes: () => NC_ALARM_MODES,
2954
3201
  noteRuleTrigger: (source) => panel.noteRuleTrigger(source),
2955
3202
  excludedDevices: () => panel.excludedDevices(),
@@ -5448,7 +5695,7 @@ function isAdoptable(record, nowMs) {
5448
5695
  * what `localDayMinute` does for the same input — one behaviour for a bad
5449
5696
  * zone, not two.
5450
5697
  */
5451
- function clockOf$5(atMs, timeZone) {
5698
+ function clockOf$6(atMs, timeZone) {
5452
5699
  if (timeZone !== void 0) try {
5453
5700
  return new Intl.DateTimeFormat("en-GB", {
5454
5701
  timeZone,
@@ -5584,7 +5831,7 @@ var TimelapseRenderer = class {
5584
5831
  includeAudio: false,
5585
5832
  maxLifeMs: await this.ports.exportLifeMs(),
5586
5833
  deleteAfterDownload: false,
5587
- title: `${request.ruleName} ${clockOf$5(window.startMs, request.timezone)}→${clockOf$5(window.endMs, request.timezone)}`,
5834
+ title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
5588
5835
  timelapse: {
5589
5836
  everyMs,
5590
5837
  outputFps: request.framerate,
@@ -5915,7 +6162,7 @@ var TimelapseRenderer = class {
5915
6162
  ...meta,
5916
6163
  unservedWindowCount: verdict.unservedWindows.length,
5917
6164
  unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
5918
- at: clockOf$5(request.window.startMs + w.fromSec * 1e3, request.timezone),
6165
+ at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
5919
6166
  fromSec: Math.round(w.fromSec),
5920
6167
  toSec: Math.round(w.toSec),
5921
6168
  expectedDenseFrames: w.expectedDenseFrames,
@@ -13900,7 +14147,7 @@ var NcPruneRecordSchema = require_clip_model_registry.object({
13900
14147
  source: require_clip_model_registry._enum(["event", "reconcile"])
13901
14148
  });
13902
14149
  /** Rules × deleted subjects — small; a generous ceiling. */
13903
- var LOAD_LIMIT$4 = 1e4;
14150
+ var LOAD_LIMIT$5 = 1e4;
13904
14151
  function keyOf$1(row) {
13905
14152
  return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
13906
14153
  }
@@ -13919,7 +14166,7 @@ var NC_DEVICE_PRUNES_SPEC = {
13919
14166
  return parsed.success ? parsed.data : null;
13920
14167
  },
13921
14168
  deviceIdOf: (row) => row.subjectKind === "device" ? Number(row.subject) : void 0,
13922
- loadLimit: LOAD_LIMIT$4
14169
+ loadLimit: LOAD_LIMIT$5
13923
14170
  };
13924
14171
  /** The outcomes that mean "this last reference was already dealt with". */
13925
14172
  var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
@@ -13954,7 +14201,7 @@ var NcPruneLedger = class {
13954
14201
  try {
13955
14202
  const records = await this.store.query.query({
13956
14203
  collection: NC_DEVICE_PRUNES_COLLECTION,
13957
- filter: { limit: LOAD_LIMIT$4 }
14204
+ filter: { limit: LOAD_LIMIT$5 }
13958
14205
  });
13959
14206
  const next = /* @__PURE__ */ new Map();
13960
14207
  for (const record of records) {
@@ -17279,12 +17526,12 @@ var NcSummaryTemplateSchema = require_clip_model_registry.object({
17279
17526
  title: require_clip_model_registry.string().max(500).optional(),
17280
17527
  body: require_clip_model_registry.string().max(2e3).optional()
17281
17528
  });
17282
- var NameField = require_clip_model_registry.string().min(1).max(200);
17529
+ var NameField$1 = require_clip_model_registry.string().min(1).max(200);
17283
17530
  /** MULTI-CAMERA is the point of the feature — a one-camera digest is a
17284
17531
  * timelapse with extra steps, but it is allowed and it is cheap. */
17285
17532
  var DeviceIdsField = require_clip_model_registry.array(require_clip_model_registry.number()).min(1).max(64);
17286
- var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
17287
- var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
17533
+ var TargetsField$1 = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
17534
+ var PriorityField$1 = require_clip_model_registry.number().int().min(1).max(5);
17288
17535
  /**
17289
17536
  * The mosaic's tile ceiling. 9 (a 3×3 grid) is the shipped default: it is the
17290
17537
  * most a phone lock-screen thumbnail can carry and still be read, and it is the
@@ -17449,7 +17696,7 @@ var NcSummaryAiSchema = require_clip_model_registry.object({
17449
17696
  onFailure: require_clip_model_registry._enum(["deliver-without-text", "skip"]).optional()
17450
17697
  });
17451
17698
  var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17452
- name: NameField,
17699
+ name: NameField$1,
17453
17700
  enabled: require_clip_model_registry.boolean().default(true),
17454
17701
  deviceIds: DeviceIdsField,
17455
17702
  window: NcSummaryWindowSchema,
@@ -17458,7 +17705,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17458
17705
  maxAiImages: MaxAiImagesField.default(NC_SUMMARY_DEFAULT_AI_IMAGES),
17459
17706
  rank: NcSummaryRankSchema.default("importance"),
17460
17707
  barrierSeconds: BarrierSecondsField.default(120),
17461
- targets: TargetsField,
17708
+ targets: TargetsField$1,
17462
17709
  template: NcSummaryTemplateSchema.optional(),
17463
17710
  /** Burned onto the mosaic's title band — and ONLY when `captions` is `all`. */
17464
17711
  captions: NcSummaryCaptionsSchema.default(NC_SUMMARY_DEFAULT_CAPTIONS),
@@ -17468,7 +17715,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17468
17715
  /** Deliver a window that matched NOTHING? Default no: a nightly "0 events"
17469
17716
  * is a line nobody reads, and it costs the lines beside it their reader. */
17470
17717
  deliverEmpty: require_clip_model_registry.boolean().default(false),
17471
- priority: PriorityField.default(3),
17718
+ priority: PriorityField$1.default(3),
17472
17719
  ai: NcSummaryAiSchema.optional()
17473
17720
  });
17474
17721
  /**
@@ -17480,7 +17727,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17480
17727
  * plain `.optional()`.
17481
17728
  */
17482
17729
  var NcSummaryRulePatchSchema = require_clip_model_registry.object({
17483
- name: NameField.optional(),
17730
+ name: NameField$1.optional(),
17484
17731
  enabled: require_clip_model_registry.boolean().optional(),
17485
17732
  deviceIds: DeviceIdsField.optional(),
17486
17733
  window: NcSummaryWindowSchema.optional(),
@@ -17489,12 +17736,12 @@ var NcSummaryRulePatchSchema = require_clip_model_registry.object({
17489
17736
  maxAiImages: MaxAiImagesField.optional(),
17490
17737
  rank: NcSummaryRankSchema.optional(),
17491
17738
  barrierSeconds: BarrierSecondsField.optional(),
17492
- targets: TargetsField.optional(),
17739
+ targets: TargetsField$1.optional(),
17493
17740
  template: NcSummaryTemplateSchema.nullable().optional(),
17494
17741
  captions: NcSummaryCaptionsSchema.optional(),
17495
17742
  captionText: require_clip_model_registry.string().max(200).optional(),
17496
17743
  deliverEmpty: require_clip_model_registry.boolean().optional(),
17497
- priority: PriorityField.optional(),
17744
+ priority: PriorityField$1.optional(),
17498
17745
  ai: NcSummaryAiSchema.nullable().optional()
17499
17746
  });
17500
17747
  /**
@@ -17527,6 +17774,89 @@ function assertSummaryBudget(rule) {
17527
17774
  if (rule.maxAiImages > rule.maxTiles) throw new Error(`maxAiImages (${rule.maxAiImages}) cannot exceed maxTiles (${rule.maxTiles}) — the model cannot judge frames the mosaic does not contain`);
17528
17775
  }
17529
17776
  //#endregion
17777
+ //#region src/notification-center/calendar-events/calendar-events-window.ts
17778
+ /** One day in milliseconds. */
17779
+ var CALENDAR_EVENTS_DAY_MS = 1440 * 60 * 1e3;
17780
+ /**
17781
+ * Floor / ceiling on the horizon days a rule may declare.
17782
+ * 1 day is the minimum useful digest; 30 keeps a single notification bounded.
17783
+ */
17784
+ var CALENDAR_EVENTS_MIN_HORIZON_DAYS = 1;
17785
+ var CALENDAR_EVENTS_MAX_HORIZON_DAYS = 30;
17786
+ var CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS = 7;
17787
+ /**
17788
+ * Build the producer window for one fire.
17789
+ *
17790
+ * `horizonDays` is clamped into the legal range rather than refused — a
17791
+ * stored rule that predates a tighter bound must still fire.
17792
+ */
17793
+ function calendarEventsWindow(nowMs, horizonDays) {
17794
+ const days = Math.min(CALENDAR_EVENTS_MAX_HORIZON_DAYS, Math.max(CALENDAR_EVENTS_MIN_HORIZON_DAYS, Math.floor(horizonDays)));
17795
+ return {
17796
+ startMs: nowMs,
17797
+ endMs: nowMs + days * CALENDAR_EVENTS_DAY_MS,
17798
+ horizonDays: days
17799
+ };
17800
+ }
17801
+ //#endregion
17802
+ //#region src/notification-center/calendar-events/calendar-events-rule.ts
17803
+ /**
17804
+ * NcCalendarEventsRule — scheduled "upcoming calendar events" digest.
17805
+ *
17806
+ * Same ownership model as Summaries/Timelapses: lives in the addon (not
17807
+ * `@camstack/types`) so a field is live on the next post-analysis deploy.
17808
+ * Rides no `NcDelivery` member; reaches the outside world through
17809
+ * `nc.*CalendarEvents*` bridge actions.
17810
+ *
17811
+ * Calendar selection mirrors detection classes: `calendars` = Only,
17812
+ * `calendarsExclude` = Except, both empty = All.
17813
+ */
17814
+ var NcCalendarEventsTemplateSchema = require_clip_model_registry.object({
17815
+ title: require_clip_model_registry.string().max(500).optional(),
17816
+ body: require_clip_model_registry.string().max(4e3).optional()
17817
+ });
17818
+ var NameField = require_clip_model_registry.string().min(1).max(200);
17819
+ var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
17820
+ var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
17821
+ var HorizonDaysField = require_clip_model_registry.number().int().min(CALENDAR_EVENTS_MIN_HORIZON_DAYS).max(CALENDAR_EVENTS_MAX_HORIZON_DAYS);
17822
+ var CalendarIdsField = require_clip_model_registry.array(require_clip_model_registry.string().min(1).max(200)).max(64);
17823
+ var NcCalendarEventsRuleInputSchema = require_clip_model_registry.object({
17824
+ name: NameField,
17825
+ enabled: require_clip_model_registry.boolean().default(true),
17826
+ schedule: require_clip_model_registry.NcScheduleSchema,
17827
+ /** Only these calendars. Empty/absent with no exclude = All. */
17828
+ calendars: CalendarIdsField.optional(),
17829
+ /** All except these. Ignored when `calendars` is non-empty. */
17830
+ calendarsExclude: CalendarIdsField.optional(),
17831
+ horizonDays: HorizonDaysField.default(CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS),
17832
+ /** Deliver when the horizon holds no events? Default no. */
17833
+ deliverEmpty: require_clip_model_registry.boolean().default(false),
17834
+ targets: TargetsField,
17835
+ template: NcCalendarEventsTemplateSchema.optional(),
17836
+ priority: PriorityField.default(3)
17837
+ });
17838
+ var NcCalendarEventsRulePatchSchema = require_clip_model_registry.object({
17839
+ name: NameField.optional(),
17840
+ enabled: require_clip_model_registry.boolean().optional(),
17841
+ schedule: require_clip_model_registry.NcScheduleSchema.optional(),
17842
+ calendars: CalendarIdsField.nullable().optional(),
17843
+ calendarsExclude: CalendarIdsField.nullable().optional(),
17844
+ horizonDays: HorizonDaysField.optional(),
17845
+ deliverEmpty: require_clip_model_registry.boolean().optional(),
17846
+ targets: TargetsField.optional(),
17847
+ template: NcCalendarEventsTemplateSchema.nullable().optional(),
17848
+ priority: PriorityField.optional()
17849
+ });
17850
+ var NcCalendarEventsRuleSchema = NcCalendarEventsRuleInputSchema.extend({
17851
+ id: require_clip_model_registry.string(),
17852
+ ownerUserId: require_clip_model_registry.string().optional(),
17853
+ /** Epoch-ms of the schedule window end that was last delivered. */
17854
+ generatedAt: require_clip_model_registry.number().optional(),
17855
+ createdBy: require_clip_model_registry.string(),
17856
+ createdAt: require_clip_model_registry.number(),
17857
+ updatedAt: require_clip_model_registry.number()
17858
+ });
17859
+ //#endregion
17530
17860
  //#region src/notification-center/unstored-media-id.ts
17531
17861
  /**
17532
17862
  * An id for media bytes that were NEVER STORED.
@@ -18687,6 +19017,22 @@ var ncActions = require_clip_model_registry.defineCustomActions({
18687
19017
  kind: "mutation",
18688
19018
  caller: "required"
18689
19019
  }),
19020
+ "nc.listCalendarEventsRules": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({
19021
+ rules: require_clip_model_registry.array(NcCalendarEventsRuleSchema.extend({ readOnly: require_clip_model_registry.boolean() })),
19022
+ canAuthor: require_clip_model_registry.boolean()
19023
+ }), { caller: "required" }),
19024
+ "nc.upsertCalendarEventsRule": require_clip_model_registry.customAction(require_clip_model_registry.object({
19025
+ ruleId: require_clip_model_registry.string().optional(),
19026
+ rule: NcCalendarEventsRuleInputSchema.optional(),
19027
+ patch: NcCalendarEventsRulePatchSchema.optional()
19028
+ }), require_clip_model_registry.object({ rule: NcCalendarEventsRuleSchema }), {
19029
+ kind: "mutation",
19030
+ caller: "required"
19031
+ }),
19032
+ "nc.deleteCalendarEventsRule": require_clip_model_registry.customAction(require_clip_model_registry.object({ ruleId: require_clip_model_registry.string() }), require_clip_model_registry.object({ success: require_clip_model_registry.literal(true) }), {
19033
+ kind: "mutation",
19034
+ caller: "required"
19035
+ }),
18690
19036
  /**
18691
19037
  * Produce one digest window RIGHT NOW — the operator's Test button.
18692
19038
  *
@@ -18973,6 +19319,11 @@ function makeNcActionHandlers(deps) {
18973
19319
  if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
18974
19320
  return sum;
18975
19321
  };
19322
+ const requireCalendarEvents = () => {
19323
+ const ce = deps.calendarEvents;
19324
+ if (ce === void 0) throw new Error("this node does not host calendar-events rules — use the hub");
19325
+ return ce;
19326
+ };
18976
19327
  /** The artefact index, or an honest refusal. */
18977
19328
  const requireArtifacts = () => {
18978
19329
  const index = deps.artifacts;
@@ -19001,6 +19352,13 @@ function makeNcActionHandlers(deps) {
19001
19352
  if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
19002
19353
  return rule;
19003
19354
  };
19355
+ const assertCalendarEventsEditable = (ruleId, caller, ce) => {
19356
+ const rule = ce.store.get(ruleId);
19357
+ if (rule === null) throw new Error(`forbidden: calendar-events rule not found: ${ruleId}`);
19358
+ if (caller.isAdmin) return rule;
19359
+ if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: calendar-events rule not owned: ${ruleId}`);
19360
+ return rule;
19361
+ };
19004
19362
  /** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
19005
19363
  const assertTimelapseEditable = (ruleId, caller, tl) => {
19006
19364
  const rule = tl.store.get(ruleId);
@@ -19542,6 +19900,53 @@ function makeNcActionHandlers(deps) {
19542
19900
  ...out.skippedByAi === true ? { skippedByAi: true } : {}
19543
19901
  };
19544
19902
  },
19903
+ "nc.listCalendarEventsRules": async (_input, caller) => {
19904
+ const c = requireCaller$1(caller);
19905
+ const ce = requireCalendarEvents();
19906
+ return {
19907
+ rules: (c.isAdmin ? ce.store.list() : ce.store.listForOwner(c.userId)).map((r) => ({
19908
+ ...r,
19909
+ readOnly: !c.isAdmin && r.ownerUserId !== c.userId
19910
+ })),
19911
+ canAuthor: true
19912
+ };
19913
+ },
19914
+ "nc.upsertCalendarEventsRule": async (input, caller) => {
19915
+ const c = requireCaller$1(caller);
19916
+ const ce = requireCalendarEvents();
19917
+ if (input.ruleId === void 0) {
19918
+ if (input.rule === void 0) throw new Error("upsertCalendarEventsRule: rule or ruleId required");
19919
+ const parsed = NcCalendarEventsRuleInputSchema.parse(input.rule);
19920
+ await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
19921
+ const rule = await ce.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
19922
+ deps.logger.info("calendar-events rule created", { meta: {
19923
+ ruleId: rule.id,
19924
+ owner: c.userId
19925
+ } });
19926
+ return { rule };
19927
+ }
19928
+ assertCalendarEventsEditable(input.ruleId, c, ce);
19929
+ if (input.patch === void 0) throw new Error("upsertCalendarEventsRule: patch required");
19930
+ const patch = NcCalendarEventsRulePatchSchema.parse(input.patch);
19931
+ if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
19932
+ const rule = await ce.store.update(input.ruleId, patch);
19933
+ deps.logger.info("calendar-events rule updated", { meta: {
19934
+ ruleId: rule.id,
19935
+ owner: c.userId
19936
+ } });
19937
+ return { rule };
19938
+ },
19939
+ "nc.deleteCalendarEventsRule": async (input, caller) => {
19940
+ const c = requireCaller$1(caller);
19941
+ const ce = requireCalendarEvents();
19942
+ assertCalendarEventsEditable(input.ruleId, c, ce);
19943
+ await ce.store.delete(input.ruleId);
19944
+ deps.logger.info("calendar-events rule deleted", { meta: {
19945
+ ruleId: input.ruleId,
19946
+ owner: c.userId
19947
+ } });
19948
+ return { success: true };
19949
+ },
19545
19950
  "nc.getTexts": async (input) => {
19546
19951
  const texts = requireTexts();
19547
19952
  return input.language === void 0 ? texts.view() : texts.view(input.language);
@@ -28405,7 +28810,8 @@ var OUTBOX_RECORD_KINDS = new Set([
28405
28810
  "audio-window",
28406
28811
  "system-event",
28407
28812
  "timelapse",
28408
- "ai-summary"
28813
+ "ai-summary",
28814
+ "calendar-events"
28409
28815
  ]);
28410
28816
  function isOutboxRecordKind(x) {
28411
28817
  return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
@@ -28758,7 +29164,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
28758
29164
  ].join("");
28759
29165
  }
28760
29166
  /** `HH:MM` in the host timezone. */
28761
- function clockOf$4(atMs) {
29167
+ function clockOf$5(atMs) {
28762
29168
  const d = new Date(atMs);
28763
29169
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
28764
29170
  }
@@ -28796,7 +29202,7 @@ function buildRuleAiPrompt(input) {
28796
29202
  if (shape === "detection-track") {
28797
29203
  lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
28798
29204
  for (const [i, frame] of input.frames.entries()) {
28799
- const when = frame.atMs !== void 0 ? ` · ${clockOf$4(frame.atMs)}` : "";
29205
+ const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
28800
29206
  lines.push(`${String(i + 1)}.${when}`);
28801
29207
  }
28802
29208
  return lines.join("\n");
@@ -29093,7 +29499,7 @@ var NcRuleAiRowSchema = require_clip_model_registry.object({
29093
29499
  updatedAt: require_clip_model_registry.number()
29094
29500
  });
29095
29501
  /** Operator-authored and tiny; a generous ceiling. */
29096
- var LOAD_LIMIT$3 = 1e4;
29502
+ var LOAD_LIMIT$4 = 1e4;
29097
29503
  var NC_RULE_AI_SPEC = {
29098
29504
  collection: NC_RULE_AI_COLLECTION,
29099
29505
  columns: [...NC_RULE_AI_COLUMNS],
@@ -29109,7 +29515,7 @@ var NC_RULE_AI_SPEC = {
29109
29515
  const parsed = NcRuleAiRowSchema.safeParse(data["row"]);
29110
29516
  return parsed.success ? parsed.data : null;
29111
29517
  },
29112
- loadLimit: LOAD_LIMIT$3
29518
+ loadLimit: LOAD_LIMIT$4
29113
29519
  };
29114
29520
  var NcRuleAiStore = class {
29115
29521
  ledger;
@@ -29446,7 +29852,7 @@ var NC_RULES_INDEXES = [{
29446
29852
  columns: ["enabled"]
29447
29853
  }];
29448
29854
  /** Query cap — the rule set is operator-authored and tiny; a generous ceiling. */
29449
- var LOAD_LIMIT$2 = 1e4;
29855
+ var LOAD_LIMIT$3 = 1e4;
29450
29856
  function isRecord$3(value) {
29451
29857
  return typeof value === "object" && value !== null && !Array.isArray(value);
29452
29858
  }
@@ -29495,7 +29901,7 @@ function makeRulesSpec(onProjection) {
29495
29901
  if (projection.kind === "projected") onProjection?.(key, projection);
29496
29902
  return parsed.data;
29497
29903
  },
29498
- loadLimit: LOAD_LIMIT$2
29904
+ loadLimit: LOAD_LIMIT$3
29499
29905
  };
29500
29906
  }
29501
29907
  /** The spec `declare` needs — no sink: declaring a collection projects nothing. */
@@ -30567,7 +30973,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
30567
30973
  */
30568
30974
  var SummaryAnswerSchema = require_clip_model_registry.object({ summary: require_clip_model_registry.string() });
30569
30975
  /** `HH:MM` in the host timezone — the two numbers a frame label needs. */
30570
- function clockOf$3(atMs) {
30976
+ function clockOf$4(atMs) {
30571
30977
  const d = new Date(atMs);
30572
30978
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
30573
30979
  }
@@ -30590,7 +30996,7 @@ function buildSummaryAiPrompt(input, frames) {
30590
30996
  const roster = frames.map((f, i) => {
30591
30997
  const location = plainLabel(f.location ?? "");
30592
30998
  const where = location.length > 0 ? ` (${location})` : "";
30593
- return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$3(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
30999
+ return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
30594
31000
  }).join("\n");
30595
31001
  const described = /* @__PURE__ */ new Map();
30596
31002
  for (const f of frames) {
@@ -31193,7 +31599,7 @@ var NO_DETECTIONS$1 = {
31193
31599
  * has to check is a key somebody can get wrong. */
31194
31600
  var BODY_SEPARATOR$1 = " · ";
31195
31601
  /** `HH:MM` in the host timezone. */
31196
- function clockOf$2(atMs) {
31602
+ function clockOf$3(atMs) {
31197
31603
  const d = new Date(atMs);
31198
31604
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
31199
31605
  }
@@ -31207,8 +31613,8 @@ function clockOf$2(atMs) {
31207
31613
  function summaryTemplateVars(input) {
31208
31614
  return {
31209
31615
  rule: input.ruleName,
31210
- from: clockOf$2(input.window.startMs),
31211
- to: clockOf$2(input.window.endMs),
31616
+ from: clockOf$3(input.window.startMs),
31617
+ to: clockOf$3(input.window.endMs),
31212
31618
  time: ncClockText(input.texts.language, input.generatedAt),
31213
31619
  events: String(input.tileCount),
31214
31620
  cameras: String(input.cameraCount),
@@ -31252,7 +31658,7 @@ function composeSummaryBody(input) {
31252
31658
  * string, for the reason the timelapse learned: a hand-rolled `n === 1` shipped
31253
31659
  * "1 intervalli ripresi" to a live phone.
31254
31660
  */
31255
- function derivedBody$1(input, vars) {
31661
+ function derivedBody$2(input, vars) {
31256
31662
  const texts = input.texts;
31257
31663
  const parts = [texts.text({
31258
31664
  key: "summary.body.window",
@@ -31287,7 +31693,7 @@ function derivedBody$1(input, vars) {
31287
31693
  }
31288
31694
  /** Per-target params with the derived `clickUrl` filled in where absent. A
31289
31695
  * value the operator set on the target WINS — he chose it. */
31290
- function paramsFor$1(target, clickUrl) {
31696
+ function paramsFor$2(target, clickUrl) {
31291
31697
  if (clickUrl === void 0) return target.params;
31292
31698
  if (target.params === void 0) return { clickUrl };
31293
31699
  if (typeof target.params["clickUrl"] === "string") return target.params;
@@ -31303,11 +31709,11 @@ function summaryRecordId(ruleId, windowEndMs) {
31303
31709
  /** The caption burned onto the mosaic's title band, with the same `{{var}}`
31304
31710
  * vocabulary the title and body use. `''` is a real answer ("no caption"). */
31305
31711
  function summaryCaptionText(input) {
31306
- if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$2(input.window.startMs)}→${clockOf$2(input.window.endMs)}`;
31712
+ if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
31307
31713
  return renderTemplate(input.captionText, {
31308
31714
  rule: input.ruleName,
31309
- from: clockOf$2(input.window.startMs),
31310
- to: clockOf$2(input.window.endMs)
31715
+ from: clockOf$3(input.window.startMs),
31716
+ to: clockOf$3(input.window.endMs)
31311
31717
  }) ?? "";
31312
31718
  }
31313
31719
  /**
@@ -31325,7 +31731,7 @@ function buildSummaryOutboxInputs(input) {
31325
31731
  const body = composeSummaryBody({
31326
31732
  aiText: aiSentence(input),
31327
31733
  renderedTemplateBody: renderTemplate(input.template?.body, vars),
31328
- derivedBody: derivedBody$1(input, vars),
31734
+ derivedBody: derivedBody$2(input, vars),
31329
31735
  templateNamesAi: templatePlaceholders(input.template?.body ?? "").includes("ai")
31330
31736
  });
31331
31737
  const recordId = summaryRecordId(input.ruleId, input.window.endMs);
@@ -31344,7 +31750,7 @@ function buildSummaryOutboxInputs(input) {
31344
31750
  },
31345
31751
  media: "none",
31346
31752
  ...artifacts.length > 0 ? { artifacts } : {},
31347
- ...paramsFor$1(target, input.clickUrl) !== void 0 ? { params: paramsFor$1(target, input.clickUrl) } : {},
31753
+ ...paramsFor$2(target, input.clickUrl) !== void 0 ? { params: paramsFor$2(target, input.clickUrl) } : {},
31348
31754
  subject: {
31349
31755
  deviceId: input.primaryDeviceId,
31350
31756
  className: "summary",
@@ -32239,7 +32645,7 @@ var NcSummaryProducer = class {
32239
32645
  purpose: "summary",
32240
32646
  tiles: tiles.map(({ candidate, jpeg }) => ({
32241
32647
  jpeg,
32242
- ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$1(candidate.firstSeen)} · ${candidate.className}` } : {},
32648
+ ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
32243
32649
  deviceId: candidate.deviceId,
32244
32650
  sourceId: candidate.trackId
32245
32651
  })),
@@ -32462,7 +32868,7 @@ var NcSummaryProducer = class {
32462
32868
  }
32463
32869
  };
32464
32870
  /** `HH:MM` in the host timezone — the two numbers a tile label needs. */
32465
- function clockOf$1(atMs) {
32871
+ function clockOf$2(atMs) {
32466
32872
  const d = new Date(atMs);
32467
32873
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32468
32874
  }
@@ -32538,7 +32944,7 @@ var NC_SUMMARY_RULES_INDEXES = [{
32538
32944
  columns: ["enabled"]
32539
32945
  }];
32540
32946
  /** The rule set is operator-authored and tiny; a generous ceiling. */
32541
- var LOAD_LIMIT$1 = 1e4;
32947
+ var LOAD_LIMIT$2 = 1e4;
32542
32948
  /**
32543
32949
  * The ledger contract for the digest rule set.
32544
32950
  *
@@ -32568,7 +32974,7 @@ var NC_SUMMARY_RULES_SPEC = {
32568
32974
  const parsed = NcSummaryRuleSchema.safeParse(data["rule"]);
32569
32975
  return parsed.success ? parsed.data : null;
32570
32976
  },
32571
- loadLimit: LOAD_LIMIT$1
32977
+ loadLimit: LOAD_LIMIT$2
32572
32978
  };
32573
32979
  /**
32574
32980
  * The three-way CLEARABLE patch signal, one function per field: `undefined`
@@ -32579,7 +32985,7 @@ var NC_SUMMARY_RULES_SPEC = {
32579
32985
  * cast here would be a cast on the exact path that decides whether a persisted
32580
32986
  * rule keeps a field. Three literal destructurings are three provable ones.
32581
32987
  */
32582
- function applyTemplatePatch$1(merged, template) {
32988
+ function applyTemplatePatch$2(merged, template) {
32583
32989
  if (template === void 0) return merged;
32584
32990
  if (template !== null) return {
32585
32991
  ...merged,
@@ -32725,7 +33131,7 @@ var NcSummaryStore = class {
32725
33131
  createdAt: existing.createdAt,
32726
33132
  updatedAt: this.now()
32727
33133
  };
32728
- return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$1(merged, template), filters), ai));
33134
+ return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$2(merged, template), filters), ai));
32729
33135
  }
32730
33136
  async setEnabled(ruleId, enabled) {
32731
33137
  return this.update(ruleId, { enabled });
@@ -32777,6 +33183,408 @@ var NcSummaryStore = class {
32777
33183
  }
32778
33184
  };
32779
33185
  //#endregion
33186
+ //#region src/notification-center/calendar-events/calendar-events-store.ts
33187
+ /**
33188
+ * Durable calendar-events rule set — same DurableLedger mechanics as
33189
+ * Summaries, with a per-rule `generatedAt` stamp (one fire per schedule window).
33190
+ */
33191
+ /**
33192
+ * @durable class=config owner=notification-center
33193
+ * write="an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated"
33194
+ * retention="none — a row goes only when the operator deletes the rule."
33195
+ */
33196
+ var NC_CALENDAR_EVENTS_RULES_COLLECTION = "notification-center:calendar-events-rules";
33197
+ var NC_CALENDAR_EVENTS_RULES_COLUMNS = [
33198
+ {
33199
+ name: "id",
33200
+ type: "TEXT",
33201
+ primaryKey: true,
33202
+ notNull: true
33203
+ },
33204
+ {
33205
+ name: "name",
33206
+ type: "TEXT",
33207
+ notNull: true
33208
+ },
33209
+ {
33210
+ name: "enabled",
33211
+ type: "BOOLEAN",
33212
+ notNull: true
33213
+ },
33214
+ {
33215
+ name: "updatedAt",
33216
+ type: "INTEGER",
33217
+ notNull: true
33218
+ },
33219
+ {
33220
+ name: "rule",
33221
+ type: "JSON",
33222
+ notNull: true
33223
+ }
33224
+ ];
33225
+ var NC_CALENDAR_EVENTS_RULES_INDEXES = [{
33226
+ name: "idx_nc_calendar_events_rules_enabled",
33227
+ columns: ["enabled"]
33228
+ }];
33229
+ var LOAD_LIMIT$1 = 1e4;
33230
+ var NC_CALENDAR_EVENTS_RULES_SPEC = {
33231
+ collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
33232
+ columns: [...NC_CALENDAR_EVENTS_RULES_COLUMNS],
33233
+ indexes: [...NC_CALENDAR_EVENTS_RULES_INDEXES],
33234
+ writeMode: "write-through",
33235
+ keyOf: (rule) => rule.id,
33236
+ toValue: (rule) => ({
33237
+ name: rule.name,
33238
+ enabled: rule.enabled,
33239
+ updatedAt: rule.updatedAt,
33240
+ rule
33241
+ }),
33242
+ fromRecord: (_key, data) => {
33243
+ const parsed = NcCalendarEventsRuleSchema.safeParse(data["rule"]);
33244
+ return parsed.success ? parsed.data : null;
33245
+ },
33246
+ loadLimit: LOAD_LIMIT$1
33247
+ };
33248
+ function applyTemplatePatch$1(merged, template) {
33249
+ if (template === void 0) return merged;
33250
+ if (template !== null) return {
33251
+ ...merged,
33252
+ template
33253
+ };
33254
+ const { template: _cleared, ...rest } = merged;
33255
+ return rest;
33256
+ }
33257
+ function applyOptionalStringList(merged, key, value) {
33258
+ if (value === void 0) return merged;
33259
+ if (value !== null) return {
33260
+ ...merged,
33261
+ [key]: [...value]
33262
+ };
33263
+ const { [key]: _cleared, ...rest } = merged;
33264
+ return rest;
33265
+ }
33266
+ var NcCalendarEventsStore = class {
33267
+ ledger;
33268
+ store;
33269
+ logger;
33270
+ now;
33271
+ newId;
33272
+ constructor(deps) {
33273
+ this.store = deps.store;
33274
+ this.ledger = new DurableLedger({
33275
+ spec: NC_CALENDAR_EVENTS_RULES_SPEC,
33276
+ store: deps.store,
33277
+ logger: deps.logger
33278
+ });
33279
+ this.logger = deps.logger;
33280
+ this.now = deps.now ?? (() => Date.now());
33281
+ this.newId = deps.newId ?? (() => (0, node_crypto.randomUUID)());
33282
+ }
33283
+ static async declare(store) {
33284
+ await DurableLedger.declare(store, NC_CALENDAR_EVENTS_RULES_SPEC);
33285
+ }
33286
+ async load() {
33287
+ await this.ledger.load();
33288
+ this.logger.debug("calendar-events rules loaded", { meta: { rules: this.ledger.size } });
33289
+ }
33290
+ list() {
33291
+ return this.ledger.snapshot().toSorted((a, b) => b.updatedAt - a.updatedAt);
33292
+ }
33293
+ listEnabled() {
33294
+ return this.list().filter((r) => r.enabled);
33295
+ }
33296
+ get(ruleId) {
33297
+ return this.ledger.get(ruleId) ?? null;
33298
+ }
33299
+ listForOwner(userId) {
33300
+ return this.list().filter((r) => r.ownerUserId === void 0 || r.ownerUserId === userId);
33301
+ }
33302
+ async create(input, createdBy, ownerUserId) {
33303
+ const now = this.now();
33304
+ const rule = NcCalendarEventsRuleSchema.parse({
33305
+ ...NcCalendarEventsRuleInputSchema.parse(input),
33306
+ id: this.newId(),
33307
+ ...ownerUserId !== void 0 ? { ownerUserId } : {},
33308
+ createdBy,
33309
+ createdAt: now,
33310
+ updatedAt: now
33311
+ });
33312
+ await this.ledger.put(rule);
33313
+ return rule;
33314
+ }
33315
+ async update(ruleId, patch) {
33316
+ const existing = this.ledger.get(ruleId);
33317
+ if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
33318
+ const { template, calendars, calendarsExclude, ...rest } = patch;
33319
+ const merged = {
33320
+ ...existing,
33321
+ ...rest,
33322
+ id: existing.id,
33323
+ ...existing.ownerUserId !== void 0 ? { ownerUserId: existing.ownerUserId } : {},
33324
+ ...existing.generatedAt !== void 0 ? { generatedAt: existing.generatedAt } : {},
33325
+ createdBy: existing.createdBy,
33326
+ createdAt: existing.createdAt,
33327
+ updatedAt: this.now()
33328
+ };
33329
+ return this.write(applyOptionalStringList(applyOptionalStringList(applyTemplatePatch$1(merged, template), "calendars", calendars), "calendarsExclude", calendarsExclude));
33330
+ }
33331
+ async setEnabled(ruleId, enabled) {
33332
+ return this.update(ruleId, { enabled });
33333
+ }
33334
+ async markGenerated(ruleId, windowEndMs) {
33335
+ const existing = this.ledger.get(ruleId);
33336
+ if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
33337
+ return this.write({
33338
+ ...existing,
33339
+ generatedAt: Math.max(existing.generatedAt ?? 0, windowEndMs)
33340
+ });
33341
+ }
33342
+ /**
33343
+ * Idempotent delete — unknown ids are a no-op in RAM, still attempted on disk.
33344
+ *
33345
+ * Deliberately NOT {@link DurableLedger.forget}: that swallows a failed
33346
+ * durable delete. Mirror eviction first; durable half re-throws on failure
33347
+ * (same shape as Summaries).
33348
+ */
33349
+ async delete(ruleId) {
33350
+ this.ledger.evict(ruleId);
33351
+ try {
33352
+ await this.store.delete.mutate({
33353
+ collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
33354
+ key: ruleId
33355
+ });
33356
+ } catch (err) {
33357
+ this.logger.warn("calendar-events rule durable delete failed", { meta: {
33358
+ ruleId,
33359
+ error: err instanceof Error ? err.message : String(err)
33360
+ } });
33361
+ throw err instanceof Error ? err : new Error(String(err));
33362
+ }
33363
+ }
33364
+ async write(rule) {
33365
+ const validated = NcCalendarEventsRuleSchema.parse(rule);
33366
+ await this.ledger.put(validated);
33367
+ return validated;
33368
+ }
33369
+ };
33370
+ //#endregion
33371
+ //#region src/notification-center/calendar-events/calendar-events-delivery.ts
33372
+ function calendarEventsRecordId(ruleId, windowEndMs) {
33373
+ return `calendar-events:${ruleId}:${windowEndMs}`;
33374
+ }
33375
+ /** `YYYY-MM-DD HH:MM` in the host timezone. */
33376
+ function clockOf$1(atMs) {
33377
+ const d = new Date(atMs);
33378
+ return `${`${String(d.getFullYear())}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`} ${`${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`}`;
33379
+ }
33380
+ function formatEventLine(event) {
33381
+ const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
33382
+ const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
33383
+ return `• ${when} ${event.title}${where}`;
33384
+ }
33385
+ function derivedBody$1(input) {
33386
+ if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
33387
+ const lines = input.events.slice(0, 40).map(formatEventLine);
33388
+ const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
33389
+ return lines.join("\n") + extra;
33390
+ }
33391
+ function templateVars(input) {
33392
+ return {
33393
+ rule: input.ruleName,
33394
+ count: String(input.events.length),
33395
+ horizonDays: String(input.horizonDays),
33396
+ from: clockOf$1(input.eventWindow.startMs),
33397
+ to: clockOf$1(input.eventWindow.endMs),
33398
+ events: derivedBody$1(input),
33399
+ calendars: String(input.calendarIds.length)
33400
+ };
33401
+ }
33402
+ function paramsFor$1(target) {
33403
+ const params = target.params;
33404
+ if (params === void 0) return void 0;
33405
+ const out = {};
33406
+ for (const [k, v] of Object.entries(params)) if (typeof v === "string") out[k] = v;
33407
+ return Object.keys(out).length > 0 ? out : void 0;
33408
+ }
33409
+ /**
33410
+ * One outbox row per enabled target. Ready for `NcOutbox.enqueue`.
33411
+ */
33412
+ function buildCalendarEventsOutboxInputs(input) {
33413
+ const vars = templateVars(input);
33414
+ const optedOut = new Set(input.disabledTargetIds ?? []);
33415
+ const title = renderTemplate(input.template?.title, vars) ?? `${input.ruleName}: ${String(input.events.length)} upcoming`;
33416
+ const body = renderTemplate(input.template?.body, vars) ?? derivedBody$1(input);
33417
+ const recordId = calendarEventsRecordId(input.ruleId, input.scheduleWindow.endMs);
33418
+ return input.targets.filter((t) => !optedOut.has(t.targetId)).map((target) => {
33419
+ const payload = {
33420
+ ruleName: input.ruleName,
33421
+ delivery: "calendar-events",
33422
+ priority: input.priority,
33423
+ template: {
33424
+ title,
33425
+ body
33426
+ },
33427
+ media: "none",
33428
+ ...paramsFor$1(target) !== void 0 ? { params: paramsFor$1(target) } : {},
33429
+ subject: {
33430
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
33431
+ className: "calendar-events",
33432
+ zones: [],
33433
+ timestamp: input.scheduleWindow.endMs
33434
+ }
33435
+ };
33436
+ return {
33437
+ ruleId: input.ruleId,
33438
+ targetId: target.targetId,
33439
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
33440
+ recordKind: "calendar-events",
33441
+ recordId,
33442
+ payload
33443
+ };
33444
+ });
33445
+ }
33446
+ //#endregion
33447
+ //#region src/notification-center/calendar-events/calendar-events-selection.ts
33448
+ /**
33449
+ * Expand a rule's calendar scope against the known calendar ids.
33450
+ *
33451
+ * - All (both lists empty/absent) → every known id
33452
+ * - Only → the include list, intersected with known (unknown ids dropped)
33453
+ * - Except → known minus the exclude list
33454
+ *
33455
+ * An empty known set always yields empty — there is nothing to notify about.
33456
+ */
33457
+ function expandCalendarScope(scope, knownIds) {
33458
+ const known = [...knownIds];
33459
+ if (known.length === 0) return [];
33460
+ const include = scope?.calendars ?? [];
33461
+ const exclude = scope?.calendarsExclude ?? [];
33462
+ if (include.length > 0) {
33463
+ const wanted = new Set(include);
33464
+ return known.filter((id) => wanted.has(id));
33465
+ }
33466
+ if (exclude.length > 0) {
33467
+ const banned = new Set(exclude);
33468
+ return known.filter((id) => !banned.has(id));
33469
+ }
33470
+ return known;
33471
+ }
33472
+ //#endregion
33473
+ //#region src/notification-center/calendar-events/calendar-events-producer.ts
33474
+ /** How far back a closed schedule window may still be owed. */
33475
+ var CALENDAR_EVENTS_LOOKBACK_MS = 2160 * 60 * 1e3;
33476
+ var NcCalendarEventsProducer = class {
33477
+ deps;
33478
+ inFlight = false;
33479
+ now;
33480
+ constructor(deps) {
33481
+ this.deps = deps;
33482
+ this.now = deps.now ?? (() => Date.now());
33483
+ }
33484
+ async tick() {
33485
+ if (this.inFlight) return {
33486
+ examined: 0,
33487
+ sent: 0,
33488
+ empty: 0,
33489
+ skipped: 0,
33490
+ failed: 0
33491
+ };
33492
+ this.inFlight = true;
33493
+ const report = {
33494
+ examined: 0,
33495
+ sent: 0,
33496
+ empty: 0,
33497
+ skipped: 0,
33498
+ failed: 0
33499
+ };
33500
+ try {
33501
+ const rules = this.deps.store.listEnabled();
33502
+ for (const rule of rules) {
33503
+ report.examined += 1;
33504
+ try {
33505
+ const outcome = await this.runRule(rule);
33506
+ if (outcome === "sent") report.sent += 1;
33507
+ else if (outcome === "empty") report.empty += 1;
33508
+ else report.skipped += 1;
33509
+ } catch (err) {
33510
+ report.failed += 1;
33511
+ this.deps.logger.warn("calendar-events rule failed", { meta: {
33512
+ ruleId: rule.id,
33513
+ error: err instanceof Error ? err.message : String(err)
33514
+ } });
33515
+ }
33516
+ }
33517
+ return report;
33518
+ } finally {
33519
+ this.inFlight = false;
33520
+ }
33521
+ }
33522
+ async runRule(rule) {
33523
+ const nowMs = this.now();
33524
+ const scheduleWindow = findLastClosedWindow({
33525
+ schedule: rule.schedule,
33526
+ nowMs,
33527
+ lookbackMs: CALENDAR_EVENTS_LOOKBACK_MS
33528
+ });
33529
+ if (scheduleWindow === null) return "skipped";
33530
+ if ((rule.generatedAt ?? 0) >= scheduleWindow.endMs) return "skipped";
33531
+ const known = await this.deps.calendars.listCalendars();
33532
+ const calendarIds = expandCalendarScope({
33533
+ calendars: rule.calendars,
33534
+ calendarsExclude: rule.calendarsExclude
33535
+ }, known.map((c) => c.id));
33536
+ if (calendarIds.length === 0) {
33537
+ this.deps.logger.info("calendar-events: no calendars in scope", { meta: { ruleId: rule.id } });
33538
+ if (!rule.deliverEmpty) {
33539
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33540
+ return "empty";
33541
+ }
33542
+ }
33543
+ const eventWindow = calendarEventsWindow(nowMs, rule.horizonDays);
33544
+ const events = calendarIds.length === 0 ? [] : await this.deps.calendars.listEvents({
33545
+ calendarIds,
33546
+ startMs: eventWindow.startMs,
33547
+ endMs: eventWindow.endMs
33548
+ });
33549
+ if (events.length === 0 && !rule.deliverEmpty) {
33550
+ this.deps.logger.info("calendar-events: empty horizon, not delivering", { meta: {
33551
+ ruleId: rule.id,
33552
+ horizonDays: eventWindow.horizonDays
33553
+ } });
33554
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33555
+ return "empty";
33556
+ }
33557
+ const sorted = [...events].toSorted((a, b) => a.startMs - b.startMs);
33558
+ await this.enqueue(rule, scheduleWindow, eventWindow, calendarIds, sorted);
33559
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33560
+ return "sent";
33561
+ }
33562
+ async enqueue(rule, scheduleWindow, eventWindow, calendarIds, events) {
33563
+ const inputs = buildCalendarEventsOutboxInputs({
33564
+ ruleId: rule.id,
33565
+ ruleName: rule.name,
33566
+ scheduleWindow,
33567
+ eventWindow: {
33568
+ startMs: eventWindow.startMs,
33569
+ endMs: eventWindow.endMs
33570
+ },
33571
+ horizonDays: eventWindow.horizonDays,
33572
+ events,
33573
+ calendarIds,
33574
+ targets: rule.targets,
33575
+ priority: rule.priority,
33576
+ ...rule.template !== void 0 ? { template: rule.template } : {}
33577
+ });
33578
+ for (const input of inputs) await this.deps.outbox.enqueue(input);
33579
+ this.deps.logger.info("calendar-events delivered", { meta: {
33580
+ ruleId: rule.id,
33581
+ events: events.length,
33582
+ targets: inputs.length,
33583
+ windowEndMs: scheduleWindow.endMs
33584
+ } });
33585
+ }
33586
+ };
33587
+ //#endregion
32780
33588
  //#region src/notification-center/text-catalog-editor.ts
32781
33589
  var NcTextCatalogEditor = class {
32782
33590
  store;
@@ -35860,7 +36668,7 @@ function packagePhaseOf(ev) {
35860
36668
  */
35861
36669
  function historyRecordKind(kind) {
35862
36670
  if (kind === "audio-event" || kind === "audio-window") return "object-event";
35863
- if (kind === "timelapse" || kind === "ai-summary") return "system-event";
36671
+ if (kind === "timelapse" || kind === "ai-summary" || kind === "calendar-events") return "system-event";
35864
36672
  return kind;
35865
36673
  }
35866
36674
  /**
@@ -35875,7 +36683,8 @@ function historyRecordKind(kind) {
35875
36683
  function cooldownSubjectKind(kind) {
35876
36684
  switch (kind) {
35877
36685
  case "timelapse":
35878
- case "ai-summary": return "device-event";
36686
+ case "ai-summary":
36687
+ case "calendar-events": return "device-event";
35879
36688
  default: return kind;
35880
36689
  }
35881
36690
  }
@@ -35975,7 +36784,7 @@ function outboxEntryToHistory(texts, entry) {
35975
36784
  id: entry.id,
35976
36785
  ruleId: entry.ruleId,
35977
36786
  ruleName: entry.payload.ruleName,
35978
- delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" ? "system-event" : entry.payload.delivery,
36787
+ delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" || entry.payload.delivery === "calendar-events" ? "system-event" : entry.payload.delivery,
35979
36788
  targetId: entry.targetId,
35980
36789
  deviceId: entry.deviceId,
35981
36790
  recordKind: historyRecordKind(entry.recordKind),
@@ -36185,6 +36994,9 @@ var NotificationCenter = class {
36185
36994
  */
36186
36995
  summaryRules;
36187
36996
  summaryProducer = null;
36997
+ /** Calendar-events digest rules + producer (hub-only producer). */
36998
+ calendarEventsRules;
36999
+ calendarEventsProducer = null;
36188
37000
  /**
36189
37001
  * What the deleted-device / deleted-target prune did, and why — its
36190
37002
  * disable-once guard and the source of both UIs' banner. Loaded on EVERY
@@ -37175,6 +37987,11 @@ var NotificationCenter = class {
37175
37987
  logger: this.logger.child("summary"),
37176
37988
  ...deps.now !== void 0 ? { now: deps.now } : {}
37177
37989
  });
37990
+ this.calendarEventsRules = new NcCalendarEventsStore({
37991
+ store: deps.store,
37992
+ logger: this.logger.child("calendar-events"),
37993
+ ...deps.now !== void 0 ? { now: deps.now } : {}
37994
+ });
37178
37995
  this.pruneLedger = new NcPruneLedger({
37179
37996
  store: deps.store,
37180
37997
  logger: this.logger.child("device-prune")
@@ -37221,6 +38038,14 @@ var NotificationCenter = class {
37221
38038
  zoneOwner: this.zoneOwners.lookup()
37222
38039
  });
37223
38040
  }
38041
+ const calendarsPort = deps.calendars;
38042
+ if (calendarsPort !== void 0) this.calendarEventsProducer = new NcCalendarEventsProducer({
38043
+ store: this.calendarEventsRules,
38044
+ outbox: this.outbox,
38045
+ calendars: calendarsPort,
38046
+ logger: this.logger.child("calendar-events"),
38047
+ now: this.now
38048
+ });
37224
38049
  }
37225
38050
  /**
37226
38051
  * The durable timelapse rule set — exposed for the `nc.*Timelapse*` bridge
@@ -37234,6 +38059,10 @@ var NotificationCenter = class {
37234
38059
  get summaryStore() {
37235
38060
  return this.summaryRules;
37236
38061
  }
38062
+ /** The durable calendar-events rule set — exposed for the `nc.*CalendarEvents*` bridge. */
38063
+ get calendarEventsStore() {
38064
+ return this.calendarEventsRules;
38065
+ }
37237
38066
  /**
37238
38067
  * Produce one digest window for one rule, right now.
37239
38068
  *
@@ -37307,6 +38136,7 @@ var NotificationCenter = class {
37307
38136
  await NcDeviceMuteStore.declare(store);
37308
38137
  await TimelapseStore.declare(store);
37309
38138
  await NcSummaryStore.declare(store);
38139
+ await NcCalendarEventsStore.declare(store);
37310
38140
  await NcRuleAiStore.declare(store);
37311
38141
  await NcTextCatalogStore.declare(store);
37312
38142
  await NcArtifactIndex.declare(store);
@@ -38721,7 +39551,18 @@ var NotificationCenter = class {
38721
39551
  },
38722
39552
  getAlarmConfig: async () => this.alarmConfig(),
38723
39553
  getAlarmSensors: async (input) => this.alarmSensors(input),
38724
- setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
39554
+ setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch),
39555
+ createAlarmProfile: async ({ name, cloneFromId }) => this.mutateAlarmProfile((panel) => panel.createProfile({
39556
+ name,
39557
+ newId: (0, node_crypto.randomUUID)(),
39558
+ source: cloneFromId === void 0 ? { kind: "defaults" } : {
39559
+ kind: "clone",
39560
+ fromId: cloneFromId
39561
+ }
39562
+ })),
39563
+ renameAlarmProfile: async ({ id, name }) => this.mutateAlarmProfile((panel) => panel.renameProfile(id, name)),
39564
+ deleteAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.deleteProfile(id)),
39565
+ activateAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.activateProfile(id))
38725
39566
  };
38726
39567
  }
38727
39568
  /**
@@ -39014,6 +39855,13 @@ var NotificationCenter = class {
39014
39855
  });
39015
39856
  return this.alarmConfig();
39016
39857
  }
39858
+ /** Profile CRUD — each mutation returns the whole config (settings applied). */
39859
+ async mutateAlarmProfile(run) {
39860
+ const panel = this.alarmPanel;
39861
+ if (panel === null) throw new Error("this node has no alarm panel");
39862
+ await run(panel);
39863
+ return this.alarmConfig();
39864
+ }
39017
39865
  /**
39018
39866
  * The panel as it is NOW — see `alarm-live.ts`.
39019
39867
  *
@@ -39110,6 +39958,7 @@ var NotificationCenter = class {
39110
39958
  return {
39111
39959
  deviceId: panel.deviceId,
39112
39960
  settings: panel.settings(),
39961
+ profiles: panel.profilesView(),
39113
39962
  live: await this.alarmLive(panel),
39114
39963
  coverage: this.coverageFor(panel).map((c) => ({
39115
39964
  ...c,
@@ -40929,11 +41778,17 @@ var NotificationCenter = class {
40929
41778
  await this.rules.load();
40930
41779
  this.noteInertGroupedRules();
40931
41780
  if (this.evaluationActive) this.deviceAlerts.tick();
40932
- if (this.evaluationActive) this.stateSummary.tick().catch((err) => {
40933
- this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
40934
- });
41781
+ if (this.evaluationActive) {
41782
+ this.stateSummary.tick().catch((err) => {
41783
+ this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
41784
+ });
41785
+ this.calendarEventsProducer?.tick().catch((err) => {
41786
+ this.logger.warn("calendar-events tick failed", { meta: { error: String(err) } });
41787
+ });
41788
+ }
40935
41789
  await this.timelapseRules.load();
40936
41790
  await this.summaryRules.load();
41791
+ await this.calendarEventsRules.load();
40937
41792
  await this.ruleAi.load();
40938
41793
  await this.reloadTexts();
40939
41794
  this.refreshOccupancyWatch();
@@ -104816,6 +105671,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
104816
105671
  store: center.summaryStore,
104817
105672
  runNow: (input) => center.runSummaryNow(input)
104818
105673
  },
105674
+ calendarEvents: { store: center.calendarEventsStore },
104819
105675
  texts: center.textCatalogEditor,
104820
105676
  previewTexts: center.textCatalog,
104821
105677
  ...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
@@ -106003,7 +106859,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
106003
106859
  })).tracks;
106004
106860
  },
106005
106861
  ...this.isPostProcessingNode ? { timelapse: this.buildTimelapsePorts(api, stores) } : {},
106006
- ...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {}
106862
+ ...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {},
106863
+ ...this.isPostProcessingNode ? { calendars: {
106864
+ listCalendars: async () => {
106865
+ return (await api.calendars.listCalendars.query()).map((c) => ({ id: c.id }));
106866
+ },
106867
+ listEvents: async (input) => api.calendars.listEvents.query({
106868
+ calendarIds: [...input.calendarIds],
106869
+ startMs: input.startMs,
106870
+ endMs: input.endMs
106871
+ })
106872
+ } } : {}
106007
106873
  });
106008
106874
  this.wireAlarmPanel(this.notificationCenter);
106009
106875
  await this.serveNcActionPlane(this.notificationCenter);