@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.
@@ -9432,6 +9432,100 @@ function shallowEqual(a, b) {
9432
9432
  }
9433
9433
  new Set(["devices", "classes"]);
9434
9434
  /**
9435
+ * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9436
+ * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9437
+ * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9438
+ * arming / pending / triggered / disarming.
9439
+ *
9440
+ * Many panels require a PIN code on arm / disarm — the optional
9441
+ * `code` field on the methods passes it through to the upstream
9442
+ * service; it's NEVER persisted in the runtime slice or any event
9443
+ * payload. The presence of a required code is signalled by
9444
+ * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9445
+ * field without a slice fetch.
9446
+ *
9447
+ * `availableModes` mirrors HA's `supported_features`-derived arm
9448
+ * mode list — the UI renders only the buttons the panel accepts.
9449
+ */
9450
+ var AlarmStateSchema = _enum([
9451
+ "disarmed",
9452
+ "armed_home",
9453
+ "armed_away",
9454
+ "armed_night",
9455
+ "armed_vacation",
9456
+ "armed_custom_bypass",
9457
+ "arming",
9458
+ "disarming",
9459
+ "pending",
9460
+ "triggered"
9461
+ ]);
9462
+ var AlarmArmModeSchema = _enum([
9463
+ "home",
9464
+ "away",
9465
+ "night",
9466
+ "vacation",
9467
+ "custom_bypass"
9468
+ ]);
9469
+ var AlarmPanelStatusSchema = object({
9470
+ /** Current lifecycle state. */
9471
+ state: AlarmStateSchema,
9472
+ /** Subset of arm modes the panel accepts. UI renders one button per
9473
+ * mode in this list. */
9474
+ availableModes: array(AlarmArmModeSchema),
9475
+ /** Whether the panel requires a PIN on arm / disarm. Mirrors
9476
+ * `DeviceFeature.AlarmPinRequired` for slice consumers. */
9477
+ requiresCode: boolean(),
9478
+ /** Ms epoch when the slice was last updated. */
9479
+ lastChangedAt: number()
9480
+ });
9481
+ var alarmPanelCapability = {
9482
+ name: "alarm-panel",
9483
+ scope: "device",
9484
+ deviceNative: true,
9485
+ mode: "singleton",
9486
+ deviceTypes: [DeviceType.AlarmPanel],
9487
+ methods: {
9488
+ arm: method(object({
9489
+ deviceId: number().int().nonnegative(),
9490
+ mode: AlarmArmModeSchema,
9491
+ /** Optional PIN code. Required when `requiresCode === true`.
9492
+ * Passed through to the upstream service; never persisted. */
9493
+ code: string().min(1).optional()
9494
+ }), _void(), {
9495
+ kind: "mutation",
9496
+ auth: "admin"
9497
+ }),
9498
+ disarm: method(object({
9499
+ deviceId: number().int().nonnegative(),
9500
+ code: string().min(1).optional()
9501
+ }), _void(), {
9502
+ kind: "mutation",
9503
+ auth: "admin"
9504
+ }),
9505
+ /**
9506
+ * Force the panel into the `triggered` state — used by HA
9507
+ * automations to surface external sensor events through the panel
9508
+ * (e.g. a Reolink camera intrusion event firing the security
9509
+ * system). Provider rejects when the panel hardware doesn't
9510
+ * support a software-initiated trigger.
9511
+ */
9512
+ trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
9513
+ kind: "mutation",
9514
+ auth: "admin"
9515
+ })
9516
+ },
9517
+ status: {
9518
+ schema: AlarmPanelStatusSchema,
9519
+ kind: "push"
9520
+ },
9521
+ /**
9522
+ * Runtime-state slice — mirrored by the kernel. UI panel reads the
9523
+ * full slice; renders an arm button per `availableModes` entry and
9524
+ * a PIN field iff `requiresCode === true`.
9525
+ */
9526
+ runtimeState: AlarmPanelStatusSchema
9527
+ };
9528
+ /**
9435
9529
  * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9436
9530
  * motion-zones, and the detection zones/lines editor all speak this one
9437
9531
  * language so a single drawing-plane editor and the providers stay
@@ -9493,6 +9587,276 @@ var MaskGridDimsSchema = object({
9493
9587
  height: number()
9494
9588
  });
9495
9589
  /**
9590
+ * notification-output — canonical, capability-gated notification delivery.
9591
+ *
9592
+ * Apprise-derived model (see
9593
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
9594
+ * callers emit ONE canonical `Notification`; each provider declares a
9595
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
9596
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
9597
+ * message to what the kind supports — callers never special-case a service.
9598
+ *
9599
+ * DESIGN DECISIONS (locked):
9600
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
9601
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
9602
+ * cap. Rationale: the admin UI needs one uniform surface across the
9603
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
9604
+ * alternative would fork the UI per addon and cannot host the
9605
+ * discovery→adopt flow.
9606
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
9607
+ * the generated cap-mount auto-`concatCollection`-fans them across every
9608
+ * registered provider (notifiers addon + HA addon) so one catalog is
9609
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
9610
+ * `addonId` the generated collection router extracts from the call input.
9611
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
9612
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
9613
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
9614
+ * base64 fallback needed.
9615
+ *
9616
+ * TODO (deferred, closed-set change — separate decision): add
9617
+ * `providerKind: 'notify'` so notification providers surface on the unified
9618
+ * admin "Integrations" page.
9619
+ */
9620
+ /**
9621
+ * Zentik-derived typed-media enum — the superset across every kind. Each
9622
+ * adapter picks what it supports and the degrade engine filters the rest.
9623
+ */
9624
+ var AttachmentMediaTypeSchema = _enum([
9625
+ "image",
9626
+ "video",
9627
+ "gif",
9628
+ "audio",
9629
+ "icon"
9630
+ ]);
9631
+ /**
9632
+ * A single attachment. Exactly one of `url` (remote source, most adapters
9633
+ * prefer this) or `bytes` (inline source; required for Pushover-style
9634
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
9635
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
9636
+ */
9637
+ var AttachmentSchema = object({
9638
+ mediaType: AttachmentMediaTypeSchema,
9639
+ url: string().optional(),
9640
+ bytes: _instanceof(Uint8Array).optional(),
9641
+ mime: string().optional(),
9642
+ name: string().optional()
9643
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
9644
+ var NotificationFormatSchema = _enum([
9645
+ "text",
9646
+ "markdown",
9647
+ "html"
9648
+ ]);
9649
+ /**
9650
+ * The CLOSED icon vocabulary an action button may use.
9651
+ *
9652
+ * A closed set, not a free string, and that is the whole point: an arbitrary
9653
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
9654
+ * notices — the same class of gap as a zone vocabulary nothing produced
9655
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
9656
+ * declares `actionIcons: false` and the degrade engine strips the field.
9657
+ *
9658
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
9659
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
9660
+ * differently.
9661
+ */
9662
+ var NotificationActionIconSchema = _enum([
9663
+ "acknowledge",
9664
+ "dismiss",
9665
+ "silence",
9666
+ "view",
9667
+ "play",
9668
+ "open",
9669
+ "close",
9670
+ "lock",
9671
+ "unlock",
9672
+ "arm",
9673
+ "disarm",
9674
+ "light",
9675
+ "alert"
9676
+ ]);
9677
+ /** A single tap-through action button. */
9678
+ var NotificationActionSchema = object({
9679
+ id: string(),
9680
+ label: string(),
9681
+ url: string().optional(),
9682
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
9683
+ icon: NotificationActionIconSchema.optional(),
9684
+ /**
9685
+ * Renders in a warning style where the notifier supports it.
9686
+ *
9687
+ * A HINT, never a gate. The callback's authority is its token and nothing
9688
+ * else — see `notification-center/action-token.ts` for what that does and
9689
+ * does not buy.
9690
+ */
9691
+ destructive: boolean().optional()
9692
+ });
9693
+ /**
9694
+ * The canonical notification. `body` is the only hard field (Apprise model).
9695
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
9696
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
9697
+ * the adapter maps this ordinal onto its native level. `level?` is an
9698
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
9699
+ * `priority` for that one target.
9700
+ */
9701
+ var NotificationSchema = object({
9702
+ body: string(),
9703
+ title: string().optional(),
9704
+ format: NotificationFormatSchema.default("text"),
9705
+ priority: number().int().min(1).max(5).default(3),
9706
+ level: string().optional(),
9707
+ attachments: array(AttachmentSchema).optional(),
9708
+ clickUrl: string().optional(),
9709
+ actions: array(NotificationActionSchema).optional(),
9710
+ sound: string().optional(),
9711
+ ttl: number().optional(),
9712
+ tag: string().optional(),
9713
+ deviceId: number().optional(),
9714
+ eventId: string().optional(),
9715
+ metadata: record(string(), unknown()).optional()
9716
+ });
9717
+ /** One declared native severity/priority level for a kind. */
9718
+ var TargetKindLevelSchema = object({
9719
+ id: string(),
9720
+ label: string(),
9721
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
9722
+ ordinal: number().int().min(1).max(5).nullable(),
9723
+ flags: object({
9724
+ critical: boolean().optional(),
9725
+ silent: boolean().optional(),
9726
+ noPush: boolean().optional()
9727
+ }).optional(),
9728
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
9729
+ requires: array(string()).optional(),
9730
+ description: string().optional()
9731
+ });
9732
+ /** The full capability block consulted before dispatch. */
9733
+ var TargetKindCapsSchema = object({
9734
+ attachments: object({
9735
+ mediaTypes: array(AttachmentMediaTypeSchema),
9736
+ mode: _enum([
9737
+ "url",
9738
+ "bytes",
9739
+ "both"
9740
+ ]),
9741
+ max: number().int().nonnegative(),
9742
+ maxBytes: number().int().positive().optional()
9743
+ }),
9744
+ /** Max action buttons (0 = none). */
9745
+ actions: number().int().nonnegative(),
9746
+ /**
9747
+ * Whether this kind renders a per-action ICON.
9748
+ *
9749
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
9750
+ * run on the addon cap path — three production failures in one day taught
9751
+ * this repo that once. Absent is read as false by the degrade engine, which
9752
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
9753
+ * assumed and dropped costs the operator's trust in the field.
9754
+ */
9755
+ actionIcons: boolean().optional(),
9756
+ levels: array(TargetKindLevelSchema),
9757
+ format: array(NotificationFormatSchema),
9758
+ clickUrl: boolean(),
9759
+ sound: boolean(),
9760
+ ttl: boolean(),
9761
+ bodyMaxLen: number().int().positive()
9762
+ });
9763
+ /**
9764
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
9765
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
9766
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
9767
+ * the union is large and not meant for runtime validation here; the exported
9768
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
9769
+ */
9770
+ var ConfigSchemaPassthrough$1 = unknown();
9771
+ var TargetKindSchema = object({
9772
+ kind: string(),
9773
+ label: string(),
9774
+ icon: string(),
9775
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
9776
+ addonId: string(),
9777
+ /**
9778
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
9779
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
9780
+ * when the addon bundles no icon for that kind — the client then falls back
9781
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
9782
+ *
9783
+ * Root-relative on purpose: it resolves against whatever origin serves a web
9784
+ * client, and a native client joins it onto its own hub base.
9785
+ *
9786
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
9787
+ * field that survived only because the runtime cap-router forwards provider
9788
+ * output verbatim — so every consumer had to re-declare it by hand to stop
9789
+ * its own Zod parse from stripping it, and the whole arrangement would have
9790
+ * broken silently the moment output validation was tightened anywhere.
9791
+ */
9792
+ iconUrl: string().optional(),
9793
+ /**
9794
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
9795
+ *
9796
+ * The server knows this and therefore says it, because the client cannot
9797
+ * safely guess: a React-Native client renders SVG and raster through two
9798
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
9799
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
9800
+ * placeholder glyph for every vector icon while the web build looked fine.
9801
+ *
9802
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
9803
+ * not been updated — a client that cannot determine the type should prefer
9804
+ * its raster path, which is the safe default for an unknown image.
9805
+ */
9806
+ iconMediaType: string().optional(),
9807
+ configSchema: ConfigSchemaPassthrough$1,
9808
+ supportsDiscovery: boolean(),
9809
+ caps: TargetKindCapsSchema
9810
+ });
9811
+ /**
9812
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
9813
+ * (return a presence marker only) when serving `listTargets` — never
9814
+ * round-trip a stored secret to the UI.
9815
+ */
9816
+ var TargetSchema = object({
9817
+ id: string(),
9818
+ name: string(),
9819
+ kind: string(),
9820
+ addonId: string(),
9821
+ enabled: boolean(),
9822
+ config: record(string(), unknown())
9823
+ });
9824
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
9825
+ var DiscoveredTargetSchema = object({
9826
+ kind: string(),
9827
+ suggestedName: string(),
9828
+ config: record(string(), unknown())
9829
+ });
9830
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
9831
+ var RenderedAsSchema = object({
9832
+ level: string(),
9833
+ format: NotificationFormatSchema,
9834
+ attachmentsSent: number().int().nonnegative(),
9835
+ actionsSent: number().int().nonnegative(),
9836
+ truncated: boolean(),
9837
+ dropped: array(string())
9838
+ });
9839
+ var SendResultSchema = object({
9840
+ success: boolean(),
9841
+ error: string().optional(),
9842
+ renderedAs: RenderedAsSchema.optional()
9843
+ });
9844
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
9845
+ var TestResultSchema = SendResultSchema;
9846
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
9847
+ kind: string(),
9848
+ config: record(string(), unknown()).optional()
9849
+ }), array(DiscoveredTargetSchema)), method(object({
9850
+ targetId: string(),
9851
+ notification: NotificationSchema
9852
+ }), SendResultSchema, { kind: "mutation" }), method(object({
9853
+ targetId: string(),
9854
+ sample: NotificationSchema.optional()
9855
+ }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
9856
+ targetId: string(),
9857
+ enabled: boolean()
9858
+ }), _void(), { kind: "mutation" });
9859
+ /**
9496
9860
  * notification-rules — the Notification Center rule surface (P1 core).
9497
9861
  *
9498
9862
  * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
@@ -9662,6 +10026,41 @@ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9662
10026
  args: record(string(), unknown()).optional()
9663
10027
  })]);
9664
10028
  /**
10029
+ * A named, ordered run of steps with its own throttle.
10030
+ *
10031
+ * `minDelaySec` exists because a noisy rule otherwise hammers a physical
10032
+ * actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
10033
+ * different budget from "how often may this gate actually open".
10034
+ */
10035
+ var NcRuleActionSequenceSchema = object({
10036
+ name: string().min(1).max(120),
10037
+ enabled: boolean(),
10038
+ minDelaySec: number().int().min(0).max(86400).optional(),
10039
+ actions: array(NcRuleActionSchema).min(1)
10040
+ });
10041
+ /**
10042
+ * One button carried by the notification, running a named sequence on tap.
10043
+ *
10044
+ * **Read this before adding a button that does something physical.** The tap
10045
+ * arrives over a link that travelled through third-party infrastructure — ntfy,
10046
+ * a push relay, whatever forwarded the message — and the callback's ONLY
10047
+ * authority is the token in that link: single-use, short-lived, bound to this
10048
+ * one action of this one notification. It does not identify who tapped.
10049
+ * Whoever holds the notification can run the button, once, inside the window.
10050
+ * That is the operator's explicit choice (2026-08-05), and `destructive` is a
10051
+ * rendering hint, not a second gate. [D47](decisions/adr-0047.md).
10052
+ */
10053
+ var NcRuleNotificationButtonSchema = object({
10054
+ /** Stable id — travels in the callback and identifies the button in logs. */
10055
+ id: string().min(1).max(64),
10056
+ label: string().min(1).max(40),
10057
+ /** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
10058
+ * sequence does not exist rather than minting a token for nothing. */
10059
+ sequence: string().min(1).max(120),
10060
+ icon: NotificationActionIconSchema.optional(),
10061
+ destructive: boolean().optional()
10062
+ });
10063
+ /**
9665
10064
  * Sequences a rule runs, by hook point.
9666
10065
  *
9667
10066
  * ONLY `onTrigger` is here, deliberately. The reference also has activation /
@@ -9669,14 +10068,26 @@ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9669
10068
  * repo's expensive failure mode is declaring a surface nothing produces, so a
9670
10069
  * hook appears here in the same change that produces its edge, never before.
9671
10070
  */
