@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.js CHANGED
@@ -9236,8 +9236,11 @@ function prepareNotification(caps, n) {
9236
9236
  });
9237
9237
  }
9238
9238
  const inActions = n.actions ?? [];
9239
- const actions = inActions.slice(0, Math.max(0, caps.actions));
9240
- if (inActions.length > actions.length) dropped.push("actions");
9239
+ const kept = inActions.slice(0, Math.max(0, caps.actions));
9240
+ if (inActions.length > kept.length) dropped.push("actions");
9241
+ const iconsSupported = caps.actionIcons === true;
9242
+ if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
9243
+ const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
9241
9244
  let clickUrl = null;
9242
9245
  if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
9243
9246
  else dropped.push("clickUrl");
@@ -9336,6 +9339,290 @@ var MaskGridDimsSchema = object({
9336
9339
  height: number()
9337
9340
  });
9338
9341
  /**
9342
+ * notification-output — canonical, capability-gated notification delivery.
9343
+ *
9344
+ * Apprise-derived model (see
9345
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
9346
+ * callers emit ONE canonical `Notification`; each provider declares a
9347
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
9348
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
9349
+ * message to what the kind supports — callers never special-case a service.
9350
+ *
9351
+ * DESIGN DECISIONS (locked):
9352
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
9353
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
9354
+ * cap. Rationale: the admin UI needs one uniform surface across the
9355
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
9356
+ * alternative would fork the UI per addon and cannot host the
9357
+ * discovery→adopt flow.
9358
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
9359
+ * the generated cap-mount auto-`concatCollection`-fans them across every
9360
+ * registered provider (notifiers addon + HA addon) so one catalog is
9361
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
9362
+ * `addonId` the generated collection router extracts from the call input.
9363
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
9364
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
9365
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
9366
+ * base64 fallback needed.
9367
+ *
9368
+ * TODO (deferred, closed-set change — separate decision): add
9369
+ * `providerKind: 'notify'` so notification providers surface on the unified
9370
+ * admin "Integrations" page.
9371
+ */
9372
+ /**
9373
+ * Zentik-derived typed-media enum — the superset across every kind. Each
9374
+ * adapter picks what it supports and the degrade engine filters the rest.
9375
+ */
9376
+ var AttachmentMediaTypeSchema = _enum([
9377
+ "image",
9378
+ "video",
9379
+ "gif",
9380
+ "audio",
9381
+ "icon"
9382
+ ]);
9383
+ /**
9384
+ * A single attachment. Exactly one of `url` (remote source, most adapters
9385
+ * prefer this) or `bytes` (inline source; required for Pushover-style
9386
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
9387
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
9388
+ */
9389
+ var AttachmentSchema = object({
9390
+ mediaType: AttachmentMediaTypeSchema,
9391
+ url: string().optional(),
9392
+ bytes: _instanceof(Uint8Array).optional(),
9393
+ mime: string().optional(),
9394
+ name: string().optional()
9395
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
9396
+ var NotificationFormatSchema = _enum([
9397
+ "text",
9398
+ "markdown",
9399
+ "html"
9400
+ ]);
9401
+ /**
9402
+ * The CLOSED icon vocabulary an action button may use.
9403
+ *
9404
+ * A closed set, not a free string, and that is the whole point: an arbitrary
9405
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
9406
+ * notices — the same class of gap as a zone vocabulary nothing produced
9407
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
9408
+ * declares `actionIcons: false` and the degrade engine strips the field.
9409
+ *
9410
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
9411
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
9412
+ * differently.
9413
+ */
9414
+ var NotificationActionIconSchema = _enum([
9415
+ "acknowledge",
9416
+ "dismiss",
9417
+ "silence",
9418
+ "view",
9419
+ "play",
9420
+ "open",
9421
+ "close",
9422
+ "lock",
9423
+ "unlock",
9424
+ "arm",
9425
+ "disarm",
9426
+ "light",
9427
+ "alert"
9428
+ ]);
9429
+ /** A single tap-through action button. */
9430
+ var NotificationActionSchema = object({
9431
+ id: string(),
9432
+ label: string(),
9433
+ url: string().optional(),
9434
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
9435
+ icon: NotificationActionIconSchema.optional(),
9436
+ /**
9437
+ * Renders in a warning style where the notifier supports it.
9438
+ *
9439
+ * A HINT, never a gate. The callback's authority is its token and nothing
9440
+ * else — see `notification-center/action-token.ts` for what that does and
9441
+ * does not buy.
9442
+ */
9443
+ destructive: boolean().optional()
9444
+ });
9445
+ /**
9446
+ * The canonical notification. `body` is the only hard field (Apprise model).
9447
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
9448
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
9449
+ * the adapter maps this ordinal onto its native level. `level?` is an
9450
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
9451
+ * `priority` for that one target.
9452
+ */
9453
+ var NotificationSchema = object({
9454
+ body: string(),
9455
+ title: string().optional(),
9456
+ format: NotificationFormatSchema.default("text"),
9457
+ priority: number().int().min(1).max(5).default(3),
9458
+ level: string().optional(),
9459
+ attachments: array(AttachmentSchema).optional(),
9460
+ clickUrl: string().optional(),
9461
+ actions: array(NotificationActionSchema).optional(),
9462
+ sound: string().optional(),
9463
+ ttl: number().optional(),
9464
+ tag: string().optional(),
9465
+ deviceId: number().optional(),
9466
+ eventId: string().optional(),
9467
+ metadata: record(string(), unknown()).optional()
9468
+ });
9469
+ /** One declared native severity/priority level for a kind. */
9470
+ var TargetKindLevelSchema = object({
9471
+ id: string(),
9472
+ label: string(),
9473
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
9474
+ ordinal: number().int().min(1).max(5).nullable(),
9475
+ flags: object({
9476
+ critical: boolean().optional(),
9477
+ silent: boolean().optional(),
9478
+ noPush: boolean().optional()
9479
+ }).optional(),
9480
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
9481
+ requires: array(string()).optional(),
9482
+ description: string().optional()
9483
+ });
9484
+ /** The full capability block consulted before dispatch. */
9485
+ var TargetKindCapsSchema = object({
9486
+ attachments: object({
9487
+ mediaTypes: array(AttachmentMediaTypeSchema),
9488
+ mode: _enum([
9489
+ "url",
9490
+ "bytes",
9491
+ "both"
9492
+ ]),
9493
+ max: number().int().nonnegative(),
9494
+ maxBytes: number().int().positive().optional()
9495
+ }),
9496
+ /** Max action buttons (0 = none). */
9497
+ actions: number().int().nonnegative(),
9498
+ /**
9499
+ * Whether this kind renders a per-action ICON.
9500
+ *
9501
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
9502
+ * run on the addon cap path — three production failures in one day taught
9503
+ * this repo that once. Absent is read as false by the degrade engine, which
9504
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
9505
+ * assumed and dropped costs the operator's trust in the field.
9506
+ */
9507
+ actionIcons: boolean().optional(),
9508
+ levels: array(TargetKindLevelSchema),
9509
+ format: array(NotificationFormatSchema),
9510
+ clickUrl: boolean(),
9511
+ sound: boolean(),
9512
+ ttl: boolean(),
9513
+ bodyMaxLen: number().int().positive()
9514
+ });
9515
+ /**
9516
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
9517
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
9518
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
9519
+ * the union is large and not meant for runtime validation here; the exported
9520
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
9521
+ */
9522
+ var ConfigSchemaPassthrough$1 = unknown();
9523
+ var TargetKindSchema = object({
9524
+ kind: string(),
9525
+ label: string(),
9526
+ icon: string(),
9527
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
9528
+ addonId: string(),
9529
+ /**
9530
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
9531
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
9532
+ * when the addon bundles no icon for that kind — the client then falls back
9533
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
9534
+ *
9535
+ * Root-relative on purpose: it resolves against whatever origin serves a web
9536
+ * client, and a native client joins it onto its own hub base.
9537
+ *
9538
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
9539
+ * field that survived only because the runtime cap-router forwards provider
9540
+ * output verbatim — so every consumer had to re-declare it by hand to stop
9541
+ * its own Zod parse from stripping it, and the whole arrangement would have
9542
+ * broken silently the moment output validation was tightened anywhere.
9543
+ */
9544
+ iconUrl: string().optional(),
9545
+ /**
9546
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
9547
+ *
9548
+ * The server knows this and therefore says it, because the client cannot
9549
+ * safely guess: a React-Native client renders SVG and raster through two
9550
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
9551
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
9552
+ * placeholder glyph for every vector icon while the web build looked fine.
9553
+ *
9554
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
9555
+ * not been updated — a client that cannot determine the type should prefer
9556
+ * its raster path, which is the safe default for an unknown image.
9557
+ */
9558
+ iconMediaType: string().optional(),
9559
+ configSchema: ConfigSchemaPassthrough$1,
9560
+ supportsDiscovery: boolean(),
9561
+ caps: TargetKindCapsSchema
9562
+ });
9563
+ /**
9564
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
9565
+ * (return a presence marker only) when serving `listTargets` — never
9566
+ * round-trip a stored secret to the UI.
9567
+ */
9568
+ var TargetSchema = object({
9569
+ id: string(),
9570
+ name: string(),
9571
+ kind: string(),
9572
+ addonId: string(),
9573
+ enabled: boolean(),
9574
+ config: record(string(), unknown())
9575
+ });
9576
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
9577
+ var DiscoveredTargetSchema = object({
9578
+ kind: string(),
9579
+ suggestedName: string(),
9580
+ config: record(string(), unknown())
9581
+ });
9582
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
9583
+ var RenderedAsSchema = object({
9584
+ level: string(),
9585
+ format: NotificationFormatSchema,
9586
+ attachmentsSent: number().int().nonnegative(),
9587
+ actionsSent: number().int().nonnegative(),
9588
+ truncated: boolean(),
9589
+ dropped: array(string())
9590
+ });
9591
+ var SendResultSchema = object({
9592
+ success: boolean(),
9593
+ error: string().optional(),
9594
+ renderedAs: RenderedAsSchema.optional()
9595
+ });
9596
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
9597
+ var TestResultSchema = SendResultSchema;
9598
+ var notificationOutputCapability = {
9599
+ name: "notification-output",
9600
+ scope: "system",
9601
+ mode: "collection",
9602
+ methods: {
9603
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
9604
+ listTargets: method(object({}), array(TargetSchema)),
9605
+ discoverTargets: method(object({
9606
+ kind: string(),
9607
+ config: record(string(), unknown()).optional()
9608
+ }), array(DiscoveredTargetSchema)),
9609
+ send: method(object({
9610
+ targetId: string(),
9611
+ notification: NotificationSchema
9612
+ }), SendResultSchema, { kind: "mutation" }),
9613
+ testTarget: method(object({
9614
+ targetId: string(),
9615
+ sample: NotificationSchema.optional()
9616
+ }), TestResultSchema, { kind: "mutation" }),
9617
+ upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
9618
+ deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
9619
+ setTargetEnabled: method(object({
9620
+ targetId: string(),
9621
+ enabled: boolean()
9622
+ }), _void(), { kind: "mutation" })
9623
+ }
9624
+ };
9625
+ /**
9339
9626
  * notification-rules — the Notification Center rule surface (P1 core).
9340
9627
  *
9341
9628
  * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
@@ -9465,7 +9752,115 @@ var NcZoneConditionSchema = object({
9465
9752
  * The P1 condition set — a flat AND of groups; absent group = pass;
9466
9753
  * membership lists are OR within the list (spec §2.3).
9467
9754
  */
9755
+ /**
9756
+ * What a rule may actuate.
9757
+ *
9758
+ * **No hand-maintained allowlist** (operator decision, and the right one — a
9759
+ * written list of methods is a third parallel map to keep aligned, and this
9760
+ * repo has paid for those). The boundary instead comes from a property the
9761
+ * capabilities already carry: an action may target only a **device-scoped**
9762
+ * capability method.
9763
+ *
9764
+ * That is not decoration. A rule can be authored by a NON-ADMIN — personal
9765
+ * rules are a supported flow — and the executor runs with the addon's
9766
+ * privileges, so an unbounded action is an arbitrary RPC channel with a
9767
+ * privilege escalation attached. Restricting to device scope excludes the
9768
+ * system caps (`device-manager.removeDevice` and friends) by construction,
9769
+ * costs nothing to maintain, and cannot rot: a cap that stops being
9770
+ * device-scoped stops being actuatable in the same change.
9771
+ *
9772
+ * The executor enforces it; {@link NcRuleActionSchema} carries the intent.
9773
+ */
9774
+ /**
9775
+ * One step of a sequence.
9776
+ *
9777
+ * `wait` is a first-class step rather than a property of the next action: it is
9778
+ * what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
9779
+ * cannot be expressed otherwise.
9780
+ */
9781
+ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9782
+ kind: literal("wait"),
9783
+ seconds: number().min(0).max(300)
9784
+ }), object({
9785
+ kind: literal("cap"),
9786
+ deviceId: number().int(),
9787
+ /** Capability name, e.g. `alarm-panel`. */
9788
+ cap: string().min(1),
9789
+ /** Method on it. The executor refuses a non-device-scoped cap. */
9790
+ method: string().min(1),
9791
+ /** Method arguments, minus `deviceId` (the executor injects it). */
9792
+ args: record(string(), unknown()).optional()
9793
+ })]);
9794
+ /**
9795
+ * A named, ordered run of steps with its own throttle.
9796
+ *
9797
+ * `minDelaySec` exists because a noisy rule otherwise hammers a physical
9798
+ * actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
9799
+ * different budget from "how often may this gate actually open".
9800
+ */
9801
+ var NcRuleActionSequenceSchema = object({
9802
+ name: string().min(1).max(120),
9803
+ enabled: boolean(),
9804
+ minDelaySec: number().int().min(0).max(86400).optional(),
9805
+ actions: array(NcRuleActionSchema).min(1)
9806
+ });
9807
+ /**
9808
+ * One button carried by the notification, running a named sequence on tap.
9809
+ *
9810
+ * **Read this before adding a button that does something physical.** The tap
9811
+ * arrives over a link that travelled through third-party infrastructure — ntfy,
9812
+ * a push relay, whatever forwarded the message — and the callback's ONLY
9813
+ * authority is the token in that link: single-use, short-lived, bound to this
9814
+ * one action of this one notification. It does not identify who tapped.
9815
+ * Whoever holds the notification can run the button, once, inside the window.
9816
+ * That is the operator's explicit choice (2026-08-05), and `destructive` is a
9817
+ * rendering hint, not a second gate. [D47](decisions/adr-0047.md).
9818
+ */
9819
+ var NcRuleNotificationButtonSchema = object({
9820
+ /** Stable id — travels in the callback and identifies the button in logs. */
9821
+ id: string().min(1).max(64),
9822
+ label: string().min(1).max(40),
9823
+ /** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
9824
+ * sequence does not exist rather than minting a token for nothing. */
9825
+ sequence: string().min(1).max(120),
9826
+ icon: NotificationActionIconSchema.optional(),
9827
+ destructive: boolean().optional()
9828
+ });
9829
+ /**
9830
+ * Sequences a rule runs, by hook point.
9831
+ *
9832
+ * ONLY `onTrigger` is here, deliberately. The reference also has activation /
9833
+ * deactivation / reset / post-generation hooks, and they are wanted — but this
9834
+ * repo's expensive failure mode is declaring a surface nothing produces, so a
9835
+ * hook appears here in the same change that produces its edge, never before.
9836
+ */
9837
+ var NcRuleActionsSchema = object({
9838
+ /** Runs when the rule MATCHES. */
9839
+ onTrigger: array(NcRuleActionSequenceSchema).optional(),
9840
+ /**
9841
+ * Buttons the NOTIFICATION carries, each running one of this rule's
9842
+ * sequences when tapped.
9843
+ *
9844
+ * Deliberately a REFERENCE to a sequence rather than a second place to
9845
+ * author steps. A button that could define its own actions would be a
9846
+ * parallel actuation vocabulary — the executor's device-scope check, the
9847
+ * stop-at-first-failure rule and the per-sequence throttle all live on
9848
+ * sequences, and a second authoring surface would drift from every one of
9849
+ * them.
9850
+ *
9851
+ * A sequence reachable ONLY by a button simply appears in `onTrigger` with
9852
+ * `enabled: false`: it is then authored, throttled and validated like the
9853
+ * rest, and nothing runs it automatically.
9854
+ */
9855
+ buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
9856
+ });
9468
9857
  var NcConditionsSchema = object({
9858
+ /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
9859
+ deviceState: object({
9860
+ deviceId: number().int(),
9861
+ /** Any of these matches. */
9862
+ states: array(string().min(1)).min(1)
9863
+ }).optional(),
9469
9864
  /** Device scope — absent = all devices. */
9470
9865
  devices: array(number()).optional(),
9471
9866
  /** Detector class names (any overlap with the record's class set). */
@@ -9666,6 +10061,14 @@ var NcMediaPolicySchema = object({
9666
10061
  clipPreRollSec: number().int().min(0).max(30).optional(),
9667
10062
  clipPostRollSec: number().int().min(0).max(30).optional(),
9668
10063
  /**
10064
+ * Playback rate of the attached gif / clip. Absent = 2x.
10065
+ *
10066
+ * A notification clip is GLANCED at on a lock screen, not watched: at real
10067
+ * time an eight-second passage is eight seconds of the recipient's attention
10068
+ * and twice the bytes. 1 is real time for the operator who wants it.
10069
+ */
10070
+ clipSpeed: number().min(1).max(8).optional(),
10071
+ /**
9669
10072
  * Which stream profile the footage is cut from. Absent = the CHEAPEST
9670
10073
  * assigned profile: a notification is watched on a phone, so the 4K
9671
10074
  * rendition would burn CPU to produce a file the client downscales anyway.
@@ -9737,7 +10140,30 @@ var NcRuleInputSchema = object({
9737
10140
  * behaviour, visible to all, read-only in the viewer). Present = personal
9738
10141
  * rule owned by this userId. Server-stamped; never trusted from a client.
9739
10142
  */
9740
- ownerUserId: string().optional()
10143
+ ownerUserId: string().optional(),
10144
+ /**
10145
+ * May a non-admin snooze this rule for EVERYONE, not just themselves?
10146
+ *
10147
+ * A snooze is personal by default — it silences the person who set it. This
10148
+ * opts THIS rule into the "the gardener is here all afternoon" case, where
10149
+ * silencing the camera for the whole household is legitimate. It silences
10150
+ * other people, so it is off unless a rule deliberately allows it.
10151
+ *
10152
+ * `.optional()`, deliberately NOT `.default()`: a Zod default does not run on
10153
+ * the addon cap path (three production failures in one day), so absent is
10154
+ * read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
10155
+ * by this flag — see the scope rules on that function.
10156
+ */
10157
+ snoozeAllowGlobal: boolean().optional(),
10158
+ /**
10159
+ * Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
10160
+ *
10161
+ * This is what makes the rule set the alarm's trigger set without the alarm
10162
+ * being a special case: arming is
10163
+ * `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
10164
+ * shape as every other actuation.
10165
+ */
10166
+ actions: NcRuleActionsSchema.optional()
9741
10167
  });
9742
10168
  /**
9743
10169
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -9808,7 +10234,8 @@ var NcConditionDescriptorSchema = object({
9808
10234
  "packagePhase",
9809
10235
  "crossingSelect",
9810
10236
  "polygonDraw",
9811
- "occupancy"
10237
+ "occupancy",
10238
+ "deviceState"
9812
10239
  ]),
9813
10240
  operator: _enum([
9814
10241
  "in",
@@ -9822,7 +10249,28 @@ var NcConditionDescriptorSchema = object({
9822
10249
  /** Which delivery kinds the condition applies to. */
9823
10250
  appliesTo: array(NcDeliverySchema),
9824
10251
  phase: string(),
9825
- description: string().optional()
10252
+ description: string().optional(),
10253
+ /**
10254
+ * The CHOICES for a single-choice widget (`sourceSelect`, `crossingSelect`,
10255
+ * `packagePhase`, …), served with the descriptor.
10256
+ *
10257
+ * Before this the descriptor said which widget to render and not what to put
10258
+ * in it, so every option list lived in three places: this file's enums, the
10259
+ * admin's `NC_*_OPTIONS` and the viewer's `NC_*_VALUES`. That triple mirror
10260
+ * is the drift that emptied the viewer's rule editor on 2026-08-04 — the app
10261
+ * mirrors the cap by hand, so it is only ever as current as its last build.
10262
+ *
10263
+ * With the options on the wire, a condition of an EXISTING `valueType` costs
10264
+ * zero client changes. Clients keep a local fallback for an older hub that
10265
+ * does not send them; absent here is "use your own list", not "no choices".
10266
+ */
10267
+ options: array(object({
10268
+ /** Written to the rule verbatim. `''` means the ABSENT state. */
10269
+ value: string(),
10270
+ label: string(),
10271
+ /** What THIS choice matches — shown one at a time, under the control. */
10272
+ hint: string().optional()
10273
+ })).readonly().optional()
9826
10274
  });
