@camstack/addon-post-analysis 1.2.321 → 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 (14) hide show
  1. package/dist/{clip-model-registry-BCx3qCL5.js → clip-model-registry-BFbP9U7z.js} +338 -0
  2. package/dist/{clip-model-registry-Dnm9dfy0.mjs → clip-model-registry-CwK2SLQD.mjs} +285 -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-CPcZN3mA.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-CGUnHrR_.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mzYK6V9S.mjs} +1 -1
  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-Dt-jaGuG.mjs → hostInit-DtKO_7UT.mjs} +2 -2
  10. package/dist/pipeline-analytics/index.js +269 -3
  11. package/dist/pipeline-analytics/index.mjs +269 -3
  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_ui_mf_2_library__loadShare__.js-CVH5tT7I.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-BCx3qCL5.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-BFbP9U7z.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -1948,6 +1948,21 @@ var ncAlarmPanelSchema = require_clip_model_registry.object({
1948
1948
  exitDelaySec: require_clip_model_registry.number().optional(),
1949
1949
  entryDelaySec: require_clip_model_registry.number().optional(),
1950
1950
  triggeredDurationSec: require_clip_model_registry.number().optional()
1951
+ })).default([]),
1952
+ /**
1953
+ * Active alarm configuration profile id. Empty / absent on a blob older than
1954
+ * profiles → seeded to Default on first read via {@link ensureAlarmProfiles}.
1955
+ */
1956
+ activeProfileId: require_clip_model_registry.string().default(require_clip_model_registry.NC_ALARM_DEFAULT_PROFILE_ID),
1957
+ /**
1958
+ * Named settings blobs. Loose on purpose — each profile's settings are
1959
+ * re-validated through {@link settingsBlobFromPersisted} so a corrupt entry
1960
+ * drops rather than taking down the panel.
1961
+ */
1962
+ profiles: require_clip_model_registry.array(require_clip_model_registry.object({
1963
+ id: require_clip_model_registry.string(),
1964
+ name: require_clip_model_registry.string(),
1965
+ settings: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.unknown())
1951
1966
  })).default([])
1952
1967
  });
