@camstack/addon-post-analysis 1.2.33 → 1.2.35

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.
@@ -9454,6 +9454,100 @@ function shallowEqual(a, b) {
9454
9454
  }
9455
9455
  new Set(["devices", "classes"]);
9456
9456
  /**
9457
+ * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9458
+ * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9459
+ * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9460
+ * arming / pending / triggered / disarming.
9461
+ *
9462
+ * Many panels require a PIN code on arm / disarm — the optional
9463
+ * `code` field on the methods passes it through to the upstream
9464
+ * service; it's NEVER persisted in the runtime slice or any event
9465
+ * payload. The presence of a required code is signalled by
9466
+ * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9467
+ * field without a slice fetch.
9468
+ *
9469
+ * `availableModes` mirrors HA's `supported_features`-derived arm
9470
+ * mode list — the UI renders only the buttons the panel accepts.
9471
+ */
9472
+ var AlarmStateSchema = _enum([
9473
+ "disarmed",
9474
+ "armed_home",
9475
+ "armed_away",
9476
+ "armed_night",
9477
+ "armed_vacation",
9478
+ "armed_custom_bypass",
9479
+ "arming",
9480
+ "disarming",
9481
+ "pending",
9482
+ "triggered"
9483
+ ]);
9484
+ var AlarmArmModeSchema = _enum([
9485
+ "home",
9486
+ "away",
9487
+ "night",
9488
+ "vacation",
9489
+ "custom_bypass"
9490
+ ]);
9491
+ var AlarmPanelStatusSchema = object({
9492
+ /** Current lifecycle state. */
9493
+ state: AlarmStateSchema,
9494
+ /** Subset of arm modes the panel accepts. UI renders one button per
9495
+ * mode in this list. */
9496
+ availableModes: array(AlarmArmModeSchema),
9497
+ /** Whether the panel requires a PIN on arm / disarm. Mirrors
9498
+ * `DeviceFeature.AlarmPinRequired` for slice consumers. */
9499
+ requiresCode: boolean(),
9500
+ /** Ms epoch when the slice was last updated. */
9501
+ lastChangedAt: number()
9502
+ });
9503
+ var alarmPanelCapability = {
9504
+ name: "alarm-panel",
9505
+ scope: "device",
9506
+ deviceNative: true,
9507
+ mode: "singleton",
9508
+ deviceTypes: [DeviceType.AlarmPanel],
9509
+ methods: {
9510
+ arm: method(object({
9511
+ deviceId: number().int().nonnegative(),
9512
+ mode: AlarmArmModeSchema,
9513
+ /** Optional PIN code. Required when `requiresCode === true`.
9514
+ * Passed through to the upstream service; never persisted. */
9515
+ code: string().min(1).optional()
9516
+ }), _void(), {
9517
+ kind: "mutation",
9518
+ auth: "admin"
9519
+ }),
9520
+ disarm: method(object({
9521
+ deviceId: number().int().nonnegative(),
9522
+ code: string().min(1).optional()
9523
+ }), _void(), {
9524
+ kind: "mutation",
9525
+ auth: "admin"
9526
+ }),
9527
+ /**
9528
+ * Force the panel into the `triggered` state — used by HA
9529
+ * automations to surface external sensor events through the panel
9530
+ * (e.g. a Reolink camera intrusion event firing the security
9531
+ * system). Provider rejects when the panel hardware doesn't
9532
+ * support a software-initiated trigger.
9533
+ */
9534
+ trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
9535
+ kind: "mutation",
9536
+ auth: "admin"
9537
+ })
9538
+ },
9539
+ status: {
9540
+ schema: AlarmPanelStatusSchema,
9541
+ kind: "push"
9542
+ },
9543
+ /**
9544
+ * Runtime-state slice — mirrored by the kernel. UI panel reads the
9545
+ * full slice; renders an arm button per `availableModes` entry and
9546
+ * a PIN field iff `requiresCode === true`.
9547
+ */
9548
+ runtimeState: AlarmPanelStatusSchema
9549
+ };
9550
+ /**
9457
9551
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9458
9552
  * motion-zones, and the detection zones/lines editor all speak this one
9459
9553
  * language so a single drawing-plane editor and the providers stay
@@ -9515,6 +9609,276 @@ var MaskGridDimsSchema = object({
9515
9609
  height: number()
9516
9610
  });
9517
9611
  /**
9612
+ * notification-output — canonical, capability-gated notification delivery.
9613
+ *
9614
+ * Apprise-derived model (see
9615
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
9616
+ * callers emit ONE canonical `Notification`; each provider declares a
9617
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
9618
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
9619
+ * message to what the kind supports — callers never special-case a service.
9620
+ *
9621
+ * DESIGN DECISIONS (locked):
9622
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
9623
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
9624
+ * cap. Rationale: the admin UI needs one uniform surface across the
9625
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
9626
+ * alternative would fork the UI per addon and cannot host the
9627
+ * discovery→adopt flow.
9628
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
9629
+ * the generated cap-mount auto-`concatCollection`-fans them across every
9630
+ * registered provider (notifiers addon + HA addon) so one catalog is
9631
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
9632
+ * `addonId` the generated collection router extracts from the call input.
9633
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
9634
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
9635
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
9636
+ * base64 fallback needed.
9637
+ *
9638
+ * TODO (deferred, closed-set change — separate decision): add
9639
+ * `providerKind: 'notify'` so notification providers surface on the unified
9640
+ * admin "Integrations" page.
9641
+ */
9642
+ /**
9643
+ * Zentik-derived typed-media enum — the superset across every kind. Each
9644
+ * adapter picks what it supports and the degrade engine filters the rest.
9645
+ */
9646
+ var AttachmentMediaTypeSchema = _enum([
9647
+ "image",
9648
+ "video",
9649
+ "gif",
9650
+ "audio",
9651
+ "icon"
9652
+ ]);
9653
+ /**
9654
+ * A single attachment. Exactly one of `url` (remote source, most adapters
9655
+ * prefer this) or `bytes` (inline source; required for Pushover-style
9656
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
9657
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
9658
+ */
9659
+ var AttachmentSchema = object({
9660
+ mediaType: AttachmentMediaTypeSchema,
9661
+ url: string().optional(),
9662
+ bytes: _instanceof(Uint8Array).optional(),
9663
+ mime: string().optional(),
9664
+ name: string().optional()
9665
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
9666
+ var NotificationFormatSchema = _enum([
9667
+ "text",
9668
+ "markdown",
9669
+ "html"
9670
+ ]);
9671
+ /**
9672
+ * The CLOSED icon vocabulary an action button may use.
9673
+ *
9674
+ * A closed set, not a free string, and that is the whole point: an arbitrary
9675
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
9676
+ * notices — the same class of gap as a zone vocabulary nothing produced
9677
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
9678
+ * declares `actionIcons: false` and the degrade engine strips the field.
9679
+ *
9680
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
9681
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
9682
+ * differently.
9683
+ */
9684
+ var NotificationActionIconSchema = _enum([
9685
+ "acknowledge",
9686
+ "dismiss",
9687
+ "silence",
9688
+ "view",
9689
+ "play",
9690
+ "open",
9691
+ "close",
9692
+ "lock",
9693
+ "unlock",
9694
+ "arm",
9695
+ "disarm",
9696
+ "light",
9697
+ "alert"
9698
+ ]);
9699
+ /** A single tap-through action button. */
9700
+ var NotificationActionSchema = object({
9701
+ id: string(),
9702
+ label: string(),
9703
+ url: string().optional(),
9704
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
9705
+ icon: NotificationActionIconSchema.optional(),
9706
+ /**
9707
+ * Renders in a warning style where the notifier supports it.
9708
+ *
9709
+ * A HINT, never a gate. The callback's authority is its token and nothing
9710
+ * else — see `notification-center/action-token.ts` for what that does and
9711
+ * does not buy.
9712
+ */
9713
+ destructive: boolean().optional()
9714
+ });
9715
+ /**
9716
+ * The canonical notification. `body` is the only hard field (Apprise model).
9717
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
9718
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
9719
+ * the adapter maps this ordinal onto its native level. `level?` is an
9720
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
9721
+ * `priority` for that one target.
9722
+ */
9723
+ var NotificationSchema = object({
9724
+ body: string(),
9725
+ title: string().optional(),
9726
+ format: NotificationFormatSchema.default("text"),
9727
+ priority: number().int().min(1).max(5).default(3),
9728
+ level: string().optional(),
9729
+ attachments: array(AttachmentSchema).optional(),
9730
+ clickUrl: string().optional(),
9731
+ actions: array(NotificationActionSchema).optional(),
9732
+ sound: string().optional(),
9733
+ ttl: number().optional(),
9734
+ tag: string().optional(),
9735
+ deviceId: number().optional(),
9736
+ eventId: string().optional(),
9737
+ metadata: record(string(), unknown()).optional()
9738
+ });
9739
+ /** One declared native severity/priority level for a kind. */
9740
+ var TargetKindLevelSchema = object({
9741
+ id: string(),
9742
+ label: string(),
9743
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
9744
+ ordinal: number().int().min(1).max(5).nullable(),
9745
+ flags: object({
9746
+ critical: boolean().optional(),
9747
+ silent: boolean().optional(),
9748
+ noPush: boolean().optional()
9749
+ }).optional(),
9750
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
9751
+ requires: array(string()).optional(),
9752
+ description: string().optional()
9753
+ });
9754
+ /** The full capability block consulted before dispatch. */
9755
+ var TargetKindCapsSchema = object({
9756
+ attachments: object({
9757
+ mediaTypes: array(AttachmentMediaTypeSchema),
9758
+ mode: _enum([
9759
+ "url",
9760
+ "bytes",
9761
+ "both"
9762
+ ]),
9763
+ max: number().int().nonnegative(),
9764
+ maxBytes: number().int().positive().optional()
9765
+ }),
9766
+ /** Max action buttons (0 = none). */
9767
+ actions: number().int().nonnegative(),
9768
+ /**
9769
+ * Whether this kind renders a per-action ICON.
9770
+ *
9771
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
9772
+ * run on the addon cap path — three production failures in one day taught
9773
+ * this repo that once. Absent is read as false by the degrade engine, which
9774
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
9775
+ * assumed and dropped costs the operator's trust in the field.
9776
+ */
9777
+ actionIcons: boolean().optional(),
9778
+ levels: array(TargetKindLevelSchema),
9779
+ format: array(NotificationFormatSchema),
9780
+ clickUrl: boolean(),
9781
+ sound: boolean(),
9782
+ ttl: boolean(),
9783
+ bodyMaxLen: number().int().positive()
9784
+ });
9785
+ /**
9786
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
9787
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
9788
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
9789
+ * the union is large and not meant for runtime validation here; the exported
9790
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
9791
+ */
9792
+ var ConfigSchemaPassthrough$1 = unknown();
9793
+ var TargetKindSchema = object({
9794
+ kind: string(),
9795
+ label: string(),
9796
+ icon: string(),
9797
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
9798
+ addonId: string(),
9799
+ /**
9800
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
9801
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
9802
+ * when the addon bundles no icon for that kind — the client then falls back
9803
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
9804
+ *
9805
+ * Root-relative on purpose: it resolves against whatever origin serves a web
9806
+ * client, and a native client joins it onto its own hub base.
9807
+ *
9808
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
9809
+ * field that survived only because the runtime cap-router forwards provider
9810
+ * output verbatim — so every consumer had to re-declare it by hand to stop
9811
+ * its own Zod parse from stripping it, and the whole arrangement would have
9812
+ * broken silently the moment output validation was tightened anywhere.
9813
+ */
9814
+ iconUrl: string().optional(),
9815
+ /**
9816
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
9817
+ *
9818
+ * The server knows this and therefore says it, because the client cannot
9819
+ * safely guess: a React-Native client renders SVG and raster through two
9820
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
9821
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
9822
+ * placeholder glyph for every vector icon while the web build looked fine.
9823
+ *
9824
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
9825
+ * not been updated — a client that cannot determine the type should prefer
9826
+ * its raster path, which is the safe default for an unknown image.
9827
+ */
9828
+ iconMediaType: string().optional(),
9829
+ configSchema: ConfigSchemaPassthrough$1,
9830
+ supportsDiscovery: boolean(),
9831
+ caps: TargetKindCapsSchema
9832
+ });
9833
+ /**
9834
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
9835
+ * (return a presence marker only) when serving `listTargets` — never
9836
+ * round-trip a stored secret to the UI.
9837
+ */
9838
+ var TargetSchema = object({
9839
+ id: string(),
9840
+ name: string(),
9841
+ kind: string(),
9842
+ addonId: string(),
9843
+ enabled: boolean(),
9844
+ config: record(string(), unknown())
9845
+ });
9846
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
9847
+ var DiscoveredTargetSchema = object({
9848
+ kind: string(),
9849
+ suggestedName: string(),
9850
+ config: record(string(), unknown())
9851
+ });
9852
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
9853
+ var RenderedAsSchema = object({
9854
+ level: string(),
9855
+ format: NotificationFormatSchema,
9856
+ attachmentsSent: number().int().nonnegative(),
9857
+ actionsSent: number().int().nonnegative(),
9858
+ truncated: boolean(),
9859
+ dropped: array(string())
9860
+ });
9861
+ var SendResultSchema = object({
9862
+ success: boolean(),
9863
+ error: string().optional(),
9864
+ renderedAs: RenderedAsSchema.optional()
9865
+ });
9866
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
9867
+ var TestResultSchema = SendResultSchema;
9868
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
9869
+ kind: string(),
9870
+ config: record(string(), unknown()).optional()
9871
+ }), array(DiscoveredTargetSchema)), method(object({
9872
+ targetId: string(),
9873
+ notification: NotificationSchema
9874
+ }), SendResultSchema, { kind: "mutation" }), method(object({
9875
+ targetId: string(),
9876
+ sample: NotificationSchema.optional()
9877
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
9878
+ targetId: string(),
9879
+ enabled: boolean()
9880
+ }), _void(), { kind: "mutation" });
9881
+ /**
9518
9882
  * notification-rules — the Notification Center rule surface (P1 core).
9519
9883
  *
9520
9884
  * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
@@ -9684,6 +10048,41 @@ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9684
10048
  args: record(string(), unknown()).optional()
9685
10049
  })]);
9686
10050
  /**
10051
+ * A named, ordered run of steps with its own throttle.
10052
+ *
10053
+ * `minDelaySec` exists because a noisy rule otherwise hammers a physical
10054
+ * actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
10055
+ * different budget from "how often may this gate actually open".
10056
+ */
10057
+ var NcRuleActionSequenceSchema = object({
10058
+ name: string().min(1).max(120),
10059
+ enabled: boolean(),
10060
+ minDelaySec: number().int().min(0).max(86400).optional(),
10061
+ actions: array(NcRuleActionSchema).min(1)
10062
+ });
10063
+ /**
10064
+ * One button carried by the notification, running a named sequence on tap.
10065
+ *
10066
+ * **Read this before adding a button that does something physical.** The tap
10067
+ * arrives over a link that travelled through third-party infrastructure — ntfy,
10068
+ * a push relay, whatever forwarded the message — and the callback's ONLY
10069
+ * authority is the token in that link: single-use, short-lived, bound to this
10070
+ * one action of this one notification. It does not identify who tapped.
10071
+ * Whoever holds the notification can run the button, once, inside the window.
10072
+ * That is the operator's explicit choice (2026-08-05), and `destructive` is a
10073
+ * rendering hint, not a second gate. [D47](decisions/adr-0047.md).
10074
+ */
10075
+ var NcRuleNotificationButtonSchema = object({
10076
+ /** Stable id — travels in the callback and identifies the button in logs. */
10077
+ id: string().min(1).max(64),
10078
+ label: string().min(1).max(40),
10079
+ /** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
10080
+ * sequence does not exist rather than minting a token for nothing. */
10081
+ sequence: string().min(1).max(120),
10082
+ icon: NotificationActionIconSchema.optional(),
10083
+ destructive: boolean().optional()
10084
+ });
10085
+ /**
9687
10086
  * Sequences a rule runs, by hook point.
9688
10087
  *
9689
10088
  * ONLY `onTrigger` is here, deliberately. The reference also has activation /
@@ -9691,14 +10090,26 @@ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9691
10090
  * repo's expensive failure mode is declaring a surface nothing produces, so a
9692
10091
  * hook appears here in the same change that produces its edge, never before.
9693
10092
  */
