@camstack/addon-notifiers 1.2.12 → 1.2.13

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 (3) hide show
  1. package/dist/addon.js +776 -275
  2. package/dist/addon.mjs +776 -275
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -9209,8 +9209,11 @@ function prepareNotification(caps, n) {
9209
9209
  });
9210
9210
  }
9211
9211
  const inActions = n.actions ?? [];
9212
- const actions = inActions.slice(0, Math.max(0, caps.actions));
9213
- if (inActions.length > actions.length) dropped.push("actions");
9212
+ const kept = inActions.slice(0, Math.max(0, caps.actions));
9213
+ if (inActions.length > kept.length) dropped.push("actions");
9214
+ const iconsSupported = caps.actionIcons === true;
9215
+ if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
9216
+ const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
9214
9217
  let clickUrl = null;
9215
9218
  if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
9216
9219
  else dropped.push("clickUrl");
@@ -9309,6 +9312,290 @@ var MaskGridDimsSchema = object({
9309
9312
  height: number()
9310
9313
  });
9311
9314
  /**
9315
+ * notification-output — canonical, capability-gated notification delivery.
9316
+ *
9317
+ * Apprise-derived model (see
9318
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
9319
+ * callers emit ONE canonical `Notification`; each provider declares a
9320
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
9321
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
9322
+ * message to what the kind supports — callers never special-case a service.
9323
+ *
9324
+ * DESIGN DECISIONS (locked):
9325
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
9326
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
9327
+ * cap. Rationale: the admin UI needs one uniform surface across the
9328
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
9329
+ * alternative would fork the UI per addon and cannot host the
9330
+ * discovery→adopt flow.
9331
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
9332
+ * the generated cap-mount auto-`concatCollection`-fans them across every
9333
+ * registered provider (notifiers addon + HA addon) so one catalog is
9334
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
9335
+ * `addonId` the generated collection router extracts from the call input.
9336
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
9337
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
9338
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
9339
+ * base64 fallback needed.
9340
+ *
9341
+ * TODO (deferred, closed-set change — separate decision): add
9342
+ * `providerKind: 'notify'` so notification providers surface on the unified
9343
+ * admin "Integrations" page.
9344
+ */
9345
+ /**
9346
+ * Zentik-derived typed-media enum — the superset across every kind. Each
9347
+ * adapter picks what it supports and the degrade engine filters the rest.
9348
+ */
9349
+ var AttachmentMediaTypeSchema = _enum([
9350
+ "image",
9351
+ "video",
9352
+ "gif",
9353
+ "audio",
9354
+ "icon"
9355
+ ]);
9356
+ /**
9357
+ * A single attachment. Exactly one of `url` (remote source, most adapters
9358
+ * prefer this) or `bytes` (inline source; required for Pushover-style
9359
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
9360
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
9361
+ */
9362
+ var AttachmentSchema = object({
9363
+ mediaType: AttachmentMediaTypeSchema,
9364
+ url: string().optional(),
9365
+ bytes: _instanceof(Uint8Array).optional(),
9366
+ mime: string().optional(),
9367
+ name: string().optional()
9368
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
9369
+ var NotificationFormatSchema = _enum([
9370
+ "text",
9371
+ "markdown",
9372
+ "html"
9373
+ ]);
9374
+ /**
9375
+ * The CLOSED icon vocabulary an action button may use.
9376
+ *
9377
+ * A closed set, not a free string, and that is the whole point: an arbitrary
9378
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
9379
+ * notices — the same class of gap as a zone vocabulary nothing produced
9380
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
9381
+ * declares `actionIcons: false` and the degrade engine strips the field.
9382
+ *
9383
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
9384
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
9385
+ * differently.
9386
+ */
9387
+ var NotificationActionIconSchema = _enum([
9388
+ "acknowledge",
9389
+ "dismiss",
9390
+ "silence",
9391
+ "view",
9392
+ "play",
9393
+ "open",
9394
+ "close",
9395
+ "lock",
9396
+ "unlock",
9397
+ "arm",
9398
+ "disarm",
9399
+ "light",
9400
+ "alert"
9401
+ ]);
9402
+ /** A single tap-through action button. */
9403
+ var NotificationActionSchema = object({
9404
+ id: string(),
9405
+ label: string(),
9406
+ url: string().optional(),
9407
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
9408
+ icon: NotificationActionIconSchema.optional(),
9409
+ /**
9410
+ * Renders in a warning style where the notifier supports it.
9411
+ *
9412
+ * A HINT, never a gate. The callback's authority is its token and nothing
9413
+ * else — see `notification-center/action-token.ts` for what that does and
9414
+ * does not buy.
9415
+ */
9416
+ destructive: boolean().optional()
9417
+ });
9418
+ /**
9419
+ * The canonical notification. `body` is the only hard field (Apprise model).
9420
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
9421
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
9422
+ * the adapter maps this ordinal onto its native level. `level?` is an
9423
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
9424
+ * `priority` for that one target.
9425
+ */
9426
+ var NotificationSchema = object({
9427
+ body: string(),
9428
+ title: string().optional(),
9429
+ format: NotificationFormatSchema.default("text"),
9430
+ priority: number().int().min(1).max(5).default(3),
9431
+ level: string().optional(),
9432
+ attachments: array(AttachmentSchema).optional(),
9433
+ clickUrl: string().optional(),
9434
+ actions: array(NotificationActionSchema).optional(),
9435
+ sound: string().optional(),
9436
+ ttl: number().optional(),
9437
+ tag: string().optional(),
9438
+ deviceId: number().optional(),
9439
+ eventId: string().optional(),
9440
+ metadata: record(string(), unknown()).optional()
9441
+ });
9442
+ /** One declared native severity/priority level for a kind. */
9443
+ var TargetKindLevelSchema = object({
9444
+ id: string(),
9445
+ label: string(),
9446
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
9447
+ ordinal: number().int().min(1).max(5).nullable(),
9448
+ flags: object({
9449
+ critical: boolean().optional(),
9450
+ silent: boolean().optional(),
9451
+ noPush: boolean().optional()
9452
+ }).optional(),
9453
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
9454
+ requires: array(string()).optional(),
9455
+ description: string().optional()
9456
+ });
9457
+ /** The full capability block consulted before dispatch. */
9458
+ var TargetKindCapsSchema = object({
9459
+ attachments: object({
9460
+ mediaTypes: array(AttachmentMediaTypeSchema),
9461
+ mode: _enum([
9462
+ "url",
9463
+ "bytes",
9464
+ "both"
9465
+ ]),
9466
+ max: number().int().nonnegative(),
9467
+ maxBytes: number().int().positive().optional()
9468
+ }),
9469
+ /** Max action buttons (0 = none). */
9470
+ actions: number().int().nonnegative(),
9471
+ /**
9472
+ * Whether this kind renders a per-action ICON.
9473
+ *
9474
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
9475
+ * run on the addon cap path — three production failures in one day taught
9476
+ * this repo that once. Absent is read as false by the degrade engine, which
9477
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
9478
+ * assumed and dropped costs the operator's trust in the field.
9479
+ */
9480
+ actionIcons: boolean().optional(),
9481
+ levels: array(TargetKindLevelSchema),
9482
+ format: array(NotificationFormatSchema),
9483
+ clickUrl: boolean(),
9484
+ sound: boolean(),
9485
+ ttl: boolean(),
9486
+ bodyMaxLen: number().int().positive()
9487
+ });
9488
+ /**
9489
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
9490
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
9491
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
9492
+ * the union is large and not meant for runtime validation here; the exported
9493
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
9494
+ */
9495
+ var ConfigSchemaPassthrough$1 = unknown();
9496
+ var TargetKindSchema = object({
9497
+ kind: string(),
9498
+ label: string(),
9499
+ icon: string(),
9500
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
9501
+ addonId: string(),
9502
+ /**
9503
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
9504
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
9505
+ * when the addon bundles no icon for that kind — the client then falls back
9506
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
9507
+ *
9508
+ * Root-relative on purpose: it resolves against whatever origin serves a web
9509
+ * client, and a native client joins it onto its own hub base.
9510
+ *
9511
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
9512
+ * field that survived only because the runtime cap-router forwards provider
9513
+ * output verbatim — so every consumer had to re-declare it by hand to stop
9514
+ * its own Zod parse from stripping it, and the whole arrangement would have
9515
+ * broken silently the moment output validation was tightened anywhere.
9516
+ */
9517
+ iconUrl: string().optional(),
9518
+ /**
9519
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
9520
+ *
9521
+ * The server knows this and therefore says it, because the client cannot
9522
+ * safely guess: a React-Native client renders SVG and raster through two
9523
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
9524
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
9525
+ * placeholder glyph for every vector icon while the web build looked fine.
9526
+ *
9527
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
9528
+ * not been updated — a client that cannot determine the type should prefer
9529
+ * its raster path, which is the safe default for an unknown image.
9530
+ */
9531
+ iconMediaType: string().optional(),
9532
+ configSchema: ConfigSchemaPassthrough$1,
9533
+ supportsDiscovery: boolean(),
9534
+ caps: TargetKindCapsSchema
9535
+ });
9536
+ /**
9537
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
9538
+ * (return a presence marker only) when serving `listTargets` — never
9539
+ * round-trip a stored secret to the UI.
9540
+ */
9541
+ var TargetSchema = object({
9542
+ id: string(),
9543
+ name: string(),
9544
+ kind: string(),
9545
+ addonId: string(),
9546
+ enabled: boolean(),
9547
+ config: record(string(), unknown())
9548
+ });
9549
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
9550
+ var DiscoveredTargetSchema = object({
9551
+ kind: string(),
9552
+ suggestedName: string(),
9553
+ config: record(string(), unknown())
9554
+ });
9555
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
9556
+ var RenderedAsSchema = object({
9557
+ level: string(),
9558
+ format: NotificationFormatSchema,
9559
+ attachmentsSent: number().int().nonnegative(),
9560
+ actionsSent: number().int().nonnegative(),
9561
+ truncated: boolean(),
9562
+ dropped: array(string())
9563
+ });
9564
+ var SendResultSchema = object({
9565
+ success: boolean(),
9566
+ error: string().optional(),
9567
+ renderedAs: RenderedAsSchema.optional()
9568
+ });
9569
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
9570
+ var TestResultSchema = SendResultSchema;
9571
+ var notificationOutputCapability = {
9572
+ name: "notification-output",
9573
+ scope: "system",
9574
+ mode: "collection",
9575
+ methods: {
9576
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
9577
+ listTargets: method(object({}), array(TargetSchema)),
9578
+ discoverTargets: method(object({
9579
+ kind: string(),
9580
+ config: record(string(), unknown()).optional()
9581
+ }), array(DiscoveredTargetSchema)),
9582
+ send: method(object({
9583
+ targetId: string(),
9584
+ notification: NotificationSchema
9585
+ }), SendResultSchema, { kind: "mutation" }),
9586
+ testTarget: method(object({
9587
+ targetId: string(),
9588
+ sample: NotificationSchema.optional()
9589
+ }), TestResultSchema, { kind: "mutation" }),
9590
+ upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
9591
+ deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
9592
+ setTargetEnabled: method(object({
9593
+ targetId: string(),
9594
+ enabled: boolean()
9595
+ }), _void(), { kind: "mutation" })
9596
+ }
9597
+ };
9598
+ /**
9312
9599
  * notification-rules — the Notification Center rule surface (P1 core).
9313
9600
  *
9314
9601
  * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
@@ -9438,7 +9725,115 @@ var NcZoneConditionSchema = object({
9438
9725
  * The P1 condition set — a flat AND of groups; absent group = pass;
9439
9726
  * membership lists are OR within the list (spec §2.3).
9440
9727
  */
9728
+ /**
9729
+ * What a rule may actuate.
9730
+ *
9731
+ * **No hand-maintained allowlist** (operator decision, and the right one — a
9732
+ * written list of methods is a third parallel map to keep aligned, and this
9733
+ * repo has paid for those). The boundary instead comes from a property the
9734
+ * capabilities already carry: an action may target only a **device-scoped**
9735
+ * capability method.
9736
+ *
9737
+ * That is not decoration. A rule can be authored by a NON-ADMIN — personal
9738
+ * rules are a supported flow — and the executor runs with the addon's
9739
+ * privileges, so an unbounded action is an arbitrary RPC channel with a
9740
+ * privilege escalation attached. Restricting to device scope excludes the
9741
+ * system caps (`device-manager.removeDevice` and friends) by construction,
9742
+ * costs nothing to maintain, and cannot rot: a cap that stops being
9743
+ * device-scoped stops being actuatable in the same change.
9744
+ *
9745
+ * The executor enforces it; {@link NcRuleActionSchema} carries the intent.
9746
+ */
9747
+ /**
9748
+ * One step of a sequence.
9749
+ *
9750
+ * `wait` is a first-class step rather than a property of the next action: it is
9751
+ * what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
9752
+ * cannot be expressed otherwise.
9753
+ */
9754
+ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9755
+ kind: literal("wait"),
9756
+ seconds: number().min(0).max(300)
9757
+ }), object({
9758
+ kind: literal("cap"),
9759
+ deviceId: number().int(),
9760
+ /** Capability name, e.g. `alarm-panel`. */
9761
+ cap: string().min(1),
9762
+ /** Method on it. The executor refuses a non-device-scoped cap. */
9763
+ method: string().min(1),
9764
+ /** Method arguments, minus `deviceId` (the executor injects it). */
9765
+ args: record(string(), unknown()).optional()
9766
+ })]);
9767
+ /**
9768
+ * A named, ordered run of steps with its own throttle.
9769
+ *
9770
+ * `minDelaySec` exists because a noisy rule otherwise hammers a physical
9771
+ * actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
9772
+ * different budget from "how often may this gate actually open".
9773
+ */
9774
+ var NcRuleActionSequenceSchema = object({
9775
+ name: string().min(1).max(120),
9776
+ enabled: boolean(),
9777
+ minDelaySec: number().int().min(0).max(86400).optional(),
9778
+ actions: array(NcRuleActionSchema).min(1)
9779
+ });
9780
+ /**
9781
+ * One button carried by the notification, running a named sequence on tap.
9782
+ *
9783
+ * **Read this before adding a button that does something physical.** The tap
9784
+ * arrives over a link that travelled through third-party infrastructure — ntfy,
9785
+ * a push relay, whatever forwarded the message — and the callback's ONLY
9786
+ * authority is the token in that link: single-use, short-lived, bound to this
9787
+ * one action of this one notification. It does not identify who tapped.
9788
+ * Whoever holds the notification can run the button, once, inside the window.
9789
+ * That is the operator's explicit choice (2026-08-05), and `destructive` is a
9790
+ * rendering hint, not a second gate. [D47](decisions/adr-0047.md).
9791
+ */
9792
+ var NcRuleNotificationButtonSchema = object({
9793
+ /** Stable id — travels in the callback and identifies the button in logs. */
9794
+ id: string().min(1).max(64),
9795
+ label: string().min(1).max(40),
9796
+ /** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
9797
+ * sequence does not exist rather than minting a token for nothing. */
9798
+ sequence: string().min(1).max(120),
9799
+ icon: NotificationActionIconSchema.optional(),
9800
+ destructive: boolean().optional()
9801
+ });
9802
+ /**
9803
+ * Sequences a rule runs, by hook point.
9804
+ *
9805
+ * ONLY `onTrigger` is here, deliberately. The reference also has activation /
9806
+ * deactivation / reset / post-generation hooks, and they are wanted — but this
9807
+ * repo's expensive failure mode is declaring a surface nothing produces, so a
9808
+ * hook appears here in the same change that produces its edge, never before.
9809
+ */
9810
+ var NcRuleActionsSchema = object({
9811
+ /** Runs when the rule MATCHES. */
9812
+ onTrigger: array(NcRuleActionSequenceSchema).optional(),
9813
+ /**
9814
+ * Buttons the NOTIFICATION carries, each running one of this rule's
9815
+ * sequences when tapped.
9816
+ *
9817
+ * Deliberately a REFERENCE to a sequence rather than a second place to
9818
+ * author steps. A button that could define its own actions would be a
9819
+ * parallel actuation vocabulary — the executor's device-scope check, the
9820
+ * stop-at-first-failure rule and the per-sequence throttle all live on
9821
+ * sequences, and a second authoring surface would drift from every one of
9822
+ * them.
9823
+ *
9824
+ * A sequence reachable ONLY by a button simply appears in `onTrigger` with
9825
+ * `enabled: false`: it is then authored, throttled and validated like the
9826
+ * rest, and nothing runs it automatically.
9827
+ */
9828
+ buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
9829
+ });
9441
9830
  var NcConditionsSchema = object({
9831
+ /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
9832
+ deviceState: object({
9833
+ deviceId: number().int(),
9834
+ /** Any of these matches. */
9835
+ states: array(string().min(1)).min(1)
9836
+ }).optional(),
9442
9837
  /** Device scope — absent = all devices. */
9443
9838
  devices: array(number()).optional(),
9444
9839
  /** Detector class names (any overlap with the record's class set). */
@@ -9639,6 +10034,14 @@ var NcMediaPolicySchema = object({
9639
10034
  clipPreRollSec: number().int().min(0).max(30).optional(),
9640
10035
  clipPostRollSec: number().int().min(0).max(30).optional(),
9641
10036
  /**
10037
+ * Playback rate of the attached gif / clip. Absent = 2x.
10038
+ *
10039
+ * A notification clip is GLANCED at on a lock screen, not watched: at real
10040
+ * time an eight-second passage is eight seconds of the recipient's attention
10041
+ * and twice the bytes. 1 is real time for the operator who wants it.
10042
+ */
10043
+ clipSpeed: number().min(1).max(8).optional(),
10044
+ /**
9642
10045
  * Which stream profile the footage is cut from. Absent = the CHEAPEST
9643
10046
  * assigned profile: a notification is watched on a phone, so the 4K
9644
10047
  * rendition would burn CPU to produce a file the client downscales anyway.
@@ -9710,7 +10113,30 @@ var NcRuleInputSchema = object({
9710
10113
  * behaviour, visible to all, read-only in the viewer). Present = personal
9711
10114
  * rule owned by this userId. Server-stamped; never trusted from a client.
9712
10115
  */
9713
- ownerUserId: string().optional()
10116
+ ownerUserId: string().optional(),
10117
+ /**
10118
+ * May a non-admin snooze this rule for EVERYONE, not just themselves?
10119
+ *
10120
+ * A snooze is personal by default — it silences the person who set it. This
10121
+ * opts THIS rule into the "the gardener is here all afternoon" case, where
10122
+ * silencing the camera for the whole household is legitimate. It silences
10123
+ * other people, so it is off unless a rule deliberately allows it.
10124
+ *
10125
+ * `.optional()`, deliberately NOT `.default()`: a Zod default does not run on
10126
+ * the addon cap path (three production failures in one day), so absent is
10127
+ * read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
10128
+ * by this flag — see the scope rules on that function.
10129
+ */
10130
+ snoozeAllowGlobal: boolean().optional(),
10131
+ /**
10132
+ * Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
10133
+ *
10134
+ * This is what makes the rule set the alarm's trigger set without the alarm
10135
+ * being a special case: arming is
10136
+ * `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
10137
+ * shape as every other actuation.
10138
+ */
10139
+ actions: NcRuleActionsSchema.optional()
9714
10140
  });
9715
10141
  /**
9716
10142
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -9781,7 +10207,8 @@ var NcConditionDescriptorSchema = object({
9781
10207
  "packagePhase",
9782
10208
  "crossingSelect",
9783
10209
  "polygonDraw",
9784
- "occupancy"
10210
+ "occupancy",
10211
+ "deviceState"
9785
10212
  ]),
9786
10213
  operator: _enum([
9787
10214
  "in",
@@ -9795,7 +10222,28 @@ var NcConditionDescriptorSchema = object({
9795
10222
  /** Which delivery kinds the condition applies to. */
9796
10223
  appliesTo: array(NcDeliverySchema),
9797
10224
  phase: string(),
9798
- description: string().optional()
10225
+ description: string().optional(),
10226
+ /**
10227
+ * The CHOICES for a single-choice widget (`sourceSelect`, `crossingSelect`,
10228
+ * `packagePhase`, …), served with the descriptor.
10229
+ *
10230
+ * Before this the descriptor said which widget to render and not what to put
10231
+ * in it, so every option list lived in three places: this file's enums, the
10232
+ * admin's `NC_*_OPTIONS` and the viewer's `NC_*_VALUES`. That triple mirror
10233
+ * is the drift that emptied the viewer's rule editor on 2026-08-04 — the app
10234
+ * mirrors the cap by hand, so it is only ever as current as its last build.
10235
+ *
10236
+ * With the options on the wire, a condition of an EXISTING `valueType` costs
10237
+ * zero client changes. Clients keep a local fallback for an older hub that
10238
+ * does not send them; absent here is "use your own list", not "no choices".
10239
+ */
10240
+ options: array(object({
10241
+ /** Written to the rule verbatim. `''` means the ABSENT state. */
10242
+ value: string(),
10243
+ label: string(),
10244
+ /** What THIS choice matches — shown one at a time, under the control. */
10245
+ hint: string().optional()
10246
+ })).readonly().optional()
9799
10247
  });