9827
10275
  /**
9828
10276
  * The delivery lifecycle status of a history row — a straight read of the
@@ -9907,6 +10355,74 @@ var NcHistoryFilterSchema = object({
9907
10355
  until: number().optional(),
9908
10356
  limit: number().int().min(1).max(500).default(100)
9909
10357
  });
10358
+ /**
10359
+ * What a snooze covers. Broader scopes win when several overlap, so one window
10360
+ * leaves ONE digest rather than a rule snooze and a whole-feed snooze both
10361
+ * summarising the same silence.
10362
+ */
10363
+ var NcSnoozeScopeSchema = _enum([
10364
+ "rule",
10365
+ "device",
10366
+ "all"
10367
+ ]);
10368
+ /**
10369
+ * Client-authored snooze. The server stamps `userId`, `startedAt` and
10370
+ * `expiresAt` — a DURATION is sent rather than an instant so a client with a
10371
+ * skewed clock cannot author a window that is already over, or never ends.
10372
+ */
10373
+ var NcSnoozeInputSchema = object({
10374
+ scope: NcSnoozeScopeSchema,
10375
+ /** Required when `scope: 'rule'` — a scoped snooze with no id matches
10376
+ * NOTHING rather than degrading to "everything". */
10377
+ ruleId: string().optional(),
10378
+ /** Required when `scope: 'device'`. */
10379
+ deviceId: number().int().optional(),
10380
+ durationMinutes: number().int().min(1).max(1440),
10381
+ /**
10382
+ * Silence this for EVERY recipient, not just the caller. Permission is
10383
+ * checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
10384
+ * broader scopes). Absent = personal.
10385
+ */
10386
+ global: boolean().optional(),
10387
+ /**
10388
+ * Deliver a summary of what was suppressed when the window ends. Absent =
10389
+ * ON: someone silencing a nuisance camera wants it off, someone silencing a
10390
+ * SECURITY camera wants to know what they missed, and choosing "off" for
10391
+ * everybody is how a snooze becomes an outage. Resolved to a concrete
10392
+ * boolean by the server at create time — never left to a Zod default, which
10393
+ * does not run on the addon cap path.
10394
+ */
10395
+ summary: boolean().optional()
10396
+ });
10397
+ /** A persisted snooze window. */
10398
+ var NcSnoozeSchema = object({
10399
+ id: string(),
10400
+ /** Who set it. Also who it silences, unless `global`. */
10401
+ userId: string(),
10402
+ scope: NcSnoozeScopeSchema,
10403
+ ruleId: string().optional(),
10404
+ deviceId: number().int().optional(),
10405
+ startedAt: number(),
10406
+ /** Exclusive: at exactly this instant the snooze is over. Expiry is a
10407
+ * COMPARISON, not a job — no sweeper can leave the operator silenced. */
10408
+ expiresAt: number(),
10409
+ global: boolean(),
10410
+ summary: boolean(),
10411
+ /** When the end-of-window digest went out. Absent = not sent (yet, or the
10412
+ * window has not closed, or `summary` is false). */
10413
+ digestSentAt: number().optional()
10414
+ });
10415
+ object({
10416
+ snoozeId: string(),
10417
+ targetId: string(),
10418
+ ruleId: string(),
10419
+ ruleName: string(),
10420
+ deviceId: number().int(),
10421
+ /** How many notifications this snooze hid for that pair. */
10422
+ count: number().int(),
10423
+ firstAt: number(),
10424
+ lastAt: number()
10425
+ });
9910
10426
  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 }), {
9911
10427
  kind: "mutation",
9912
10428
  auth: "admin",
@@ -9936,7 +10452,13 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
9936
10452
  }), method(object({}), object({
9937
10453
  catalog: array(NcConditionDescriptorSchema),
9938
10454
  taxonomy: NcTaxonomySchema.optional()
9939
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10455
+ })), 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 }), {
10456
+ kind: "mutation",
10457
+ caller: "required"
10458
+ }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
10459
+ kind: "mutation",
10460
+ caller: "required"
10461
+ });
9940
10462
  /**
9941
10463
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9942
10464
  *
@@ -10684,7 +11206,15 @@ method(object({
10684
11206
  format: _enum(["gif", "mp4"]).default("gif"),
10685
11207
  maxWidth: number().int().min(120).max(1920).default(480),
10686
11208
  /** GIF only — MP4 keeps the source cadence. */
