@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
@@ -1,4 +1,4 @@
1
- import { $ as NC_ALARM_SYSTEM_EVENT_KINDS, $n as isVectorIndexDimConflict, $r as boolean, $t as SCENE_DEFAULT_UNCOVERED_POLICY, A as EMBEDDING_REQUIREMENTS_FAILED, An as describeSystemEventDeliveryProblem, Ar as templateCarriesSummary, At as NcScheduleSchema, B as ListTrackRelationsInputSchema, Bn as failureContributionCapability, Br as BaseAddon, Bt as OPS_LOG_DEFAULT_LIMIT, C as DEFAULT_EVENT_COLOR, Cn as assertTimelapseCadences, Cr as storageOccupancyCapability, Ct as NcConditionDescriptorSchema, D as DeclaredDevices, Dn as consumableLowPercentOf, Dr as systemEventFilterApplies, Dt as NcRuleSchema, E as DETECTION_PIPELINE_CAP_NAME, En as buildEventKindDescriptor, Er as systemEventFamilyProblems, Et as NcRulePatchSchema, F as EXPORT_DENSE_MAX_RANGES, Fr as canonicalHash, Ft as NcSystemEventKindSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as isDetectionMacroClass, Gr as hydrateSchema, Gt as RebuildObjectEmbeddingsRefusalSchema, H as LockStateSchema, Hn as gifFromMp4Args, Hr as createEvent, I as FIRST_LEVEL_MACRO_CLASSES, Ir as evaluateSensorEdge, It as NcTaxonomySchema, J as MOTION_CLOSE_AFTER_MS, Jn as isMediaOwnerType, Jt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, K as MAX_TRACK_PURITIES, Kn as isEventOwnerType, Kr as isDeviceScopedCap, Kt as RetrainStatusSchema, L as FULL_IMAGE_BBOX, Ln as encodeVectorBase64, Lr as isSourceCap, Lt as NcTemplatePreviewInputSchema, M as EVENT_DENSITY_BATCH_MAX, Mr as vectorDimFromBase64, Mt as NcSnoozeSchema, N as EVENT_KIND_BY_CAP, Nr as zoneAnalyticsCapability, Nt as NcSnoozeSuppressedSchema, O as DetectionSourceSchema, On as cosineSimilarity$1, Or as systemEventSummaryScheduleOf, Ot as NcRuleTargetSchema, P as EVENT_OWNER_TYPES, Pn as embeddingRefusalOf, Pr as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, Pt as NcStateSummarySectionIdSchema, Q as NC_ALARM_DELAY_MAX_SEC, Qn as isSystemEventSummaryRule, Qr as array, Qt as SCENE_DEFAULT_QUIET_SECONDS, R as FailureCounters, Rn as evictionPolicyOfLocation, Rr as DeviceType, Rt as NcTemplatePreviewSchema, S as CoverStateSchema, Sn as applySecureOnArmEdit, Sr as servedTemplateVars, St as NcAlarmSettingsPatchSchema, T as DETECTION_MACRO_CLASSES, Tn as audioModeOf, Tr as systemEventDeliveryProblems, Tt as NcRuleInputSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as isAlarmControllableCap, Ur as customAction, Ut as RECORDING_EXPORT_MAX_READ_BYTES, V as ListTrackRelationsResultSchema, Vn as gifDeriveSpeed, Vr as CamProfileSchema, Vt as OpsLogEntrySchema, W as MAX_BIRTH_DECISION_RECORDS, Wn as isAudioClassName, Wr as defineCustomActions, Wt as RECORDING_SOURCE_CAMSTACK_ADDON, X as MotionTriggeredBySchema, Xn as isScheduleActive, Xr as ZodObject, Xt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, Y as MediaFileKindEnum, Yn as isRetiredClearKey, Yr as sleep$1, Yt as SCENE_DEFAULT_ANCHOR_THRESHOLD, Z as NC_ALARM_DELAY_FIELDS, Zn as isSystemDelivery, Zr as _enum, Zt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, _ as BaseDevice, _n as alarmDeviceSecureOnArm, _r as ruleGroupingApplies, _t as NC_TAXONOMY, a as CLIP_EMBEDDING_STEP_ID, ai as partialRecord, an as SetTrackRelationInputSchema, at as NC_CLIP_RETENTION_MAX_SEC, b as ClearTrackRelationInputSchema, bn as applyModeDelaysEdits, br as secureActuationFor, bt as NcAlarmSensorsInputSchema, c as resolveClusterModelPin, ci as string, cn as TimelapseRuleInputSchema, cr as readDeviceStateFrom, ct as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as union, dn as TrackPuritySchema, dr as resolveClipSearchMinScore, dt as NC_GROUPING_MAX_SEC, ei as discriminatedUnion, en as SCENE_DIVERGED, er as kebabToCamel, et as NC_ALARM_TRIGGERED_MAX_SEC, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as unknown, fn as TrackRelationKindSchema, fr as resolveLocationMode, ft as NC_GROUPING_MIN_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gn as addonWidgetsSourceCapability, gr as ruleDeviceCompatCapQueries, gt as NC_TANK_SYSTEM_EVENT_KINDS, h as ArchivedDebugNoteSchema, hn as TrackSourceSchema, hr as ruleBelongsToAlarm$1, ht as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, ii as object, in as SetTrackPurityResultSchema, ir as occupancyScope, it as NC_CLIP_PRE_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as describeSystemEventFamilyProblem, jr as templateVarsFor, jt as NcSnoozeInputSchema, k as EMBEDDING_MODEL_FAILED, kn as deriveRecordingMode, kr as systemEventSummarySplitOf, kt as NcRuleUpdateInputSchema, l as APPEARANCE_BIN_COUNT, li as templateLiteral, ln as TimelapseRulePatchSchema, lr as readMotionTriggeredBy, lt as NC_DEFAULT_SNOOZE_MINUTES, m as AlarmArmModeSchema, mn as TrackSideSchema, mt as NC_SNOOZE_MAX_MINUTES, ni as looseObject, nn as SceneMonitorSchema, nr as memoizePinnedFfmpeg, nt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as record, on as TIMELAPSE_DENSE_FLOOR_SEC, or as pipelineAnalyticsCapability, ot as NC_CONDITION_CATALOG, p as AUDIO_MACRO_LABELS, pi as EventCategory, pn as TrackRelationSchema, pr as resolveModeDelays, pt as NC_SENSOR_TRANSITION_KINDS, q as MAX_TRACK_RELATIONS, qn as isKnownTemplateVar, qr as nodePin, qt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, ri as number, rn as SetTrackPurityInputSchema, rr as notificationRulesCapability, rt as NC_CLIP_POST_ROLL_DEFAULT_SEC, s as readClusterModelPinStrict, si as strictObject, sn as TRACK_RELATIONS_DEFAULT_LIMIT, sr as plateGalleryCapability, st as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, t as BUILTIN_CLIP_MODELS, ti as literal, tn as SCENE_RESET_RECAPTURES, tr as mayWriteToLocation, tt as NC_AUDIO_CONFIRM_HITS_DEFAULT, u as APPEARANCE_BLOCKS, ui as tuple, un as TimelapseRuleSchema, ur as readTimelapseGeneratedAt, ut as NC_GROUPING_DEFAULT_SEC, v as BirthDecisionRecordSchema, vn as alarmPanelCapability, vr as ruleKindOf, vt as NC_TEMPLATE_VARS, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as audioMetricsCapability, wr as subKindsOf, wt as NcConditionsSchema, x as ClearTrackRelationResultSchema, xn as applyNonBlockingEdit, xr as sensorRaisedOf, xt as NcAlarmSensorsSchema, y as COCO_TO_MACRO, yn as applyBlockingEdit, yr as sceneMonitorCapability, yt as NcAlarmConfigSchema, z as LabelAttributionSchema, zn as faceGalleryCapability, zr as errMsg, zt as NcTemplateVarDescriptorSchema } from "../clip-model-registry-C-IEAIQQ.mjs";
1
+ import { $ as NC_ALARM_DELAY_MAX_SEC, $n as isEventOwnerType, $r as defineCustomActions, $t as SCENE_DEFAULT_QUIET_SECONDS, A as EMBEDDING_REQUIREMENTS_FAILED, An as consumableLowPercentOf, Ar as servedTemplateVars, At as NcRuleUpdateInputSchema, B as ListTrackRelationsInputSchema, Br as templateVarsFor, Bt as NcTemplateVarDescriptorSchema, C as DEFAULT_EVENT_COLOR, Cn as applyNonBlockingEdit, Cr as ruleBelongsToAlarm$1, Ct as NcAlarmSettingsPatchSchema, D as DeclaredDevices, Dn as audioModeOf, Dr as sceneMonitorCapability, Dt as NcRulePatchSchema, E as DETECTION_PIPELINE_CAP_NAME, En as audioMetricsCapability, Er as ruleKindOf, Et as NcRuleInputSchema, F as EXPORT_DENSE_MAX_RANGES, Fn as describeSystemEventDeliveryProblem, Fr as systemEventFamilyProblems, Ft as NcStateSummarySectionIdSchema, G as MAX_BIRTH_LATENCY_PROXY_MS, Gn as faceGalleryCapability, Gr as evaluateSensorEdge, Gt as RECORDING_SOURCE_CAMSTACK_ADDON, H as LockStateSchema, Hn as encodeVectorBase64, Hr as zoneAnalyticsCapability, Ht as OpsLogEntrySchema, I as FIRST_LEVEL_MACRO_CLASSES, In as describeSystemEventFamilyProblem, Ir as systemEventFilterApplies, It as NcSystemEventKindSchema, J as MOTION_CLOSE_AFTER_MS, Jn as gifFromMp4Args, Jr as errMsg, Jt as SCENE_CONFIRM_DEFAULT_MAX_IMAGE_PX, K as MAX_TRACK_PURITIES, Kn as failureContributionCapability, Kr as isSourceCap, Kt as RebuildObjectEmbeddingsRefusalSchema, L as FULL_IMAGE_BBOX, Lr as systemEventSummaryScheduleOf, Lt as NcTaxonomySchema, M as EVENT_DENSITY_BATCH_MAX, Mn as createAlarmProfile, Mr as subKindsOf, Mt as NcSnoozeInputSchema, N as EVENT_KIND_BY_CAP, Nn as deleteAlarmProfile, Nr as syncActiveAlarmProfileSettings, Nt as NcSnoozeSchema, O as DetectionSourceSchema, On as buildEventKindDescriptor, Or as secureActuationFor, Ot as NcRuleSchema, P as EVENT_OWNER_TYPES, Pn as deriveRecordingMode, Pr as systemEventDeliveryProblems, Pt as NcSnoozeSuppressedSchema, Q as NC_ALARM_DELAY_FIELDS, Qn as isDetectionMacroClass, Qr as customAction, Qt as SCENE_DEFAULT_OBSERVATION_SPACING_SEC, R as FailureCounters, Rr as systemEventSummarySplitOf, Rt as NcTemplatePreviewInputSchema, S as CoverStateSchema, Si as EventCategory, Sn as applyModeDelaysEdits, St as NcAlarmSensorsSchema, T as DETECTION_MACRO_CLASSES, Tn as assertTimelapseCadences, Tr as ruleGroupingApplies, Tt as NcConditionsSchema, U as MAX_ARCHIVED_DEBUG_NOTES, Un as ensureAlarmProfiles, Ur as DEFAULT_FIRST_SIGHTING_FRESHNESS_MS, V as ListTrackRelationsResultSchema, Vr as vectorDimFromBase64, Vt as OPS_LOG_DEFAULT_LIMIT, W as MAX_BIRTH_DECISION_RECORDS, Wn as evictionPolicyOfLocation, Wr as canonicalHash, Wt as RECORDING_EXPORT_MAX_READ_BYTES, X as MotionTriggeredBySchema, Xn as isAudioClassName, Xr as CamProfileSchema, Xt as SCENE_DEFAULT_ANCHOR_THRESHOLD, Y as MediaFileKindEnum, Yn as isAlarmControllableCap, Yr as BaseAddon, Yt as SCENE_CONFIRM_DEFAULT_TIMEOUT_MS, Z as NC_ALARM_DEFAULT_PROFILE_ID, Zn as isDefaultAlarmProfileId, Zr as createEvent, Zt as SCENE_DEFAULT_CHECK_INTERVAL_SEC, _ as BaseDevice, _i as string, _n as activateAlarmProfile, _r as readTimelapseGeneratedAt, _t as NC_TANK_SYSTEM_EVENT_KINDS, a as CLIP_EMBEDDING_STEP_ID, ai as ZodObject, an as SetTrackPurityResultSchema, ar as isSystemEventSummaryRule, at as NC_CLIP_PRE_ROLL_DEFAULT_SEC, b as ClearTrackRelationInputSchema, bi as union, bn as alarmPanelCapability, br as resolveLocationMode, bt as NcAlarmConfigSchema, c as resolveClusterModelPin, ci as boolean, cn as TRACK_RELATIONS_DEFAULT_LIMIT, cr as mayWriteToLocation, ct as NC_CONSUMABLE_LOW_PERCENT_DEFAULT, d as APPEARANCE_RESOLVES_AMBIGUITY_DEFAULT, di as looseObject, dn as TimelapseRuleSchema, dr as occupancyScope, dt as NC_GROUPING_DEFAULT_SEC, ei as hydrateSchema, en as SCENE_DEFAULT_UNCOVERED_POLICY, er as isKnownTemplateVar, et as NC_ALARM_SYSTEM_EVENT_KINDS, f as ARCHIVED_DEBUG_NOTES_DEFAULT_LIMIT, fi as number, fn as TrackPuritySchema, ft as NC_GROUPING_MAX_SEC, g as BIRTH_DECISION_DEFAULT_LIMIT, gi as strictObject, gn as TrackSourceSchema, gr as readMotionTriggeredBy, gt as NC_SYSTEM_EVENT_KIND_GROUP_SPECS, h as ArchivedDebugNoteSchema, hi as record, hn as TrackSideSchema, hr as readDeviceStateFrom, ht as NC_SNOOZE_MAX_MINUTES, ii as sleep$1, in as SetTrackPurityInputSchema, ir as isSystemDelivery, it as NC_CLIP_POST_ROLL_DEFAULT_SEC, j as EMBEDDING_REQUIREMENTS_UNAVAILABLE, jn as cosineSimilarity$1, jr as storageOccupancyCapability, jt as NcScheduleSchema, k as EMBEDDING_MODEL_FAILED, kn as cloneAlarmSettings, kr as sensorRaisedOf, kt as NcRuleTargetSchema, l as APPEARANCE_BIN_COUNT, li as discriminatedUnion, ln as TimelapseRuleInputSchema, lr as memoizePinnedFfmpeg, lt as NC_CONSUMABLE_SYSTEM_EVENT_KINDS, m as AlarmArmModeSchema, mi as partialRecord, mn as TrackRelationSchema, mr as plateGalleryCapability, mt as NC_SENSOR_TRANSITION_KINDS, ni as nodePin, nn as SCENE_RESET_RECAPTURES, nr as isRetiredClearKey, nt as NC_AUDIO_CONFIRM_HITS_DEFAULT, o as FACE_EMBEDDING_STEP_ID, oi as _enum, on as SetTrackRelationInputSchema, or as isVectorIndexDimConflict, ot as NC_CLIP_RETENTION_MAX_SEC, p as AUDIO_MACRO_LABELS, pi as object, pn as TrackRelationKindSchema, pr as pipelineAnalyticsCapability, pt as NC_GROUPING_MIN_SEC, q as MAX_TRACK_RELATIONS, qn as gifDeriveSpeed, qr as DeviceType, qt as RetrainStatusSchema, rn as SceneMonitorSchema, rr as isScheduleActive, rt as NC_AUDIO_CONFIRM_WINDOW_SEC_DEFAULT, s as readClusterModelPinStrict, si as array, sn as TIMELAPSE_DENSE_FLOOR_SEC, sr as kebabToCamel, st as NC_CONDITION_CATALOG, t as BUILTIN_CLIP_MODELS, ti as isDeviceScopedCap, tn as SCENE_DIVERGED, tr as isMediaOwnerType, tt as NC_ALARM_TRIGGERED_MAX_SEC, u as APPEARANCE_BLOCKS, ui as literal, un as TimelapseRulePatchSchema, ur as notificationRulesCapability, ut as NC_DEFAULT_SNOOZE_MINUTES, v as BirthDecisionRecordSchema, vi as templateLiteral, vn as addonWidgetsSourceCapability, vr as renameAlarmProfile, vt as NC_TAXONOMY, w as DEFAULT_TIMELAPSE_PREVIEW_TEXT, wn as applySecureOnArmEdit, wr as ruleDeviceCompatCapQueries, wt as NcConditionDescriptorSchema, x as ClearTrackRelationResultSchema, xi as unknown, xn as applyBlockingEdit, xr as resolveModeDelays, xt as NcAlarmSensorsInputSchema, y as COCO_TO_MACRO, yi as tuple, yn as alarmDeviceSecureOnArm, yr as resolveClipSearchMinScore, yt as NC_TEMPLATE_VARS, z as LabelAttributionSchema, zn as embeddingRefusalOf, zr as templateCarriesSummary, zt as NcTemplatePreviewSchema } from "../clip-model-registry-CwK2SLQD.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs$1 from "node:fs";
4
4
  import { createWriteStream, promises } from "node:fs";