9800
10248
  /**
9801
10249
  * The delivery lifecycle status of a history row — a straight read of the
@@ -9880,6 +10328,74 @@ var NcHistoryFilterSchema = object({
9880
10328
  until: number().optional(),
9881
10329
  limit: number().int().min(1).max(500).default(100)
9882
10330
  });
10331
+ /**
10332
+ * What a snooze covers. Broader scopes win when several overlap, so one window
10333
+ * leaves ONE digest rather than a rule snooze and a whole-feed snooze both
10334
+ * summarising the same silence.
10335
+ */
10336
+ var NcSnoozeScopeSchema = _enum([
10337
+ "rule",
10338
+ "device",
10339
+ "all"
10340
+ ]);
10341
+ /**
10342
+ * Client-authored snooze. The server stamps `userId`, `startedAt` and
10343
+ * `expiresAt` — a DURATION is sent rather than an instant so a client with a
10344
+ * skewed clock cannot author a window that is already over, or never ends.
10345
+ */
10346
+ var NcSnoozeInputSchema = object({
10347
+ scope: NcSnoozeScopeSchema,
10348
+ /** Required when `scope: 'rule'` — a scoped snooze with no id matches
10349
+ * NOTHING rather than degrading to "everything". */
10350
+ ruleId: string().optional(),
10351
+ /** Required when `scope: 'device'`. */
10352
+ deviceId: number().int().optional(),
10353
+ durationMinutes: number().int().min(1).max(1440),
10354
+ /**
10355
+ * Silence this for EVERY recipient, not just the caller. Permission is
10356
+ * checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
10357
+ * broader scopes). Absent = personal.
10358
+ */
10359
+ global: boolean().optional(),
10360
+ /**
10361
+ * Deliver a summary of what was suppressed when the window ends. Absent =
10362
+ * ON: someone silencing a nuisance camera wants it off, someone silencing a
10363
+ * SECURITY camera wants to know what they missed, and choosing "off" for
10364
+ * everybody is how a snooze becomes an outage. Resolved to a concrete
10365
+ * boolean by the server at create time — never left to a Zod default, which
10366
+ * does not run on the addon cap path.
10367
+ */
10368
+ summary: boolean().optional()
10369
+ });
10370
+ /** A persisted snooze window. */
10371
+ var NcSnoozeSchema = object({
10372
+ id: string(),
10373
+ /** Who set it. Also who it silences, unless `global`. */
10374
+ userId: string(),
10375
+ scope: NcSnoozeScopeSchema,
10376
+ ruleId: string().optional(),
10377
+ deviceId: number().int().optional(),
10378
+ startedAt: number(),
10379
+ /** Exclusive: at exactly this instant the snooze is over. Expiry is a
10380
+ * COMPARISON, not a job — no sweeper can leave the operator silenced. */
10381
+ expiresAt: number(),
10382
+ global: boolean(),
10383
+ summary: boolean(),
10384
+ /** When the end-of-window digest went out. Absent = not sent (yet, or the
10385
+ * window has not closed, or `summary` is false). */
10386
+ digestSentAt: number().optional()
10387
+ });
10388
+ object({
10389
+ snoozeId: string(),
10390
+ targetId: string(),
10391
+ ruleId: string(),
10392
+ ruleName: string(),
10393
+ deviceId: number().int(),
10394
+ /** How many notifications this snooze hid for that pair. */
10395
+ count: number().int(),
10396
+ firstAt: number(),
10397
+ lastAt: number()
10398
+ });
9883
10399
  method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
