@camstack/addon-post-analysis 1.2.320 → 1.2.321

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-BCx3qCL5.js} +89 -0
  2. package/dist/{clip-model-registry-C-IEAIQQ.mjs → clip-model-registry-Dnm9dfy0.mjs} +89 -0
  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-CPcZN3mA.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-CGUnHrR_.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-CVH5tT7I.mjs +26 -0
  9. package/dist/pipeline-analytics/{hostInit-ByDyL2zi.mjs → hostInit-Dt-jaGuG.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +648 -48
  11. package/dist/pipeline-analytics/index.mjs +648 -48
  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-BCx3qCL5.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");
@@ -5448,7 +5448,7 @@ function isAdoptable(record, nowMs) {
5448
5448
  * what `localDayMinute` does for the same input — one behaviour for a bad
5449
5449
  * zone, not two.
5450
5450
  */
5451
- function clockOf$5(atMs, timeZone) {
5451
+ function clockOf$6(atMs, timeZone) {
5452
5452
  if (timeZone !== void 0) try {
5453
5453
  return new Intl.DateTimeFormat("en-GB", {
5454
5454
  timeZone,
@@ -5584,7 +5584,7 @@ var TimelapseRenderer = class {
5584
5584
  includeAudio: false,
5585
5585
  maxLifeMs: await this.ports.exportLifeMs(),
5586
5586
  deleteAfterDownload: false,
5587
- title: `${request.ruleName} ${clockOf$5(window.startMs, request.timezone)}→${clockOf$5(window.endMs, request.timezone)}`,
5587
+ title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
5588
5588
  timelapse: {
5589
5589
  everyMs,
5590
5590
  outputFps: request.framerate,
@@ -5915,7 +5915,7 @@ var TimelapseRenderer = class {
5915
5915
  ...meta,
5916
5916
  unservedWindowCount: verdict.unservedWindows.length,
5917
5917
  unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
5918
- at: clockOf$5(request.window.startMs + w.fromSec * 1e3, request.timezone),
5918
+ at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
5919
5919
  fromSec: Math.round(w.fromSec),
5920
5920
  toSec: Math.round(w.toSec),
5921
5921
  expectedDenseFrames: w.expectedDenseFrames,
@@ -13900,7 +13900,7 @@ var NcPruneRecordSchema = require_clip_model_registry.object({
13900
13900
  source: require_clip_model_registry._enum(["event", "reconcile"])
13901
13901
  });
13902
13902
  /** Rules × deleted subjects — small; a generous ceiling. */
13903
- var LOAD_LIMIT$4 = 1e4;
13903
+ var LOAD_LIMIT$5 = 1e4;
13904
13904
  function keyOf$1(row) {
13905
13905
  return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
13906
13906
  }
@@ -13919,7 +13919,7 @@ var NC_DEVICE_PRUNES_SPEC = {
13919
13919
  return parsed.success ? parsed.data : null;
13920
13920
  },
13921
13921
  deviceIdOf: (row) => row.subjectKind === "device" ? Number(row.subject) : void 0,
13922
- loadLimit: LOAD_LIMIT$4
13922
+ loadLimit: LOAD_LIMIT$5
13923
13923
  };
13924
13924
  /** The outcomes that mean "this last reference was already dealt with". */
13925
13925
  var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
@@ -13954,7 +13954,7 @@ var NcPruneLedger = class {
13954
13954
  try {
13955
13955
  const records = await this.store.query.query({
13956
13956
  collection: NC_DEVICE_PRUNES_COLLECTION,
13957
- filter: { limit: LOAD_LIMIT$4 }
13957
+ filter: { limit: LOAD_LIMIT$5 }
13958
13958
  });
13959
13959
  const next = /* @__PURE__ */ new Map();
13960
13960
  for (const record of records) {
@@ -17279,12 +17279,12 @@ var NcSummaryTemplateSchema = require_clip_model_registry.object({
17279
17279
  title: require_clip_model_registry.string().max(500).optional(),
17280
17280
  body: require_clip_model_registry.string().max(2e3).optional()
17281
17281
  });
17282
- var NameField = require_clip_model_registry.string().min(1).max(200);
17282
+ var NameField$1 = require_clip_model_registry.string().min(1).max(200);
17283
17283
  /** MULTI-CAMERA is the point of the feature — a one-camera digest is a
17284
17284
  * timelapse with extra steps, but it is allowed and it is cheap. */
17285
17285
  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);
17286
+ var TargetsField$1 = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
17287
+ var PriorityField$1 = require_clip_model_registry.number().int().min(1).max(5);
17288
17288
  /**
17289
17289
  * The mosaic's tile ceiling. 9 (a 3×3 grid) is the shipped default: it is the
17290
17290
  * most a phone lock-screen thumbnail can carry and still be read, and it is the
@@ -17449,7 +17449,7 @@ var NcSummaryAiSchema = require_clip_model_registry.object({
17449
17449
  onFailure: require_clip_model_registry._enum(["deliver-without-text", "skip"]).optional()
17450
17450
  });
17451
17451
  var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17452
- name: NameField,
17452
+ name: NameField$1,
17453
17453
  enabled: require_clip_model_registry.boolean().default(true),
17454
17454
  deviceIds: DeviceIdsField,
17455
17455
  window: NcSummaryWindowSchema,
@@ -17458,7 +17458,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17458
17458
  maxAiImages: MaxAiImagesField.default(NC_SUMMARY_DEFAULT_AI_IMAGES),
17459
17459
  rank: NcSummaryRankSchema.default("importance"),
17460
17460
  barrierSeconds: BarrierSecondsField.default(120),
17461
- targets: TargetsField,
17461
+ targets: TargetsField$1,
17462
17462
  template: NcSummaryTemplateSchema.optional(),
17463
17463
  /** Burned onto the mosaic's title band — and ONLY when `captions` is `all`. */
17464
17464
  captions: NcSummaryCaptionsSchema.default(NC_SUMMARY_DEFAULT_CAPTIONS),
@@ -17468,7 +17468,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17468
17468
  /** Deliver a window that matched NOTHING? Default no: a nightly "0 events"
17469
17469
  * is a line nobody reads, and it costs the lines beside it their reader. */
17470
17470
  deliverEmpty: require_clip_model_registry.boolean().default(false),
17471
- priority: PriorityField.default(3),
17471
+ priority: PriorityField$1.default(3),
17472
17472
  ai: NcSummaryAiSchema.optional()
17473
17473
  });
17474
17474
  /**
@@ -17480,7 +17480,7 @@ var NcSummaryRuleInputSchema = require_clip_model_registry.object({
17480
17480
  * plain `.optional()`.
17481
17481
  */
17482
17482
  var NcSummaryRulePatchSchema = require_clip_model_registry.object({
17483
- name: NameField.optional(),
17483
+ name: NameField$1.optional(),
17484
17484
  enabled: require_clip_model_registry.boolean().optional(),
17485
17485
  deviceIds: DeviceIdsField.optional(),
17486
17486
  window: NcSummaryWindowSchema.optional(),
@@ -17489,12 +17489,12 @@ var NcSummaryRulePatchSchema = require_clip_model_registry.object({
17489
17489
  maxAiImages: MaxAiImagesField.optional(),
17490
17490
  rank: NcSummaryRankSchema.optional(),
17491
17491
  barrierSeconds: BarrierSecondsField.optional(),
17492
- targets: TargetsField.optional(),
17492
+ targets: TargetsField$1.optional(),
17493
17493
  template: NcSummaryTemplateSchema.nullable().optional(),
17494
17494
  captions: NcSummaryCaptionsSchema.optional(),
17495
17495
  captionText: require_clip_model_registry.string().max(200).optional(),
17496
17496
  deliverEmpty: require_clip_model_registry.boolean().optional(),
17497
- priority: PriorityField.optional(),
17497
+ priority: PriorityField$1.optional(),
17498
17498
  ai: NcSummaryAiSchema.nullable().optional()
17499
17499
  });
17500
17500
  /**
@@ -17527,6 +17527,89 @@ function assertSummaryBudget(rule) {
17527
17527
  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
17528
  }
17529
17529
  //#endregion
17530
+ //#region src/notification-center/calendar-events/calendar-events-window.ts
17531
+ /** One day in milliseconds. */
17532
+ var CALENDAR_EVENTS_DAY_MS = 1440 * 60 * 1e3;
17533
+ /**
17534
+ * Floor / ceiling on the horizon days a rule may declare.
17535
+ * 1 day is the minimum useful digest; 30 keeps a single notification bounded.
17536
+ */
17537
+ var CALENDAR_EVENTS_MIN_HORIZON_DAYS = 1;
17538
+ var CALENDAR_EVENTS_MAX_HORIZON_DAYS = 30;
17539
+ var CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS = 7;
17540
+ /**
17541
+ * Build the producer window for one fire.
17542
+ *
17543
+ * `horizonDays` is clamped into the legal range rather than refused — a
17544
+ * stored rule that predates a tighter bound must still fire.
17545
+ */
17546
+ function calendarEventsWindow(nowMs, horizonDays) {
17547
+ const days = Math.min(CALENDAR_EVENTS_MAX_HORIZON_DAYS, Math.max(CALENDAR_EVENTS_MIN_HORIZON_DAYS, Math.floor(horizonDays)));
17548
+ return {
17549
+ startMs: nowMs,
17550
+ endMs: nowMs + days * CALENDAR_EVENTS_DAY_MS,
17551
+ horizonDays: days
17552
+ };
17553
+ }
17554
+ //#endregion
17555
+ //#region src/notification-center/calendar-events/calendar-events-rule.ts
17556
+ /**
17557
+ * NcCalendarEventsRule — scheduled "upcoming calendar events" digest.
17558
+ *
17559
+ * Same ownership model as Summaries/Timelapses: lives in the addon (not
17560
+ * `@camstack/types`) so a field is live on the next post-analysis deploy.
17561
+ * Rides no `NcDelivery` member; reaches the outside world through
17562
+ * `nc.*CalendarEvents*` bridge actions.
17563
+ *
17564
+ * Calendar selection mirrors detection classes: `calendars` = Only,
17565
+ * `calendarsExclude` = Except, both empty = All.
17566
+ */
17567
+ var NcCalendarEventsTemplateSchema = require_clip_model_registry.object({
17568
+ title: require_clip_model_registry.string().max(500).optional(),
17569
+ body: require_clip_model_registry.string().max(4e3).optional()
17570
+ });
17571
+ var NameField = require_clip_model_registry.string().min(1).max(200);
17572
+ var TargetsField = require_clip_model_registry.array(require_clip_model_registry.NcRuleTargetSchema).min(1);
17573
+ var PriorityField = require_clip_model_registry.number().int().min(1).max(5);
17574
+ var HorizonDaysField = require_clip_model_registry.number().int().min(CALENDAR_EVENTS_MIN_HORIZON_DAYS).max(CALENDAR_EVENTS_MAX_HORIZON_DAYS);
17575
+ var CalendarIdsField = require_clip_model_registry.array(require_clip_model_registry.string().min(1).max(200)).max(64);
17576
+ var NcCalendarEventsRuleInputSchema = require_clip_model_registry.object({
17577
+ name: NameField,
17578
+ enabled: require_clip_model_registry.boolean().default(true),
17579
+ schedule: require_clip_model_registry.NcScheduleSchema,
17580
+ /** Only these calendars. Empty/absent with no exclude = All. */
17581
+ calendars: CalendarIdsField.optional(),
17582
+ /** All except these. Ignored when `calendars` is non-empty. */
17583
+ calendarsExclude: CalendarIdsField.optional(),
17584
+ horizonDays: HorizonDaysField.default(CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS),
17585
+ /** Deliver when the horizon holds no events? Default no. */
17586
+ deliverEmpty: require_clip_model_registry.boolean().default(false),
17587
+ targets: TargetsField,
17588
+ template: NcCalendarEventsTemplateSchema.optional(),
17589
+ priority: PriorityField.default(3)
17590
+ });
17591
+ var NcCalendarEventsRulePatchSchema = require_clip_model_registry.object({
17592
+ name: NameField.optional(),
17593
+ enabled: require_clip_model_registry.boolean().optional(),
17594
+ schedule: require_clip_model_registry.NcScheduleSchema.optional(),
17595
+ calendars: CalendarIdsField.nullable().optional(),
17596
+ calendarsExclude: CalendarIdsField.nullable().optional(),
17597
+ horizonDays: HorizonDaysField.optional(),
17598
+ deliverEmpty: require_clip_model_registry.boolean().optional(),
17599
+ targets: TargetsField.optional(),
17600
+ template: NcCalendarEventsTemplateSchema.nullable().optional(),
17601
+ priority: PriorityField.optional()
17602
+ });
17603
+ var NcCalendarEventsRuleSchema = NcCalendarEventsRuleInputSchema.extend({
17604
+ id: require_clip_model_registry.string(),
17605
+ ownerUserId: require_clip_model_registry.string().optional(),
17606
+ /** Epoch-ms of the schedule window end that was last delivered. */
17607
+ generatedAt: require_clip_model_registry.number().optional(),
17608
+ createdBy: require_clip_model_registry.string(),
17609
+ createdAt: require_clip_model_registry.number(),
17610
+ updatedAt: require_clip_model_registry.number()
17611
+ });
17612
+ //#endregion
17530
17613
  //#region src/notification-center/unstored-media-id.ts
17531
17614
  /**
17532
17615
  * An id for media bytes that were NEVER STORED.
@@ -18687,6 +18770,22 @@ var ncActions = require_clip_model_registry.defineCustomActions({
18687
18770
  kind: "mutation",
18688
18771
  caller: "required"
18689
18772
  }),
18773
+ "nc.listCalendarEventsRules": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({
18774
+ rules: require_clip_model_registry.array(NcCalendarEventsRuleSchema.extend({ readOnly: require_clip_model_registry.boolean() })),
18775
+ canAuthor: require_clip_model_registry.boolean()
18776
+ }), { caller: "required" }),
18777
+ "nc.upsertCalendarEventsRule": require_clip_model_registry.customAction(require_clip_model_registry.object({
18778
+ ruleId: require_clip_model_registry.string().optional(),
18779
+ rule: NcCalendarEventsRuleInputSchema.optional(),
18780
+ patch: NcCalendarEventsRulePatchSchema.optional()
18781
+ }), require_clip_model_registry.object({ rule: NcCalendarEventsRuleSchema }), {
18782
+ kind: "mutation",
18783
+ caller: "required"
18784
+ }),
18785
+ "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) }), {
18786
+ kind: "mutation",
18787
+ caller: "required"
18788
+ }),
18690
18789
  /**
18691
18790
  * Produce one digest window RIGHT NOW — the operator's Test button.
18692
18791
  *
@@ -18973,6 +19072,11 @@ function makeNcActionHandlers(deps) {
18973
19072
  if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
18974
19073
  return sum;
18975
19074
  };
19075
+ const requireCalendarEvents = () => {
19076
+ const ce = deps.calendarEvents;
19077
+ if (ce === void 0) throw new Error("this node does not host calendar-events rules — use the hub");
19078
+ return ce;
19079
+ };
18976
19080
  /** The artefact index, or an honest refusal. */
18977
19081
  const requireArtifacts = () => {
18978
19082
  const index = deps.artifacts;
@@ -19001,6 +19105,13 @@ function makeNcActionHandlers(deps) {
19001
19105
  if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
19002
19106
  return rule;
19003
19107
  };
19108
+ const assertCalendarEventsEditable = (ruleId, caller, ce) => {
19109
+ const rule = ce.store.get(ruleId);
19110
+ if (rule === null) throw new Error(`forbidden: calendar-events rule not found: ${ruleId}`);
19111
+ if (caller.isAdmin) return rule;
19112
+ if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: calendar-events rule not owned: ${ruleId}`);
19113
+ return rule;
19114
+ };
19004
19115
  /** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
19005
19116
  const assertTimelapseEditable = (ruleId, caller, tl) => {
19006
19117
  const rule = tl.store.get(ruleId);
@@ -19542,6 +19653,53 @@ function makeNcActionHandlers(deps) {
19542
19653
  ...out.skippedByAi === true ? { skippedByAi: true } : {}
19543
19654
  };
19544
19655
  },
19656
+ "nc.listCalendarEventsRules": async (_input, caller) => {
19657
+ const c = requireCaller$1(caller);
19658
+ const ce = requireCalendarEvents();
19659
+ return {
19660
+ rules: (c.isAdmin ? ce.store.list() : ce.store.listForOwner(c.userId)).map((r) => ({
19661
+ ...r,
19662
+ readOnly: !c.isAdmin && r.ownerUserId !== c.userId
19663
+ })),
19664
+ canAuthor: true
19665
+ };
19666
+ },
19667
+ "nc.upsertCalendarEventsRule": async (input, caller) => {
19668
+ const c = requireCaller$1(caller);
19669
+ const ce = requireCalendarEvents();
19670
+ if (input.ruleId === void 0) {
19671
+ if (input.rule === void 0) throw new Error("upsertCalendarEventsRule: rule or ruleId required");
19672
+ const parsed = NcCalendarEventsRuleInputSchema.parse(input.rule);
19673
+ await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
19674
+ const rule = await ce.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
19675
+ deps.logger.info("calendar-events rule created", { meta: {
19676
+ ruleId: rule.id,
19677
+ owner: c.userId
19678
+ } });
19679
+ return { rule };
19680
+ }
19681
+ assertCalendarEventsEditable(input.ruleId, c, ce);
19682
+ if (input.patch === void 0) throw new Error("upsertCalendarEventsRule: patch required");
19683
+ const patch = NcCalendarEventsRulePatchSchema.parse(input.patch);
19684
+ if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
19685
+ const rule = await ce.store.update(input.ruleId, patch);
19686
+ deps.logger.info("calendar-events rule updated", { meta: {
19687
+ ruleId: rule.id,
19688
+ owner: c.userId
19689
+ } });
19690
+ return { rule };
19691
+ },
19692
+ "nc.deleteCalendarEventsRule": async (input, caller) => {
19693
+ const c = requireCaller$1(caller);
19694
+ const ce = requireCalendarEvents();
19695
+ assertCalendarEventsEditable(input.ruleId, c, ce);
19696
+ await ce.store.delete(input.ruleId);
19697
+ deps.logger.info("calendar-events rule deleted", { meta: {
19698
+ ruleId: input.ruleId,
19699
+ owner: c.userId
19700
+ } });
19701
+ return { success: true };
19702
+ },
19545
19703
  "nc.getTexts": async (input) => {
19546
19704
  const texts = requireTexts();
19547
19705
  return input.language === void 0 ? texts.view() : texts.view(input.language);
@@ -28405,7 +28563,8 @@ var OUTBOX_RECORD_KINDS = new Set([
28405
28563
  "audio-window",
28406
28564
  "system-event",
28407
28565
  "timelapse",
28408
- "ai-summary"
28566
+ "ai-summary",
28567
+ "calendar-events"
28409
28568
  ]);
28410
28569
  function isOutboxRecordKind(x) {
28411
28570
  return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
@@ -28758,7 +28917,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
28758
28917
  ].join("");
28759
28918
  }
28760
28919
  /** `HH:MM` in the host timezone. */
28761
- function clockOf$4(atMs) {
28920
+ function clockOf$5(atMs) {
28762
28921
  const d = new Date(atMs);
28763
28922
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
28764
28923
  }
@@ -28796,7 +28955,7 @@ function buildRuleAiPrompt(input) {
28796
28955
  if (shape === "detection-track") {
28797
28956
  lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
28798
28957
  for (const [i, frame] of input.frames.entries()) {
28799
- const when = frame.atMs !== void 0 ? ` · ${clockOf$4(frame.atMs)}` : "";
28958
+ const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
28800
28959
  lines.push(`${String(i + 1)}.${when}`);
28801
28960
  }
28802
28961
  return lines.join("\n");
@@ -29093,7 +29252,7 @@ var NcRuleAiRowSchema = require_clip_model_registry.object({
29093
29252
  updatedAt: require_clip_model_registry.number()
29094
29253
  });
29095
29254
  /** Operator-authored and tiny; a generous ceiling. */
29096
- var LOAD_LIMIT$3 = 1e4;
29255
+ var LOAD_LIMIT$4 = 1e4;
29097
29256
  var NC_RULE_AI_SPEC = {
29098
29257
  collection: NC_RULE_AI_COLLECTION,
29099
29258
  columns: [...NC_RULE_AI_COLUMNS],
@@ -29109,7 +29268,7 @@ var NC_RULE_AI_SPEC = {
29109
29268
  const parsed = NcRuleAiRowSchema.safeParse(data["row"]);
29110
29269
  return parsed.success ? parsed.data : null;
29111
29270
  },
29112
- loadLimit: LOAD_LIMIT$3
29271
+ loadLimit: LOAD_LIMIT$4
29113
29272
  };
29114
29273
  var NcRuleAiStore = class {
29115
29274
  ledger;
@@ -29446,7 +29605,7 @@ var NC_RULES_INDEXES = [{
29446
29605
  columns: ["enabled"]
29447
29606
  }];
29448
29607
  /** Query cap — the rule set is operator-authored and tiny; a generous ceiling. */
29449
- var LOAD_LIMIT$2 = 1e4;
29608
+ var LOAD_LIMIT$3 = 1e4;
29450
29609
  function isRecord$3(value) {
29451
29610
  return typeof value === "object" && value !== null && !Array.isArray(value);
29452
29611
  }
@@ -29495,7 +29654,7 @@ function makeRulesSpec(onProjection) {
29495
29654
  if (projection.kind === "projected") onProjection?.(key, projection);
29496
29655
  return parsed.data;
29497
29656
  },
29498
- loadLimit: LOAD_LIMIT$2
29657
+ loadLimit: LOAD_LIMIT$3
29499
29658
  };
29500
29659
  }
29501
29660
  /** The spec `declare` needs — no sink: declaring a collection projects nothing. */
@@ -30567,7 +30726,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
30567
30726
  */
30568
30727
  var SummaryAnswerSchema = require_clip_model_registry.object({ summary: require_clip_model_registry.string() });
30569
30728
  /** `HH:MM` in the host timezone — the two numbers a frame label needs. */
30570
- function clockOf$3(atMs) {
30729
+ function clockOf$4(atMs) {
30571
30730
  const d = new Date(atMs);
30572
30731
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
30573
30732
  }
@@ -30590,7 +30749,7 @@ function buildSummaryAiPrompt(input, frames) {
30590
30749
  const roster = frames.map((f, i) => {
30591
30750
  const location = plainLabel(f.location ?? "");
30592
30751
  const where = location.length > 0 ? ` (${location})` : "";
30593
- return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$3(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
30752
+ return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
30594
30753
  }).join("\n");
30595
30754
  const described = /* @__PURE__ */ new Map();
30596
30755
  for (const f of frames) {
@@ -31193,7 +31352,7 @@ var NO_DETECTIONS$1 = {
31193
31352
  * has to check is a key somebody can get wrong. */
31194
31353
  var BODY_SEPARATOR$1 = " · ";
31195
31354
  /** `HH:MM` in the host timezone. */
31196
- function clockOf$2(atMs) {
31355
+ function clockOf$3(atMs) {
31197
31356
  const d = new Date(atMs);
31198
31357
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
31199
31358
  }
@@ -31207,8 +31366,8 @@ function clockOf$2(atMs) {
31207
31366
  function summaryTemplateVars(input) {
31208
31367
  return {
31209
31368
  rule: input.ruleName,
31210
- from: clockOf$2(input.window.startMs),
31211
- to: clockOf$2(input.window.endMs),
31369
+ from: clockOf$3(input.window.startMs),
31370
+ to: clockOf$3(input.window.endMs),
31212
31371
  time: ncClockText(input.texts.language, input.generatedAt),
31213
31372
  events: String(input.tileCount),
31214
31373
  cameras: String(input.cameraCount),
@@ -31252,7 +31411,7 @@ function composeSummaryBody(input) {
31252
31411
  * string, for the reason the timelapse learned: a hand-rolled `n === 1` shipped
31253
31412
  * "1 intervalli ripresi" to a live phone.
31254
31413
  */
31255
- function derivedBody$1(input, vars) {
31414
+ function derivedBody$2(input, vars) {
31256
31415
  const texts = input.texts;
31257
31416
  const parts = [texts.text({
31258
31417
  key: "summary.body.window",
@@ -31287,7 +31446,7 @@ function derivedBody$1(input, vars) {
31287
31446
  }
31288
31447
  /** Per-target params with the derived `clickUrl` filled in where absent. A
31289
31448
  * value the operator set on the target WINS — he chose it. */
31290
- function paramsFor$1(target, clickUrl) {
31449
+ function paramsFor$2(target, clickUrl) {
31291
31450
  if (clickUrl === void 0) return target.params;
31292
31451
  if (target.params === void 0) return { clickUrl };
31293
31452
  if (typeof target.params["clickUrl"] === "string") return target.params;
@@ -31303,11 +31462,11 @@ function summaryRecordId(ruleId, windowEndMs) {
31303
31462
  /** The caption burned onto the mosaic's title band, with the same `{{var}}`
31304
31463
  * vocabulary the title and body use. `''` is a real answer ("no caption"). */
31305
31464
  function summaryCaptionText(input) {
31306
- if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$2(input.window.startMs)}→${clockOf$2(input.window.endMs)}`;
31465
+ if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
31307
31466
  return renderTemplate(input.captionText, {
31308
31467
  rule: input.ruleName,
31309
- from: clockOf$2(input.window.startMs),
31310
- to: clockOf$2(input.window.endMs)
31468
+ from: clockOf$3(input.window.startMs),
31469
+ to: clockOf$3(input.window.endMs)
31311
31470
  }) ?? "";
31312
31471
  }
31313
31472
  /**
@@ -31325,7 +31484,7 @@ function buildSummaryOutboxInputs(input) {
31325
31484
  const body = composeSummaryBody({
31326
31485
  aiText: aiSentence(input),
31327
31486
  renderedTemplateBody: renderTemplate(input.template?.body, vars),
31328
- derivedBody: derivedBody$1(input, vars),
31487
+ derivedBody: derivedBody$2(input, vars),
31329
31488
  templateNamesAi: templatePlaceholders(input.template?.body ?? "").includes("ai")
31330
31489
  });
31331
31490
  const recordId = summaryRecordId(input.ruleId, input.window.endMs);
@@ -31344,7 +31503,7 @@ function buildSummaryOutboxInputs(input) {
31344
31503
  },
31345
31504
  media: "none",
31346
31505
  ...artifacts.length > 0 ? { artifacts } : {},
31347
- ...paramsFor$1(target, input.clickUrl) !== void 0 ? { params: paramsFor$1(target, input.clickUrl) } : {},
31506
+ ...paramsFor$2(target, input.clickUrl) !== void 0 ? { params: paramsFor$2(target, input.clickUrl) } : {},
31348
31507
  subject: {
31349
31508
  deviceId: input.primaryDeviceId,
31350
31509
  className: "summary",
@@ -32239,7 +32398,7 @@ var NcSummaryProducer = class {
32239
32398
  purpose: "summary",
32240
32399
  tiles: tiles.map(({ candidate, jpeg }) => ({
32241
32400
  jpeg,
32242
- ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$1(candidate.firstSeen)} · ${candidate.className}` } : {},
32401
+ ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
32243
32402
  deviceId: candidate.deviceId,
32244
32403
  sourceId: candidate.trackId
32245
32404
  })),
@@ -32462,7 +32621,7 @@ var NcSummaryProducer = class {
32462
32621
  }
32463
32622
  };
32464
32623
  /** `HH:MM` in the host timezone — the two numbers a tile label needs. */
32465
- function clockOf$1(atMs) {
32624
+ function clockOf$2(atMs) {
32466
32625
  const d = new Date(atMs);
32467
32626
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32468
32627
  }
@@ -32538,7 +32697,7 @@ var NC_SUMMARY_RULES_INDEXES = [{
32538
32697
  columns: ["enabled"]
32539
32698
  }];
32540
32699
  /** The rule set is operator-authored and tiny; a generous ceiling. */
32541
- var LOAD_LIMIT$1 = 1e4;
32700
+ var LOAD_LIMIT$2 = 1e4;
32542
32701
  /**
32543
32702
  * The ledger contract for the digest rule set.
32544
32703
  *
@@ -32568,7 +32727,7 @@ var NC_SUMMARY_RULES_SPEC = {
32568
32727
  const parsed = NcSummaryRuleSchema.safeParse(data["rule"]);
32569
32728
  return parsed.success ? parsed.data : null;
32570
32729
  },
32571
- loadLimit: LOAD_LIMIT$1
32730
+ loadLimit: LOAD_LIMIT$2
32572
32731
  };
32573
32732
  /**
32574
32733
  * The three-way CLEARABLE patch signal, one function per field: `undefined`
@@ -32579,7 +32738,7 @@ var NC_SUMMARY_RULES_SPEC = {
32579
32738
  * cast here would be a cast on the exact path that decides whether a persisted
32580
32739
  * rule keeps a field. Three literal destructurings are three provable ones.
32581
32740
  */
32582
- function applyTemplatePatch$1(merged, template) {
32741
+ function applyTemplatePatch$2(merged, template) {
32583
32742
  if (template === void 0) return merged;
32584
32743
  if (template !== null) return {
32585
32744
  ...merged,
@@ -32725,7 +32884,7 @@ var NcSummaryStore = class {
32725
32884
  createdAt: existing.createdAt,
32726
32885
  updatedAt: this.now()
32727
32886
  };
32728
- return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$1(merged, template), filters), ai));
32887
+ return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$2(merged, template), filters), ai));
32729
32888
  }
32730
32889
  async setEnabled(ruleId, enabled) {
32731
32890
  return this.update(ruleId, { enabled });
@@ -32777,6 +32936,408 @@ var NcSummaryStore = class {
32777
32936
  }
32778
32937
  };
32779
32938
  //#endregion
32939
+ //#region src/notification-center/calendar-events/calendar-events-store.ts
32940
+ /**
32941
+ * Durable calendar-events rule set — same DurableLedger mechanics as
32942
+ * Summaries, with a per-rule `generatedAt` stamp (one fire per schedule window).
32943
+ */
32944
+ /**
32945
+ * @durable class=config owner=notification-center
32946
+ * write="an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated"
32947
+ * retention="none — a row goes only when the operator deletes the rule."
32948
+ */
32949
+ var NC_CALENDAR_EVENTS_RULES_COLLECTION = "notification-center:calendar-events-rules";
32950
+ var NC_CALENDAR_EVENTS_RULES_COLUMNS = [
32951
+ {
32952
+ name: "id",
32953
+ type: "TEXT",
32954
+ primaryKey: true,
32955
+ notNull: true
32956
+ },
32957
+ {
32958
+ name: "name",
32959
+ type: "TEXT",
32960
+ notNull: true
32961
+ },
32962
+ {
32963
+ name: "enabled",
32964
+ type: "BOOLEAN",
32965
+ notNull: true
32966
+ },
32967
+ {
32968
+ name: "updatedAt",
32969
+ type: "INTEGER",
32970
+ notNull: true
32971
+ },
32972
+ {
32973
+ name: "rule",
32974
+ type: "JSON",
32975
+ notNull: true
32976
+ }
32977
+ ];
32978
+ var NC_CALENDAR_EVENTS_RULES_INDEXES = [{
32979
+ name: "idx_nc_calendar_events_rules_enabled",
32980
+ columns: ["enabled"]
32981
+ }];
32982
+ var LOAD_LIMIT$1 = 1e4;
32983
+ var NC_CALENDAR_EVENTS_RULES_SPEC = {
32984
+ collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
32985
+ columns: [...NC_CALENDAR_EVENTS_RULES_COLUMNS],
32986
+ indexes: [...NC_CALENDAR_EVENTS_RULES_INDEXES],
32987
+ writeMode: "write-through",
32988
+ keyOf: (rule) => rule.id,
32989
+ toValue: (rule) => ({
32990
+ name: rule.name,
32991
+ enabled: rule.enabled,
32992
+ updatedAt: rule.updatedAt,
32993
+ rule
32994
+ }),
32995
+ fromRecord: (_key, data) => {
32996
+ const parsed = NcCalendarEventsRuleSchema.safeParse(data["rule"]);
32997
+ return parsed.success ? parsed.data : null;
32998
+ },
32999
+ loadLimit: LOAD_LIMIT$1
33000
+ };
33001
+ function applyTemplatePatch$1(merged, template) {
33002
+ if (template === void 0) return merged;
33003
+ if (template !== null) return {
33004
+ ...merged,
33005
+ template
33006
+ };
33007
+ const { template: _cleared, ...rest } = merged;
33008
+ return rest;
33009
+ }
33010
+ function applyOptionalStringList(merged, key, value) {
33011
+ if (value === void 0) return merged;
33012
+ if (value !== null) return {
33013
+ ...merged,
33014
+ [key]: [...value]
33015
+ };
33016
+ const { [key]: _cleared, ...rest } = merged;
33017
+ return rest;
33018
+ }
33019
+ var NcCalendarEventsStore = class {
33020
+ ledger;
33021
+ store;
33022
+ logger;
33023
+ now;
33024
+ newId;
33025
+ constructor(deps) {
33026
+ this.store = deps.store;
33027
+ this.ledger = new DurableLedger({
33028
+ spec: NC_CALENDAR_EVENTS_RULES_SPEC,
33029
+ store: deps.store,
33030
+ logger: deps.logger
33031
+ });
33032
+ this.logger = deps.logger;
33033
+ this.now = deps.now ?? (() => Date.now());
33034
+ this.newId = deps.newId ?? (() => (0, node_crypto.randomUUID)());
33035
+ }
33036
+ static async declare(store) {
33037
+ await DurableLedger.declare(store, NC_CALENDAR_EVENTS_RULES_SPEC);
33038
+ }
33039
+ async load() {
33040
+ await this.ledger.load();
33041
+ this.logger.debug("calendar-events rules loaded", { meta: { rules: this.ledger.size } });
33042
+ }
33043
+ list() {
33044
+ return this.ledger.snapshot().toSorted((a, b) => b.updatedAt - a.updatedAt);
33045
+ }
33046
+ listEnabled() {
33047
+ return this.list().filter((r) => r.enabled);
33048
+ }
33049
+ get(ruleId) {
33050
+ return this.ledger.get(ruleId) ?? null;
33051
+ }
33052
+ listForOwner(userId) {
33053
+ return this.list().filter((r) => r.ownerUserId === void 0 || r.ownerUserId === userId);
33054
+ }
33055
+ async create(input, createdBy, ownerUserId) {
33056
+ const now = this.now();
33057
+ const rule = NcCalendarEventsRuleSchema.parse({
33058
+ ...NcCalendarEventsRuleInputSchema.parse(input),
33059
+ id: this.newId(),
33060
+ ...ownerUserId !== void 0 ? { ownerUserId } : {},
33061
+ createdBy,
33062
+ createdAt: now,
33063
+ updatedAt: now
33064
+ });
33065
+ await this.ledger.put(rule);
33066
+ return rule;
33067
+ }
33068
+ async update(ruleId, patch) {
33069
+ const existing = this.ledger.get(ruleId);
33070
+ if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
33071
+ const { template, calendars, calendarsExclude, ...rest } = patch;
33072
+ const merged = {
33073
+ ...existing,
33074
+ ...rest,
33075
+ id: existing.id,
33076
+ ...existing.ownerUserId !== void 0 ? { ownerUserId: existing.ownerUserId } : {},
33077
+ ...existing.generatedAt !== void 0 ? { generatedAt: existing.generatedAt } : {},
33078
+ createdBy: existing.createdBy,
33079
+ createdAt: existing.createdAt,
33080
+ updatedAt: this.now()
33081
+ };
33082
+ return this.write(applyOptionalStringList(applyOptionalStringList(applyTemplatePatch$1(merged, template), "calendars", calendars), "calendarsExclude", calendarsExclude));
33083
+ }
33084
+ async setEnabled(ruleId, enabled) {
33085
+ return this.update(ruleId, { enabled });
33086
+ }
33087
+ async markGenerated(ruleId, windowEndMs) {
33088
+ const existing = this.ledger.get(ruleId);
33089
+ if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
33090
+ return this.write({
33091
+ ...existing,
33092
+ generatedAt: Math.max(existing.generatedAt ?? 0, windowEndMs)
33093
+ });
33094
+ }
33095
+ /**
33096
+ * Idempotent delete — unknown ids are a no-op in RAM, still attempted on disk.
33097
+ *
33098
+ * Deliberately NOT {@link DurableLedger.forget}: that swallows a failed
33099
+ * durable delete. Mirror eviction first; durable half re-throws on failure
33100
+ * (same shape as Summaries).
33101
+ */
33102
+ async delete(ruleId) {
33103
+ this.ledger.evict(ruleId);
33104
+ try {
33105
+ await this.store.delete.mutate({
33106
+ collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
33107
+ key: ruleId
33108
+ });
33109
+ } catch (err) {
33110
+ this.logger.warn("calendar-events rule durable delete failed", { meta: {
33111
+ ruleId,
33112
+ error: err instanceof Error ? err.message : String(err)
33113
+ } });
33114
+ throw err instanceof Error ? err : new Error(String(err));
33115
+ }
33116
+ }
33117
+ async write(rule) {
33118
+ const validated = NcCalendarEventsRuleSchema.parse(rule);
33119
+ await this.ledger.put(validated);
33120
+ return validated;
33121
+ }
33122
+ };
33123
+ //#endregion
33124
+ //#region src/notification-center/calendar-events/calendar-events-delivery.ts
33125
+ function calendarEventsRecordId(ruleId, windowEndMs) {
33126
+ return `calendar-events:${ruleId}:${windowEndMs}`;
33127
+ }
33128
+ /** `YYYY-MM-DD HH:MM` in the host timezone. */
33129
+ function clockOf$1(atMs) {
33130
+ const d = new Date(atMs);
33131
+ 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")}`}`;
33132
+ }
33133
+ function formatEventLine(event) {
33134
+ const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
33135
+ const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
33136
+ return `• ${when} ${event.title}${where}`;
33137
+ }
33138
+ function derivedBody$1(input) {
33139
+ if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
33140
+ const lines = input.events.slice(0, 40).map(formatEventLine);
33141
+ const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
33142
+ return lines.join("\n") + extra;
33143
+ }
33144
+ function templateVars(input) {
33145
+ return {
33146
+ rule: input.ruleName,
33147
+ count: String(input.events.length),
33148
+ horizonDays: String(input.horizonDays),
33149
+ from: clockOf$1(input.eventWindow.startMs),
33150
+ to: clockOf$1(input.eventWindow.endMs),
33151
+ events: derivedBody$1(input),
33152
+ calendars: String(input.calendarIds.length)
33153
+ };
33154
+ }
33155
+ function paramsFor$1(target) {
33156
+ const params = target.params;
33157
+ if (params === void 0) return void 0;
33158
+ const out = {};
33159
+ for (const [k, v] of Object.entries(params)) if (typeof v === "string") out[k] = v;
33160
+ return Object.keys(out).length > 0 ? out : void 0;
33161
+ }
33162
+ /**
33163
+ * One outbox row per enabled target. Ready for `NcOutbox.enqueue`.
33164
+ */
33165
+ function buildCalendarEventsOutboxInputs(input) {
33166
+ const vars = templateVars(input);
33167
+ const optedOut = new Set(input.disabledTargetIds ?? []);
33168
+ const title = renderTemplate(input.template?.title, vars) ?? `${input.ruleName}: ${String(input.events.length)} upcoming`;
33169
+ const body = renderTemplate(input.template?.body, vars) ?? derivedBody$1(input);
33170
+ const recordId = calendarEventsRecordId(input.ruleId, input.scheduleWindow.endMs);
33171
+ return input.targets.filter((t) => !optedOut.has(t.targetId)).map((target) => {
33172
+ const payload = {
33173
+ ruleName: input.ruleName,
33174
+ delivery: "calendar-events",
33175
+ priority: input.priority,
33176
+ template: {
33177
+ title,
33178
+ body
33179
+ },
33180
+ media: "none",
33181
+ ...paramsFor$1(target) !== void 0 ? { params: paramsFor$1(target) } : {},
33182
+ subject: {
33183
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
33184
+ className: "calendar-events",
33185
+ zones: [],
33186
+ timestamp: input.scheduleWindow.endMs
33187
+ }
33188
+ };
33189
+ return {
33190
+ ruleId: input.ruleId,
33191
+ targetId: target.targetId,
33192
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
33193
+ recordKind: "calendar-events",
33194
+ recordId,
33195
+ payload
33196
+ };
33197
+ });
33198
+ }
33199
+ //#endregion
33200
+ //#region src/notification-center/calendar-events/calendar-events-selection.ts
33201
+ /**
33202
+ * Expand a rule's calendar scope against the known calendar ids.
33203
+ *
33204
+ * - All (both lists empty/absent) → every known id
33205
+ * - Only → the include list, intersected with known (unknown ids dropped)
33206
+ * - Except → known minus the exclude list
33207
+ *
33208
+ * An empty known set always yields empty — there is nothing to notify about.
33209
+ */
33210
+ function expandCalendarScope(scope, knownIds) {
33211
+ const known = [...knownIds];
33212
+ if (known.length === 0) return [];
33213
+ const include = scope?.calendars ?? [];
33214
+ const exclude = scope?.calendarsExclude ?? [];
33215
+ if (include.length > 0) {
33216
+ const wanted = new Set(include);
33217
+ return known.filter((id) => wanted.has(id));
33218
+ }
33219
+ if (exclude.length > 0) {
33220
+ const banned = new Set(exclude);
33221
+ return known.filter((id) => !banned.has(id));
33222
+ }
33223
+ return known;
33224
+ }
33225
+ //#endregion
33226
+ //#region src/notification-center/calendar-events/calendar-events-producer.ts
33227
+ /** How far back a closed schedule window may still be owed. */
33228
+ var CALENDAR_EVENTS_LOOKBACK_MS = 2160 * 60 * 1e3;
33229
+ var NcCalendarEventsProducer = class {
33230
+ deps;
33231
+ inFlight = false;
33232
+ now;
33233
+ constructor(deps) {
33234
+ this.deps = deps;
33235
+ this.now = deps.now ?? (() => Date.now());
33236
+ }
33237
+ async tick() {
33238
+ if (this.inFlight) return {
33239
+ examined: 0,
33240
+ sent: 0,
33241
+ empty: 0,
33242
+ skipped: 0,
33243
+ failed: 0
33244
+ };
33245
+ this.inFlight = true;
33246
+ const report = {
33247
+ examined: 0,
33248
+ sent: 0,
33249
+ empty: 0,
33250
+ skipped: 0,
33251
+ failed: 0
33252
+ };
33253
+ try {
33254
+ const rules = this.deps.store.listEnabled();
33255
+ for (const rule of rules) {
33256
+ report.examined += 1;
33257
+ try {
33258
+ const outcome = await this.runRule(rule);
33259
+ if (outcome === "sent") report.sent += 1;
33260
+ else if (outcome === "empty") report.empty += 1;
33261
+ else report.skipped += 1;
33262
+ } catch (err) {
33263
+ report.failed += 1;
33264
+ this.deps.logger.warn("calendar-events rule failed", { meta: {
33265
+ ruleId: rule.id,
33266
+ error: err instanceof Error ? err.message : String(err)
33267
+ } });
33268
+ }
33269
+ }
33270
+ return report;
33271
+ } finally {
33272
+ this.inFlight = false;
33273
+ }
33274
+ }
33275
+ async runRule(rule) {
33276
+ const nowMs = this.now();
33277
+ const scheduleWindow = findLastClosedWindow({
33278
+ schedule: rule.schedule,
33279
+ nowMs,
33280
+ lookbackMs: CALENDAR_EVENTS_LOOKBACK_MS
33281
+ });
33282
+ if (scheduleWindow === null) return "skipped";
33283
+ if ((rule.generatedAt ?? 0) >= scheduleWindow.endMs) return "skipped";
33284
+ const known = await this.deps.calendars.listCalendars();
33285
+ const calendarIds = expandCalendarScope({
33286
+ calendars: rule.calendars,
33287
+ calendarsExclude: rule.calendarsExclude
33288
+ }, known.map((c) => c.id));
33289
+ if (calendarIds.length === 0) {
33290
+ this.deps.logger.info("calendar-events: no calendars in scope", { meta: { ruleId: rule.id } });
33291
+ if (!rule.deliverEmpty) {
33292
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33293
+ return "empty";
33294
+ }
33295
+ }
33296
+ const eventWindow = calendarEventsWindow(nowMs, rule.horizonDays);
33297
+ const events = calendarIds.length === 0 ? [] : await this.deps.calendars.listEvents({
33298
+ calendarIds,
33299
+ startMs: eventWindow.startMs,
33300
+ endMs: eventWindow.endMs
33301
+ });
33302
+ if (events.length === 0 && !rule.deliverEmpty) {
33303
+ this.deps.logger.info("calendar-events: empty horizon, not delivering", { meta: {
33304
+ ruleId: rule.id,
33305
+ horizonDays: eventWindow.horizonDays
33306
+ } });
33307
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33308
+ return "empty";
33309
+ }
33310
+ const sorted = [...events].toSorted((a, b) => a.startMs - b.startMs);
33311
+ await this.enqueue(rule, scheduleWindow, eventWindow, calendarIds, sorted);
33312
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33313
+ return "sent";
33314
+ }
33315
+ async enqueue(rule, scheduleWindow, eventWindow, calendarIds, events) {
33316
+ const inputs = buildCalendarEventsOutboxInputs({
33317
+ ruleId: rule.id,
33318
+ ruleName: rule.name,
33319
+ scheduleWindow,
33320
+ eventWindow: {
33321
+ startMs: eventWindow.startMs,
33322
+ endMs: eventWindow.endMs
33323
+ },
33324
+ horizonDays: eventWindow.horizonDays,
33325
+ events,
33326
+ calendarIds,
33327
+ targets: rule.targets,
33328
+ priority: rule.priority,
33329
+ ...rule.template !== void 0 ? { template: rule.template } : {}
33330
+ });
33331
+ for (const input of inputs) await this.deps.outbox.enqueue(input);
33332
+ this.deps.logger.info("calendar-events delivered", { meta: {
33333
+ ruleId: rule.id,
33334
+ events: events.length,
33335
+ targets: inputs.length,
33336
+ windowEndMs: scheduleWindow.endMs
33337
+ } });
33338
+ }
33339
+ };
33340
+ //#endregion
32780
33341
  //#region src/notification-center/text-catalog-editor.ts
32781
33342
  var NcTextCatalogEditor = class {
32782
33343
  store;
@@ -35860,7 +36421,7 @@ function packagePhaseOf(ev) {
35860
36421
  */
35861
36422
  function historyRecordKind(kind) {
35862
36423
  if (kind === "audio-event" || kind === "audio-window") return "object-event";
35863
- if (kind === "timelapse" || kind === "ai-summary") return "system-event";
36424
+ if (kind === "timelapse" || kind === "ai-summary" || kind === "calendar-events") return "system-event";
35864
36425
  return kind;
35865
36426
  }
35866
36427
  /**
@@ -35875,7 +36436,8 @@ function historyRecordKind(kind) {
35875
36436
  function cooldownSubjectKind(kind) {
35876
36437
  switch (kind) {
35877
36438
  case "timelapse":
35878
- case "ai-summary": return "device-event";
36439
+ case "ai-summary":
36440
+ case "calendar-events": return "device-event";
35879
36441
  default: return kind;
35880
36442
  }
35881
36443
  }
@@ -35975,7 +36537,7 @@ function outboxEntryToHistory(texts, entry) {
35975
36537
  id: entry.id,
35976
36538
  ruleId: entry.ruleId,
35977
36539
  ruleName: entry.payload.ruleName,
35978
- delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" ? "system-event" : entry.payload.delivery,
36540
+ delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" || entry.payload.delivery === "calendar-events" ? "system-event" : entry.payload.delivery,
35979
36541
  targetId: entry.targetId,
35980
36542
  deviceId: entry.deviceId,
35981
36543
  recordKind: historyRecordKind(entry.recordKind),
@@ -36185,6 +36747,9 @@ var NotificationCenter = class {
36185
36747
  */
36186
36748
  summaryRules;
36187
36749
  summaryProducer = null;
36750
+ /** Calendar-events digest rules + producer (hub-only producer). */
36751
+ calendarEventsRules;
36752
+ calendarEventsProducer = null;
36188
36753
  /**
36189
36754
  * What the deleted-device / deleted-target prune did, and why — its
36190
36755
  * disable-once guard and the source of both UIs' banner. Loaded on EVERY
@@ -37175,6 +37740,11 @@ var NotificationCenter = class {
37175
37740
  logger: this.logger.child("summary"),
37176
37741
  ...deps.now !== void 0 ? { now: deps.now } : {}
37177
37742
  });
37743
+ this.calendarEventsRules = new NcCalendarEventsStore({
37744
+ store: deps.store,
37745
+ logger: this.logger.child("calendar-events"),
37746
+ ...deps.now !== void 0 ? { now: deps.now } : {}
37747
+ });
37178
37748
  this.pruneLedger = new NcPruneLedger({
37179
37749
  store: deps.store,
37180
37750
  logger: this.logger.child("device-prune")
@@ -37221,6 +37791,14 @@ var NotificationCenter = class {
37221
37791
  zoneOwner: this.zoneOwners.lookup()
37222
37792
  });
37223
37793
  }
37794
+ const calendarsPort = deps.calendars;
37795
+ if (calendarsPort !== void 0) this.calendarEventsProducer = new NcCalendarEventsProducer({
37796
+ store: this.calendarEventsRules,
37797
+ outbox: this.outbox,
37798
+ calendars: calendarsPort,
37799
+ logger: this.logger.child("calendar-events"),
37800
+ now: this.now
37801
+ });
37224
37802
  }
37225
37803
  /**
37226
37804
  * The durable timelapse rule set — exposed for the `nc.*Timelapse*` bridge
@@ -37234,6 +37812,10 @@ var NotificationCenter = class {
37234
37812
  get summaryStore() {
37235
37813
  return this.summaryRules;
37236
37814
  }
37815
+ /** The durable calendar-events rule set — exposed for the `nc.*CalendarEvents*` bridge. */
37816
+ get calendarEventsStore() {
37817
+ return this.calendarEventsRules;
37818
+ }
37237
37819
  /**
37238
37820
  * Produce one digest window for one rule, right now.
37239
37821
  *
@@ -37307,6 +37889,7 @@ var NotificationCenter = class {
37307
37889
  await NcDeviceMuteStore.declare(store);
37308
37890
  await TimelapseStore.declare(store);
37309
37891
  await NcSummaryStore.declare(store);
37892
+ await NcCalendarEventsStore.declare(store);
37310
37893
  await NcRuleAiStore.declare(store);
37311
37894
  await NcTextCatalogStore.declare(store);
37312
37895
  await NcArtifactIndex.declare(store);
@@ -40929,11 +41512,17 @@ var NotificationCenter = class {
40929
41512
  await this.rules.load();
40930
41513
  this.noteInertGroupedRules();
40931
41514
  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
- });
41515
+ if (this.evaluationActive) {
41516
+ this.stateSummary.tick().catch((err) => {
41517
+ this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
41518
+ });
41519
+ this.calendarEventsProducer?.tick().catch((err) => {
41520
+ this.logger.warn("calendar-events tick failed", { meta: { error: String(err) } });
41521
+ });
41522
+ }
40935
41523
  await this.timelapseRules.load();
40936
41524
  await this.summaryRules.load();
41525
+ await this.calendarEventsRules.load();
40937
41526
  await this.ruleAi.load();
40938
41527
  await this.reloadTexts();
40939
41528
  this.refreshOccupancyWatch();
@@ -104816,6 +105405,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
104816
105405
  store: center.summaryStore,
104817
105406
  runNow: (input) => center.runSummaryNow(input)
104818
105407
  },
105408
+ calendarEvents: { store: center.calendarEventsStore },
104819
105409
  texts: center.textCatalogEditor,
104820
105410
  previewTexts: center.textCatalog,
104821
105411
  ...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
@@ -106003,7 +106593,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
106003
106593
  })).tracks;
106004
106594
  },
106005
106595
  ...this.isPostProcessingNode ? { timelapse: this.buildTimelapsePorts(api, stores) } : {},
106006
- ...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {}
106596
+ ...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {},
106597
+ ...this.isPostProcessingNode ? { calendars: {
106598
+ listCalendars: async () => {
106599
+ return (await api.calendars.listCalendars.query()).map((c) => ({ id: c.id }));
106600
+ },
106601
+ listEvents: async (input) => api.calendars.listEvents.query({
106602
+ calendarIds: [...input.calendarIds],
106603
+ startMs: input.startMs,
106604
+ endMs: input.endMs
106605
+ })
106606
+ } } : {}
106007
106607
  });
106008
106608
  this.wireAlarmPanel(this.notificationCenter);
106009
106609
  await this.serveNcActionPlane(this.notificationCenter);