@@ -1941,6 +1941,21 @@ var ncAlarmPanelSchema = object({
1941
1941
  exitDelaySec: number().optional(),
1942
1942
  entryDelaySec: number().optional(),
1943
1943
  triggeredDurationSec: number().optional()
1944
+ })).default([]),
1945
+ /**
1946
+ * Active alarm configuration profile id. Empty / absent on a blob older than
1947
+ * profiles → seeded to Default on first read via {@link ensureAlarmProfiles}.
1948
+ */
1949
+ activeProfileId: string().default(NC_ALARM_DEFAULT_PROFILE_ID),
1950
+ /**
1951
+ * Named settings blobs. Loose on purpose — each profile's settings are
1952
+ * re-validated through {@link settingsBlobFromPersisted} so a corrupt entry
1953
+ * drops rather than taking down the panel.
1954
+ */
1955
+ profiles: array(object({
1956
+ id: string(),
1957
+ name: string(),
1958
+ settings: record(string(), unknown())
1944
1959
  })).default([])
1945
1960
  });
1946
1961
  /**
@@ -1963,6 +1978,106 @@ function modeDeviceListFromPersisted(raw) {
1963
1978
  return lists;
1964
1979
  }
1965
1980
  /**
1981
+ * Rebuild a trusted {@link NcAlarmSettings} from a persisted profile blob.
1982
+ * Unknown / corrupt fields fall back to system-safe empties via the flat-key
1983
+ * readers the panel already uses for the live settings.
1984
+ */
1985
+ function settingsBlobFromPersisted(raw) {
1986
+ const exit = typeof raw["exitDelaySec"] === "number" ? raw["exitDelaySec"] : DEFAULT_DELAYS.exitDelaySec;
1987
+ const entry = typeof raw["entryDelaySec"] === "number" ? raw["entryDelaySec"] : DEFAULT_DELAYS.entryDelaySec;
1988
+ const triggered = typeof raw["triggeredDurationSec"] === "number" ? raw["triggeredDurationSec"] : DEFAULT_DELAYS.triggeredDurationSec;
1989
+ const announceTargets = Array.isArray(raw["announceTargets"]) ? raw["announceTargets"].filter((t) => typeof t === "string") : [];
1990
+ const lists = (key) => {
1991
+ const v = raw[key];
1992
+ if (!Array.isArray(v)) return [];
1993
+ const rows = [];
1994
+ for (const item of v) {
1995
+ if (typeof item !== "object" || item === null) continue;
1996
+ const rec = item;
1997
+ if (typeof rec["mode"] !== "string" || !Array.isArray(rec["deviceIds"])) continue;
1998
+ rows.push({
1999
+ mode: rec["mode"],
2000
+ deviceIds: rec["deviceIds"].filter((id) => typeof id === "number")
2001
+ });
2002
+ }
2003
+ return modeDeviceListFromPersisted(rows);
2004
+ };
2005
+ const modeDelaysRaw = Array.isArray(raw["modeDelays"]) ? raw["modeDelays"] : [];
2006
+ const modeDelaysLoose = [];
2007
+ for (const item of modeDelaysRaw) {
2008
+ if (typeof item !== "object" || item === null) continue;
2009
+ const rec = item;
2010
+ if (typeof rec["mode"] !== "string") continue;
2011
+ const entry = { mode: rec["mode"] };
2012
+ if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
2013
+ if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
2014
+ if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
2015
+ modeDelaysLoose.push(entry);
2016
+ }
2017
+ return {
2018
+ exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
2019
+ entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
2020
+ triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
2021
+ announceTargets,
2022
+ nonBlocking: lists("nonBlocking"),
2023
+ blocking: lists("blocking"),
2024
+ secureOnArmOff: lists("secureOnArmOff"),
2025
+ modeDelays: modeDelaysFromPersisted(modeDelaysLoose).entries
2026
+ };
2027
+ }
2028
+ function settingsToPersisted(settings) {
2029
+ return {
2030
+ exitDelaySec: settings.exitDelaySec,
2031
+ entryDelaySec: settings.entryDelaySec,
2032
+ triggeredDurationSec: settings.triggeredDurationSec,
2033
+ announceTargets: [...settings.announceTargets],
2034
+ nonBlocking: settings.nonBlocking.map((l) => ({
2035
+ mode: l.mode,
2036
+ deviceIds: [...l.deviceIds]
2037
+ })),
2038
+ blocking: settings.blocking.map((l) => ({
2039
+ mode: l.mode,
2040
+ deviceIds: [...l.deviceIds]
2041
+ })),
2042
+ secureOnArmOff: settings.secureOnArmOff.map((l) => ({
2043
+ mode: l.mode,
2044
+ deviceIds: [...l.deviceIds]
2045
+ })),
2046
+ modeDelays: settings.modeDelays.map((e) => ({ ...e }))
2047
+ };
2048
+ }
2049
+ function profilesStateFromPersisted(activeId, rows, current) {
2050
+ const profiles = rows.filter((r) => r.id.length > 0 && r.name.trim().length > 0).map((r) => ({
2051
+ id: r.id,
2052
+ name: r.name,
2053
+ settings: settingsBlobFromPersisted(r.settings)
2054
+ }));
2055
+ return ensureAlarmProfiles(profiles.length > 0 ? {
2056
+ activeId,
2057
+ profiles
2058
+ } : null, current);
2059
+ }
2060
+ function profilesToPersisted(state) {
2061
+ return {
2062
+ activeProfileId: state.activeId,
2063
+ profiles: state.profiles.map((p) => ({
2064
+ id: p.id,
2065
+ name: p.name,
2066
+ settings: settingsToPersisted(p.settings)
2067
+ }))
2068
+ };
2069
+ }
2070
+ function profilesViewOf(state) {
2071
+ return {
2072
+ activeId: state.activeId,
2073
+ items: state.profiles.map((p) => ({
2074
+ id: p.id,
2075
+ name: p.name,
2076
+ isDefault: isDefaultAlarmProfileId(p.id)
2077
+ }))
2078
+ };
2079
+ }
2080
+ /**
1966
2081
  * The log line for a trigger the state machine ignored, per state. `trigger`
1967
2082
  * ignores four: disarmed, arming (the exit delay), and pending / triggered
1968
2083
  * (already counting down or going off — a second sensor must not restart it).
@@ -2210,7 +2325,134 @@ var NcAlarmPanelDevice = class extends BaseDevice {
2210
2325
  changed: membershipChanged
2211
2326
  }
2212
2327
  });
2213
- return this.settings();
2328
+ const settled = this.settings();
2329
+ await this.persistProfilesState(syncActiveAlarmProfileSettings(this.readProfilesState(), settled));
2330
+ return settled;
2331
+ }
2332
+ /** Named profiles + which one is active — for {@link NcAlarmConfig.profiles}. */
2333
+ profilesView() {
2334
+ return profilesViewOf(this.readProfilesState());
2335
+ }
2336
+ /**
2337
+ * Create a named profile (system defaults, or clone). Does not activate.
2338
+ * `newId` is assigned by the caller (usually `randomUUID`).
2339
+ */
2340
+ async createProfile(input) {
2341
+ const result = createAlarmProfile(this.readProfilesState(), {
2342
+ name: input.name,
2343
+ id: input.newId,
2344
+ source: input.source
2345
+ });
2346
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2347
+ await this.persistProfilesState(result.state);
2348
+ this.ctx.logger.info("alarm profile created", {
2349
+ tags: { deviceId: this.id },
2350
+ meta: {
2351
+ profileId: input.newId,
2352
+ name: input.name.trim(),
2353
+ source: input.source.kind
2354
+ }
2355
+ });
2356
+ return profilesViewOf(result.state);
2357
+ }
2358
+ async renameProfile(id, name) {
2359
+ const result = renameAlarmProfile(this.readProfilesState(), {
2360
+ id,
2361
+ name
2362
+ });
2363
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2364
+ await this.persistProfilesState(result.state);
2365
+ this.ctx.logger.info("alarm profile renamed", {
2366
+ tags: { deviceId: this.id },
2367
+ meta: {
2368
+ profileId: id,
2369
+ name: name.trim()
2370
+ }
2371
+ });
2372
+ return profilesViewOf(result.state);
2373
+ }
2374
+ /**
2375
+ * Delete a non-default profile. If it was active, Default becomes active and
2376
+ * its settings are applied to the flat keys (arm path).
2377
+ */
2378
+ async deleteProfile(id) {
2379
+ const before = this.readProfilesState();
2380
+ const result = deleteAlarmProfile(before, id);
2381
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2382
+ const switched = before.activeId === id;
2383
+ await this.persistProfilesState(result.state);
2384
+ if (switched) {
2385
+ const def = result.state.profiles.find((p) => p.id === NC_ALARM_DEFAULT_PROFILE_ID);
2386
+ if (def !== void 0) await this.replaceFlatSettings(def.settings);
2387
+ }
2388
+ this.ctx.logger.info("alarm profile deleted", {
2389
+ tags: { deviceId: this.id },
2390
+ meta: {
2391
+ profileId: id,
2392
+ activatedDefault: switched
2393
+ }
2394
+ });
2395
+ return profilesViewOf(result.state);
2396
+ }
2397
+ /**
2398
+ * Make a profile active and apply its blob onto the flat settings the arm
2399
+ * path reads.
2400
+ */
2401
+ async activateProfile(id) {
2402
+ const result = activateAlarmProfile(syncActiveAlarmProfileSettings(this.readProfilesState(), this.settings()), id);
2403
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2404
+ const next = result.state.profiles.find((p) => p.id === id);
2405
+ if (next === void 0) throw this.profilesRefuseError("not-found", id);
2406
+ await this.persistProfilesState(result.state);
2407
+ await this.replaceFlatSettings(next.settings);
2408
+ this.ctx.logger.info("alarm profile activated", {
2409
+ tags: { deviceId: this.id },
2410
+ meta: {
2411
+ profileId: id,
2412
+ name: next.name
2413
+ }
2414
+ });
2415
+ return profilesViewOf(result.state);
2416
+ }
2417
+ readProfilesState() {
2418
+ return profilesStateFromPersisted(this.config.get("activeProfileId"), this.config.get("profiles"), this.settings());
2419
+ }
2420
+ async persistProfilesState(state) {
2421
+ const persisted = profilesToPersisted(state);
2422
+ await Promise.all([this.config.set("activeProfileId", persisted.activeProfileId), this.config.set("profiles", persisted.profiles)]);
2423
+ }
2424
+ /** Write a full settings blob onto the flat keys the arm path reads. */
2425
+ async replaceFlatSettings(settings) {
2426
+ const blob = cloneAlarmSettings(settings);
2427
+ await Promise.all([
2428
+ this.config.set("exitDelaySec", blob.exitDelaySec),
2429
+ this.config.set("entryDelaySec", blob.entryDelaySec),
2430
+ this.config.set("triggeredDurationSec", blob.triggeredDurationSec),
2431
+ this.config.set("announceTargets", [...blob.announceTargets]),
2432
+ this.config.set("nonBlocking", blob.nonBlocking.map((l) => ({
2433
+ mode: l.mode,
2434
+ deviceIds: [...l.deviceIds]
2435
+ }))),
2436
+ this.config.set("blocking", blob.blocking.map((l) => ({
2437
+ mode: l.mode,
2438
+ deviceIds: [...l.deviceIds]
2439
+ }))),
2440
+ this.config.set("secureOnArmOff", blob.secureOnArmOff.map((l) => ({
2441
+ mode: l.mode,
2442
+ deviceIds: [...l.deviceIds]
2443
+ }))),
2444
+ this.config.set("modeDelays", blob.modeDelays.map((e) => ({ ...e })))
2445
+ ]);
2446
+ }
2447
+ profilesRefuseError(reason, detail) {
2448
+ switch (reason) {
2449
+ case "name-empty": return /* @__PURE__ */ new Error("alarm profile name must not be empty");
2450
+ case "name-taken": return /* @__PURE__ */ new Error(`an alarm profile named '${detail ?? ""}' already exists`);
2451
+ case "cannot-delete-default": return /* @__PURE__ */ new Error("the Default alarm profile cannot be deleted");
2452
+ case "clone-source-missing": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found to clone`);
2453
+ case "not-found": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found`);
2454
+ default: return /* @__PURE__ */ new Error(`alarm profile operation refused (${reason})`);
2455
+ }
2214
2456
  }