1953
1968
  /**
@@ -1970,6 +1985,106 @@ function modeDeviceListFromPersisted(raw) {
1970
1985
  return lists;
1971
1986
  }
1972
1987
  /**
1988
+ * Rebuild a trusted {@link NcAlarmSettings} from a persisted profile blob.
1989
+ * Unknown / corrupt fields fall back to system-safe empties via the flat-key
1990
+ * readers the panel already uses for the live settings.
1991
+ */
1992
+ function settingsBlobFromPersisted(raw) {
1993
+ const exit = typeof raw["exitDelaySec"] === "number" ? raw["exitDelaySec"] : DEFAULT_DELAYS.exitDelaySec;
1994
+ const entry = typeof raw["entryDelaySec"] === "number" ? raw["entryDelaySec"] : DEFAULT_DELAYS.entryDelaySec;
1995
+ const triggered = typeof raw["triggeredDurationSec"] === "number" ? raw["triggeredDurationSec"] : DEFAULT_DELAYS.triggeredDurationSec;
1996
+ const announceTargets = Array.isArray(raw["announceTargets"]) ? raw["announceTargets"].filter((t) => typeof t === "string") : [];
1997
+ const lists = (key) => {
1998
+ const v = raw[key];
1999
+ if (!Array.isArray(v)) return [];
2000
+ const rows = [];
2001
+ for (const item of v) {
2002
+ if (typeof item !== "object" || item === null) continue;
2003
+ const rec = item;
2004
+ if (typeof rec["mode"] !== "string" || !Array.isArray(rec["deviceIds"])) continue;
2005
+ rows.push({
2006
+ mode: rec["mode"],
2007
+ deviceIds: rec["deviceIds"].filter((id) => typeof id === "number")
2008
+ });
2009
+ }
2010
+ return modeDeviceListFromPersisted(rows);
2011
+ };
2012
+ const modeDelaysRaw = Array.isArray(raw["modeDelays"]) ? raw["modeDelays"] : [];
2013
+ const modeDelaysLoose = [];
2014
+ for (const item of modeDelaysRaw) {
2015
+ if (typeof item !== "object" || item === null) continue;
2016
+ const rec = item;
2017
+ if (typeof rec["mode"] !== "string") continue;
2018
+ const entry = { mode: rec["mode"] };
2019
+ if (typeof rec["exitDelaySec"] === "number") entry.exitDelaySec = rec["exitDelaySec"];
2020
+ if (typeof rec["entryDelaySec"] === "number") entry.entryDelaySec = rec["entryDelaySec"];
2021
+ if (typeof rec["triggeredDurationSec"] === "number") entry.triggeredDurationSec = rec["triggeredDurationSec"];
2022
+ modeDelaysLoose.push(entry);
2023
+ }
2024
+ return {
2025
+ exitDelaySec: Math.max(0, Math.min(600, Math.trunc(exit))),
2026
+ entryDelaySec: Math.max(0, Math.min(600, Math.trunc(entry))),
2027
+ triggeredDurationSec: Math.max(0, Math.min(3600, Math.trunc(triggered))),
2028
+ announceTargets,
2029
+ nonBlocking: lists("nonBlocking"),
2030
+ blocking: lists("blocking"),
2031
+ secureOnArmOff: lists("secureOnArmOff"),
2032
+ modeDelays: modeDelaysFromPersisted(modeDelaysLoose).entries
2033
+ };
2034
+ }
2035
+ function settingsToPersisted(settings) {
2036
+ return {
2037
+ exitDelaySec: settings.exitDelaySec,
2038
+ entryDelaySec: settings.entryDelaySec,
2039
+ triggeredDurationSec: settings.triggeredDurationSec,
2040
+ announceTargets: [...settings.announceTargets],
2041
+ nonBlocking: settings.nonBlocking.map((l) => ({
2042
+ mode: l.mode,
2043
+ deviceIds: [...l.deviceIds]
2044
+ })),
2045
+ blocking: settings.blocking.map((l) => ({
2046
+ mode: l.mode,
2047
+ deviceIds: [...l.deviceIds]
2048
+ })),
2049
+ secureOnArmOff: settings.secureOnArmOff.map((l) => ({
2050
+ mode: l.mode,
2051
+ deviceIds: [...l.deviceIds]
2052
+ })),
2053
+ modeDelays: settings.modeDelays.map((e) => ({ ...e }))
2054
+ };
2055
+ }
2056
+ function profilesStateFromPersisted(activeId, rows, current) {
2057
+ const profiles = rows.filter((r) => r.id.length > 0 && r.name.trim().length > 0).map((r) => ({
2058
+ id: r.id,
2059
+ name: r.name,
2060
+ settings: settingsBlobFromPersisted(r.settings)
2061
+ }));
2062
+ return require_clip_model_registry.ensureAlarmProfiles(profiles.length > 0 ? {
2063
+ activeId,
2064
+ profiles
2065
+ } : null, current);
2066
+ }
2067
+ function profilesToPersisted(state) {
2068
+ return {
2069
+ activeProfileId: state.activeId,
2070
+ profiles: state.profiles.map((p) => ({
2071
+ id: p.id,
2072
+ name: p.name,
2073
+ settings: settingsToPersisted(p.settings)
2074
+ }))
2075
+ };
2076
+ }
2077
+ function profilesViewOf(state) {
2078
+ return {
2079
+ activeId: state.activeId,
2080
+ items: state.profiles.map((p) => ({
2081
+ id: p.id,
2082
+ name: p.name,
2083
+ isDefault: require_clip_model_registry.isDefaultAlarmProfileId(p.id)
2084
+ }))
2085
+ };
2086
+ }
2087
+ /**
1973
2088
  * The log line for a trigger the state machine ignored, per state. `trigger`
1974
2089
  * ignores four: disarmed, arming (the exit delay), and pending / triggered
1975
2090
  * (already counting down or going off — a second sensor must not restart it).
@@ -2217,7 +2332,134 @@ var NcAlarmPanelDevice = class extends require_clip_model_registry.BaseDevice {
2217
2332
  changed: membershipChanged
2218
2333
  }
2219
2334
  });
2220
- return this.settings();
2335
+ const settled = this.settings();
2336
+ await this.persistProfilesState(require_clip_model_registry.syncActiveAlarmProfileSettings(this.readProfilesState(), settled));
2337
+ return settled;
2338
+ }
2339
+ /** Named profiles + which one is active — for {@link NcAlarmConfig.profiles}. */
2340
+ profilesView() {
2341
+ return profilesViewOf(this.readProfilesState());
2342
+ }
2343
+ /**
2344
+ * Create a named profile (system defaults, or clone). Does not activate.
2345
+ * `newId` is assigned by the caller (usually `randomUUID`).
2346
+ */
2347
+ async createProfile(input) {
2348
+ const result = require_clip_model_registry.createAlarmProfile(this.readProfilesState(), {
2349
+ name: input.name,
2350
+ id: input.newId,
2351
+ source: input.source
2352
+ });
2353
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2354
+ await this.persistProfilesState(result.state);
2355
+ this.ctx.logger.info("alarm profile created", {
2356
+ tags: { deviceId: this.id },
2357
+ meta: {
2358
+ profileId: input.newId,
2359
+ name: input.name.trim(),
2360
+ source: input.source.kind
2361
+ }
2362
+ });
2363
+ return profilesViewOf(result.state);
2364
+ }
2365
+ async renameProfile(id, name) {
2366
+ const result = require_clip_model_registry.renameAlarmProfile(this.readProfilesState(), {
2367
+ id,
2368
+ name
2369
+ });
2370
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2371
+ await this.persistProfilesState(result.state);
2372
+ this.ctx.logger.info("alarm profile renamed", {
2373
+ tags: { deviceId: this.id },
2374
+ meta: {
2375
+ profileId: id,
2376
+ name: name.trim()
2377
+ }
2378
+ });
2379
+ return profilesViewOf(result.state);
2380
+ }
2381
+ /**
2382
+ * Delete a non-default profile. If it was active, Default becomes active and
2383
+ * its settings are applied to the flat keys (arm path).
2384
+ */
2385
+ async deleteProfile(id) {
2386
+ const before = this.readProfilesState();
2387
+ const result = require_clip_model_registry.deleteAlarmProfile(before, id);
2388
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2389
+ const switched = before.activeId === id;
2390
+ await this.persistProfilesState(result.state);
2391
+ if (switched) {
2392
+ const def = result.state.profiles.find((p) => p.id === require_clip_model_registry.NC_ALARM_DEFAULT_PROFILE_ID);
2393
+ if (def !== void 0) await this.replaceFlatSettings(def.settings);
2394
+ }
2395
+ this.ctx.logger.info("alarm profile deleted", {
2396
+ tags: { deviceId: this.id },
2397
+ meta: {
2398
+ profileId: id,
2399
+ activatedDefault: switched
2400
+ }
2401
+ });
2402
+ return profilesViewOf(result.state);
2403
+ }
2404
+ /**
2405
+ * Make a profile active and apply its blob onto the flat settings the arm
2406
+ * path reads.
2407
+ */
2408
+ async activateProfile(id) {
2409
+ const result = require_clip_model_registry.activateAlarmProfile(require_clip_model_registry.syncActiveAlarmProfileSettings(this.readProfilesState(), this.settings()), id);
2410
+ if (result.kind === "refused") throw this.profilesRefuseError(result.reason, result.detail);
2411
+ const next = result.state.profiles.find((p) => p.id === id);
2412
+ if (next === void 0) throw this.profilesRefuseError("not-found", id);
2413
+ await this.persistProfilesState(result.state);
2414
+ await this.replaceFlatSettings(next.settings);
2415
+ this.ctx.logger.info("alarm profile activated", {
2416
+ tags: { deviceId: this.id },
2417
+ meta: {
2418
+ profileId: id,
2419
+ name: next.name
2420
+ }
2421
+ });
2422
+ return profilesViewOf(result.state);
2423
+ }
2424
+ readProfilesState() {
2425
+ return profilesStateFromPersisted(this.config.get("activeProfileId"), this.config.get("profiles"), this.settings());
2426
+ }
2427
+ async persistProfilesState(state) {
2428
+ const persisted = profilesToPersisted(state);
2429
+ await Promise.all([this.config.set("activeProfileId", persisted.activeProfileId), this.config.set("profiles", persisted.profiles)]);
2430
+ }
2431
+ /** Write a full settings blob onto the flat keys the arm path reads. */
2432
+ async replaceFlatSettings(settings) {
2433
+ const blob = require_clip_model_registry.cloneAlarmSettings(settings);
2434
+ await Promise.all([
2435
+ this.config.set("exitDelaySec", blob.exitDelaySec),
2436
+ this.config.set("entryDelaySec", blob.entryDelaySec),
2437
+ this.config.set("triggeredDurationSec", blob.triggeredDurationSec),
2438
+ this.config.set("announceTargets", [...blob.announceTargets]),
2439
+ this.config.set("nonBlocking", blob.nonBlocking.map((l) => ({
2440
+ mode: l.mode,
2441
+ deviceIds: [...l.deviceIds]
2442
+ }))),
2443
+ this.config.set("blocking", blob.blocking.map((l) => ({
2444
+ mode: l.mode,
2445
+ deviceIds: [...l.deviceIds]
2446
+ }))),
2447
+ this.config.set("secureOnArmOff", blob.secureOnArmOff.map((l) => ({
2448
+ mode: l.mode,
2449
+ deviceIds: [...l.deviceIds]
2450
+ }))),
2451
+ this.config.set("modeDelays", blob.modeDelays.map((e) => ({ ...e })))
2452
+ ]);
2453
+ }
2454
+ profilesRefuseError(reason, detail) {
2455
+ switch (reason) {
2456
+ case "name-empty": return /* @__PURE__ */ new Error("alarm profile name must not be empty");
2457
+ case "name-taken": return /* @__PURE__ */ new Error(`an alarm profile named '${detail ?? ""}' already exists`);
2458
+ case "cannot-delete-default": return /* @__PURE__ */ new Error("the Default alarm profile cannot be deleted");
2459
+ case "clone-source-missing": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found to clone`);
2460
+ case "not-found": return /* @__PURE__ */ new Error(`alarm profile '${detail ?? ""}' was not found`);
2461
+ default: return /* @__PURE__ */ new Error(`alarm profile operation refused (${reason})`);
2462
+ }
2221
2463
  }
2222
2464
  /**
2223
2465
  * Refuse a mode this panel does not offer — logged by reason, then thrown. A membership edit
@@ -2950,6 +3192,11 @@ function ncAlarmPanelPort(panel) {
2950
3192
  deviceId: panel.id,
2951
3193
  settings: () => panel.settings(),
2952
3194
  applySettings: (patch) => panel.applySettings(patch),
3195
+ profilesView: () => panel.profilesView(),
3196
+ createProfile: (input) => panel.createProfile(input),
3197
+ renameProfile: (id, name) => panel.renameProfile(id, name),
3198
+ deleteProfile: (id) => panel.deleteProfile(id),
3199
+ activateProfile: (id) => panel.activateProfile(id),
2953
3200
  availableModes: () => NC_ALARM_MODES,
2954
3201
  noteRuleTrigger: (source) => panel.noteRuleTrigger(source),
2955
3202
  excludedDevices: () => panel.excludedDevices(),
@@ -39304,7 +39551,18 @@ var NotificationCenter = class {
39304
39551
  },
39305
39552
  getAlarmConfig: async () => this.alarmConfig(),
39306
39553
  getAlarmSensors: async (input) => this.alarmSensors(input),
39307
- setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch)
39554
+ setAlarmConfig: async ({ patch }) => this.setAlarmConfig(patch),
39555
+ createAlarmProfile: async ({ name, cloneFromId }) => this.mutateAlarmProfile((panel) => panel.createProfile({
39556
+ name,
39557
+ newId: (0, node_crypto.randomUUID)(),
39558
+ source: cloneFromId === void 0 ? { kind: "defaults" } : {
39559
+ kind: "clone",
39560
+ fromId: cloneFromId
39561
+ }
39562
+ })),
39563
+ renameAlarmProfile: async ({ id, name }) => this.mutateAlarmProfile((panel) => panel.renameProfile(id, name)),
39564
+ deleteAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.deleteProfile(id)),
39565
+ activateAlarmProfile: async ({ id }) => this.mutateAlarmProfile((panel) => panel.activateProfile(id))
39308
39566
  };
39309
39567
  }
39310
39568
  /**
@@ -39597,6 +39855,13 @@ var NotificationCenter = class {
39597
39855
  });
39598
39856
  return this.alarmConfig();
39599
39857
  }
39858
+ /** Profile CRUD — each mutation returns the whole config (settings applied). */
39859
+ async mutateAlarmProfile(run) {
39860
+ const panel = this.alarmPanel;
39861
+ if (panel === null) throw new Error("this node has no alarm panel");
39862
+ await run(panel);
39863
+ return this.alarmConfig();
39864
+ }
39600
39865
  /**
39601
39866
  * The panel as it is NOW — see `alarm-live.ts`.
39602
39867
  *
@@ -39693,6 +39958,7 @@ var NotificationCenter = class {
39693
39958
  return {
39694
39959
  deviceId: panel.deviceId,
39695
39960
  settings: panel.settings(),
39961
+ profiles: panel.profilesView(),
39696
39962
  live: await this.alarmLive(panel),
39697
39963
  coverage: this.coverageFor(panel).map((c) => ({
39698
39964
  ...c,
@@ -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-Dnm9dfy0.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(),
@@ -39281,7 +39528,18 @@ var NotificationCenter = class {
39281
39528
  },
39282
39529
  getAlarmConfig: async () => this.alarmConfig(),
39283
39530
  getAlarmSensors: async (input) => this.alarmSensors(input),
39284
- 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))
39285
39543
  };
39286
39544
  }
39287
39545
  /**
@@ -39574,6 +39832,13 @@ var NotificationCenter = class {
39574
39832
  });
39575
39833
  return this.alarmConfig();
39576
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
+ }
39577
39842
  /**
39578
39843
  * The panel as it is NOW — see `alarm-live.ts`.
39579
39844
  *
@@ -39670,6 +39935,7 @@ var NotificationCenter = class {
39670
39935
  return {
39671
39936
  deviceId: panel.deviceId,
39672
39937
  settings: panel.settings(),
39938
+ profiles: panel.profilesView(),
39673
39939
  live: await this.alarmLive(panel),
39674
39940
  coverage: this.coverageFor(panel).map((c) => ({
39675
39941
  ...c,