9884
10400
  kind: "mutation",
9885
10401
  auth: "admin",
@@ -9909,7 +10425,13 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
9909
10425
  }), method(object({}), object({
9910
10426
  catalog: array(NcConditionDescriptorSchema),
9911
10427
  taxonomy: NcTaxonomySchema.optional()
9912
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10428
+ })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
10429
+ kind: "mutation",
10430
+ caller: "required"
10431
+ }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
10432
+ kind: "mutation",
10433
+ caller: "required"
10434
+ });
9913
10435
  /**
9914
10436
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9915
10437
  *
@@ -10657,7 +11179,15 @@ method(object({
10657
11179
  format: _enum(["gif", "mp4"]).default("gif"),
10658
11180
  maxWidth: number().int().min(120).max(1920).default(480),
10659
11181
  /** GIF only — MP4 keeps the source cadence. */
10660
- fps: number().int().min(1).max(15).default(5)
11182
+ fps: number().int().min(1).max(15).default(5),
11183
+ /**
11184
+ * Playback rate. A notification clip is GLANCED at on a lock screen,
11185
+ * not watched, so 2x is the default: the recipient sees the whole
11186
+ * passage in half the time and the GIF is half the bytes. `1` is real
11187
+ * time. Applies to MP4 as well — the operator set a speed, not a GIF
11188
+ * speed.
11189
+ */
11190
+ speed: number().min(1).max(8).default(2)
10661
11191
  }), object({
10662
11192
  base64: string(),
10663
11193
  mime: string(),
@@ -16325,7 +16855,20 @@ method(object({
16325
16855
  }), _void(), {
16326
16856
  kind: "mutation",
16327
16857
  auth: "admin"
16328
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
16858
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
16859
+ addonId: string().optional(),
16860
+ /**
16861
+ * `slim` omits `config` (returned `{}`), `metadata` (null) and the
16862
+ * `sourceInfo` derived from config — and skips the per-device settings
16863
+ * read that produces them. Everything identifying a device (id, name,
16864
+ * type, online, features, isCamera, parent/link ids) is unchanged.
16865
+ * Do not use it for dispatch routing, which needs `sourceInfo`.
16866
+ */
16867
+ projection: _enum(["full", "slim"]).optional(),
16868
+ /** Return only camera devices. Filtering server-side instead of
16869
+ * shipping 293 rows to find 12. */
16870
+ isCamera: boolean().optional()
16871
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
16329
16872
  mode: DeviceLinkModeSchema,
16330
16873
  devices: array(LinkedDeviceSchema)
16331
16874
  })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -16764,14 +17307,14 @@ var LlmProfileSchema = object({
16764
17307
  /** ConfigUISchema tree passed through untyped on the wire (the
16765
17308
  * notification-output `ConfigSchemaPassthrough` precedent at
16766
17309
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
16767
- var ConfigSchemaPassthrough$1 = unknown();
17310
+ var ConfigSchemaPassthrough = unknown();
16768
17311
  var LlmProfileKindDescriptorSchema = object({
16769
17312
  kind: LlmProfileKindSchema,
16770
17313
  label: string(),
16771
17314
  icon: string(),
16772
17315
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
16773
17316
  addonId: string(),
16774
- configSchema: ConfigSchemaPassthrough$1
17317
+ configSchema: ConfigSchemaPassthrough
16775
17318
  });
16776
17319
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
16777
17320
  var LlmDefaultSchema = object({
@@ -17261,282 +17804,143 @@ var StartEmbeddedInputSchema = object({
17261
17804
  });
17262
17805
  var StartEmbeddedResultSchema = object({
17263
17806
  id: string(),
17264
- url: string()
17265
- });
17266
- var StatusSchema = object({
17267
- brokerCount: number(),
17268
- embeddedRunning: boolean()
17269
- });
17270
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
17271
- var NetworkEndpointSchema = object({
17272
- url: string(),
17273
- hostname: string(),
17274
- port: number(),
17275
- protocol: _enum(["http", "https"])
17276
- });
17277
- var NetworkAccessStatusSchema = object({
17278
- connected: boolean(),
17279
- endpoint: NetworkEndpointSchema.nullable(),
17280
- error: string().optional()
17281
- });
17282
- /**
17283
- * Optional, richer endpoint shape returned by providers that expose
17284
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
17285
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
17286
- * the originating provider config (mode + sourcePort) so the
17287
- * orchestrator UI can label rows distinctly. Providers that expose only
17288
- * one endpoint just omit `listEndpoints` from their provider impl.
17289
- */
17290
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
17291
- /**
17292
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
17293
- * the orchestrator can dedupe across `listEndpoints` polls.
17294
- */
17295
- id: string(),
17296
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
17297
- label: string(),
17298
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
17299
- mode: string().optional(),
17300
- /** Originating local port the ingress fronts (informational). */
17301
- sourcePort: number().optional()
17302
- });
17303
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17304
- /**
17305
- * notification-output — canonical, capability-gated notification delivery.
17306
- *
17307
- * Apprise-derived model (see
17308
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
17309
- * callers emit ONE canonical `Notification`; each provider declares a
17310
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
17311
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
17312
- * message to what the kind supports — callers never special-case a service.
17313
- *
17314
- * DESIGN DECISIONS (locked):
17315
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
17316
- * `setTargetEnabled`), each provider persisting via the `settings-store`
17317
- * cap. Rationale: the admin UI needs one uniform surface across the
17318
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
17319
- * alternative would fork the UI per addon and cannot host the
17320
- * discovery→adopt flow.
17321
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
17322
- * the generated cap-mount auto-`concatCollection`-fans them across every
17323
- * registered provider (notifiers addon + HA addon) so one catalog is
17324
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
17325
- * `addonId` the generated collection router extracts from the call input.
17326
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
17327
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
17328
- * `storage` / `storage-provider` / `recording` caps over the same path. No
17329
- * base64 fallback needed.
17330
- *
17331
- * TODO (deferred, closed-set change — separate decision): add
17332
- * `providerKind: 'notify'` so notification providers surface on the unified
17333
- * admin "Integrations" page.
17334
- */
17335
- /**
17336
- * Zentik-derived typed-media enum — the superset across every kind. Each
17337
- * adapter picks what it supports and the degrade engine filters the rest.
17338
- */
17339
- var AttachmentMediaTypeSchema = _enum([
17340
- "image",
17341
- "video",
17342
- "gif",
17343
- "audio",
17344
- "icon"
17345
- ]);
17346
- /**
17347
- * A single attachment. Exactly one of `url` (remote source, most adapters
17348
- * prefer this) or `bytes` (inline source; required for Pushover-style
17349
- * bytes-only kinds) MUST be present — the degrade engine expresses a
17350
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
17351
- */
17352
- var AttachmentSchema = object({
17353
- mediaType: AttachmentMediaTypeSchema,
17354
- url: string().optional(),
17355
- bytes: _instanceof(Uint8Array).optional(),
17356
- mime: string().optional(),
17357
- name: string().optional()
17358
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
17359
- var NotificationFormatSchema = _enum([
17360
- "text",
17361
- "markdown",
17362
- "html"
17363
- ]);
17364
- /** A single tap-through action button. */
17365
- var NotificationActionSchema = object({
17366
- id: string(),
17367
- label: string(),
17368
- url: string().optional()
17369
- });
17370
- /**
17371
- * The canonical notification. `body` is the only hard field (Apprise model).
17372
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
17373
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
17374
- * the adapter maps this ordinal onto its native level. `level?` is an
17375
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
17376
- * `priority` for that one target.
17377
- */
17378
- var NotificationSchema = object({
17379
- body: string(),
17380
- title: string().optional(),
17381
- format: NotificationFormatSchema.default("text"),
17382
- priority: number().int().min(1).max(5).default(3),
17383
- level: string().optional(),
17384
- attachments: array(AttachmentSchema).optional(),
17385
- clickUrl: string().optional(),
17386
- actions: array(NotificationActionSchema).optional(),
17387
- sound: string().optional(),
17388
- ttl: number().optional(),
17389
- tag: string().optional(),
17390
- deviceId: number().optional(),
17391
- eventId: string().optional(),
17392
- metadata: record(string(), unknown()).optional()
17393
- });
17394
- /** One declared native severity/priority level for a kind. */
17395
- var TargetKindLevelSchema = object({
17396
- id: string(),
17397
- label: string(),
17398
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
17399
- ordinal: number().int().min(1).max(5).nullable(),
17400
- flags: object({
17401
- critical: boolean().optional(),
17402
- silent: boolean().optional(),
17403
- noPush: boolean().optional()
17404
- }).optional(),
17405
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
17406
- requires: array(string()).optional(),
17407
- description: string().optional()
17807
+ url: string()
17408
17808
  });
17409
- /** The full capability block consulted before dispatch. */
17410
- var TargetKindCapsSchema = object({
17411
- attachments: object({
17412
- mediaTypes: array(AttachmentMediaTypeSchema),
17413
- mode: _enum([
17414
- "url",
17415
- "bytes",
17416
- "both"
17417
- ]),
17418
- max: number().int().nonnegative(),
17419
- maxBytes: number().int().positive().optional()
17420
- }),
17421
- /** Max action buttons (0 = none). */
17422
- actions: number().int().nonnegative(),
17423
- levels: array(TargetKindLevelSchema),
17424
- format: array(NotificationFormatSchema),
17425
- clickUrl: boolean(),
17426
- sound: boolean(),
17427
- ttl: boolean(),
17428
- bodyMaxLen: number().int().positive()
17809
+ var StatusSchema = object({
17810
+ brokerCount: number(),
17811
+ embeddedRunning: boolean()
17812
+ });
17813
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
17814
+ var NetworkEndpointSchema = object({
17815
+ url: string(),
17816
+ hostname: string(),
17817
+ port: number(),
17818
+ protocol: _enum(["http", "https"])
17819
+ });
17820
+ var NetworkAccessStatusSchema = object({
17821
+ connected: boolean(),
17822
+ endpoint: NetworkEndpointSchema.nullable(),
17823
+ error: string().optional()
17429
17824
  });
17430
17825
  /**
17431
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17432
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17433
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
17434
- * the union is large and not meant for runtime validation here; the exported
17435
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
17826
+ * Optional, richer endpoint shape returned by providers that expose
17827
+ * MORE than one ingress concurrently (Tailscale Ingress with mixed
17828
+ * serve+funnel rules, future ngrok multi-tunnel, ). Each entry carries
17829
+ * the originating provider config (mode + sourcePort) so the
17830
+ * orchestrator UI can label rows distinctly. Providers that expose only
17831
+ * one endpoint just omit `listEndpoints` from their provider impl.
17436
17832
  */
17437
- var ConfigSchemaPassthrough = unknown();
17438
- var TargetKindSchema = object({
17439
- kind: string(),
17440
- label: string(),
17441
- icon: string(),
17442
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
17443
- addonId: string(),
17444
- /**
17445
- * URL of the kind's bundled BRAND icon, served by the providing addon over
17446
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
17447
- * when the addon bundles no icon for that kind — the client then falls back
17448
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
17449
- *
17450
- * Root-relative on purpose: it resolves against whatever origin serves a web
17451
- * client, and a native client joins it onto its own hub base.
17452
- *
17453
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
17454
- * field that survived only because the runtime cap-router forwards provider
17455
- * output verbatim — so every consumer had to re-declare it by hand to stop
17456
- * its own Zod parse from stripping it, and the whole arrangement would have
17457
- * broken silently the moment output validation was tightened anywhere.
17458
- */
17459
- iconUrl: string().optional(),
17833
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
17460
17834
  /**
17461
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
17462
- *
17463
- * The server knows this and therefore says it, because the client cannot
17464
- * safely guess: a React-Native client renders SVG and raster through two
17465
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
17466
- * not decode SVG on iOS/Android), so without this it silently fell back to a
17467
- * placeholder glyph for every vector icon while the web build looked fine.
17468
- *
17469
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
17470
- * not been updated — a client that cannot determine the type should prefer
17471
- * its raster path, which is the safe default for an unknown image.
17835
+ * Stable id within the provider typically `<mode>-<sourcePort>` so
17836
+ * the orchestrator can dedupe across `listEndpoints` polls.
17472
17837
  */
17473
- iconMediaType: string().optional(),
17474
- configSchema: ConfigSchemaPassthrough,
17475
- supportsDiscovery: boolean(),
17476
- caps: TargetKindCapsSchema
17838
+ id: string(),
17839
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
17840
+ label: string(),
17841
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
17842
+ mode: string().optional(),
17843
+ /** Originating local port the ingress fronts (informational). */
17844
+ sourcePort: number().optional()
17477
17845
  });
17846
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17478
17847
  /**
17479
- * A persisted target. `config` holds secrets; providers REDACT secret fields
17480
- * (return a presence marker only) when serving `listTargets` — never
17481
- * round-trip a stored secret to the UI.
17482
- */
17483
- var TargetSchema = object({
17484
- id: string(),
17485
- name: string(),
17486
- kind: string(),
17487
- addonId: string(),
17848
+ * core-blocks user-authored TypeScript, stored in the kernel and executed in
17849
+ * its own process.
17850
+ *
17851
+ * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
17852
+ *
17853
+ * The first use is **owning devices without being a device provider**: a block
17854
+ * declares devices under a system or custom integration and drives their state,
17855
+ * with the same `ctx` an addon gets. Automations come later; nothing here
17856
+ * models a trigger.
17857
+ *
17858
+ * **Stated plainly, because it does not change by being true:** a block has an
17859
+ * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
17860
+ * with no review step. What makes that survivable is not a sandbox, it is
17861
+ * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
17862
+ * so a block that throws or never returns is marked `failed` and visible
17863
+ * instead of taking the hub with it (D6). Every method here is admin-only, and
17864
+ * must stay so.
17865
+ */
17866
+ /** Where a block runs. The operator chooses — a block driving a device on an
17867
+ * agent is the reason placement is not fixed to the hub. */
17868
+ var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
17869
+ /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
17870
+ * a failing block reads the same way a failing addon does. */
17871
+ var CoreBlockStatusSchema = _enum([
17872
+ "stopped",
17873
+ "starting",
17874
+ "running",
17875
+ "failed"
17876
+ ]);
17877
+ /** Client-authored fields. */
17878
+ var CoreBlockInputSchema = object({
17879
+ name: string().min(1).max(120),
17880
+ /** TypeScript source. Compiled server-side before it is ever stored — a
17881
+ * block that does not compile is a fork failure the operator would meet
17882
+ * minutes later, in a log, instead of in the editor. */
17883
+ code: string().max(2e5),
17488
17884
  enabled: boolean(),
17489
- config: record(string(), unknown())
17490
- });
17491
- /** A discovery-surfaced candidate (config is partial + non-secret). */
17492
- var DiscoveredTargetSchema = object({
17493
- kind: string(),
17494
- suggestedName: string(),
17495
- config: record(string(), unknown())
17885
+ placement: CoreBlockPlacementSchema,
17886
+ /**
17887
+ * Integration the block's devices hang from. Absent = the system integration
17888
+ * blocks share. A block may declare its own instead.
17889
+ */
17890
+ integrationId: string().optional()
17496
17891
  });
17497
- /** The degrade engine's report — what was resolved / dropped / degraded. */
17498
- var RenderedAsSchema = object({
17499
- level: string(),
17500
- format: NotificationFormatSchema,
17501
- attachmentsSent: number().int().nonnegative(),
17502
- actionsSent: number().int().nonnegative(),
17503
- truncated: boolean(),
17504
- dropped: array(string())
17892
+ /** A stored block. */
17893
+ var CoreBlockSchema = CoreBlockInputSchema.extend({
17894
+ id: string(),
17895
+ createdAt: number(),
17896
+ updatedAt: number(),
17897
+ /** Server-stamped author. */
17898
+ createdBy: string(),
17899
+ status: CoreBlockStatusSchema,
17900
+ /**
17901
+ * Why the block is not running, when it is not. The operator's ONLY window
17902
+ * into a block that failed at load — a block that is silently absent is the
17903
+ * failure mode this whole feature has to avoid.
17904
+ */
17905
+ lastError: string().optional(),
17906
+ /** Ms epoch of the last state change. */
17907
+ lastChangedAt: number()
17505
17908
  });
17506
- var SendResultSchema = object({
17507
- success: boolean(),
17909
+ /** What a compile attempt produced. */
17910
+ var CoreBlockCompileResultSchema = object({
17911
+ ok: boolean(),
17912
+ /** Present when `ok` is false — the first error, in the author's words. */
17508
17913
  error: string().optional(),
17509
- renderedAs: RenderedAsSchema.optional()
17914
+ line: number().optional(),
17915
+ column: number().optional()
17510
17916
  });
17511
- /** Same shape as SendResult kept as a distinct name for the test panel. */
17512
- var TestResultSchema = SendResultSchema;
17513
- var notificationOutputCapability = {
17514
- name: "notification-output",
17515
- scope: "system",
17516
- mode: "collection",
17517
- methods: {
17518
- listTargetKinds: method(object({}), array(TargetKindSchema)),
17519
- listTargets: method(object({}), array(TargetSchema)),
17520
- discoverTargets: method(object({
17521
- kind: string(),
17522
- config: record(string(), unknown()).optional()
17523
- }), array(DiscoveredTargetSchema)),
17524
- send: method(object({
17525
- targetId: string(),
17526
- notification: NotificationSchema
17527
- }), SendResultSchema, { kind: "mutation" }),
17528
- testTarget: method(object({
17529
- targetId: string(),
17530
- sample: NotificationSchema.optional()
17531
- }), TestResultSchema, { kind: "mutation" }),
17532
- upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
17533
- deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
17534
- setTargetEnabled: method(object({
17535
- targetId: string(),
17536
- enabled: boolean()
17537
- }), _void(), { kind: "mutation" })
17538
- }
17539
- };
17917
+ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }), method(object({ blockId: string() }), object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }), method(object({ block: CoreBlockInputSchema }), object({ block: CoreBlockSchema }), {
17918
+ kind: "mutation",
17919
+ auth: "admin",
17920
+ caller: "required"
17921
+ }), method(object({
17922
+ blockId: string(),
17923
+ block: CoreBlockInputSchema.partial()
17924
+ }), object({ block: CoreBlockSchema }), {
17925
+ kind: "mutation",
17926
+ auth: "admin",
17927
+ caller: "required"
17928
+ }), method(object({ blockId: string() }), object({ success: literal(true) }), {
17929
+ kind: "mutation",
17930
+ auth: "admin"
17931
+ }), method(object({
17932
+ blockId: string(),
17933
+ enabled: boolean()
17934
+ }), object({ block: CoreBlockSchema }), {
17935
+ kind: "mutation",
17936
+ auth: "admin"
17937
+ }), method(object({ code: string() }), CoreBlockCompileResultSchema, {
17938
+ kind: "mutation",
17939
+ auth: "admin"
17940
+ }), method(object({}), object({ libs: array(object({
17941
+ filePath: string(),
17942
+ content: string()
17943
+ })) }), { auth: "admin" });
17540
17944
  /**
17541
17945
  * Zod schemas for persisted record types.
17542
17946
  *
@@ -17784,6 +18188,11 @@ var EventKindDescriptorSchema = object({
17784
18188
  deviceId: number()
17785
18189
  })
17786
18190
  });
18191
+ /** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
18192
+ var EventKindsForDeviceSchema = object({
18193
+ deviceId: number(),
18194
+ kinds: array(EventKindDescriptorSchema).readonly()
18195
+ });
17787
18196
  var SensorEventSchema = object({
17788
18197
  id: string(),
17789
18198
  /** The CAMERA the event is attributed to (a sensor linked to N cameras
@@ -18040,6 +18449,19 @@ var MediaFileSchema = object({
18040
18449
  sizeBytes: number(),
18041
18450
  timestamp: number()
18042
18451
  });
18452
+ /**
18453
+ * One media row WITHOUT its bytes.
18454
+ *
18455
+ * A track's media is 5-8 MB of base64 (measured: 7.67 MB across 23 files for a
18456
+ * 140 s track), and a client that renders tiles from the media data plane needs
18457
+ * to know only WHAT EXISTS — the bytes then arrive per tile, lazily, over HTTP
18458
+ * with an immutable cache, instead of all at once inside a tRPC response that
18459
+ * blocks the whole view.
18460
+ *
18461
+ * `sizeBytes` is carried because it is what lets a client decide between the
18462
+ * stored blob and a `?variant=thumb` rendering without fetching either.
18463
+ */
18464
+ var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18043
18465
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
18044
18466
  var MAX_EVENT_QUERY_LIMIT = 5e3;
18045
18467
  var DeviceEventQueryInput = object({
@@ -18189,7 +18611,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18189
18611
  }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18190
18612
  kind: "mutation",
18191
18613
  auth: "admin"
18192
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
18614
+ }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
18193
18615
  deviceId: number(),
18194
18616
  since: number().optional(),
18195
18617
  until: number().optional(),
@@ -18263,7 +18685,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18263
18685
  }), array(MediaFileSchema).readonly()), method(object({
18264
18686
  trackId: string(),
18265
18687
  kinds: array(MediaFileKindEnum).optional()
18266
- }), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
18688
+ }), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
18267
18689
  deviceId: number(),
18268
18690
  timestamp: number(),
18269
18691
  frameWidth: number(),
@@ -22853,6 +23275,54 @@ Object.freeze({
22853
23275
  addonId: null,
22854
23276
  access: "create"
22855
23277
  },
23278
+ "coreBlocks.compile": {
23279
+ capName: "core-blocks",
23280
+ capScope: "system",
23281
+ addonId: null,
23282
+ access: "create"
23283
+ },
23284
+ "coreBlocks.create": {
23285
+ capName: "core-blocks",
23286
+ capScope: "system",
23287
+ addonId: null,
23288
+ access: "create"
23289
+ },
23290
+ "coreBlocks.delete": {
23291
+ capName: "core-blocks",
23292
+ capScope: "system",
23293
+ addonId: null,
23294
+ access: "delete"
23295
+ },
23296
+ "coreBlocks.get": {
23297
+ capName: "core-blocks",
23298
+ capScope: "system",
23299
+ addonId: null,
23300
+ access: "view"
23301
+ },
23302
+ "coreBlocks.getTypeDefs": {
23303
+ capName: "core-blocks",
23304
+ capScope: "system",
23305
+ addonId: null,
23306
+ access: "view"
23307
+ },
23308
+ "coreBlocks.list": {
23309
+ capName: "core-blocks",
23310
+ capScope: "system",
23311
+ addonId: null,
23312
+ access: "view"
23313
+ },
23314
+ "coreBlocks.setEnabled": {
23315
+ capName: "core-blocks",
23316
+ capScope: "system",
23317
+ addonId: null,
23318
+ access: "create"
23319
+ },
23320
+ "coreBlocks.update": {
23321
+ capName: "core-blocks",
23322
+ capScope: "system",
23323
+ addonId: null,
23324
+ access: "create"
23325
+ },
22856
23326
  "cover.close": {
22857
23327
  capName: "cover",
22858
23328
  capScope: "device",
@@ -24761,12 +25231,24 @@ Object.freeze({
24761
25231
  addonId: null,
24762
25232
  access: "create"
24763
25233
  },
25234
+ "notificationRules.cancelSnooze": {
25235
+ capName: "notification-rules",
25236
+ capScope: "system",
25237
+ addonId: null,
25238
+ access: "create"
25239
+ },
24764
25240
  "notificationRules.createRule": {
24765
25241
  capName: "notification-rules",
24766
25242
  capScope: "system",
24767
25243
  addonId: null,
24768
25244
  access: "create"
24769
25245
  },
25246
+ "notificationRules.createSnooze": {
25247
+ capName: "notification-rules",
25248
+ capScope: "system",
25249
+ addonId: null,
25250
+ access: "create"
25251
+ },
24770
25252
  "notificationRules.deleteRule": {
24771
25253
  capName: "notification-rules",
24772
25254
  capScope: "system",
@@ -24797,6 +25279,12 @@ Object.freeze({
24797
25279
  addonId: null,
24798
25280
  access: "view"
24799
25281
  },
25282
+ "notificationRules.listSnoozes": {
25283
+ capName: "notification-rules",
25284
+ capScope: "system",
25285
+ addonId: null,
25286
+ access: "view"
25287
+ },
24800
25288
  "notificationRules.setRuleEnabled": {
24801
25289
  capName: "notification-rules",
24802
25290
  capScope: "system",
@@ -25001,6 +25489,12 @@ Object.freeze({
25001
25489
  addonId: null,
25002
25490
  access: "view"
25003
25491
  },
25492
+ "pipelineAnalytics.listEventKindsBatch": {
25493
+ capName: "pipeline-analytics",
25494
+ capScope: "device",
25495
+ addonId: null,
25496
+ access: "view"
25497
+ },
25004
25498
  "pipelineAnalytics.listOpsLog": {
25005
25499
  capName: "pipeline-analytics",
25006
25500
  capScope: "device",
@@ -25013,6 +25507,12 @@ Object.freeze({
25013
25507
  addonId: null,
25014
25508
  access: "view"
25015
25509
  },
25510
+ "pipelineAnalytics.listTrackMedia": {
25511
+ capName: "pipeline-analytics",
25512
+ capScope: "device",
25513
+ addonId: null,
25514
+ access: "view"
25515
+ },
25016
25516
  "pipelineAnalytics.listTracks": {
25017
25517
  capName: "pipeline-analytics",
25018
25518
  capScope: "device",
@@ -28825,6 +29325,7 @@ var WEBHOOK_CAPS = {
28825
29325
  max: 99
28826
29326
  },
28827
29327
  actions: 99,
29328
+ actionIcons: true,
28828
29329
  levels: [
28829
29330
  {
28830
29331
  id: "p1",