9672
- var NcRuleActionsSchema = object({
9673
- /** Runs when the rule MATCHES. */
9674
- onTrigger: array(object({
9675
- name: string().min(1).max(120),
9676
- enabled: boolean(),
9677
- minDelaySec: number().int().min(0).max(86400).optional(),
9678
- actions: array(NcRuleActionSchema).min(1)
9679
- })).optional() });
10071
+ var NcRuleActionsSchema = object({
10072
+ /** Runs when the rule MATCHES. */
10073
+ onTrigger: array(NcRuleActionSequenceSchema).optional(),
10074
+ /**
10075
+ * Buttons the NOTIFICATION carries, each running one of this rule's
10076
+ * sequences when tapped.
10077
+ *
10078
+ * Deliberately a REFERENCE to a sequence rather than a second place to
10079
+ * author steps. A button that could define its own actions would be a
10080
+ * parallel actuation vocabulary — the executor's device-scope check, the
10081
+ * stop-at-first-failure rule and the per-sequence throttle all live on
10082
+ * sequences, and a second authoring surface would drift from every one of
10083
+ * them.
10084
+ *
10085
+ * A sequence reachable ONLY by a button simply appears in `onTrigger` with
10086
+ * `enabled: false`: it is then authored, throttled and validated like the
10087
+ * rest, and nothing runs it automatically.
10088
+ */
10089
+ buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
10090
+ });
9680
10091
  var NcConditionsSchema = object({
9681
10092
  /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
9682
10093
  deviceState: object({
@@ -10103,7 +10514,7 @@ var NC_CONDITION_CATALOG = [
10103
10514
  {
10104
10515
  id: "devices",
10105
10516
  group: "scope",
10106
- label: "Cameras",
10517
+ label: "Devices",
10107
10518
  valueType: "deviceIdList",
10108
10519
  operator: "in",
10109
10520
  appliesTo: [
@@ -10113,7 +10524,7 @@ var NC_CONDITION_CATALOG = [
10113
10524
  "package-event"
10114
10525
  ],
10115
10526
  phase: "P1",
10116
- description: "Restrict the rule to these devices; absent = all devices."
10527
+ 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."
10117
10528
  },
10118
10529
  {
10119
10530
  id: "classes",
@@ -10568,6 +10979,67 @@ var NcSnoozeSuppressedSchema = object({
10568
10979
  firstAt: number(),
10569
10980
  lastAt: number()
10570
10981
  });
10982
+ /**
10983
+ * The three durations the panel's state machine runs on, plus who hears about
10984
+ * an arm.
10985
+ *
10986
+ * They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
10987
+ * `alarm-panel` is `deviceNative` and its other provider mirrors somebody
10988
+ * else's panel, which has its own delays and no way to be told these. This is
10989
+ * configuration of the panel CamStack owns, and the Notification Center owns
10990
+ * that panel.
10991
+ */
10992
+ var NcAlarmSettingsSchema = object({
10993
+ /** Grace period between arming and the mode taking effect. 0 = immediate. */
10994
+ exitDelaySec: number().int().min(0).max(600),
10995
+ /** Grace period between a trigger and the alarm firing. 0 = immediate. */
10996
+ entryDelaySec: number().int().min(0).max(600),
10997
+ /**
10998
+ * How long `triggered` lasts before the panel re-arms itself.
10999
+ * **0 = until an operator disarms it** — the behaviour before this field
11000
+ * existed, and therefore what an untouched install keeps doing.
11001
+ */
11002
+ triggeredDurationSec: number().int().min(0).max(3600),
11003
+ /** Send a notification when a mode takes effect. */
11004
+ announceArm: boolean(),
11005
+ /**
11006
+ * Where that notification goes. Target ids from `notification-output`.
11007
+ *
11008
+ * Explicit rather than "everyone": an arm announcement is a household
11009
+ * message, and broadcasting it to every configured endpoint (including a
11010
+ * webhook wired to something else) is not a default anybody would choose.
11011
+ * Empty with `announceArm: true` sends nothing, and the server logs that —
11012
+ * silence must be attributable.
11013
+ */
11014
+ announceTargets: array(string().min(1)).max(16)
11015
+ });
11016
+ /** Every field optional — a tab edits one control at a time. */
11017
+ var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
11018
+ /**
11019
+ * What one arm mode actually arms, DERIVED from the enabled rules gated on it.
11020
+ * Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
11021
+ * copy would lie the first time a rule is disabled.
11022
+ */
11023
+ var NcAlarmModeCoverageSchema = object({
11024
+ mode: AlarmArmModeSchema,
11025
+ /** Enabled rules gated on `armed_<mode>`. Zero means the mode does nothing. */
11026
+ ruleCount: number().int().min(0),
11027
+ /** At least one covering rule has no device scope, so the mode covers all. */
11028
+ allDevices: boolean(),
11029
+ /** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
11030
+ deviceIds: array(number().int())
11031
+ });
11032
+ var NcAlarmConfigSchema = object({
11033
+ /**
11034
+ * The panel's device id, or null when this install has no panel (the ensure
11035
+ * step failed, or this is not the hub). Null is the honest answer: an editor
11036
+ * that rendered delays for a panel that does not exist would be a form whose
11037
+ * Save does nothing.
11038
+ */
11039
+ deviceId: number().int().nullable(),
11040
+ settings: NcAlarmSettingsSchema,
11041
+ coverage: array(NcAlarmModeCoverageSchema)
11042
+ });
10571
11043
  var notificationRulesCapability = {
10572
11044
  name: "notification-rules",
10573
11045
  scope: "system",
@@ -10663,6 +11135,26 @@ var notificationRulesCapability = {
10663
11135
  cancelSnooze: method(object({ snoozeId: string() }), object({ success: literal(true) }), {
10664
11136
  kind: "mutation",
10665
11137
  caller: "required"
11138
+ }),
11139
+ /**
11140
+ * The alarm panel's durations, plus what each mode ACTUALLY arms.
11141
+ *
11142
+ * Coverage is returned by the same call as the settings on purpose: they
11143
+ * are read together or not at all. An editor that showed the delays
11144
+ * without the coverage would let an operator tune an exit delay for a mode
11145
+ * that arms nothing, which is the failure this whole surface exists to
11146
+ * make visible.
11147
+ */
11148
+ getAlarmConfig: method(object({}), NcAlarmConfigSchema, { auth: "admin" }),
11149
+ /**
11150
+ * Patch the durations. Returns the WHOLE config, coverage included, so a
11151
+ * client never has to guess what the server settled on — the panel clamps
11152
+ * and normalises, and a form that re-rendered from its own input would
11153
+ * show a value the alarm is not using.
11154
+ */
11155
+ setAlarmConfig: method(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
11156
+ kind: "mutation",
11157
+ auth: "admin"
10666
11158
  })
10667
11159
  }
10668
11160
  };
@@ -10908,116 +11400,22 @@ var AirQualitySensorStatusSchema = object({
10908
11400
  unit: string().optional(),
10909
11401
  /** Suggested decimal places for numeric display.
10910
11402
  * Populated live from the upstream source when provided (e.g. HA
10911
- * `attributes.suggested_display_precision`). Falls back to
10912
- * auto-formatting when absent. */
10913
- precision: number().int().min(0).max(10).optional()
10914
- });
10915
- var airQualitySensorCapability = {
10916
- name: "air-quality-sensor",
10917
- scope: "device",
10918
- deviceNative: true,
10919
- mode: "singleton",
10920
- deviceTypes: [DeviceType.Sensor],
10921
- methods: {},
10922
- status: {
10923
- schema: AirQualitySensorStatusSchema,
10924
- kind: "push"
10925
- },
10926
- runtimeState: AirQualitySensorStatusSchema
10927
- };
10928
- /**
10929
- * Alarm-panel cap. Models HA `alarm_control_panel.*` on
10930
- * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
10931
- * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
10932
- * arming / pending / triggered / disarming.
10933
- *
10934
- * Many panels require a PIN code on arm / disarm — the optional
10935
- * `code` field on the methods passes it through to the upstream
10936
- * service; it's NEVER persisted in the runtime slice or any event
10937
- * payload. The presence of a required code is signalled by
10938
- * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
10939
- * field without a slice fetch.
10940
- *
10941
- * `availableModes` mirrors HA's `supported_features`-derived arm
10942
- * mode list — the UI renders only the buttons the panel accepts.
10943
- */
10944
- var AlarmStateSchema = _enum([
10945
- "disarmed",
10946
- "armed_home",
10947
- "armed_away",
10948
- "armed_night",
10949
- "armed_vacation",
10950
- "armed_custom_bypass",
10951
- "arming",
10952
- "disarming",
10953
- "pending",
10954
- "triggered"
10955
- ]);
10956
- var AlarmArmModeSchema = _enum([
10957
- "home",
10958
- "away",
10959
- "night",
10960
- "vacation",
10961
- "custom_bypass"
10962
- ]);
10963
- var AlarmPanelStatusSchema = object({
10964
- /** Current lifecycle state. */
10965
- state: AlarmStateSchema,
10966
- /** Subset of arm modes the panel accepts. UI renders one button per
10967
- * mode in this list. */
10968
- availableModes: array(AlarmArmModeSchema),
10969
- /** Whether the panel requires a PIN on arm / disarm. Mirrors
10970
- * `DeviceFeature.AlarmPinRequired` for slice consumers. */
10971
- requiresCode: boolean(),
10972
- /** Ms epoch when the slice was last updated. */
10973
- lastChangedAt: number()
10974
- });
10975
- var alarmPanelCapability = {
10976
- name: "alarm-panel",
10977
- scope: "device",
10978
- deviceNative: true,
10979
- mode: "singleton",
10980
- deviceTypes: [DeviceType.AlarmPanel],
10981
- methods: {
10982
- arm: method(object({
10983
- deviceId: number().int().nonnegative(),
10984
- mode: AlarmArmModeSchema,
10985
- /** Optional PIN code. Required when `requiresCode === true`.
10986
- * Passed through to the upstream service; never persisted. */
10987
- code: string().min(1).optional()
10988
- }), _void(), {
10989
- kind: "mutation",
10990
- auth: "admin"
10991
- }),
10992
- disarm: method(object({
10993
- deviceId: number().int().nonnegative(),
10994
- code: string().min(1).optional()
10995
- }), _void(), {
10996
- kind: "mutation",
10997
- auth: "admin"
10998
- }),
10999
- /**
11000
- * Force the panel into the `triggered` state — used by HA
11001
- * automations to surface external sensor events through the panel
11002
- * (e.g. a Reolink camera intrusion event firing the security
11003
- * system). Provider rejects when the panel hardware doesn't
11004
- * support a software-initiated trigger.
11005
- */
11006
- trigger: method(object({ deviceId: number().int().nonnegative() }), _void(), {
11007
- kind: "mutation",
11008
- auth: "admin"
11009
- })
11010
- },
11403
+ * `attributes.suggested_display_precision`). Falls back to
11404
+ * auto-formatting when absent. */
11405
+ precision: number().int().min(0).max(10).optional()
11406
+ });
11407
+ var airQualitySensorCapability = {
11408
+ name: "air-quality-sensor",
11409
+ scope: "device",
11410
+ deviceNative: true,
11411
+ mode: "singleton",
11412
+ deviceTypes: [DeviceType.Sensor],
11413
+ methods: {},
11011
11414
  status: {
11012
- schema: AlarmPanelStatusSchema,
11415
+ schema: AirQualitySensorStatusSchema,
11013
11416
  kind: "push"
11014
11417
  },
11015
- /**
11016
- * Runtime-state slice — mirrored by the kernel. UI panel reads the
11017
- * full slice; renders an arm button per `availableModes` entry and
11018
- * a PIN field iff `requiresCode === true`.
11019
- */
11020
- runtimeState: AlarmPanelStatusSchema
11418
+ runtimeState: AirQualitySensorStatusSchema
11021
11419
  };
11022
11420
  /**
11023
11421
  * Ambient illuminance reading in lux. Drives Home Assistant `sensor`
@@ -18468,6 +18866,20 @@ var GetInputSchema = object({ id: string() });
18468
18866
  var AddInputSchema = object({
18469
18867
  kind: string().min(1),
18470
18868
  name: string().min(1),
18869
+ /**
18870
+ * ADOPT an existing id instead of minting a new one.
18871
+ *
18872
+ * Written the day this cost an outage. A broker's id is not a detail: HA
18873
+ * devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
18874
+ * lost from config and re-added as `ha_001` leaves every one of its devices
18875
+ * bound to a broker that no longer exists. Re-entering the password under the
18876
+ * ORIGINAL id turns a multi-step device migration back into re-entering a
18877
+ * password.
18878
+ *
18879
+ * A provider MUST refuse an id that is already in use — adopting a live
18880
+ * broker's id would silently take it over.
18881
+ */
18882
+ id: string().min(1).optional(),
18471
18883
  /** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
18472
18884
  * `{baseUrl,accessToken}`). Validated by the kind-specific provider
18473
18885
  * branch on receipt — invalid shape rejects the add. */
@@ -20001,14 +20413,14 @@ var LlmProfileSchema = object({
20001
20413
  /** ConfigUISchema tree passed through untyped on the wire (the
20002
20414
  * notification-output `ConfigSchemaPassthrough` precedent at
20003
20415
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
20004
- var ConfigSchemaPassthrough$1 = unknown();
20416
+ var ConfigSchemaPassthrough = unknown();
20005
20417
  var LlmProfileKindDescriptorSchema = object({
20006
20418
  kind: LlmProfileKindSchema,
20007
20419
  label: string(),
20008
20420
  icon: string(),
20009
20421
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
20010
20422
  addonId: string(),
20011
- configSchema: ConfigSchemaPassthrough$1
20423
+ configSchema: ConfigSchemaPassthrough
20012
20424
  });
20013
20425
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
20014
20426
  var LlmDefaultSchema = object({
@@ -20539,228 +20951,6 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
20539
20951
  });
20540
20952
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
20541
20953
  /**
20542
- * notification-output — canonical, capability-gated notification delivery.
20543
- *
20544
- * Apprise-derived model (see
20545
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
20546
- * callers emit ONE canonical `Notification`; each provider declares a
20547
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
20548
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
20549
- * message to what the kind supports — callers never special-case a service.
20550
- *
20551
- * DESIGN DECISIONS (locked):
20552
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
20553
- * `setTargetEnabled`), each provider persisting via the `settings-store`
20554
- * cap. Rationale: the admin UI needs one uniform surface across the
20555
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
20556
- * alternative would fork the UI per addon and cannot host the
20557
- * discovery→adopt flow.
20558
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
20559
- * the generated cap-mount auto-`concatCollection`-fans them across every
20560
- * registered provider (notifiers addon + HA addon) so one catalog is
20561
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
20562
- * `addonId` the generated collection router extracts from the call input.
20563
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
20564
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
20565
- * `storage` / `storage-provider` / `recording` caps over the same path. No
20566
- * base64 fallback needed.
20567
- *
20568
- * TODO (deferred, closed-set change — separate decision): add
20569
- * `providerKind: 'notify'` so notification providers surface on the unified
20570
- * admin "Integrations" page.
20571
- */
20572
- /**
20573
- * Zentik-derived typed-media enum — the superset across every kind. Each
20574
- * adapter picks what it supports and the degrade engine filters the rest.
20575
- */
20576
- var AttachmentMediaTypeSchema = _enum([
20577
- "image",
20578
- "video",
20579
- "gif",
20580
- "audio",
20581
- "icon"
20582
- ]);
20583
- /**
20584
- * A single attachment. Exactly one of `url` (remote source, most adapters
20585
- * prefer this) or `bytes` (inline source; required for Pushover-style
20586
- * bytes-only kinds) MUST be present — the degrade engine expresses a
20587
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
20588
- */
20589
- var AttachmentSchema = object({
20590
- mediaType: AttachmentMediaTypeSchema,
20591
- url: string().optional(),
20592
- bytes: _instanceof(Uint8Array).optional(),
20593
- mime: string().optional(),
20594
- name: string().optional()
20595
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
20596
- var NotificationFormatSchema = _enum([
20597
- "text",
20598
- "markdown",
20599
- "html"
20600
- ]);
20601
- /** A single tap-through action button. */
20602
- var NotificationActionSchema = object({
20603
- id: string(),
20604
- label: string(),
20605
- url: string().optional()
20606
- });
20607
- /**
20608
- * The canonical notification. `body` is the only hard field (Apprise model).
20609
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
20610
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
20611
- * the adapter maps this ordinal onto its native level. `level?` is an
20612
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
20613
- * `priority` for that one target.
20614
- */
20615
- var NotificationSchema = object({
20616
- body: string(),
20617
- title: string().optional(),
20618
- format: NotificationFormatSchema.default("text"),
20619
- priority: number().int().min(1).max(5).default(3),
20620
- level: string().optional(),
20621
- attachments: array(AttachmentSchema).optional(),
20622
- clickUrl: string().optional(),
20623
- actions: array(NotificationActionSchema).optional(),
20624
- sound: string().optional(),
20625
- ttl: number().optional(),
20626
- tag: string().optional(),
20627
- deviceId: number().optional(),
20628
- eventId: string().optional(),
20629
- metadata: record(string(), unknown()).optional()
20630
- });
20631
- /** One declared native severity/priority level for a kind. */
20632
- var TargetKindLevelSchema = object({
20633
- id: string(),
20634
- label: string(),
20635
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
20636
- ordinal: number().int().min(1).max(5).nullable(),
20637
- flags: object({
20638
- critical: boolean().optional(),
20639
- silent: boolean().optional(),
20640
- noPush: boolean().optional()
20641
- }).optional(),
20642
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
20643
- requires: array(string()).optional(),
20644
- description: string().optional()
20645
- });
20646
- /** The full capability block consulted before dispatch. */
20647
- var TargetKindCapsSchema = object({
20648
- attachments: object({
20649
- mediaTypes: array(AttachmentMediaTypeSchema),
20650
- mode: _enum([
20651
- "url",
20652
- "bytes",
20653
- "both"
20654
- ]),
20655
- max: number().int().nonnegative(),
20656
- maxBytes: number().int().positive().optional()
20657
- }),
20658
- /** Max action buttons (0 = none). */
20659
- actions: number().int().nonnegative(),
20660
- levels: array(TargetKindLevelSchema),
20661
- format: array(NotificationFormatSchema),
20662
- clickUrl: boolean(),
20663
- sound: boolean(),
20664
- ttl: boolean(),
20665
- bodyMaxLen: number().int().positive()
20666
- });
20667
- /**
20668
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
20669
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
20670
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
20671
- * the union is large and not meant for runtime validation here; the exported
20672
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
20673
- */
20674
- var ConfigSchemaPassthrough = unknown();
20675
- var TargetKindSchema = object({
20676
- kind: string(),
20677
- label: string(),
20678
- icon: string(),
20679
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
20680
- addonId: string(),
20681
- /**
20682
- * URL of the kind's bundled BRAND icon, served by the providing addon over
20683
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
20684
- * when the addon bundles no icon for that kind — the client then falls back
20685
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
20686
- *
20687
- * Root-relative on purpose: it resolves against whatever origin serves a web
20688
- * client, and a native client joins it onto its own hub base.
20689
- *
20690
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
20691
- * field that survived only because the runtime cap-router forwards provider
20692
- * output verbatim — so every consumer had to re-declare it by hand to stop
20693
- * its own Zod parse from stripping it, and the whole arrangement would have
20694
- * broken silently the moment output validation was tightened anywhere.
20695
- */
20696
- iconUrl: string().optional(),
20697
- /**
20698
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
20699
- *
20700
- * The server knows this and therefore says it, because the client cannot
20701
- * safely guess: a React-Native client renders SVG and raster through two
20702
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
20703
- * not decode SVG on iOS/Android), so without this it silently fell back to a
20704
- * placeholder glyph for every vector icon while the web build looked fine.
20705
- *
20706
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
20707
- * not been updated — a client that cannot determine the type should prefer
20708
- * its raster path, which is the safe default for an unknown image.
20709
- */
20710
- iconMediaType: string().optional(),
20711
- configSchema: ConfigSchemaPassthrough,
20712
- supportsDiscovery: boolean(),
20713
- caps: TargetKindCapsSchema
20714
- });
20715
- /**
20716
- * A persisted target. `config` holds secrets; providers REDACT secret fields
20717
- * (return a presence marker only) when serving `listTargets` — never
20718
- * round-trip a stored secret to the UI.
20719
- */
20720
- var TargetSchema = object({
20721
- id: string(),
20722
- name: string(),
20723
- kind: string(),
20724
- addonId: string(),
20725
- enabled: boolean(),
20726
- config: record(string(), unknown())
20727
- });
20728
- /** A discovery-surfaced candidate (config is partial + non-secret). */
20729
- var DiscoveredTargetSchema = object({
20730
- kind: string(),
20731
- suggestedName: string(),
20732
- config: record(string(), unknown())
20733
- });
20734
- /** The degrade engine's report — what was resolved / dropped / degraded. */
20735
- var RenderedAsSchema = object({
20736
- level: string(),
20737
- format: NotificationFormatSchema,
20738
- attachmentsSent: number().int().nonnegative(),
20739
- actionsSent: number().int().nonnegative(),
20740
- truncated: boolean(),
20741
- dropped: array(string())
20742
- });
20743
- var SendResultSchema = object({
20744
- success: boolean(),
20745
- error: string().optional(),
20746
- renderedAs: RenderedAsSchema.optional()
20747
- });
20748
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
20749
- var TestResultSchema = SendResultSchema;
20750
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
20751
- kind: string(),
20752
- config: record(string(), unknown()).optional()
20753
- }), array(DiscoveredTargetSchema)), method(object({
20754
- targetId: string(),
20755
- notification: NotificationSchema
20756
- }), SendResultSchema, { kind: "mutation" }), method(object({
20757
- targetId: string(),
20758
- sample: NotificationSchema.optional()
20759
- }), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
20760
- targetId: string(),
20761
- enabled: boolean()
20762
- }), _void(), { kind: "mutation" });
20763
- /**
20764
20954
  * core-blocks — user-authored TypeScript, stored in the kernel and executed in
20765
20955
  * its own process.
20766
20956
  *
@@ -28469,6 +28659,12 @@ Object.freeze({
28469
28659
  addonId: null,
28470
28660
  access: "delete"
28471
28661
  },
28662
+ "notificationRules.getAlarmConfig": {
28663
+ capName: "notification-rules",
28664
+ capScope: "system",
28665
+ addonId: null,
28666
+ access: "view"
28667
+ },
28472
28668
  "notificationRules.getConditionCatalog": {
28473
28669
  capName: "notification-rules",
28474
28670
  capScope: "system",
@@ -28499,6 +28695,12 @@ Object.freeze({
28499
28695
  addonId: null,
28500
28696
  access: "view"
28501
28697
  },
28698
+ "notificationRules.setAlarmConfig": {
28699
+ capName: "notification-rules",
28700
+ capScope: "system",
28701
+ addonId: null,
28702
+ access: "create"
28703
+ },
28502
28704
  "notificationRules.setRuleEnabled": {
28503
28705
  capName: "notification-rules",
28504
28706
  capScope: "system",