10687
- fps: number().int().min(1).max(15).default(5)
11209
+ fps: number().int().min(1).max(15).default(5),
11210
+ /**
11211
+ * Playback rate. A notification clip is GLANCED at on a lock screen,
11212
+ * not watched, so 2x is the default: the recipient sees the whole
11213
+ * passage in half the time and the GIF is half the bytes. `1` is real
11214
+ * time. Applies to MP4 as well — the operator set a speed, not a GIF
11215
+ * speed.
11216
+ */
11217
+ speed: number().min(1).max(8).default(2)
10688
11218
  }), object({
10689
11219
  base64: string(),
10690
11220
  mime: string(),
@@ -16352,7 +16882,20 @@ method(object({
16352
16882
  }), _void(), {
16353
16883
  kind: "mutation",
16354
16884
  auth: "admin"
16355
- }), 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({
16885
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
16886
+ addonId: string().optional(),
16887
+ /**
16888
+ * `slim` omits `config` (returned `{}`), `metadata` (null) and the
16889
+ * `sourceInfo` derived from config — and skips the per-device settings
16890
+ * read that produces them. Everything identifying a device (id, name,
16891
+ * type, online, features, isCamera, parent/link ids) is unchanged.
16892
+ * Do not use it for dispatch routing, which needs `sourceInfo`.
16893
+ */
16894
+ projection: _enum(["full", "slim"]).optional(),
16895
+ /** Return only camera devices. Filtering server-side instead of
16896
+ * shipping 293 rows to find 12. */
16897
+ isCamera: boolean().optional()
16898
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
16356
16899
  mode: DeviceLinkModeSchema,
16357
16900
  devices: array(LinkedDeviceSchema)
16358
16901
  })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -16791,14 +17334,14 @@ var LlmProfileSchema = object({
16791
17334
  /** ConfigUISchema tree passed through untyped on the wire (the
16792
17335
  * notification-output `ConfigSchemaPassthrough` precedent at
16793
17336
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
16794
- var ConfigSchemaPassthrough$1 = unknown();
17337
+ var ConfigSchemaPassthrough = unknown();
16795
17338
  var LlmProfileKindDescriptorSchema = object({
16796
17339
  kind: LlmProfileKindSchema,
16797
17340
  label: string(),
16798
17341
  icon: string(),
16799
17342
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
16800
17343
  addonId: string(),
16801
- configSchema: ConfigSchemaPassthrough$1
17344
+ configSchema: ConfigSchemaPassthrough
16802
17345
  });
16803
17346
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
16804
17347
  var LlmDefaultSchema = object({
@@ -17288,282 +17831,143 @@ var StartEmbeddedInputSchema = object({
17288
17831
  });
17289
17832
  var StartEmbeddedResultSchema = object({
17290
17833
  id: string(),
17291
- url: string()
17292
- });
17293
- var StatusSchema = object({
17294
- brokerCount: number(),
17295
- embeddedRunning: boolean()
17296
- });
17297
- 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);
17298
- var NetworkEndpointSchema = object({
17299
- url: string(),
17300
- hostname: string(),
17301
- port: number(),
17302
- protocol: _enum(["http", "https"])
17303
- });
17304
- var NetworkAccessStatusSchema = object({
17305
- connected: boolean(),
17306
- endpoint: NetworkEndpointSchema.nullable(),
17307
- error: string().optional()
17308
- });
17309
- /**
17310
- * Optional, richer endpoint shape returned by providers that expose
17311
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
17312
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
17313
- * the originating provider config (mode + sourcePort) so the
17314
- * orchestrator UI can label rows distinctly. Providers that expose only
17315
- * one endpoint just omit `listEndpoints` from their provider impl.
17316
- */
17317
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
17318
- /**
17319
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
17320
- * the orchestrator can dedupe across `listEndpoints` polls.
17321
- */
17322
- id: string(),
17323
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
17324
- label: string(),
17325
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
17326
- mode: string().optional(),
17327
- /** Originating local port the ingress fronts (informational). */
17328
- sourcePort: number().optional()
17329
- });
17330
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17331
- /**
17332
- * notification-output — canonical, capability-gated notification delivery.
17333
- *
17334
- * Apprise-derived model (see
17335
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
17336
- * callers emit ONE canonical `Notification`; each provider declares a
17337
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
17338
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
17339
- * message to what the kind supports — callers never special-case a service.
17340
- *
17341
- * DESIGN DECISIONS (locked):
17342
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
17343
- * `setTargetEnabled`), each provider persisting via the `settings-store`
17344
- * cap. Rationale: the admin UI needs one uniform surface across the
17345
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
17346
- * alternative would fork the UI per addon and cannot host the
17347
- * discovery→adopt flow.
17348
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
17349
- * the generated cap-mount auto-`concatCollection`-fans them across every
17350
- * registered provider (notifiers addon + HA addon) so one catalog is
17351
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
17352
- * `addonId` the generated collection router extracts from the call input.
17353
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
17354
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
17355
- * `storage` / `storage-provider` / `recording` caps over the same path. No
17356
- * base64 fallback needed.
17357
- *
17358
- * TODO (deferred, closed-set change — separate decision): add
17359
- * `providerKind: 'notify'` so notification providers surface on the unified
17360
- * admin "Integrations" page.
17361
- */
17362
- /**
17363
- * Zentik-derived typed-media enum — the superset across every kind. Each
17364
- * adapter picks what it supports and the degrade engine filters the rest.
17365
- */
17366
- var AttachmentMediaTypeSchema = _enum([
17367
- "image",
17368
- "video",
17369
- "gif",
17370
- "audio",
17371
- "icon"
17372
- ]);
17373
- /**
17374
- * A single attachment. Exactly one of `url` (remote source, most adapters
17375
- * prefer this) or `bytes` (inline source; required for Pushover-style
17376
- * bytes-only kinds) MUST be present — the degrade engine expresses a
17377
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
17378
- */
17379
- var AttachmentSchema = object({
17380
- mediaType: AttachmentMediaTypeSchema,
17381
- url: string().optional(),
17382
- bytes: _instanceof(Uint8Array).optional(),
17383
- mime: string().optional(),
17384
- name: string().optional()
17385
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
17386
- var NotificationFormatSchema = _enum([
17387
- "text",
17388
- "markdown",
17389
- "html"
17390
- ]);
17391
- /** A single tap-through action button. */
17392
- var NotificationActionSchema = object({
17393
- id: string(),
17394
- label: string(),
17395
- url: string().optional()
17396
- });
17397
- /**
17398
- * The canonical notification. `body` is the only hard field (Apprise model).
17399
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
17400
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
17401
- * the adapter maps this ordinal onto its native level. `level?` is an
17402
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
17403
- * `priority` for that one target.
17404
- */
17405
- var NotificationSchema = object({
17406
- body: string(),
17407
- title: string().optional(),
17408
- format: NotificationFormatSchema.default("text"),
17409
- priority: number().int().min(1).max(5).default(3),
17410
- level: string().optional(),
17411
- attachments: array(AttachmentSchema).optional(),
17412
- clickUrl: string().optional(),
17413
- actions: array(NotificationActionSchema).optional(),
17414
- sound: string().optional(),
17415
- ttl: number().optional(),
17416
- tag: string().optional(),
17417
- deviceId: number().optional(),
17418
- eventId: string().optional(),
17419
- metadata: record(string(), unknown()).optional()
17420
- });
17421
- /** One declared native severity/priority level for a kind. */
17422
- var TargetKindLevelSchema = object({
17423
- id: string(),
17424
- label: string(),
17425
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
17426
- ordinal: number().int().min(1).max(5).nullable(),
17427
- flags: object({
17428
- critical: boolean().optional(),
17429
- silent: boolean().optional(),
17430
- noPush: boolean().optional()
17431
- }).optional(),
17432
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
17433
- requires: array(string()).optional(),
17434
- description: string().optional()
17834
+ url: string()
17435
17835
  });
17436
- /** The full capability block consulted before dispatch. */
17437
- var TargetKindCapsSchema = object({
17438
- attachments: object({
17439
- mediaTypes: array(AttachmentMediaTypeSchema),
17440
- mode: _enum([
17441
- "url",
17442
- "bytes",
17443
- "both"
17444
- ]),
17445
- max: number().int().nonnegative(),
17446
- maxBytes: number().int().positive().optional()
17447
- }),
17448
- /** Max action buttons (0 = none). */
17449
- actions: number().int().nonnegative(),
17450
- levels: array(TargetKindLevelSchema),
17451
- format: array(NotificationFormatSchema),
17452
- clickUrl: boolean(),
17453
- sound: boolean(),
17454
- ttl: boolean(),
17455
- bodyMaxLen: number().int().positive()
17836
+ var StatusSchema = object({
17837
+ brokerCount: number(),
17838
+ embeddedRunning: boolean()
17839
+ });
17840
+ 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);
17841
+ var NetworkEndpointSchema = object({
17842
+ url: string(),
17843
+ hostname: string(),
17844
+ port: number(),
17845
+ protocol: _enum(["http", "https"])
17846
+ });
17847
+ var NetworkAccessStatusSchema = object({
17848
+ connected: boolean(),
17849
+ endpoint: NetworkEndpointSchema.nullable(),
17850
+ error: string().optional()
17456
17851
  });
17457
17852
  /**
17458
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17459
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17460
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
17461
- * the union is large and not meant for runtime validation here; the exported
17462
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
17853
+ * Optional, richer endpoint shape returned by providers that expose
17854
+ * MORE than one ingress concurrently (Tailscale Ingress with mixed
17855
+ * serve+funnel rules, future ngrok multi-tunnel, ). Each entry carries
17856
+ * the originating provider config (mode + sourcePort) so the
17857
+ * orchestrator UI can label rows distinctly. Providers that expose only
17858
+ * one endpoint just omit `listEndpoints` from their provider impl.
17463
17859
  */
17464
- var ConfigSchemaPassthrough = unknown();
17465
- var TargetKindSchema = object({
17466
- kind: string(),
17467
- label: string(),
17468
- icon: string(),
17469
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
17470
- addonId: string(),
17471
- /**
17472
- * URL of the kind's bundled BRAND icon, served by the providing addon over
17473
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
17474
- * when the addon bundles no icon for that kind — the client then falls back
17475
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
17476
- *
17477
- * Root-relative on purpose: it resolves against whatever origin serves a web
17478
- * client, and a native client joins it onto its own hub base.
17479
- *
17480
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
17481
- * field that survived only because the runtime cap-router forwards provider
17482
- * output verbatim — so every consumer had to re-declare it by hand to stop
17483
- * its own Zod parse from stripping it, and the whole arrangement would have
17484
- * broken silently the moment output validation was tightened anywhere.
17485
- */
17486
- iconUrl: string().optional(),
17860
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
17487
17861
  /**
17488
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
17489
- *
17490
- * The server knows this and therefore says it, because the client cannot
17491
- * safely guess: a React-Native client renders SVG and raster through two
17492
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
17493
- * not decode SVG on iOS/Android), so without this it silently fell back to a
17494
- * placeholder glyph for every vector icon while the web build looked fine.
17495
- *
17496
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
17497
- * not been updated — a client that cannot determine the type should prefer
17498
- * its raster path, which is the safe default for an unknown image.
17862
+ * Stable id within the provider typically `<mode>-<sourcePort>` so
17863
+ * the orchestrator can dedupe across `listEndpoints` polls.
17499
17864
  */
17500
- iconMediaType: string().optional(),
17501
- configSchema: ConfigSchemaPassthrough,
17502
- supportsDiscovery: boolean(),
17503
- caps: TargetKindCapsSchema
17865
+ id: string(),
17866
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
17867
+ label: string(),
17868
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
17869
+ mode: string().optional(),
17870
+ /** Originating local port the ingress fronts (informational). */
17871
+ sourcePort: number().optional()
17504
17872
  });
17873
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17505
17874
  /**
17506
- * A persisted target. `config` holds secrets; providers REDACT secret fields
17507
- * (return a presence marker only) when serving `listTargets` — never
17508
- * round-trip a stored secret to the UI.
17509
- */
17510
- var TargetSchema = object({
17511
- id: string(),
17512
- name: string(),
17513
- kind: string(),
17514
- addonId: string(),
17875
+ * core-blocks user-authored TypeScript, stored in the kernel and executed in
17876
+ * its own process.
17877
+ *
17878
+ * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
17879
+ *
17880
+ * The first use is **owning devices without being a device provider**: a block
17881
+ * declares devices under a system or custom integration and drives their state,
17882
+ * with the same `ctx` an addon gets. Automations come later; nothing here
17883
+ * models a trigger.
17884
+ *
17885
+ * **Stated plainly, because it does not change by being true:** a block has an
17886
+ * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
17887
+ * with no review step. What makes that survivable is not a sandbox, it is
17888
+ * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
17889
+ * so a block that throws or never returns is marked `failed` and visible
17890
+ * instead of taking the hub with it (D6). Every method here is admin-only, and
17891
+ * must stay so.
17892
+ */
17893
+ /** Where a block runs. The operator chooses — a block driving a device on an
17894
+ * agent is the reason placement is not fixed to the hub. */
17895
+ var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
17896
+ /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
17897
+ * a failing block reads the same way a failing addon does. */
17898
+ var CoreBlockStatusSchema = _enum([
17899
+ "stopped",
17900
+ "starting",
17901
+ "running",
17902
+ "failed"
17903
+ ]);
17904
+ /** Client-authored fields. */
17905
+ var CoreBlockInputSchema = object({
17906
+ name: string().min(1).max(120),
17907
+ /** TypeScript source. Compiled server-side before it is ever stored — a
17908
+ * block that does not compile is a fork failure the operator would meet
17909
+ * minutes later, in a log, instead of in the editor. */
17910
+ code: string().max(2e5),
17515
17911
  enabled: boolean(),
17516
- config: record(string(), unknown())
17517
- });
17518
- /** A discovery-surfaced candidate (config is partial + non-secret). */
17519
- var DiscoveredTargetSchema = object({
17520
- kind: string(),
17521
- suggestedName: string(),
17522
- config: record(string(), unknown())
17912
+ placement: CoreBlockPlacementSchema,
17913
+ /**
17914
+ * Integration the block's devices hang from. Absent = the system integration
17915
+ * blocks share. A block may declare its own instead.
17916
+ */
17917
+ integrationId: string().optional()
17523
17918
  });
17524
- /** The degrade engine's report — what was resolved / dropped / degraded. */
17525
- var RenderedAsSchema = object({
17526
- level: string(),
17527
- format: NotificationFormatSchema,
17528
- attachmentsSent: number().int().nonnegative(),
17529
- actionsSent: number().int().nonnegative(),
17530
- truncated: boolean(),
17531
- dropped: array(string())
17919
+ /** A stored block. */
17920
+ var CoreBlockSchema = CoreBlockInputSchema.extend({
17921
+ id: string(),
17922
+ createdAt: number(),
17923
+ updatedAt: number(),
17924
+ /** Server-stamped author. */
17925
+ createdBy: string(),
17926
+ status: CoreBlockStatusSchema,
17927
+ /**
17928
+ * Why the block is not running, when it is not. The operator's ONLY window
17929
+ * into a block that failed at load — a block that is silently absent is the
17930
+ * failure mode this whole feature has to avoid.
17931
+ */
17932
+ lastError: string().optional(),
17933
+ /** Ms epoch of the last state change. */
17934
+ lastChangedAt: number()
17532
17935
  });
17533
- var SendResultSchema = object({
17534
- success: boolean(),
17936
+ /** What a compile attempt produced. */
17937
+ var CoreBlockCompileResultSchema = object({
17938
+ ok: boolean(),
17939
+ /** Present when `ok` is false — the first error, in the author's words. */
17535
17940
  error: string().optional(),
17536
- renderedAs: RenderedAsSchema.optional()
17941
+ line: number().optional(),
17942
+ column: number().optional()
17537
17943
  });
17538
- /** Same shape as SendResult kept as a distinct name for the test panel. */
17539
- var TestResultSchema = SendResultSchema;
17540
- var notificationOutputCapability = {
17541
- name: "notification-output",
17542
- scope: "system",
17543
- mode: "collection",
17544
- methods: {
17545
- listTargetKinds: method(object({}), array(TargetKindSchema)),
17546
- listTargets: method(object({}), array(TargetSchema)),
17547
- discoverTargets: method(object({
17548
- kind: string(),
17549
- config: record(string(), unknown()).optional()
17550
- }), array(DiscoveredTargetSchema)),
17551
- send: method(object({
17552
- targetId: string(),
17553
- notification: NotificationSchema
17554
- }), SendResultSchema, { kind: "mutation" }),
17555
- testTarget: method(object({
17556
- targetId: string(),
17557
- sample: NotificationSchema.optional()
17558
- }), TestResultSchema, { kind: "mutation" }),
17559
- upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
17560
- deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
17561
- setTargetEnabled: method(object({
17562
- targetId: string(),
17563
- enabled: boolean()
17564
- }), _void(), { kind: "mutation" })
17565
- }
17566
- };
17944
+ 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 }), {
17945
+ kind: "mutation",
17946
+ auth: "admin",
17947
+ caller: "required"
17948
+ }), method(object({
17949
+ blockId: string(),
17950
+ block: CoreBlockInputSchema.partial()
17951
+ }), object({ block: CoreBlockSchema }), {
17952
+ kind: "mutation",
17953
+ auth: "admin",
17954
+ caller: "required"
17955
+ }), method(object({ blockId: string() }), object({ success: literal(true) }), {
17956
+ kind: "mutation",
17957
+ auth: "admin"
17958
+ }), method(object({
17959
+ blockId: string(),
17960
+ enabled: boolean()
17961
+ }), object({ block: CoreBlockSchema }), {
17962
+ kind: "mutation",
17963
+ auth: "admin"
17964
+ }), method(object({ code: string() }), CoreBlockCompileResultSchema, {
17965
+ kind: "mutation",
17966
+ auth: "admin"
17967
+ }), method(object({}), object({ libs: array(object({
17968
+ filePath: string(),
17969
+ content: string()
17970
+ })) }), { auth: "admin" });
17567
17971
  /**
17568
17972
  * Zod schemas for persisted record types.
17569
17973
  *
@@ -17811,6 +18215,11 @@ var EventKindDescriptorSchema = object({
17811
18215
  deviceId: number()
17812
18216
  })
17813
18217
  });
18218
+ /** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
18219
+ var EventKindsForDeviceSchema = object({
18220
+ deviceId: number(),
18221
+ kinds: array(EventKindDescriptorSchema).readonly()
18222
+ });
17814
18223
  var SensorEventSchema = object({
17815
18224
  id: string(),
17816
18225
  /** The CAMERA the event is attributed to (a sensor linked to N cameras
@@ -18067,6 +18476,19 @@ var MediaFileSchema = object({
18067
18476
  sizeBytes: number(),
18068
18477
  timestamp: number()
18069
18478
  });
18479
+ /**
18480
+ * One media row WITHOUT its bytes.
18481
+ *
18482
+ * A track's media is 5-8 MB of base64 (measured: 7.67 MB across 23 files for a
18483
+ * 140 s track), and a client that renders tiles from the media data plane needs
18484
+ * to know only WHAT EXISTS — the bytes then arrive per tile, lazily, over HTTP
18485
+ * with an immutable cache, instead of all at once inside a tRPC response that
18486
+ * blocks the whole view.
18487
+ *
18488
+ * `sizeBytes` is carried because it is what lets a client decide between the
18489
+ * stored blob and a `?variant=thumb` rendering without fetching either.
18490
+ */
18491
+ var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
18070
18492
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
18071
18493
  var MAX_EVENT_QUERY_LIMIT = 5e3;
18072
18494
  var DeviceEventQueryInput = object({
@@ -18216,7 +18638,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18216
18638
  }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18217
18639
  kind: "mutation",
18218
18640
  auth: "admin"
18219
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
18641
+ }), 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({
18220
18642
  deviceId: number(),
18221
18643
  since: number().optional(),
18222
18644
  until: number().optional(),
@@ -18290,7 +18712,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18290
18712
  }), array(MediaFileSchema).readonly()), method(object({
18291
18713
  trackId: string(),
18292
18714
  kinds: array(MediaFileKindEnum).optional()
18293
- }), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
18715
+ }), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
18294
18716
  deviceId: number(),
18295
18717
  timestamp: number(),
18296
18718
  frameWidth: number(),
@@ -22880,6 +23302,54 @@ Object.freeze({
22880
23302
  addonId: null,
22881
23303
  access: "create"
22882
23304
  },
23305
+ "coreBlocks.compile": {
23306
+ capName: "core-blocks",
23307
+ capScope: "system",
23308
+ addonId: null,
23309
+ access: "create"
23310
+ },
23311
+ "coreBlocks.create": {
23312
+ capName: "core-blocks",
23313
+ capScope: "system",
23314
+ addonId: null,
23315
+ access: "create"
23316
+ },
23317
+ "coreBlocks.delete": {
23318
+ capName: "core-blocks",
23319
+ capScope: "system",
23320
+ addonId: null,
23321
+ access: "delete"
23322
+ },
23323
+ "coreBlocks.get": {
23324
+ capName: "core-blocks",
23325
+ capScope: "system",
23326
+ addonId: null,
23327
+ access: "view"
23328
+ },
23329
+ "coreBlocks.getTypeDefs": {
23330
+ capName: "core-blocks",
23331
+ capScope: "system",
23332
+ addonId: null,
23333
+ access: "view"
23334
+ },
23335
+ "coreBlocks.list": {
23336
+ capName: "core-blocks",
23337
+ capScope: "system",
23338
+ addonId: null,
23339
+ access: "view"
23340
+ },
23341
+ "coreBlocks.setEnabled": {
23342
+ capName: "core-blocks",
23343
+ capScope: "system",
23344
+ addonId: null,
23345
+ access: "create"
23346
+ },
23347
+ "coreBlocks.update": {
23348
+ capName: "core-blocks",
23349
+ capScope: "system",
23350
+ addonId: null,
23351
+ access: "create"
23352
+ },
22883
23353
  "cover.close": {
22884
23354
  capName: "cover",
22885
23355
  capScope: "device",
@@ -24788,12 +25258,24 @@ Object.freeze({
24788
25258
  addonId: null,
24789
25259
  access: "create"
24790
25260
  },
25261
+ "notificationRules.cancelSnooze": {
25262
+ capName: "notification-rules",
25263
+ capScope: "system",
25264
+ addonId: null,
25265
+ access: "create"
25266
+ },
24791
25267
  "notificationRules.createRule": {
24792
25268
  capName: "notification-rules",
24793
25269
  capScope: "system",
24794
25270
  addonId: null,
24795
25271
  access: "create"
24796
25272
  },
25273
+ "notificationRules.createSnooze": {
25274
+ capName: "notification-rules",
25275
+ capScope: "system",
25276
+ addonId: null,
25277
+ access: "create"
25278
+ },
24797
25279
  "notificationRules.deleteRule": {
24798
25280
  capName: "notification-rules",
24799
25281
  capScope: "system",
@@ -24824,6 +25306,12 @@ Object.freeze({
24824
25306
  addonId: null,
24825
25307
  access: "view"
24826
25308
  },
25309
+ "notificationRules.listSnoozes": {
25310
+ capName: "notification-rules",
25311
+ capScope: "system",
25312
+ addonId: null,
25313
+ access: "view"
25314
+ },
24827
25315
  "notificationRules.setRuleEnabled": {
24828
25316
  capName: "notification-rules",
24829
25317
  capScope: "system",
@@ -25028,6 +25516,12 @@ Object.freeze({
25028
25516
  addonId: null,
25029
25517
  access: "view"
25030
25518
  },
25519
+ "pipelineAnalytics.listEventKindsBatch": {
25520
+ capName: "pipeline-analytics",
25521
+ capScope: "device",
25522
+ addonId: null,
25523
+ access: "view"
25524
+ },
25031
25525
  "pipelineAnalytics.listOpsLog": {
25032
25526
  capName: "pipeline-analytics",
25033
25527
  capScope: "device",
@@ -25040,6 +25534,12 @@ Object.freeze({
25040
25534
  addonId: null,
25041
25535
  access: "view"
25042
25536
  },
25537
+ "pipelineAnalytics.listTrackMedia": {
25538
+ capName: "pipeline-analytics",
25539
+ capScope: "device",
25540
+ addonId: null,
25541
+ access: "view"
25542
+ },
25043
25543
  "pipelineAnalytics.listTracks": {
25044
25544
  capName: "pipeline-analytics",
25045
25545
  capScope: "device",
@@ -28852,6 +29352,7 @@ var WEBHOOK_CAPS = {
28852
29352
  max: 99
28853
29353
  },
28854
29354
  actions: 99,
29355
+ actionIcons: true,
28855
29356
  levels: [
28856
29357
  {
28857
29358
  id: "p1",