9694
- var NcRuleActionsSchema = object({
9695
- /** Runs when the rule MATCHES. */
9696
- onTrigger: array(object({
9697
- name: string().min(1).max(120),
9698
- enabled: boolean(),
9699
- minDelaySec: number().int().min(0).max(86400).optional(),
9700
- actions: array(NcRuleActionSchema).min(1)
9701
- })).optional() });
10093
+ var NcRuleActionsSchema = object({
10094
+ /** Runs when the rule MATCHES. */
10095
+ onTrigger: array(NcRuleActionSequenceSchema).optional(),
10096
+ /**
10097
+ * Buttons the NOTIFICATION carries, each running one of this rule's
10098
+ * sequences when tapped.
10099
+ *
10100
+ * Deliberately a REFERENCE to a sequence rather than a second place to
10101
+ * author steps. A button that could define its own actions would be a
10102
+ * parallel actuation vocabulary — the executor's device-scope check, the
10103
+ * stop-at-first-failure rule and the per-sequence throttle all live on
10104
+ * sequences, and a second authoring surface would drift from every one of
10105
+ * them.
10106
+ *
10107
+ * A sequence reachable ONLY by a button simply appears in `onTrigger` with
10108
+ * `enabled: false`: it is then authored, throttled and validated like the
10109
+ * rest, and nothing runs it automatically.
10110
+ */
10111
+ buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
10112
+ });
9702
10113
  var NcConditionsSchema = object({
9703
10114
  /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
9704
10115
  deviceState: object({
@@ -10125,7 +10536,7 @@ var NC_CONDITION_CATALOG = [
10125
10536
  {
10126
10537
  id: "devices",
10127
10538
  group: "scope",
10128
- label: "Cameras",
10539
+ label: "Devices",
10129
10540
  valueType: "deviceIdList",
10130
10541
  operator: "in",
10131
10542
  appliesTo: [
@@ -10135,7 +10546,7 @@ var NC_CONDITION_CATALOG = [
10135
10546
  "package-event"
10136
10547
  ],
10137
10548
  phase: "P1",
10138
- description: "Restrict the rule to these devices; absent = all devices."
10549
+ description: "Restrict the rule to these devices; absent = all devices. A sensor event matches either the SENSOR that fired or the camera it is attributed to, so a rule may name either one."
10139
10550
  },
10140
10551
  {
10141
10552
  id: "classes",
@@ -10590,6 +11001,67 @@ var NcSnoozeSuppressedSchema = object({
10590
11001
  firstAt: number(),
10591
11002
  lastAt: number()
10592
11003
  });
11004
+ /**
11005
+ * The three durations the panel's state machine runs on, plus who hears about
11006
+ * an arm.
11007
+ *
11008
+ * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
11009
+ * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
11010
+ * else's panel, which has its own delays and no way to be told these. This is
11011
+ * configuration of the panel CamStack owns, and the Notification Center owns
11012
+ * that panel.
11013
+ */
11014
+ var NcAlarmSettingsSchema = object({
11015
+ /** Grace period between arming and the mode taking effect. 0 = immediate. */
11016
+ exitDelaySec: number().int().min(0).max(600),
11017
+ /** Grace period between a trigger and the alarm firing. 0 = immediate. */
11018
+ entryDelaySec: number().int().min(0).max(600),
11019
+ /**
11020
+ * How long `triggered` lasts before the panel re-arms itself.
11021
+ * **0 = until an operator disarms it** — the behaviour before this field
11022
+ * existed, and therefore what an untouched install keeps doing.
11023
+ */
11024
+ triggeredDurationSec: number().int().min(0).max(3600),
11025
+ /** Send a notification when a mode takes effect. */
11026
+ announceArm: boolean(),
11027
+ /**
11028
+ * Where that notification goes. Target ids from `notification-output`.
11029
+ *
11030
+ * Explicit rather than "everyone": an arm announcement is a household
11031
+ * message, and broadcasting it to every configured endpoint (including a
11032
+ * webhook wired to something else) is not a default anybody would choose.
11033
+ * Empty with `announceArm: true` sends nothing, and the server logs that —
11034
+ * silence must be attributable.
11035
+ */
11036
+ announceTargets: array(string().min(1)).max(16)
11037
+ });
11038
+ /** Every field optional — a tab edits one control at a time. */
11039
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
11040
+ /**
11041
+ * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
11042
+ * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
11043
+ * copy would lie the first time a rule is disabled.
11044
+ */
11045
+ var NcAlarmModeCoverageSchema = object({
11046
+ mode: AlarmArmModeSchema,
11047
+ /** Enabled rules gated on `armed_<mode>`. Zero means the mode does nothing. */
11048
+ ruleCount: number().int().min(0),
11049
+ /** At least one covering rule has no device scope, so the mode covers all. */
11050
+ allDevices: boolean(),
11051
+ /** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
11052
+ deviceIds: array(number().int())
11053
+ });
11054
+ var NcAlarmConfigSchema = object({
11055
+ /**
11056
+ * The panel's device id, or null when this install has no panel (the ensure
11057
+ * step failed, or this is not the hub). Null is the honest answer: an editor
11058
+ * that rendered delays for a panel that does not exist would be a form whose
11059
+ * Save does nothing.
11060
+ */
11061
+ deviceId: number().int().nullable(),
11062
+ settings: NcAlarmSettingsSchema,
11063
+ coverage: array(NcAlarmModeCoverageSchema)
11064
+ });
10593
11065
  var notificationRulesCapability = {
10594
11066
  name: "notification-rules",
10595
11067
  scope: "system",
@@ -10685,6 +11157,26 @@ var notificationRulesCapability = {
10685
11157
  cancelSnooze: method(object({ snoozeId: string() }), object({ success: literal(true) }), {
10686
11158
  kind: "mutation",
10687
11159
  caller: "required"
11160
+ }),
11161
+ /**
11162
+ * The alarm panel's durations, plus what each mode ACTUALLY arms.
11163
+ *
11164
+ * Coverage is returned by the same call as the settings on purpose: they
11165
+ * are read together or not at all. An editor that showed the delays
11166
+ * without the coverage would let an operator tune an exit delay for a mode
11167
+ * that arms nothing, which is the failure this whole surface exists to
11168
+ * make visible.
11169
+ */
11170
+ getAlarmConfig: method(object({}), NcAlarmConfigSchema, { auth: "admin" }),
11171
+ /**
11172
+ * Patch the durations. Returns the WHOLE config, coverage included, so a
11173
+ * client never has to guess what the server settled on — the panel clamps
11174
+ * and normalises, and a form that re-rendered from its own input would
11175
+ * show a value the alarm is not using.
11176
+ */
11177
+ setAlarmConfig: method(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
11178
+ kind: "mutation",
11179
+ auth: "admin"
10688
11180
  })
10689
11181
  }
10690
11182
  };
@@ -10930,116 +11422,22 @@ var AirQualitySensorStatusSchema = object({
10930
11422
  unit: string().optional(),
10931
11423
  /** Suggested decimal places for numeric display.
10932
11424
  * Populated live from the upstream source when provided (e.g. HA
10933
- * `attributes.suggested_display_precision`). Falls back to
10934
- * auto-formatting when absent. */
10935
- precision: number().int().min(0).max(10).optional()
10936
- });
10937
- var airQualitySensorCapability = {
10938
- name: "air-quality-sensor",
10939
- scope: "device",
10940
- deviceNative: true,
10941
- mode: "singleton",
10942
- deviceTypes: [DeviceType.Sensor],
10943
- methods: {},
10944
- status: {
10945
- schema: AirQualitySensorStatusSchema,
10946
- kind: "push"
10947
- },
10948
- runtimeState: AirQualitySensorStatusSchema
10949
- };
10950
- /**
10951
- * Alarm-panel cap. Models HA `alarm_control_panel.*` on
10952
- * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
10953
- * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
10954
- * arming / pending / triggered / disarming.
10955
- *
10956
- * Many panels require a PIN code on arm / disarm — the optional
10957
- * `code` field on the methods passes it through to the upstream
10958
- * service; it's NEVER persisted in the runtime slice or any event
10959
- * payload. The presence of a required code is signalled by
10960
- * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
10961
- * field without a slice fetch.
10962
- *
10963
- * `availableModes` mirrors HA's `supported_features`-derived arm
10964
- * mode list — the UI renders only the buttons the panel accepts.
10965
- */
10966
- var AlarmStateSchema = _enum([
10967
- "disarmed",
10968
- "armed_home",
10969
- "armed_away",
10970
- "armed_night",
10971
- "armed_vacation",
10972
- "armed_custom_bypass",
10973
- "arming",
10974
- "disarming",
10975
- "pending",
10976
- "triggered"
10977
- ]);
10978
- var AlarmArmModeSchema = _enum([
10979
- "home",
10980
- "away",
10981
- "night",
10982
- "vacation",
10983
- "custom_bypass"
10984
- ]);
10985
- var AlarmPanelStatusSchema = object({
10986
- /** Current lifecycle state. */
10987
- state: AlarmStateSchema,
10988
- /** Subset of arm modes the panel accepts. UI renders one button per
10989
- * mode in this list. */
10990
- availableModes: array(AlarmArmModeSchema),
10991
- /** Whether the panel requires a PIN on arm / disarm. Mirrors
10992
- * `DeviceFeature.AlarmPinRequired` for slice consumers. */
10993
- requiresCode: boolean(),
10994
- /** Ms epoch when the slice was last updated. */
10995
- lastChangedAt: number()
10996
- });
10997
- var alarmPanelCapability = {
10998
- name: "alarm-panel",
10999
- scope: "device",
11000
- deviceNative: true,
11001
- mode: "singleton",
11002
- deviceTypes: [DeviceType.AlarmPanel],
11003
- methods: {
11004
- arm: method(object({
11005
- deviceId: number().int().nonnegative(),
11006
- mode: AlarmArmModeSchema,
11007
- /** Optional PIN code. Required when `requiresCode === true`.
11008
- * Passed through to the upstream service; never persisted. */
11009
- code: string().min(1).optional()
11010
- }), _void(), {
11011
- kind: "mutation",
11012
- auth: "admin"
11013
- }),
11014
- disarm: method(object({
11015
- deviceId: number().int().nonnegative(),
11016
- code: string().min(1).optional()
11017
- }), _void(), {
11018
- kind: "mutation",
11019
- auth: "admin"
11020
- }),
11021
- /**
11022
- * Force the panel into the `triggered` state — used by HA
11023
- * automations to surface external sensor events through the panel
11024
- * (e.g. a Reolink camera intrusion event firing the security
11025
- * system). Provider rejects when the panel hardware doesn't
11026
- * support a software-initiated trigger.
11027
- */
11028
- trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
11029
- kind: "mutation",
11030
- auth: "admin"
11031
- })
11032
- },
11425
+ * `attributes.suggested_display_precision`). Falls back to
11426
+ * auto-formatting when absent. */
11427
+ precision: number().int().min(0).max(10).optional()
11428
+ });
11429
+ var airQualitySensorCapability = {
11430
+ name: "air-quality-sensor",
11431
+ scope: "device",
11432
+ deviceNative: true,
11433
+ mode: "singleton",
11434
+ deviceTypes: [DeviceType.Sensor],
11435
+ methods: {},
11033
11436
  status: {
11034
- schema: AlarmPanelStatusSchema,
11437
+ schema: AirQualitySensorStatusSchema,
11035
11438
  kind: "push"
11036
11439
  },
11037
- /**
11038
- * Runtime-state slice — mirrored by the kernel. UI panel reads the
11039
- * full slice; renders an arm button per `availableModes` entry and
11040
- * a PIN field iff `requiresCode === true`.
11041
- */
11042
- runtimeState: AlarmPanelStatusSchema
11440
+ runtimeState: AirQualitySensorStatusSchema
11043
11441
  };
11044
11442
  /**
11045
11443
  * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
@@ -18490,6 +18888,20 @@ var GetInputSchema = object({ id: string() });
18490
18888
  var AddInputSchema = object({
18491
18889
  kind: string().min(1),
18492
18890
  name: string().min(1),
18891
+ /**
18892
+ * ADOPT an existing id instead of minting a new one.
18893
+ *
18894
+ * Written the day this cost an outage. A broker's id is not a detail: HA
18895
+ * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
18896
+ * lost from config and re-added as `ha_001` leaves every one of its devices
18897
+ * bound to a broker that no longer exists. Re-entering the password under the
18898
+ * ORIGINAL id turns a multi-step device migration back into re-entering a
18899
+ * password.
18900
+ *
18901
+ * A provider MUST refuse an id that is already in use — adopting a live
18902
+ * broker's id would silently take it over.
18903
+ */
18904
+ id: string().min(1).optional(),
18493
18905
  /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
18494
18906
  * `{baseUrl,accessToken}`). Validated by the kind-specific provider
18495
18907
  * branch on receipt — invalid shape rejects the add. */
@@ -20023,14 +20435,14 @@ var LlmProfileSchema = object({
20023
20435
  /** ConfigUISchema tree passed through untyped on the wire (the
20024
20436
  * notification-output `ConfigSchemaPassthrough` precedent at
20025
20437
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
20026
- var ConfigSchemaPassthrough$1 = unknown();
20438
+ var ConfigSchemaPassthrough = unknown();
20027
20439
  var LlmProfileKindDescriptorSchema = object({
20028
20440
  kind: LlmProfileKindSchema,
20029
20441
  label: string(),
20030
20442
  icon: string(),
20031
20443
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
20032
20444
  addonId: string(),
20033
- configSchema: ConfigSchemaPassthrough$1
20445
+ configSchema: ConfigSchemaPassthrough
20034
20446
  });
20035
20447
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
20036
20448
  var LlmDefaultSchema = object({
@@ -20561,228 +20973,6 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
20561
20973
  });
20562
20974
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
20563
20975
  /**
20564
- * notification-output — canonical, capability-gated notification delivery.
20565
- *
20566
- * Apprise-derived model (see
20567
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
20568
- * callers emit ONE canonical `Notification`; each provider declares a
20569
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
20570
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
20571
- * message to what the kind supports — callers never special-case a service.
20572
- *
20573
- * DESIGN DECISIONS (locked):
20574
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
20575
- * `setTargetEnabled`), each provider persisting via the `settings-store`
20576
- * cap. Rationale: the admin UI needs one uniform surface across the
20577
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
20578
- * alternative would fork the UI per addon and cannot host the
20579
- * discovery→adopt flow.
20580
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
20581
- * the generated cap-mount auto-`concatCollection`-fans them across every
20582
- * registered provider (notifiers addon + HA addon) so one catalog is
20583
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
20584
- * `addonId` the generated collection router extracts from the call input.
20585
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
20586
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
20587
- * `storage` / `storage-provider` / `recording` caps over the same path. No
20588
- * base64 fallback needed.
20589
- *
20590
- * TODO (deferred, closed-set change — separate decision): add
20591
- * `providerKind: 'notify'` so notification providers surface on the unified
20592
- * admin "Integrations" page.
20593
- */
20594
- /**
20595
- * Zentik-derived typed-media enum — the superset across every kind. Each
20596
- * adapter picks what it supports and the degrade engine filters the rest.
20597
- */
20598
- var AttachmentMediaTypeSchema = _enum([
20599
- "image",
20600
- "video",
20601
- "gif",
20602
- "audio",
20603
- "icon"
20604
- ]);
20605
- /**
20606
- * A single attachment. Exactly one of `url` (remote source, most adapters
20607
- * prefer this) or `bytes` (inline source; required for Pushover-style
20608
- * bytes-only kinds) MUST be present — the degrade engine expresses a
20609
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
20610
- */
20611
- var AttachmentSchema = object({
20612
- mediaType: AttachmentMediaTypeSchema,
20613
- url: string().optional(),
20614
- bytes: _instanceof(Uint8Array).optional(),
20615
- mime: string().optional(),
20616
- name: string().optional()
20617
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
20618
- var NotificationFormatSchema = _enum([
20619
- "text",
20620
- "markdown",
20621
- "html"
20622
- ]);
20623
- /** A single tap-through action button. */
20624
- var NotificationActionSchema = object({
20625
- id: string(),
20626
- label: string(),
20627
- url: string().optional()
20628
- });
20629
- /**
20630
- * The canonical notification. `body` is the only hard field (Apprise model).
20631
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
20632
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
20633
- * the adapter maps this ordinal onto its native level. `level?` is an
20634
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
20635
- * `priority` for that one target.
20636
- */
20637
- var NotificationSchema = object({
20638
- body: string(),
20639
- title: string().optional(),
20640
- format: NotificationFormatSchema.default("text"),
20641
- priority: number().int().min(1).max(5).default(3),
20642
- level: string().optional(),
20643
- attachments: array(AttachmentSchema).optional(),
20644
- clickUrl: string().optional(),
20645
- actions: array(NotificationActionSchema).optional(),
20646
- sound: string().optional(),
20647
- ttl: number().optional(),
20648
- tag: string().optional(),
20649
- deviceId: number().optional(),
20650
- eventId: string().optional(),
20651
- metadata: record(string(), unknown()).optional()
20652
- });
20653
- /** One declared native severity/priority level for a kind. */
20654
- var TargetKindLevelSchema = object({
20655
- id: string(),
20656
- label: string(),
20657
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
20658
- ordinal: number().int().min(1).max(5).nullable(),
20659
- flags: object({
20660
- critical: boolean().optional(),
20661
- silent: boolean().optional(),
20662
- noPush: boolean().optional()
20663
- }).optional(),
20664
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
20665
- requires: array(string()).optional(),
20666
- description: string().optional()
20667
- });
20668
- /** The full capability block consulted before dispatch. */
20669
- var TargetKindCapsSchema = object({
20670
- attachments: object({
20671
- mediaTypes: array(AttachmentMediaTypeSchema),
20672
- mode: _enum([
20673
- "url",
20674
- "bytes",
20675
- "both"
20676
- ]),
20677
- max: number().int().nonnegative(),
20678
- maxBytes: number().int().positive().optional()
20679
- }),
20680
- /** Max action buttons (0 = none). */
20681
- actions: number().int().nonnegative(),
20682
- levels: array(TargetKindLevelSchema),
20683
- format: array(NotificationFormatSchema),
20684
- clickUrl: boolean(),
20685
- sound: boolean(),
20686
- ttl: boolean(),
20687
- bodyMaxLen: number().int().positive()
20688
- });
20689
- /**
20690
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
20691
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
20692
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
20693
- * the union is large and not meant for runtime validation here; the exported
20694
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
20695
- */
20696
- var ConfigSchemaPassthrough = unknown();
20697
- var TargetKindSchema = object({
20698
- kind: string(),
20699
- label: string(),
20700
- icon: string(),
20701
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
20702
- addonId: string(),
20703
- /**
20704
- * URL of the kind's bundled BRAND icon, served by the providing addon over
20705
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20706
- * when the addon bundles no icon for that kind — the client then falls back
20707
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20708
- *
20709
- * Root-relative on purpose: it resolves against whatever origin serves a web
20710
- * client, and a native client joins it onto its own hub base.
20711
- *
20712
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
20713
- * field that survived only because the runtime cap-router forwards provider
20714
- * output verbatim — so every consumer had to re-declare it by hand to stop
20715
- * its own Zod parse from stripping it, and the whole arrangement would have
20716
- * broken silently the moment output validation was tightened anywhere.
20717
- */
20718
- iconUrl: string().optional(),
20719
- /**
20720
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20721
- *
20722
- * The server knows this and therefore says it, because the client cannot
20723
- * safely guess: a React-Native client renders SVG and raster through two
20724
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20725
- * not decode SVG on iOS/Android), so without this it silently fell back to a
20726
- * placeholder glyph for every vector icon while the web build looked fine.
20727
- *
20728
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20729
- * not been updated — a client that cannot determine the type should prefer
20730
- * its raster path, which is the safe default for an unknown image.
20731
- */
20732
- iconMediaType: string().optional(),
20733
- configSchema: ConfigSchemaPassthrough,
20734
- supportsDiscovery: boolean(),
20735
- caps: TargetKindCapsSchema
20736
- });
20737
- /**
20738
- * A persisted target. `config` holds secrets; providers REDACT secret fields
20739
- * (return a presence marker only) when serving `listTargets` — never
20740
- * round-trip a stored secret to the UI.
20741
- */
20742
- var TargetSchema = object({
20743
- id: string(),
20744
- name: string(),
20745
- kind: string(),
20746
- addonId: string(),
20747
- enabled: boolean(),
20748
- config: record(string(), unknown())
20749
- });
20750
- /** A discovery-surfaced candidate (config is partial + non-secret). */
20751
- var DiscoveredTargetSchema = object({
20752
- kind: string(),
20753
- suggestedName: string(),
20754
- config: record(string(), unknown())
20755
- });
20756
- /** The degrade engine's report — what was resolved / dropped / degraded. */
20757
- var RenderedAsSchema = object({
20758
- level: string(),
20759
- format: NotificationFormatSchema,
20760
- attachmentsSent: number().int().nonnegative(),
20761
- actionsSent: number().int().nonnegative(),
20762
- truncated: boolean(),
20763
- dropped: array(string())
20764
- });
20765
- var SendResultSchema = object({
20766
- success: boolean(),
20767
- error: string().optional(),
20768
- renderedAs: RenderedAsSchema.optional()
20769
- });
20770
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
20771
- var TestResultSchema = SendResultSchema;
20772
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
20773
- kind: string(),
20774
- config: record(string(), unknown()).optional()
20775
- }), array(DiscoveredTargetSchema)), method(object({
20776
- targetId: string(),
20777
- notification: NotificationSchema
20778
- }), SendResultSchema, { kind: "mutation" }), method(object({
20779
- targetId: string(),
20780
- sample: NotificationSchema.optional()
20781
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
20782
- targetId: string(),
20783
- enabled: boolean()
20784
- }), _void(), { kind: "mutation" });
20785
- /**
20786
20976
  * core-blocks — user-authored TypeScript, stored in the kernel and executed in
20787
20977
  * its own process.
20788
20978
  *
@@ -28491,6 +28681,12 @@ Object.freeze({
28491
28681
  addonId: null,
28492
28682
  access: "delete"
28493
28683
  },
28684
+ "notificationRules.getAlarmConfig": {
28685
+ capName: "notification-rules",
28686
+ capScope: "system",
28687
+ addonId: null,
28688
+ access: "view"
28689
+ },
28494
28690
  "notificationRules.getConditionCatalog": {
28495
28691
  capName: "notification-rules",
28496
28692
  capScope: "system",
@@ -28521,6 +28717,12 @@ Object.freeze({
28521
28717
  addonId: null,
28522
28718
  access: "view"
28523
28719
  },
28720
+ "notificationRules.setAlarmConfig": {
28721
+ capName: "notification-rules",
28722
+ capScope: "system",
28723
+ addonId: null,
28724
+ access: "create"
28725
+ },
28524
28726
  "notificationRules.setRuleEnabled": {
28525
28727
  capName: "notification-rules",
28526
28728
  capScope: "system",