2215
2457
  /**
2216
2458
  * Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
@@ -2943,6 +3185,11 @@ function ncAlarmPanelPort(panel) {
2943
3185
  deviceId: panel.id,
2944
3186
  settings: () => panel.settings(),
2945
3187
  applySettings: (patch) => panel.applySettings(patch),
3188
+ profilesView: () => panel.profilesView(),
3189
+ createProfile: (input) => panel.createProfile(input),
3190
+ renameProfile: (id, name) => panel.renameProfile(id, name),
3191
+ deleteProfile: (id) => panel.deleteProfile(id),
3192
+ activateProfile: (id) => panel.activateProfile(id),
2946
3193
  availableModes: () => NC_ALARM_MODES,
2947
3194
  noteRuleTrigger: (source) => panel.noteRuleTrigger(source),
2948
3195
  excludedDevices: () => panel.excludedDevices(),
@@ -5440,7 +5687,7 @@ function isAdoptable(record, nowMs) {
5440
5687
  * what `localDayMinute` does for the same input — one behaviour for a bad
5441
5688
  * zone, not two.
5442
5689
  */
5443
- function clockOf$5(atMs, timeZone) {
5690
+ function clockOf$6(atMs, timeZone) {
5444
5691
  if (timeZone !== void 0) try {
5445
5692
  return new Intl.DateTimeFormat("en-GB", {
5446
5693
  timeZone,
@@ -5576,7 +5823,7 @@ var TimelapseRenderer = class {
5576
5823
  includeAudio: false,
5577
5824
  maxLifeMs: await this.ports.exportLifeMs(),
5578
5825
  deleteAfterDownload: false,
5579
- title: `${request.ruleName} ${clockOf$5(window.startMs, request.timezone)}→${clockOf$5(window.endMs, request.timezone)}`,
5826
+ title: `${request.ruleName} ${clockOf$6(window.startMs, request.timezone)}→${clockOf$6(window.endMs, request.timezone)}`,
5580
5827
  timelapse: {
5581
5828
  everyMs,
5582
5829
  outputFps: request.framerate,
@@ -5907,7 +6154,7 @@ var TimelapseRenderer = class {
5907
6154
  ...meta,
5908
6155
  unservedWindowCount: verdict.unservedWindows.length,
5909
6156
  unservedWindows: verdict.unservedWindows.slice(0, 12).map((w) => ({
5910
- at: clockOf$5(request.window.startMs + w.fromSec * 1e3, request.timezone),
6157
+ at: clockOf$6(request.window.startMs + w.fromSec * 1e3, request.timezone),
5911
6158
  fromSec: Math.round(w.fromSec),
5912
6159
  toSec: Math.round(w.toSec),
5913
6160
  expectedDenseFrames: w.expectedDenseFrames,
@@ -13892,7 +14139,7 @@ var NcPruneRecordSchema = object({
13892
14139
  source: _enum(["event", "reconcile"])
13893
14140
  });
13894
14141
  /** Rules × deleted subjects — small; a generous ceiling. */
13895
- var LOAD_LIMIT$4 = 1e4;
14142
+ var LOAD_LIMIT$5 = 1e4;
13896
14143
  function keyOf$1(row) {
13897
14144
  return `${row.kind}:${row.ruleId}:${row.field}:${row.subject}`;
13898
14145
  }
@@ -13911,7 +14158,7 @@ var NC_DEVICE_PRUNES_SPEC = {
13911
14158
  return parsed.success ? parsed.data : null;
13912
14159
  },
13913
14160
  deviceIdOf: (row) => row.subjectKind === "device" ? Number(row.subject) : void 0,
13914
- loadLimit: LOAD_LIMIT$4
14161
+ loadLimit: LOAD_LIMIT$5
13915
14162
  };
13916
14163
  /** The outcomes that mean "this last reference was already dealt with". */
13917
14164
  var KEPT_OUTCOMES = new Set(["kept-disabled", "kept"]);
@@ -13946,7 +14193,7 @@ var NcPruneLedger = class {
13946
14193
  try {
13947
14194
  const records = await this.store.query.query({
13948
14195
  collection: NC_DEVICE_PRUNES_COLLECTION,
13949
- filter: { limit: LOAD_LIMIT$4 }
14196
+ filter: { limit: LOAD_LIMIT$5 }
13950
14197
  });
13951
14198
  const next = /* @__PURE__ */ new Map();
13952
14199
  for (const record of records) {
@@ -17271,12 +17518,12 @@ var NcSummaryTemplateSchema = object({
17271
17518
  title: string().max(500).optional(),
17272
17519
  body: string().max(2e3).optional()
17273
17520
  });
17274
- var NameField = string().min(1).max(200);
17521
+ var NameField$1 = string().min(1).max(200);
17275
17522
  /** MULTI-CAMERA is the point of the feature — a one-camera digest is a
17276
17523
  * timelapse with extra steps, but it is allowed and it is cheap. */
17277
17524
  var DeviceIdsField = array(number()).min(1).max(64);
17278
- var TargetsField = array(NcRuleTargetSchema).min(1);
17279
- var PriorityField = number().int().min(1).max(5);
17525
+ var TargetsField$1 = array(NcRuleTargetSchema).min(1);
17526
+ var PriorityField$1 = number().int().min(1).max(5);
17280
17527
  /**
17281
17528
  * The mosaic's tile ceiling. 9 (a 3×3 grid) is the shipped default: it is the
17282
17529
  * most a phone lock-screen thumbnail can carry and still be read, and it is the
@@ -17441,7 +17688,7 @@ var NcSummaryAiSchema = object({
17441
17688
  onFailure: _enum(["deliver-without-text", "skip"]).optional()
17442
17689
  });
17443
17690
  var NcSummaryRuleInputSchema = object({
17444
- name: NameField,
17691
+ name: NameField$1,
17445
17692
  enabled: boolean().default(true),
17446
17693
  deviceIds: DeviceIdsField,
17447
17694
  window: NcSummaryWindowSchema,
@@ -17450,7 +17697,7 @@ var NcSummaryRuleInputSchema = object({
17450
17697
  maxAiImages: MaxAiImagesField.default(NC_SUMMARY_DEFAULT_AI_IMAGES),
17451
17698
  rank: NcSummaryRankSchema.default("importance"),
17452
17699
  barrierSeconds: BarrierSecondsField.default(120),
17453
- targets: TargetsField,
17700
+ targets: TargetsField$1,
17454
17701
  template: NcSummaryTemplateSchema.optional(),
17455
17702
  /** Burned onto the mosaic's title band — and ONLY when `captions` is `all`. */
17456
17703
  captions: NcSummaryCaptionsSchema.default(NC_SUMMARY_DEFAULT_CAPTIONS),
@@ -17460,7 +17707,7 @@ var NcSummaryRuleInputSchema = object({
17460
17707
  /** Deliver a window that matched NOTHING? Default no: a nightly "0 events"
17461
17708
  * is a line nobody reads, and it costs the lines beside it their reader. */
17462
17709
  deliverEmpty: boolean().default(false),
17463
- priority: PriorityField.default(3),
17710
+ priority: PriorityField$1.default(3),
17464
17711
  ai: NcSummaryAiSchema.optional()
17465
17712
  });
17466
17713
  /**
@@ -17472,7 +17719,7 @@ var NcSummaryRuleInputSchema = object({
17472
17719
  * plain `.optional()`.
17473
17720
  */
17474
17721
  var NcSummaryRulePatchSchema = object({
17475
- name: NameField.optional(),
17722
+ name: NameField$1.optional(),
17476
17723
  enabled: boolean().optional(),
17477
17724
  deviceIds: DeviceIdsField.optional(),
17478
17725
  window: NcSummaryWindowSchema.optional(),
@@ -17481,12 +17728,12 @@ var NcSummaryRulePatchSchema = object({
17481
17728
  maxAiImages: MaxAiImagesField.optional(),
17482
17729
  rank: NcSummaryRankSchema.optional(),
17483
17730
  barrierSeconds: BarrierSecondsField.optional(),
17484
- targets: TargetsField.optional(),
17731
+ targets: TargetsField$1.optional(),
17485
17732
  template: NcSummaryTemplateSchema.nullable().optional(),
17486
17733
  captions: NcSummaryCaptionsSchema.optional(),
17487
17734
  captionText: string().max(200).optional(),
17488
17735
  deliverEmpty: boolean().optional(),
17489
- priority: PriorityField.optional(),
17736
+ priority: PriorityField$1.optional(),
17490
17737
  ai: NcSummaryAiSchema.nullable().optional()
17491
17738
  });
17492
17739
  /**
@@ -17519,6 +17766,89 @@ function assertSummaryBudget(rule) {
17519
17766
  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`);
17520
17767
  }
17521
17768
  //#endregion
17769
+ //#region src/notification-center/calendar-events/calendar-events-window.ts
17770
+ /** One day in milliseconds. */
17771
+ var CALENDAR_EVENTS_DAY_MS = 1440 * 60 * 1e3;
17772
+ /**
17773
+ * Floor / ceiling on the horizon days a rule may declare.
17774
+ * 1 day is the minimum useful digest; 30 keeps a single notification bounded.
17775
+ */
17776
+ var CALENDAR_EVENTS_MIN_HORIZON_DAYS = 1;
17777
+ var CALENDAR_EVENTS_MAX_HORIZON_DAYS = 30;
17778
+ var CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS = 7;
17779
+ /**
17780
+ * Build the producer window for one fire.
17781
+ *
17782
+ * `horizonDays` is clamped into the legal range rather than refused — a
17783
+ * stored rule that predates a tighter bound must still fire.
17784
+ */
17785
+ function calendarEventsWindow(nowMs, horizonDays) {
17786
+ const days = Math.min(CALENDAR_EVENTS_MAX_HORIZON_DAYS, Math.max(CALENDAR_EVENTS_MIN_HORIZON_DAYS, Math.floor(horizonDays)));
17787
+ return {
17788
+ startMs: nowMs,
17789
+ endMs: nowMs + days * CALENDAR_EVENTS_DAY_MS,
17790
+ horizonDays: days
17791
+ };
17792
+ }
17793
+ //#endregion
17794
+ //#region src/notification-center/calendar-events/calendar-events-rule.ts
17795
+ /**
17796
+ * NcCalendarEventsRule — scheduled "upcoming calendar events" digest.
17797
+ *
17798
+ * Same ownership model as Summaries/Timelapses: lives in the addon (not
17799
+ * `@camstack/types`) so a field is live on the next post-analysis deploy.
17800
+ * Rides no `NcDelivery` member; reaches the outside world through
17801
+ * `nc.*CalendarEvents*` bridge actions.
17802
+ *
17803
+ * Calendar selection mirrors detection classes: `calendars` = Only,
17804
+ * `calendarsExclude` = Except, both empty = All.
17805
+ */
17806
+ var NcCalendarEventsTemplateSchema = object({
17807
+ title: string().max(500).optional(),
17808
+ body: string().max(4e3).optional()
17809
+ });
17810
+ var NameField = string().min(1).max(200);
17811
+ var TargetsField = array(NcRuleTargetSchema).min(1);
17812
+ var PriorityField = number().int().min(1).max(5);
17813
+ var HorizonDaysField = number().int().min(CALENDAR_EVENTS_MIN_HORIZON_DAYS).max(CALENDAR_EVENTS_MAX_HORIZON_DAYS);
17814
+ var CalendarIdsField = array(string().min(1).max(200)).max(64);
17815
+ var NcCalendarEventsRuleInputSchema = object({
17816
+ name: NameField,
17817
+ enabled: boolean().default(true),
17818
+ schedule: NcScheduleSchema,
17819
+ /** Only these calendars. Empty/absent with no exclude = All. */
17820
+ calendars: CalendarIdsField.optional(),
17821
+ /** All except these. Ignored when `calendars` is non-empty. */
17822
+ calendarsExclude: CalendarIdsField.optional(),
17823
+ horizonDays: HorizonDaysField.default(CALENDAR_EVENTS_DEFAULT_HORIZON_DAYS),
17824
+ /** Deliver when the horizon holds no events? Default no. */
17825
+ deliverEmpty: boolean().default(false),
17826
+ targets: TargetsField,
17827
+ template: NcCalendarEventsTemplateSchema.optional(),
17828
+ priority: PriorityField.default(3)
17829
+ });
17830
+ var NcCalendarEventsRulePatchSchema = object({
17831
+ name: NameField.optional(),
17832
+ enabled: boolean().optional(),
17833
+ schedule: NcScheduleSchema.optional(),
17834
+ calendars: CalendarIdsField.nullable().optional(),
17835
+ calendarsExclude: CalendarIdsField.nullable().optional(),
17836
+ horizonDays: HorizonDaysField.optional(),
17837
+ deliverEmpty: boolean().optional(),
17838
+ targets: TargetsField.optional(),
17839
+ template: NcCalendarEventsTemplateSchema.nullable().optional(),
17840
+ priority: PriorityField.optional()
17841
+ });
17842
+ var NcCalendarEventsRuleSchema = NcCalendarEventsRuleInputSchema.extend({
17843
+ id: string(),
17844
+ ownerUserId: string().optional(),
17845
+ /** Epoch-ms of the schedule window end that was last delivered. */
17846
+ generatedAt: number().optional(),
17847
+ createdBy: string(),
17848
+ createdAt: number(),
17849
+ updatedAt: number()
17850
+ });
17851
+ //#endregion
17522
17852
  //#region src/notification-center/unstored-media-id.ts
17523
17853
  /**
17524
17854
  * An id for media bytes that were NEVER STORED.
@@ -18664,6 +18994,22 @@ var ncActions = defineCustomActions({
18664
18994
  kind: "mutation",
18665
18995
  caller: "required"
18666
18996
  }),
18997
+ "nc.listCalendarEventsRules": customAction(object({}), object({
18998
+ rules: array(NcCalendarEventsRuleSchema.extend({ readOnly: boolean() })),
18999
+ canAuthor: boolean()
19000
+ }), { caller: "required" }),
19001
+ "nc.upsertCalendarEventsRule": customAction(object({
19002
+ ruleId: string().optional(),
19003
+ rule: NcCalendarEventsRuleInputSchema.optional(),
19004
+ patch: NcCalendarEventsRulePatchSchema.optional()
19005
+ }), object({ rule: NcCalendarEventsRuleSchema }), {
19006
+ kind: "mutation",
19007
+ caller: "required"
19008
+ }),
19009
+ "nc.deleteCalendarEventsRule": customAction(object({ ruleId: string() }), object({ success: literal(true) }), {
19010
+ kind: "mutation",
19011
+ caller: "required"
19012
+ }),
18667
19013
  /**
18668
19014
  * Produce one digest window RIGHT NOW — the operator's Test button.
18669
19015
  *
@@ -18950,6 +19296,11 @@ function makeNcActionHandlers(deps) {
18950
19296
  if (sum === void 0) throw new Error("this node does not host the summary producer — use the hub");
18951
19297
  return sum;
18952
19298
  };
19299
+ const requireCalendarEvents = () => {
19300
+ const ce = deps.calendarEvents;
19301
+ if (ce === void 0) throw new Error("this node does not host calendar-events rules — use the hub");
19302
+ return ce;
19303
+ };
18953
19304
  /** The artefact index, or an honest refusal. */
18954
19305
  const requireArtifacts = () => {
18955
19306
  const index = deps.artifacts;
@@ -18978,6 +19329,13 @@ function makeNcActionHandlers(deps) {
18978
19329
  if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: summary rule not owned: ${ruleId}`);
18979
19330
  return rule;
18980
19331
  };
19332
+ const assertCalendarEventsEditable = (ruleId, caller, ce) => {
19333
+ const rule = ce.store.get(ruleId);
19334
+ if (rule === null) throw new Error(`forbidden: calendar-events rule not found: ${ruleId}`);
19335
+ if (caller.isAdmin) return rule;
19336
+ if (rule.ownerUserId !== caller.userId) throw new Error(`forbidden: calendar-events rule not owned: ${ruleId}`);
19337
+ return rule;
19338
+ };
18981
19339
  /** A timelapse rule the caller may EDIT. Same admin bypass as `NcRule`. */
18982
19340
  const assertTimelapseEditable = (ruleId, caller, tl) => {
18983
19341
  const rule = tl.store.get(ruleId);
@@ -19519,6 +19877,53 @@ function makeNcActionHandlers(deps) {
19519
19877
  ...out.skippedByAi === true ? { skippedByAi: true } : {}
19520
19878
  };
19521
19879
  },
19880
+ "nc.listCalendarEventsRules": async (_input, caller) => {
19881
+ const c = requireCaller$1(caller);
19882
+ const ce = requireCalendarEvents();
19883
+ return {
19884
+ rules: (c.isAdmin ? ce.store.list() : ce.store.listForOwner(c.userId)).map((r) => ({
19885
+ ...r,
19886
+ readOnly: !c.isAdmin && r.ownerUserId !== c.userId
19887
+ })),
19888
+ canAuthor: true
19889
+ };
19890
+ },
19891
+ "nc.upsertCalendarEventsRule": async (input, caller) => {
19892
+ const c = requireCaller$1(caller);
19893
+ const ce = requireCalendarEvents();
19894
+ if (input.ruleId === void 0) {
19895
+ if (input.rule === void 0) throw new Error("upsertCalendarEventsRule: rule or ruleId required");
19896
+ const parsed = NcCalendarEventsRuleInputSchema.parse(input.rule);
19897
+ await assertTargetsOwned(parsed.targets.map((t) => t.targetId), c);
19898
+ const rule = await ce.store.create(parsed, c.userId, c.isAdmin ? void 0 : c.userId);
19899
+ deps.logger.info("calendar-events rule created", { meta: {
19900
+ ruleId: rule.id,
19901
+ owner: c.userId
19902
+ } });
19903
+ return { rule };
19904
+ }
19905
+ assertCalendarEventsEditable(input.ruleId, c, ce);
19906
+ if (input.patch === void 0) throw new Error("upsertCalendarEventsRule: patch required");
19907
+ const patch = NcCalendarEventsRulePatchSchema.parse(input.patch);
19908
+ if (patch.targets !== void 0) await assertTargetsOwned(patch.targets.map((t) => t.targetId), c);
19909
+ const rule = await ce.store.update(input.ruleId, patch);
19910
+ deps.logger.info("calendar-events rule updated", { meta: {
19911
+ ruleId: rule.id,
19912
+ owner: c.userId
19913
+ } });
19914
+ return { rule };
19915
+ },
19916
+ "nc.deleteCalendarEventsRule": async (input, caller) => {
19917
+ const c = requireCaller$1(caller);
19918
+ const ce = requireCalendarEvents();
19919
+ assertCalendarEventsEditable(input.ruleId, c, ce);
19920
+ await ce.store.delete(input.ruleId);
19921
+ deps.logger.info("calendar-events rule deleted", { meta: {
19922
+ ruleId: input.ruleId,
19923
+ owner: c.userId
19924
+ } });
19925
+ return { success: true };
19926
+ },
19522
19927
  "nc.getTexts": async (input) => {
19523
19928
  const texts = requireTexts();
19524
19929
  return input.language === void 0 ? texts.view() : texts.view(input.language);
@@ -28382,7 +28787,8 @@ var OUTBOX_RECORD_KINDS = new Set([
28382
28787
  "audio-window",
28383
28788
  "system-event",
28384
28789
  "timelapse",
28385
- "ai-summary"
28790
+ "ai-summary",
28791
+ "calendar-events"
28386
28792
  ]);
28387
28793
  function isOutboxRecordKind(x) {
28388
28794
  return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
@@ -28735,7 +29141,7 @@ function buildRuleAiSystemPrompt(shape, language, maxWords, grouped = false) {
28735
29141
  ].join("");
28736
29142
  }
28737
29143
  /** `HH:MM` in the host timezone. */
28738
- function clockOf$4(atMs) {
29144
+ function clockOf$5(atMs) {
28739
29145
  const d = new Date(atMs);
28740
29146
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
28741
29147
  }
@@ -28773,7 +29179,7 @@ function buildRuleAiPrompt(input) {
28773
29179
  if (shape === "detection-track") {
28774
29180
  lines.push(`Camera: ${camera}.${located} Subject: ${subject}. The stills, in order:`);
28775
29181
  for (const [i, frame] of input.frames.entries()) {
28776
- const when = frame.atMs !== void 0 ? ` · ${clockOf$4(frame.atMs)}` : "";
29182
+ const when = frame.atMs !== void 0 ? ` · ${clockOf$5(frame.atMs)}` : "";
28777
29183
  lines.push(`${String(i + 1)}.${when}`);
28778
29184
  }
28779
29185
  return lines.join("\n");
@@ -29070,7 +29476,7 @@ var NcRuleAiRowSchema = object({
29070
29476
  updatedAt: number()
29071
29477
  });
29072
29478
  /** Operator-authored and tiny; a generous ceiling. */
29073
- var LOAD_LIMIT$3 = 1e4;
29479
+ var LOAD_LIMIT$4 = 1e4;
29074
29480
  var NC_RULE_AI_SPEC = {
29075
29481
  collection: NC_RULE_AI_COLLECTION,
29076
29482
  columns: [...NC_RULE_AI_COLUMNS],
@@ -29086,7 +29492,7 @@ var NC_RULE_AI_SPEC = {
29086
29492
  const parsed = NcRuleAiRowSchema.safeParse(data["row"]);
29087
29493
  return parsed.success ? parsed.data : null;
29088
29494
  },
29089
- loadLimit: LOAD_LIMIT$3
29495
+ loadLimit: LOAD_LIMIT$4
29090
29496
  };
29091
29497
  var NcRuleAiStore = class {
29092
29498
  ledger;
@@ -29423,7 +29829,7 @@ var NC_RULES_INDEXES = [{
29423
29829
  columns: ["enabled"]
29424
29830
  }];
29425
29831
  /** Query cap — the rule set is operator-authored and tiny; a generous ceiling. */
29426
- var LOAD_LIMIT$2 = 1e4;
29832
+ var LOAD_LIMIT$3 = 1e4;
29427
29833
  function isRecord$3(value) {
29428
29834
  return typeof value === "object" && value !== null && !Array.isArray(value);
29429
29835
  }
@@ -29472,7 +29878,7 @@ function makeRulesSpec(onProjection) {
29472
29878
  if (projection.kind === "projected") onProjection?.(key, projection);
29473
29879
  return parsed.data;
29474
29880
  },
29475
- loadLimit: LOAD_LIMIT$2
29881
+ loadLimit: LOAD_LIMIT$3
29476
29882
  };
29477
29883
  }
29478
29884
  /** The spec `declare` needs — no sink: declaring a collection projects nothing. */
@@ -30544,7 +30950,7 @@ var DEFAULT_PROMPT_EN = "What happened during this period? Summarise it for a ho
30544
30950
  */
30545
30951
  var SummaryAnswerSchema = object({ summary: string() });
30546
30952
  /** `HH:MM` in the host timezone — the two numbers a frame label needs. */
30547
- function clockOf$3(atMs) {
30953
+ function clockOf$4(atMs) {
30548
30954
  const d = new Date(atMs);
30549
30955
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
30550
30956
  }
@@ -30567,7 +30973,7 @@ function buildSummaryAiPrompt(input, frames) {
30567
30973
  const roster = frames.map((f, i) => {
30568
30974
  const location = plainLabel(f.location ?? "");
30569
30975
  const where = location.length > 0 ? ` (${location})` : "";
30570
- return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$3(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
30976
+ return `${String(i + 1)}. ${plainLabel(f.deviceName)}${where} · ${clockOf$4(f.atMs)} · ${plainVocabulary(f.className) || "subject"}`;
30571
30977
  }).join("\n");
30572
30978
  const described = /* @__PURE__ */ new Map();
30573
30979
  for (const f of frames) {
@@ -31170,7 +31576,7 @@ var NO_DETECTIONS$1 = {
31170
31576
  * has to check is a key somebody can get wrong. */
31171
31577
  var BODY_SEPARATOR$1 = " · ";
31172
31578
  /** `HH:MM` in the host timezone. */
31173
- function clockOf$2(atMs) {
31579
+ function clockOf$3(atMs) {
31174
31580
  const d = new Date(atMs);
31175
31581
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
31176
31582
  }
@@ -31184,8 +31590,8 @@ function clockOf$2(atMs) {
31184
31590
  function summaryTemplateVars(input) {
31185
31591
  return {
31186
31592
  rule: input.ruleName,
31187
- from: clockOf$2(input.window.startMs),
31188
- to: clockOf$2(input.window.endMs),
31593
+ from: clockOf$3(input.window.startMs),
31594
+ to: clockOf$3(input.window.endMs),
31189
31595
  time: ncClockText(input.texts.language, input.generatedAt),
31190
31596
  events: String(input.tileCount),
31191
31597
  cameras: String(input.cameraCount),
@@ -31229,7 +31635,7 @@ function composeSummaryBody(input) {
31229
31635
  * string, for the reason the timelapse learned: a hand-rolled `n === 1` shipped
31230
31636
  * "1 intervalli ripresi" to a live phone.
31231
31637
  */
31232
- function derivedBody$1(input, vars) {
31638
+ function derivedBody$2(input, vars) {
31233
31639
  const texts = input.texts;
31234
31640
  const parts = [texts.text({
31235
31641
  key: "summary.body.window",
@@ -31264,7 +31670,7 @@ function derivedBody$1(input, vars) {
31264
31670
  }
31265
31671
  /** Per-target params with the derived `clickUrl` filled in where absent. A
31266
31672
  * value the operator set on the target WINS — he chose it. */
31267
- function paramsFor$1(target, clickUrl) {
31673
+ function paramsFor$2(target, clickUrl) {
31268
31674
  if (clickUrl === void 0) return target.params;
31269
31675
  if (target.params === void 0) return { clickUrl };
31270
31676
  if (typeof target.params["clickUrl"] === "string") return target.params;
@@ -31280,11 +31686,11 @@ function summaryRecordId(ruleId, windowEndMs) {
31280
31686
  /** The caption burned onto the mosaic's title band, with the same `{{var}}`
31281
31687
  * vocabulary the title and body use. `''` is a real answer ("no caption"). */
31282
31688
  function summaryCaptionText(input) {
31283
- if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$2(input.window.startMs)}→${clockOf$2(input.window.endMs)}`;
31689
+ if (input.captionText === void 0) return `${input.ruleName} — ${clockOf$3(input.window.startMs)}→${clockOf$3(input.window.endMs)}`;
31284
31690
  return renderTemplate(input.captionText, {
31285
31691
  rule: input.ruleName,
31286
- from: clockOf$2(input.window.startMs),
31287
- to: clockOf$2(input.window.endMs)
31692
+ from: clockOf$3(input.window.startMs),
31693
+ to: clockOf$3(input.window.endMs)
31288
31694
  }) ?? "";
31289
31695
  }
31290
31696
  /**
@@ -31302,7 +31708,7 @@ function buildSummaryOutboxInputs(input) {
31302
31708
  const body = composeSummaryBody({
31303
31709
  aiText: aiSentence(input),
31304
31710
  renderedTemplateBody: renderTemplate(input.template?.body, vars),
31305
- derivedBody: derivedBody$1(input, vars),
31711
+ derivedBody: derivedBody$2(input, vars),
31306
31712
  templateNamesAi: templatePlaceholders(input.template?.body ?? "").includes("ai")
31307
31713
  });
31308
31714
  const recordId = summaryRecordId(input.ruleId, input.window.endMs);
@@ -31321,7 +31727,7 @@ function buildSummaryOutboxInputs(input) {
31321
31727
  },
31322
31728
  media: "none",
31323
31729
  ...artifacts.length > 0 ? { artifacts } : {},
31324
- ...paramsFor$1(target, input.clickUrl) !== void 0 ? { params: paramsFor$1(target, input.clickUrl) } : {},
31730
+ ...paramsFor$2(target, input.clickUrl) !== void 0 ? { params: paramsFor$2(target, input.clickUrl) } : {},
31325
31731
  subject: {
31326
31732
  deviceId: input.primaryDeviceId,
31327
31733
  className: "summary",
@@ -32216,7 +32622,7 @@ var NcSummaryProducer = class {
32216
32622
  purpose: "summary",
32217
32623
  tiles: tiles.map(({ candidate, jpeg }) => ({
32218
32624
  jpeg,
32219
- ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$1(candidate.firstSeen)} · ${candidate.className}` } : {},
32625
+ ...plan.tileLabels ? { label: `${names.get(candidate.deviceId) ?? candidate.deviceId} · ${clockOf$2(candidate.firstSeen)} · ${candidate.className}` } : {},
32220
32626
  deviceId: candidate.deviceId,
32221
32627
  sourceId: candidate.trackId
32222
32628
  })),
@@ -32439,7 +32845,7 @@ var NcSummaryProducer = class {
32439
32845
  }
32440
32846
  };
32441
32847
  /** `HH:MM` in the host timezone — the two numbers a tile label needs. */
32442
- function clockOf$1(atMs) {
32848
+ function clockOf$2(atMs) {
32443
32849
  const d = new Date(atMs);
32444
32850
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
32445
32851
  }
@@ -32515,7 +32921,7 @@ var NC_SUMMARY_RULES_INDEXES = [{
32515
32921
  columns: ["enabled"]
32516
32922
  }];
32517
32923
  /** The rule set is operator-authored and tiny; a generous ceiling. */
32518
- var LOAD_LIMIT$1 = 1e4;
32924
+ var LOAD_LIMIT$2 = 1e4;
32519
32925
  /**
32520
32926
  * The ledger contract for the digest rule set.
32521
32927
  *
@@ -32545,7 +32951,7 @@ var NC_SUMMARY_RULES_SPEC = {
32545
32951
  const parsed = NcSummaryRuleSchema.safeParse(data["rule"]);
32546
32952
  return parsed.success ? parsed.data : null;
32547
32953
  },
32548
- loadLimit: LOAD_LIMIT$1
32954
+ loadLimit: LOAD_LIMIT$2
32549
32955
  };
32550
32956
  /**
32551
32957
  * The three-way CLEARABLE patch signal, one function per field: `undefined`
@@ -32556,7 +32962,7 @@ var NC_SUMMARY_RULES_SPEC = {
32556
32962
  * cast here would be a cast on the exact path that decides whether a persisted
32557
32963
  * rule keeps a field. Three literal destructurings are three provable ones.
32558
32964
  */
32559
- function applyTemplatePatch$1(merged, template) {
32965
+ function applyTemplatePatch$2(merged, template) {
32560
32966
  if (template === void 0) return merged;
32561
32967
  if (template !== null) return {
32562
32968
  ...merged,
@@ -32702,7 +33108,7 @@ var NcSummaryStore = class {
32702
33108
  createdAt: existing.createdAt,
32703
33109
  updatedAt: this.now()
32704
33110
  };
32705
- return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$1(merged, template), filters), ai));
33111
+ return this.write(applyAiPatch(applyFiltersPatch(applyTemplatePatch$2(merged, template), filters), ai));
32706
33112
  }
32707
33113
  async setEnabled(ruleId, enabled) {
32708
33114
  return this.update(ruleId, { enabled });
@@ -32754,6 +33160,408 @@ var NcSummaryStore = class {
32754
33160
  }
32755
33161
  };
32756
33162
  //#endregion
33163
+ //#region src/notification-center/calendar-events/calendar-events-store.ts
33164
+ /**
33165
+ * Durable calendar-events rule set — same DurableLedger mechanics as
33166
+ * Summaries, with a per-rule `generatedAt` stamp (one fire per schedule window).
33167
+ */
33168
+ /**
33169
+ * @durable class=config owner=notification-center
33170
+ * write="an operator creates or edits a calendar-events rule, or the producer stamps a delivered window via markGenerated"
33171
+ * retention="none — a row goes only when the operator deletes the rule."
33172
+ */
33173
+ var NC_CALENDAR_EVENTS_RULES_COLLECTION = "notification-center:calendar-events-rules";
33174
+ var NC_CALENDAR_EVENTS_RULES_COLUMNS = [
33175
+ {
33176
+ name: "id",
33177
+ type: "TEXT",
33178
+ primaryKey: true,
33179
+ notNull: true
33180
+ },
33181
+ {
33182
+ name: "name",
33183
+ type: "TEXT",
33184
+ notNull: true
33185
+ },
33186
+ {
33187
+ name: "enabled",
33188
+ type: "BOOLEAN",
33189
+ notNull: true
33190
+ },
33191
+ {
33192
+ name: "updatedAt",
33193
+ type: "INTEGER",
33194
+ notNull: true
33195
+ },
33196
+ {
33197
+ name: "rule",
33198
+ type: "JSON",
33199
+ notNull: true
33200
+ }
33201
+ ];
33202
+ var NC_CALENDAR_EVENTS_RULES_INDEXES = [{
33203
+ name: "idx_nc_calendar_events_rules_enabled",
33204
+ columns: ["enabled"]
33205
+ }];
33206
+ var LOAD_LIMIT$1 = 1e4;
33207
+ var NC_CALENDAR_EVENTS_RULES_SPEC = {
33208
+ collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
33209
+ columns: [...NC_CALENDAR_EVENTS_RULES_COLUMNS],
33210
+ indexes: [...NC_CALENDAR_EVENTS_RULES_INDEXES],
33211
+ writeMode: "write-through",
33212
+ keyOf: (rule) => rule.id,
33213
+ toValue: (rule) => ({
33214
+ name: rule.name,
33215
+ enabled: rule.enabled,
33216
+ updatedAt: rule.updatedAt,
33217
+ rule
33218
+ }),
33219
+ fromRecord: (_key, data) => {
33220
+ const parsed = NcCalendarEventsRuleSchema.safeParse(data["rule"]);
33221
+ return parsed.success ? parsed.data : null;
33222
+ },
33223
+ loadLimit: LOAD_LIMIT$1
33224
+ };
33225
+ function applyTemplatePatch$1(merged, template) {
33226
+ if (template === void 0) return merged;
33227
+ if (template !== null) return {
33228
+ ...merged,
33229
+ template
33230
+ };
33231
+ const { template: _cleared, ...rest } = merged;
33232
+ return rest;
33233
+ }
33234
+ function applyOptionalStringList(merged, key, value) {
33235
+ if (value === void 0) return merged;
33236
+ if (value !== null) return {
33237
+ ...merged,
33238
+ [key]: [...value]
33239
+ };
33240
+ const { [key]: _cleared, ...rest } = merged;
33241
+ return rest;
33242
+ }
33243
+ var NcCalendarEventsStore = class {
33244
+ ledger;
33245
+ store;
33246
+ logger;
33247
+ now;
33248
+ newId;
33249
+ constructor(deps) {
33250
+ this.store = deps.store;
33251
+ this.ledger = new DurableLedger({
33252
+ spec: NC_CALENDAR_EVENTS_RULES_SPEC,
33253
+ store: deps.store,
33254
+ logger: deps.logger
33255
+ });
33256
+ this.logger = deps.logger;
33257
+ this.now = deps.now ?? (() => Date.now());
33258
+ this.newId = deps.newId ?? (() => randomUUID());
33259
+ }
33260
+ static async declare(store) {
33261
+ await DurableLedger.declare(store, NC_CALENDAR_EVENTS_RULES_SPEC);
33262
+ }
33263
+ async load() {
33264
+ await this.ledger.load();
33265
+ this.logger.debug("calendar-events rules loaded", { meta: { rules: this.ledger.size } });
33266
+ }
33267
+ list() {
33268
+ return this.ledger.snapshot().toSorted((a, b) => b.updatedAt - a.updatedAt);
33269
+ }
33270
+ listEnabled() {
33271
+ return this.list().filter((r) => r.enabled);
33272
+ }
33273
+ get(ruleId) {
33274
+ return this.ledger.get(ruleId) ?? null;
33275
+ }
33276
+ listForOwner(userId) {
33277
+ return this.list().filter((r) => r.ownerUserId === void 0 || r.ownerUserId === userId);
33278
+ }
33279
+ async create(input, createdBy, ownerUserId) {
33280
+ const now = this.now();
33281
+ const rule = NcCalendarEventsRuleSchema.parse({
33282
+ ...NcCalendarEventsRuleInputSchema.parse(input),
33283
+ id: this.newId(),
33284
+ ...ownerUserId !== void 0 ? { ownerUserId } : {},
33285
+ createdBy,
33286
+ createdAt: now,
33287
+ updatedAt: now
33288
+ });
33289
+ await this.ledger.put(rule);
33290
+ return rule;
33291
+ }
33292
+ async update(ruleId, patch) {
33293
+ const existing = this.ledger.get(ruleId);
33294
+ if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
33295
+ const { template, calendars, calendarsExclude, ...rest } = patch;
33296
+ const merged = {
33297
+ ...existing,
33298
+ ...rest,
33299
+ id: existing.id,
33300
+ ...existing.ownerUserId !== void 0 ? { ownerUserId: existing.ownerUserId } : {},
33301
+ ...existing.generatedAt !== void 0 ? { generatedAt: existing.generatedAt } : {},
33302
+ createdBy: existing.createdBy,
33303
+ createdAt: existing.createdAt,
33304
+ updatedAt: this.now()
33305
+ };
33306
+ return this.write(applyOptionalStringList(applyOptionalStringList(applyTemplatePatch$1(merged, template), "calendars", calendars), "calendarsExclude", calendarsExclude));
33307
+ }
33308
+ async setEnabled(ruleId, enabled) {
33309
+ return this.update(ruleId, { enabled });
33310
+ }
33311
+ async markGenerated(ruleId, windowEndMs) {
33312
+ const existing = this.ledger.get(ruleId);
33313
+ if (!existing) throw new Error(`calendar-events rule not found: ${ruleId}`);
33314
+ return this.write({
33315
+ ...existing,
33316
+ generatedAt: Math.max(existing.generatedAt ?? 0, windowEndMs)
33317
+ });
33318
+ }
33319
+ /**
33320
+ * Idempotent delete — unknown ids are a no-op in RAM, still attempted on disk.
33321
+ *
33322
+ * Deliberately NOT {@link DurableLedger.forget}: that swallows a failed
33323
+ * durable delete. Mirror eviction first; durable half re-throws on failure
33324
+ * (same shape as Summaries).
33325
+ */
33326
+ async delete(ruleId) {
33327
+ this.ledger.evict(ruleId);
33328
+ try {
33329
+ await this.store.delete.mutate({
33330
+ collection: NC_CALENDAR_EVENTS_RULES_COLLECTION,
33331
+ key: ruleId
33332
+ });
33333
+ } catch (err) {
33334
+ this.logger.warn("calendar-events rule durable delete failed", { meta: {
33335
+ ruleId,
33336
+ error: err instanceof Error ? err.message : String(err)
33337
+ } });
33338
+ throw err instanceof Error ? err : new Error(String(err));
33339
+ }
33340
+ }
33341
+ async write(rule) {
33342
+ const validated = NcCalendarEventsRuleSchema.parse(rule);
33343
+ await this.ledger.put(validated);
33344
+ return validated;
33345
+ }
33346
+ };
33347
+ //#endregion
33348
+ //#region src/notification-center/calendar-events/calendar-events-delivery.ts
33349
+ function calendarEventsRecordId(ruleId, windowEndMs) {
33350
+ return `calendar-events:${ruleId}:${windowEndMs}`;
33351
+ }
33352
+ /** `YYYY-MM-DD HH:MM` in the host timezone. */
33353
+ function clockOf$1(atMs) {
33354
+ const d = new Date(atMs);
33355
+ 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")}`}`;
33356
+ }
33357
+ function formatEventLine(event) {
33358
+ const when = event.allDay ? clockOf$1(event.startMs).slice(0, 10) : `${clockOf$1(event.startMs)}–${clockOf$1(event.endMs)}`;
33359
+ const where = event.location !== void 0 && event.location.length > 0 ? ` @ ${event.location}` : "";
33360
+ return `• ${when} ${event.title}${where}`;
33361
+ }
33362
+ function derivedBody$1(input) {
33363
+ if (input.events.length === 0) return `No events in the next ${String(input.horizonDays)} day(s).`;
33364
+ const lines = input.events.slice(0, 40).map(formatEventLine);
33365
+ const extra = input.events.length > 40 ? `\n…and ${String(input.events.length - 40)} more` : "";
33366
+ return lines.join("\n") + extra;
33367
+ }
33368
+ function templateVars(input) {
33369
+ return {
33370
+ rule: input.ruleName,
33371
+ count: String(input.events.length),
33372
+ horizonDays: String(input.horizonDays),
33373
+ from: clockOf$1(input.eventWindow.startMs),
33374
+ to: clockOf$1(input.eventWindow.endMs),
33375
+ events: derivedBody$1(input),
33376
+ calendars: String(input.calendarIds.length)
33377
+ };
33378
+ }
33379
+ function paramsFor$1(target) {
33380
+ const params = target.params;
33381
+ if (params === void 0) return void 0;
33382
+ const out = {};
33383
+ for (const [k, v] of Object.entries(params)) if (typeof v === "string") out[k] = v;
33384
+ return Object.keys(out).length > 0 ? out : void 0;
33385
+ }
33386
+ /**
33387
+ * One outbox row per enabled target. Ready for `NcOutbox.enqueue`.
33388
+ */
33389
+ function buildCalendarEventsOutboxInputs(input) {
33390
+ const vars = templateVars(input);
33391
+ const optedOut = new Set(input.disabledTargetIds ?? []);
33392
+ const title = renderTemplate(input.template?.title, vars) ?? `${input.ruleName}: ${String(input.events.length)} upcoming`;
33393
+ const body = renderTemplate(input.template?.body, vars) ?? derivedBody$1(input);
33394
+ const recordId = calendarEventsRecordId(input.ruleId, input.scheduleWindow.endMs);
33395
+ return input.targets.filter((t) => !optedOut.has(t.targetId)).map((target) => {
33396
+ const payload = {
33397
+ ruleName: input.ruleName,
33398
+ delivery: "calendar-events",
33399
+ priority: input.priority,
33400
+ template: {
33401
+ title,
33402
+ body
33403
+ },
33404
+ media: "none",
33405
+ ...paramsFor$1(target) !== void 0 ? { params: paramsFor$1(target) } : {},
33406
+ subject: {
33407
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
33408
+ className: "calendar-events",
33409
+ zones: [],
33410
+ timestamp: input.scheduleWindow.endMs
33411
+ }
33412
+ };
33413
+ return {
33414
+ ruleId: input.ruleId,
33415
+ targetId: target.targetId,
33416
+ deviceId: NC_SYSTEM_DEVICE_ID$1,
33417
+ recordKind: "calendar-events",
33418
+ recordId,
33419
+ payload
33420
+ };
33421
+ });
33422
+ }
33423
+ //#endregion
33424
+ //#region src/notification-center/calendar-events/calendar-events-selection.ts
33425
+ /**
33426
+ * Expand a rule's calendar scope against the known calendar ids.
33427
+ *
33428
+ * - All (both lists empty/absent) → every known id
33429
+ * - Only → the include list, intersected with known (unknown ids dropped)
33430
+ * - Except → known minus the exclude list
33431
+ *
33432
+ * An empty known set always yields empty — there is nothing to notify about.
33433
+ */
33434
+ function expandCalendarScope(scope, knownIds) {
33435
+ const known = [...knownIds];
33436
+ if (known.length === 0) return [];
33437
+ const include = scope?.calendars ?? [];
33438
+ const exclude = scope?.calendarsExclude ?? [];
33439
+ if (include.length > 0) {
33440
+ const wanted = new Set(include);
33441
+ return known.filter((id) => wanted.has(id));
33442
+ }
33443
+ if (exclude.length > 0) {
33444
+ const banned = new Set(exclude);
33445
+ return known.filter((id) => !banned.has(id));
33446
+ }
33447
+ return known;
33448
+ }
33449
+ //#endregion
33450
+ //#region src/notification-center/calendar-events/calendar-events-producer.ts
33451
+ /** How far back a closed schedule window may still be owed. */
33452
+ var CALENDAR_EVENTS_LOOKBACK_MS = 2160 * 60 * 1e3;
33453
+ var NcCalendarEventsProducer = class {
33454
+ deps;
33455
+ inFlight = false;
33456
+ now;
33457
+ constructor(deps) {
33458
+ this.deps = deps;
33459
+ this.now = deps.now ?? (() => Date.now());
33460
+ }
33461
+ async tick() {
33462
+ if (this.inFlight) return {
33463
+ examined: 0,
33464
+ sent: 0,
33465
+ empty: 0,
33466
+ skipped: 0,
33467
+ failed: 0
33468
+ };
33469
+ this.inFlight = true;
33470
+ const report = {
33471
+ examined: 0,
33472
+ sent: 0,
33473
+ empty: 0,
33474
+ skipped: 0,
33475
+ failed: 0
33476
+ };
33477
+ try {
33478
+ const rules = this.deps.store.listEnabled();
33479
+ for (const rule of rules) {
33480
+ report.examined += 1;
33481
+ try {
33482
+ const outcome = await this.runRule(rule);
33483
+ if (outcome === "sent") report.sent += 1;
33484
+ else if (outcome === "empty") report.empty += 1;
33485
+ else report.skipped += 1;
33486
+ } catch (err) {
33487
+ report.failed += 1;
33488
+ this.deps.logger.warn("calendar-events rule failed", { meta: {
33489
+ ruleId: rule.id,
33490
+ error: err instanceof Error ? err.message : String(err)
33491
+ } });
33492
+ }
33493
+ }
33494
+ return report;
33495
+ } finally {
33496
+ this.inFlight = false;
33497
+ }
33498
+ }
33499
+ async runRule(rule) {
33500
+ const nowMs = this.now();
33501
+ const scheduleWindow = findLastClosedWindow({
33502
+ schedule: rule.schedule,
33503
+ nowMs,
33504
+ lookbackMs: CALENDAR_EVENTS_LOOKBACK_MS
33505
+ });
33506
+ if (scheduleWindow === null) return "skipped";
33507
+ if ((rule.generatedAt ?? 0) >= scheduleWindow.endMs) return "skipped";
33508
+ const known = await this.deps.calendars.listCalendars();
33509
+ const calendarIds = expandCalendarScope({
33510
+ calendars: rule.calendars,
33511
+ calendarsExclude: rule.calendarsExclude
33512
+ }, known.map((c) => c.id));
33513
+ if (calendarIds.length === 0) {
33514
+ this.deps.logger.info("calendar-events: no calendars in scope", { meta: { ruleId: rule.id } });
33515
+ if (!rule.deliverEmpty) {
33516
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33517
+ return "empty";
33518
+ }
33519
+ }
33520
+ const eventWindow = calendarEventsWindow(nowMs, rule.horizonDays);
33521
+ const events = calendarIds.length === 0 ? [] : await this.deps.calendars.listEvents({
33522
+ calendarIds,
33523
+ startMs: eventWindow.startMs,
33524
+ endMs: eventWindow.endMs
33525
+ });
33526
+ if (events.length === 0 && !rule.deliverEmpty) {
33527
+ this.deps.logger.info("calendar-events: empty horizon, not delivering", { meta: {
33528
+ ruleId: rule.id,
33529
+ horizonDays: eventWindow.horizonDays
33530
+ } });
33531
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33532
+ return "empty";
33533
+ }
33534
+ const sorted = [...events].toSorted((a, b) => a.startMs - b.startMs);
33535
+ await this.enqueue(rule, scheduleWindow, eventWindow, calendarIds, sorted);
33536
+ await this.deps.store.markGenerated(rule.id, scheduleWindow.endMs);
33537
+ return "sent";
33538
+ }
33539
+ async enqueue(rule, scheduleWindow, eventWindow, calendarIds, events) {
33540
+ const inputs = buildCalendarEventsOutboxInputs({
33541
+ ruleId: rule.id,
33542
+ ruleName: rule.name,
33543
+ scheduleWindow,
33544
+ eventWindow: {
33545
+ startMs: eventWindow.startMs,
33546
+ endMs: eventWindow.endMs
33547
+ },
33548
+ horizonDays: eventWindow.horizonDays,
33549
+ events,
33550
+ calendarIds,
33551
+ targets: rule.targets,
33552
+ priority: rule.priority,
33553
+ ...rule.template !== void 0 ? { template: rule.template } : {}
33554
+ });
33555
+ for (const input of inputs) await this.deps.outbox.enqueue(input);
33556
+ this.deps.logger.info("calendar-events delivered", { meta: {
33557
+ ruleId: rule.id,
33558
+ events: events.length,
33559
+ targets: inputs.length,
33560
+ windowEndMs: scheduleWindow.endMs
33561
+ } });
33562
+ }
33563
+ };
33564
+ //#endregion
32757
33565
  //#region src/notification-center/text-catalog-editor.ts
32758
33566
  var NcTextCatalogEditor = class {
32759
33567
  store;
@@ -35837,7 +36645,7 @@ function packagePhaseOf(ev) {
35837
36645
  */
35838
36646
  function historyRecordKind(kind) {
35839
36647
  if (kind === "audio-event" || kind === "audio-window") return "object-event";
35840
- if (kind === "timelapse" || kind === "ai-summary") return "system-event";
36648
+ if (kind === "timelapse" || kind === "ai-summary" || kind === "calendar-events") return "system-event";
35841
36649
  return kind;
35842
36650
  }
35843
36651
  /**
@@ -35852,7 +36660,8 @@ function historyRecordKind(kind) {
35852
36660
  function cooldownSubjectKind(kind) {
35853
36661
  switch (kind) {
35854
36662
  case "timelapse":
35855
- case "ai-summary": return "device-event";
36663
+ case "ai-summary":
36664
+ case "calendar-events": return "device-event";
35856
36665
  default: return kind;
35857
36666
  }
35858
36667
  }
@@ -35952,7 +36761,7 @@ function outboxEntryToHistory(texts, entry) {
35952
36761
  id: entry.id,
35953
36762
  ruleId: entry.ruleId,
35954
36763
  ruleName: entry.payload.ruleName,
35955
- delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" ? "system-event" : entry.payload.delivery,
36764
+ delivery: entry.payload.delivery === "timelapse" || entry.payload.delivery === "ai-summary" || entry.payload.delivery === "calendar-events" ? "system-event" : entry.payload.delivery,
35956
36765
  targetId: entry.targetId,
35957
36766
  deviceId: entry.deviceId,
35958
36767
  recordKind: historyRecordKind(entry.recordKind),
@@ -36162,6 +36971,9 @@ var NotificationCenter = class {
36162
36971
  */
36163
36972
  summaryRules;
36164
36973
  summaryProducer = null;
36974
+ /** Calendar-events digest rules + producer (hub-only producer). */
36975
+ calendarEventsRules;
36976
+ calendarEventsProducer = null;
36165
36977
  /**
36166
36978
  * What the deleted-device / deleted-target prune did, and why — its
36167
36979
  * disable-once guard and the source of both UIs' banner. Loaded on EVERY
@@ -37152,6 +37964,11 @@ var NotificationCenter = class {
37152
37964
  logger: this.logger.child("summary"),
37153
37965
  ...deps.now !== void 0 ? { now: deps.now } : {}
37154
37966
  });
37967
+ this.calendarEventsRules = new NcCalendarEventsStore({
37968
+ store: deps.store,
37969
+ logger: this.logger.child("calendar-events"),
37970
+ ...deps.now !== void 0 ? { now: deps.now } : {}
37971
+ });
37155
37972
  this.pruneLedger = new NcPruneLedger({
37156
37973
  store: deps.store,
37157
37974
  logger: this.logger.child("device-prune")
@@ -37198,6 +38015,14 @@ var NotificationCenter = class {
37198
38015
  zoneOwner: this.zoneOwners.lookup()
37199
38016
  });
37200
38017
  }
38018
+ const calendarsPort = deps.calendars;
38019
+ if (calendarsPort !== void 0) this.calendarEventsProducer = new NcCalendarEventsProducer({
38020
+ store: this.calendarEventsRules,
38021
+ outbox: this.outbox,
38022
+ calendars: calendarsPort,
38023
+ logger: this.logger.child("calendar-events"),
38024
+ now: this.now
38025
+ });
37201
38026
  }
37202
38027
  /**
37203
38028
  * The durable timelapse rule set — exposed for the `nc.*Timelapse*` bridge
@@ -37211,6 +38036,10 @@ var NotificationCenter = class {
37211
38036
  get summaryStore() {
37212
38037
  return this.summaryRules;
37213
38038
  }
38039
+ /** The durable calendar-events rule set — exposed for the `nc.*CalendarEvents*` bridge. */
38040
+ get calendarEventsStore() {
38041
+ return this.calendarEventsRules;
38042
+ }
37214
38043
  /**
37215
38044
  * Produce one digest window for one rule, right now.
37216
38045
  *
@@ -37284,6 +38113,7 @@ var NotificationCenter = class {
37284
38113
  await NcDeviceMuteStore.declare(store);
37285
38114
  await TimelapseStore.declare(store);
37286
38115
  await NcSummaryStore.declare(store);
38116
+ await NcCalendarEventsStore.declare(store);
37287
38117
  await NcRuleAiStore.declare(store);
37288
38118
  await NcTextCatalogStore.declare(store);
37289
38119
  await NcArtifactIndex.declare(store);
@@ -38698,7 +39528,18 @@ var NotificationCenter = class {
38698
39528
  },
38699
39529
  getAlarmConfig: async () => this.alarmConfig(),
38700
39530
  getAlarmSensors: async (input) => this.alarmSensors(input),
38701
- setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
39531
+ setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch),
39532
+ createAlarmProfile: async ({ name, cloneFromId }) => this.mutateAlarmProfile((panel) => panel.createProfile({
39533
+ name,
39534
+ newId: randomUUID(),
39535
+ source: cloneFromId === void 0 ? { kind: "defaults" } : {
39536
+ kind: "clone",
39537
+ fromId: cloneFromId
39538
+ }
39539
+ })),
39540
+ renameAlarmProfile: async ({ id, name }) => this.mutateAlarmProfile((panel) => panel.renameProfile(id, name)),
39541
+ deleteAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.deleteProfile(id)),
39542
+ activateAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.activateProfile(id))
38702
39543
  };
38703
39544
  }
38704
39545
  /**
@@ -38991,6 +39832,13 @@ var NotificationCenter = class {
38991
39832
  });
38992
39833
  return this.alarmConfig();
38993
39834
  }
39835
+ /** Profile CRUD — each mutation returns the whole config (settings applied). */
39836
+ async mutateAlarmProfile(run) {
39837
+ const panel = this.alarmPanel;
39838
+ if (panel === null) throw new Error("this node has no alarm panel");
39839
+ await run(panel);
39840
+ return this.alarmConfig();
39841
+ }
38994
39842
  /**
38995
39843
  * The panel as it is NOW — see `alarm-live.ts`.
38996
39844
  *
@@ -39087,6 +39935,7 @@ var NotificationCenter = class {
39087
39935
  return {
39088
39936
  deviceId: panel.deviceId,
39089
39937
  settings: panel.settings(),
39938
+ profiles: panel.profilesView(),
39090
39939
  live: await this.alarmLive(panel),
39091
39940
  coverage: this.coverageFor(panel).map((c) => ({
39092
39941
  ...c,
@@ -40906,11 +41755,17 @@ var NotificationCenter = class {
40906
41755
  await this.rules.load();
40907
41756
  this.noteInertGroupedRules();
40908
41757
  if (this.evaluationActive) this.deviceAlerts.tick();
40909
- if (this.evaluationActive) this.stateSummary.tick().catch((err) => {
40910
- this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
40911
- });
41758
+ if (this.evaluationActive) {
41759
+ this.stateSummary.tick().catch((err) => {
41760
+ this.logger.warn("state summary tick failed", { meta: { error: String(err) } });
41761
+ });
41762
+ this.calendarEventsProducer?.tick().catch((err) => {
41763
+ this.logger.warn("calendar-events tick failed", { meta: { error: String(err) } });
41764
+ });
41765
+ }
40912
41766
  await this.timelapseRules.load();
40913
41767
  await this.summaryRules.load();
41768
+ await this.calendarEventsRules.load();
40914
41769
  await this.ruleAi.load();
40915
41770
  await this.reloadTexts();
40916
41771
  this.refreshOccupancyWatch();
@@ -104721,6 +105576,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
104721
105576
  store: center.summaryStore,
104722
105577
  runNow: (input) => center.runSummaryNow(input)
104723
105578
  },
105579
+ calendarEvents: { store: center.calendarEventsStore },
104724
105580
  texts: center.textCatalogEditor,
104725
105581
  previewTexts: center.textCatalog,
104726
105582
  ...this.ncArtifactIndex !== null ? { artifacts: this.ncArtifactIndex } : {},
@@ -105908,7 +106764,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
105908
106764
  })).tracks;
105909
106765
  },
105910
106766
  ...this.isPostProcessingNode ? { timelapse: this.buildTimelapsePorts(api, stores) } : {},
105911
- ...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {}
106767
+ ...this.isPostProcessingNode ? { summary: this.buildSummaryPorts(api, stores) } : {},
106768
+ ...this.isPostProcessingNode ? { calendars: {
106769
+ listCalendars: async () => {
106770
+ return (await api.calendars.listCalendars.query()).map((c) => ({ id: c.id }));
106771
+ },
106772
+ listEvents: async (input) => api.calendars.listEvents.query({
106773
+ calendarIds: [...input.calendarIds],
106774
+ startMs: input.startMs,
106775
+ endMs: input.endMs
106776
+ })
106777
+ } } : {}
105912
106778
  });
105913
106779
  this.wireAlarmPanel(this.notificationCenter);
105914
106780
  await this.serveNcActionPlane(this.notificationCenter);