@camstack/addon-provider-homeassistant 1.2.10 → 1.2.12
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.
- package/dist/addon.js +1140 -360
- package/dist/addon.mjs +1140 -360
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let node_crypto = require("node:crypto");
|
|
3
3
|
let node_zlib = require("node:zlib");
|
|
4
|
-
//#region ../types/dist/event-category-
|
|
4
|
+
//#region ../types/dist/event-category-41fKf-q9.mjs
|
|
5
5
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
6
6
|
EventCategory["SystemBoot"] = "system.boot";
|
|
7
7
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -61,6 +61,26 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
61
61
|
*/
|
|
62
62
|
EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
|
|
63
63
|
/**
|
|
64
|
+
* A liveness invariant of this node is false — it has no devices, every
|
|
65
|
+
* camera is offline, or a camera that was recording has stopped producing
|
|
66
|
+
* segments. Emitted by the `liveness-monitor` builtin, once per fault (the
|
|
67
|
+
* `findingId` is stable across ticks), only after its boot grace period.
|
|
68
|
+
* AlertCenter raises a persistent operator-visible alert.
|
|
69
|
+
*
|
|
70
|
+
* It exists because on 2026-08-03 the hub ran four hours with zero devices
|
|
71
|
+
* and zero recordings while every surface stayed quiet.
|
|
72
|
+
*
|
|
73
|
+
* Payload: `{ findingId, severity, title, message, deviceId? }`.
|
|
74
|
+
*/
|
|
75
|
+
EventCategory["SystemLivenessFailed"] = "system.liveness-failed";
|
|
76
|
+
/**
|
|
77
|
+
* A previously reported liveness fault is true again. AlertCenter dismisses
|
|
78
|
+
* the matching `SystemLivenessFailed` alert.
|
|
79
|
+
*
|
|
80
|
+
* Payload: `{ findingId }`.
|
|
81
|
+
*/
|
|
82
|
+
EventCategory["SystemLivenessRecovered"] = "system.liveness-recovered";
|
|
83
|
+
/**
|
|
64
84
|
* Monitor is attempting to reload a failed addon NOW. UI uses this
|
|
65
85
|
* to show a spinner during the retry attempt. Same transient nature
|
|
66
86
|
* as AddonRetryScheduled.
|
|
@@ -7444,14 +7464,7 @@ var RecordingConfigSchema = object({
|
|
|
7444
7464
|
* windows only — existing sheets are immutable, and each window's index
|
|
7445
7465
|
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7446
7466
|
*/
|
|
7447
|
-
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7448
|
-
/**
|
|
7449
|
-
* OPT-IN thumbnail-strip generation for this camera: every keyframe of the
|
|
7450
|
-
* low recording saved as a JPEG (the fast-drag scrub depth), a derived
|
|
7451
|
-
* cache that eviction reclaims with the footage. Absent/false = no strips
|
|
7452
|
-
* are written and scrub reads exact keyframes at every velocity.
|
|
7453
|
-
*/
|
|
7454
|
-
stripsEnabled: boolean().optional()
|
|
7467
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7455
7468
|
}).strict();
|
|
7456
7469
|
/**
|
|
7457
7470
|
* Ops-log — the durable, append-only operations audit shared by the
|
|
@@ -7513,7 +7526,7 @@ var OpsLogQueryInputSchema = object({
|
|
|
7513
7526
|
/**
|
|
7514
7527
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7515
7528
|
*
|
|
7516
|
-
* One shape shared by the recorder's `relocateFootage` (segments
|
|
7529
|
+
* One shape shared by the recorder's `relocateFootage` (segments) and
|
|
7517
7530
|
* pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
|
|
7518
7531
|
* page renders both movers with one component. Jobs are in-RAM (a restart
|
|
7519
7532
|
* forgets them — re-running is safe by construction: copy-if-absent, delete
|
|
@@ -7535,7 +7548,7 @@ var RelocateJobSchema = object({
|
|
|
7535
7548
|
toLocationId: string(),
|
|
7536
7549
|
/** Scoped device, or null = every device. */
|
|
7537
7550
|
deviceId: number().nullable(),
|
|
7538
|
-
/** What the job moves (owner-addon specific: segments
|
|
7551
|
+
/** What the job moves (owner-addon specific: segments or media). */
|
|
7539
7552
|
entities: array(string()),
|
|
7540
7553
|
filesMoved: number().int(),
|
|
7541
7554
|
bytesMoved: number().int(),
|
|
@@ -7549,7 +7562,7 @@ var RelocateFootageInputSchema = object({
|
|
|
7549
7562
|
deviceId: number().optional(),
|
|
7550
7563
|
fromLocationId: string(),
|
|
7551
7564
|
toLocationId: string(),
|
|
7552
|
-
entities: array(_enum(["segments"
|
|
7565
|
+
entities: array(_enum(["segments"])).optional(),
|
|
7553
7566
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7554
7567
|
* never allowed to starve live writers. */
|
|
7555
7568
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
@@ -9454,8 +9467,11 @@ function prepareNotification(caps, n) {
|
|
|
9454
9467
|
});
|
|
9455
9468
|
}
|
|
9456
9469
|
const inActions = n.actions ?? [];
|
|
9457
|
-
const
|
|
9458
|
-
if (inActions.length >
|
|
9470
|
+
const kept = inActions.slice(0, Math.max(0, caps.actions));
|
|
9471
|
+
if (inActions.length > kept.length) dropped.push("actions");
|
|
9472
|
+
const iconsSupported = caps.actionIcons === true;
|
|
9473
|
+
if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
|
|
9474
|
+
const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
|
|
9459
9475
|
let clickUrl = null;
|
|
9460
9476
|
if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
|
|
9461
9477
|
else dropped.push("clickUrl");
|
|
@@ -9554,6 +9570,290 @@ var MaskGridDimsSchema = object({
|
|
|
9554
9570
|
height: number()
|
|
9555
9571
|
});
|
|
9556
9572
|
/**
|
|
9573
|
+
* notification-output — canonical, capability-gated notification delivery.
|
|
9574
|
+
*
|
|
9575
|
+
* Apprise-derived model (see
|
|
9576
|
+
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
9577
|
+
* callers emit ONE canonical `Notification`; each provider declares a
|
|
9578
|
+
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
9579
|
+
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
9580
|
+
* message to what the kind supports — callers never special-case a service.
|
|
9581
|
+
*
|
|
9582
|
+
* DESIGN DECISIONS (locked):
|
|
9583
|
+
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
9584
|
+
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
9585
|
+
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
9586
|
+
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
9587
|
+
* alternative would fork the UI per addon and cannot host the
|
|
9588
|
+
* discovery→adopt flow.
|
|
9589
|
+
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
9590
|
+
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
9591
|
+
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
9592
|
+
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
9593
|
+
* `addonId` the generated collection router extracts from the call input.
|
|
9594
|
+
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
9595
|
+
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
9596
|
+
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
9597
|
+
* base64 fallback needed.
|
|
9598
|
+
*
|
|
9599
|
+
* TODO (deferred, closed-set change — separate decision): add
|
|
9600
|
+
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
9601
|
+
* admin "Integrations" page.
|
|
9602
|
+
*/
|
|
9603
|
+
/**
|
|
9604
|
+
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
9605
|
+
* adapter picks what it supports and the degrade engine filters the rest.
|
|
9606
|
+
*/
|
|
9607
|
+
var AttachmentMediaTypeSchema = _enum([
|
|
9608
|
+
"image",
|
|
9609
|
+
"video",
|
|
9610
|
+
"gif",
|
|
9611
|
+
"audio",
|
|
9612
|
+
"icon"
|
|
9613
|
+
]);
|
|
9614
|
+
/**
|
|
9615
|
+
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
9616
|
+
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
9617
|
+
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
9618
|
+
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
9619
|
+
*/
|
|
9620
|
+
var AttachmentSchema = object({
|
|
9621
|
+
mediaType: AttachmentMediaTypeSchema,
|
|
9622
|
+
url: string().optional(),
|
|
9623
|
+
bytes: _instanceof(Uint8Array).optional(),
|
|
9624
|
+
mime: string().optional(),
|
|
9625
|
+
name: string().optional()
|
|
9626
|
+
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
9627
|
+
var NotificationFormatSchema = _enum([
|
|
9628
|
+
"text",
|
|
9629
|
+
"markdown",
|
|
9630
|
+
"html"
|
|
9631
|
+
]);
|
|
9632
|
+
/**
|
|
9633
|
+
* The CLOSED icon vocabulary an action button may use.
|
|
9634
|
+
*
|
|
9635
|
+
* A closed set, not a free string, and that is the whole point: an arbitrary
|
|
9636
|
+
* icon name is one that ntfy renders, zentik silently drops, and nobody
|
|
9637
|
+
* notices — the same class of gap as a zone vocabulary nothing produced
|
|
9638
|
+
* ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
|
|
9639
|
+
* declares `actionIcons: false` and the degrade engine strips the field.
|
|
9640
|
+
*
|
|
9641
|
+
* Named by INTENT, never by glyph. "check" would tie the vocabulary to one
|
|
9642
|
+
* renderer's icon set; "acknowledge" survives an adapter that draws it
|
|
9643
|
+
* differently.
|
|
9644
|
+
*/
|
|
9645
|
+
var NotificationActionIconSchema = _enum([
|
|
9646
|
+
"acknowledge",
|
|
9647
|
+
"dismiss",
|
|
9648
|
+
"silence",
|
|
9649
|
+
"view",
|
|
9650
|
+
"play",
|
|
9651
|
+
"open",
|
|
9652
|
+
"close",
|
|
9653
|
+
"lock",
|
|
9654
|
+
"unlock",
|
|
9655
|
+
"arm",
|
|
9656
|
+
"disarm",
|
|
9657
|
+
"light",
|
|
9658
|
+
"alert"
|
|
9659
|
+
]);
|
|
9660
|
+
/** A single tap-through action button. */
|
|
9661
|
+
var NotificationActionSchema = object({
|
|
9662
|
+
id: string(),
|
|
9663
|
+
label: string(),
|
|
9664
|
+
url: string().optional(),
|
|
9665
|
+
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
9666
|
+
icon: NotificationActionIconSchema.optional(),
|
|
9667
|
+
/**
|
|
9668
|
+
* Renders in a warning style where the notifier supports it.
|
|
9669
|
+
*
|
|
9670
|
+
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
9671
|
+
* else — see `notification-center/action-token.ts` for what that does and
|
|
9672
|
+
* does not buy.
|
|
9673
|
+
*/
|
|
9674
|
+
destructive: boolean().optional()
|
|
9675
|
+
});
|
|
9676
|
+
/**
|
|
9677
|
+
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
9678
|
+
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
9679
|
+
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
9680
|
+
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
9681
|
+
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
9682
|
+
* `priority` for that one target.
|
|
9683
|
+
*/
|
|
9684
|
+
var NotificationSchema = object({
|
|
9685
|
+
body: string(),
|
|
9686
|
+
title: string().optional(),
|
|
9687
|
+
format: NotificationFormatSchema.default("text"),
|
|
9688
|
+
priority: number().int().min(1).max(5).default(3),
|
|
9689
|
+
level: string().optional(),
|
|
9690
|
+
attachments: array(AttachmentSchema).optional(),
|
|
9691
|
+
clickUrl: string().optional(),
|
|
9692
|
+
actions: array(NotificationActionSchema).optional(),
|
|
9693
|
+
sound: string().optional(),
|
|
9694
|
+
ttl: number().optional(),
|
|
9695
|
+
tag: string().optional(),
|
|
9696
|
+
deviceId: number().optional(),
|
|
9697
|
+
eventId: string().optional(),
|
|
9698
|
+
metadata: record(string(), unknown()).optional()
|
|
9699
|
+
});
|
|
9700
|
+
/** One declared native severity/priority level for a kind. */
|
|
9701
|
+
var TargetKindLevelSchema = object({
|
|
9702
|
+
id: string(),
|
|
9703
|
+
label: string(),
|
|
9704
|
+
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
9705
|
+
ordinal: number().int().min(1).max(5).nullable(),
|
|
9706
|
+
flags: object({
|
|
9707
|
+
critical: boolean().optional(),
|
|
9708
|
+
silent: boolean().optional(),
|
|
9709
|
+
noPush: boolean().optional()
|
|
9710
|
+
}).optional(),
|
|
9711
|
+
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
9712
|
+
requires: array(string()).optional(),
|
|
9713
|
+
description: string().optional()
|
|
9714
|
+
});
|
|
9715
|
+
/** The full capability block consulted before dispatch. */
|
|
9716
|
+
var TargetKindCapsSchema = object({
|
|
9717
|
+
attachments: object({
|
|
9718
|
+
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
9719
|
+
mode: _enum([
|
|
9720
|
+
"url",
|
|
9721
|
+
"bytes",
|
|
9722
|
+
"both"
|
|
9723
|
+
]),
|
|
9724
|
+
max: number().int().nonnegative(),
|
|
9725
|
+
maxBytes: number().int().positive().optional()
|
|
9726
|
+
}),
|
|
9727
|
+
/** Max action buttons (0 = none). */
|
|
9728
|
+
actions: number().int().nonnegative(),
|
|
9729
|
+
/**
|
|
9730
|
+
* Whether this kind renders a per-action ICON.
|
|
9731
|
+
*
|
|
9732
|
+
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
9733
|
+
* run on the addon cap path — three production failures in one day taught
|
|
9734
|
+
* this repo that once. Absent is read as false by the degrade engine, which
|
|
9735
|
+
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
9736
|
+
* assumed and dropped costs the operator's trust in the field.
|
|
9737
|
+
*/
|
|
9738
|
+
actionIcons: boolean().optional(),
|
|
9739
|
+
levels: array(TargetKindLevelSchema),
|
|
9740
|
+
format: array(NotificationFormatSchema),
|
|
9741
|
+
clickUrl: boolean(),
|
|
9742
|
+
sound: boolean(),
|
|
9743
|
+
ttl: boolean(),
|
|
9744
|
+
bodyMaxLen: number().int().positive()
|
|
9745
|
+
});
|
|
9746
|
+
/**
|
|
9747
|
+
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
9748
|
+
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
9749
|
+
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
9750
|
+
* the union is large and not meant for runtime validation here; the exported
|
|
9751
|
+
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
9752
|
+
*/
|
|
9753
|
+
var ConfigSchemaPassthrough$1 = unknown();
|
|
9754
|
+
var TargetKindSchema = object({
|
|
9755
|
+
kind: string(),
|
|
9756
|
+
label: string(),
|
|
9757
|
+
icon: string(),
|
|
9758
|
+
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
9759
|
+
addonId: string(),
|
|
9760
|
+
/**
|
|
9761
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
9762
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
9763
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
9764
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
9765
|
+
*
|
|
9766
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
9767
|
+
* client, and a native client joins it onto its own hub base.
|
|
9768
|
+
*
|
|
9769
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
9770
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
9771
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
9772
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
9773
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
9774
|
+
*/
|
|
9775
|
+
iconUrl: string().optional(),
|
|
9776
|
+
/**
|
|
9777
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
9778
|
+
*
|
|
9779
|
+
* The server knows this and therefore says it, because the client cannot
|
|
9780
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
9781
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
9782
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
9783
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
9784
|
+
*
|
|
9785
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
9786
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
9787
|
+
* its raster path, which is the safe default for an unknown image.
|
|
9788
|
+
*/
|
|
9789
|
+
iconMediaType: string().optional(),
|
|
9790
|
+
configSchema: ConfigSchemaPassthrough$1,
|
|
9791
|
+
supportsDiscovery: boolean(),
|
|
9792
|
+
caps: TargetKindCapsSchema
|
|
9793
|
+
});
|
|
9794
|
+
/**
|
|
9795
|
+
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
9796
|
+
* (return a presence marker only) when serving `listTargets` — never
|
|
9797
|
+
* round-trip a stored secret to the UI.
|
|
9798
|
+
*/
|
|
9799
|
+
var TargetSchema = object({
|
|
9800
|
+
id: string(),
|
|
9801
|
+
name: string(),
|
|
9802
|
+
kind: string(),
|
|
9803
|
+
addonId: string(),
|
|
9804
|
+
enabled: boolean(),
|
|
9805
|
+
config: record(string(), unknown())
|
|
9806
|
+
});
|
|
9807
|
+
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
9808
|
+
var DiscoveredTargetSchema = object({
|
|
9809
|
+
kind: string(),
|
|
9810
|
+
suggestedName: string(),
|
|
9811
|
+
config: record(string(), unknown())
|
|
9812
|
+
});
|
|
9813
|
+
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
9814
|
+
var RenderedAsSchema = object({
|
|
9815
|
+
level: string(),
|
|
9816
|
+
format: NotificationFormatSchema,
|
|
9817
|
+
attachmentsSent: number().int().nonnegative(),
|
|
9818
|
+
actionsSent: number().int().nonnegative(),
|
|
9819
|
+
truncated: boolean(),
|
|
9820
|
+
dropped: array(string())
|
|
9821
|
+
});
|
|
9822
|
+
var SendResultSchema = object({
|
|
9823
|
+
success: boolean(),
|
|
9824
|
+
error: string().optional(),
|
|
9825
|
+
renderedAs: RenderedAsSchema.optional()
|
|
9826
|
+
});
|
|
9827
|
+
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
9828
|
+
var TestResultSchema = SendResultSchema;
|
|
9829
|
+
var notificationOutputCapability = {
|
|
9830
|
+
name: "notification-output",
|
|
9831
|
+
scope: "system",
|
|
9832
|
+
mode: "collection",
|
|
9833
|
+
methods: {
|
|
9834
|
+
listTargetKinds: method(object({}), array(TargetKindSchema)),
|
|
9835
|
+
listTargets: method(object({}), array(TargetSchema)),
|
|
9836
|
+
discoverTargets: method(object({
|
|
9837
|
+
kind: string(),
|
|
9838
|
+
config: record(string(), unknown()).optional()
|
|
9839
|
+
}), array(DiscoveredTargetSchema)),
|
|
9840
|
+
send: method(object({
|
|
9841
|
+
targetId: string(),
|
|
9842
|
+
notification: NotificationSchema
|
|
9843
|
+
}), SendResultSchema, { kind: "mutation" }),
|
|
9844
|
+
testTarget: method(object({
|
|
9845
|
+
targetId: string(),
|
|
9846
|
+
sample: NotificationSchema.optional()
|
|
9847
|
+
}), TestResultSchema, { kind: "mutation" }),
|
|
9848
|
+
upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
|
|
9849
|
+
deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
|
|
9850
|
+
setTargetEnabled: method(object({
|
|
9851
|
+
targetId: string(),
|
|
9852
|
+
enabled: boolean()
|
|
9853
|
+
}), _void(), { kind: "mutation" })
|
|
9854
|
+
}
|
|
9855
|
+
};
|
|
9856
|
+
/**
|
|
9557
9857
|
* notification-rules — the Notification Center rule surface (P1 core).
|
|
9558
9858
|
*
|
|
9559
9859
|
* Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
|
|
@@ -9683,7 +9983,115 @@ var NcZoneConditionSchema = object({
|
|
|
9683
9983
|
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9684
9984
|
* membership lists are OR within the list (spec §2.3).
|
|
9685
9985
|
*/
|
|
9986
|
+
/**
|
|
9987
|
+
* What a rule may actuate.
|
|
9988
|
+
*
|
|
9989
|
+
* **No hand-maintained allowlist** (operator decision, and the right one — a
|
|
9990
|
+
* written list of methods is a third parallel map to keep aligned, and this
|
|
9991
|
+
* repo has paid for those). The boundary instead comes from a property the
|
|
9992
|
+
* capabilities already carry: an action may target only a **device-scoped**
|
|
9993
|
+
* capability method.
|
|
9994
|
+
*
|
|
9995
|
+
* That is not decoration. A rule can be authored by a NON-ADMIN — personal
|
|
9996
|
+
* rules are a supported flow — and the executor runs with the addon's
|
|
9997
|
+
* privileges, so an unbounded action is an arbitrary RPC channel with a
|
|
9998
|
+
* privilege escalation attached. Restricting to device scope excludes the
|
|
9999
|
+
* system caps (`device-manager.removeDevice` and friends) by construction,
|
|
10000
|
+
* costs nothing to maintain, and cannot rot: a cap that stops being
|
|
10001
|
+
* device-scoped stops being actuatable in the same change.
|
|
10002
|
+
*
|
|
10003
|
+
* The executor enforces it; {@link NcRuleActionSchema} carries the intent.
|
|
10004
|
+
*/
|
|
10005
|
+
/**
|
|
10006
|
+
* One step of a sequence.
|
|
10007
|
+
*
|
|
10008
|
+
* `wait` is a first-class step rather than a property of the next action: it is
|
|
10009
|
+
* what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
|
|
10010
|
+
* cannot be expressed otherwise.
|
|
10011
|
+
*/
|
|
10012
|
+
var NcRuleActionSchema = discriminatedUnion("kind", [object({
|
|
10013
|
+
kind: literal("wait"),
|
|
10014
|
+
seconds: number().min(0).max(300)
|
|
10015
|
+
}), object({
|
|
10016
|
+
kind: literal("cap"),
|
|
10017
|
+
deviceId: number().int(),
|
|
10018
|
+
/** Capability name, e.g. `alarm-panel`. */
|
|
10019
|
+
cap: string().min(1),
|
|
10020
|
+
/** Method on it. The executor refuses a non-device-scoped cap. */
|
|
10021
|
+
method: string().min(1),
|
|
10022
|
+
/** Method arguments, minus `deviceId` (the executor injects it). */
|
|
10023
|
+
args: record(string(), unknown()).optional()
|
|
10024
|
+
})]);
|
|
10025
|
+
/**
|
|
10026
|
+
* A named, ordered run of steps with its own throttle.
|
|
10027
|
+
*
|
|
10028
|
+
* `minDelaySec` exists because a noisy rule otherwise hammers a physical
|
|
10029
|
+
* actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
|
|
10030
|
+
* different budget from "how often may this gate actually open".
|
|
10031
|
+
*/
|
|
10032
|
+
var NcRuleActionSequenceSchema = object({
|
|
10033
|
+
name: string().min(1).max(120),
|
|
10034
|
+
enabled: boolean(),
|
|
10035
|
+
minDelaySec: number().int().min(0).max(86400).optional(),
|
|
10036
|
+
actions: array(NcRuleActionSchema).min(1)
|
|
10037
|
+
});
|
|
10038
|
+
/**
|
|
10039
|
+
* One button carried by the notification, running a named sequence on tap.
|
|
10040
|
+
*
|
|
10041
|
+
* **Read this before adding a button that does something physical.** The tap
|
|
10042
|
+
* arrives over a link that travelled through third-party infrastructure — ntfy,
|
|
10043
|
+
* a push relay, whatever forwarded the message — and the callback's ONLY
|
|
10044
|
+
* authority is the token in that link: single-use, short-lived, bound to this
|
|
10045
|
+
* one action of this one notification. It does not identify who tapped.
|
|
10046
|
+
* Whoever holds the notification can run the button, once, inside the window.
|
|
10047
|
+
* That is the operator's explicit choice (2026-08-05), and `destructive` is a
|
|
10048
|
+
* rendering hint, not a second gate. [D47](decisions/adr-0047.md).
|
|
10049
|
+
*/
|
|
10050
|
+
var NcRuleNotificationButtonSchema = object({
|
|
10051
|
+
/** Stable id — travels in the callback and identifies the button in logs. */
|
|
10052
|
+
id: string().min(1).max(64),
|
|
10053
|
+
label: string().min(1).max(40),
|
|
10054
|
+
/** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
|
|
10055
|
+
* sequence does not exist rather than minting a token for nothing. */
|
|
10056
|
+
sequence: string().min(1).max(120),
|
|
10057
|
+
icon: NotificationActionIconSchema.optional(),
|
|
10058
|
+
destructive: boolean().optional()
|
|
10059
|
+
});
|
|
10060
|
+
/**
|
|
10061
|
+
* Sequences a rule runs, by hook point.
|
|
10062
|
+
*
|
|
10063
|
+
* ONLY `onTrigger` is here, deliberately. The reference also has activation /
|
|
10064
|
+
* deactivation / reset / post-generation hooks, and they are wanted — but this
|
|
10065
|
+
* repo's expensive failure mode is declaring a surface nothing produces, so a
|
|
10066
|
+
* hook appears here in the same change that produces its edge, never before.
|
|
10067
|
+
*/
|
|
10068
|
+
var NcRuleActionsSchema = object({
|
|
10069
|
+
/** Runs when the rule MATCHES. */
|
|
10070
|
+
onTrigger: array(NcRuleActionSequenceSchema).optional(),
|
|
10071
|
+
/**
|
|
10072
|
+
* Buttons the NOTIFICATION carries, each running one of this rule's
|
|
10073
|
+
* sequences when tapped.
|
|
10074
|
+
*
|
|
10075
|
+
* Deliberately a REFERENCE to a sequence rather than a second place to
|
|
10076
|
+
* author steps. A button that could define its own actions would be a
|
|
10077
|
+
* parallel actuation vocabulary — the executor's device-scope check, the
|
|
10078
|
+
* stop-at-first-failure rule and the per-sequence throttle all live on
|
|
10079
|
+
* sequences, and a second authoring surface would drift from every one of
|
|
10080
|
+
* them.
|
|
10081
|
+
*
|
|
10082
|
+
* A sequence reachable ONLY by a button simply appears in `onTrigger` with
|
|
10083
|
+
* `enabled: false`: it is then authored, throttled and validated like the
|
|
10084
|
+
* rest, and nothing runs it automatically.
|
|
10085
|
+
*/
|
|
10086
|
+
buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
|
|
10087
|
+
});
|
|
9686
10088
|
var NcConditionsSchema = object({
|
|
10089
|
+
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
10090
|
+
deviceState: object({
|
|
10091
|
+
deviceId: number().int(),
|
|
10092
|
+
/** Any of these matches. */
|
|
10093
|
+
states: array(string().min(1)).min(1)
|
|
10094
|
+
}).optional(),
|
|
9687
10095
|
/** Device scope — absent = all devices. */
|
|
9688
10096
|
devices: array(number()).optional(),
|
|
9689
10097
|
/** Detector class names (any overlap with the record's class set). */
|
|
@@ -9884,6 +10292,14 @@ var NcMediaPolicySchema = object({
|
|
|
9884
10292
|
clipPreRollSec: number().int().min(0).max(30).optional(),
|
|
9885
10293
|
clipPostRollSec: number().int().min(0).max(30).optional(),
|
|
9886
10294
|
/**
|
|
10295
|
+
* Playback rate of the attached gif / clip. Absent = 2x.
|
|
10296
|
+
*
|
|
10297
|
+
* A notification clip is GLANCED at on a lock screen, not watched: at real
|
|
10298
|
+
* time an eight-second passage is eight seconds of the recipient's attention
|
|
10299
|
+
* and twice the bytes. 1 is real time for the operator who wants it.
|
|
10300
|
+
*/
|
|
10301
|
+
clipSpeed: number().min(1).max(8).optional(),
|
|
10302
|
+
/**
|
|
9887
10303
|
* Which stream profile the footage is cut from. Absent = the CHEAPEST
|
|
9888
10304
|
* assigned profile: a notification is watched on a phone, so the 4K
|
|
9889
10305
|
* rendition would burn CPU to produce a file the client downscales anyway.
|
|
@@ -9955,7 +10371,30 @@ var NcRuleInputSchema = object({
|
|
|
9955
10371
|
* behaviour, visible to all, read-only in the viewer). Present = personal
|
|
9956
10372
|
* rule owned by this userId. Server-stamped; never trusted from a client.
|
|
9957
10373
|
*/
|
|
9958
|
-
ownerUserId: string().optional()
|
|
10374
|
+
ownerUserId: string().optional(),
|
|
10375
|
+
/**
|
|
10376
|
+
* May a non-admin snooze this rule for EVERYONE, not just themselves?
|
|
10377
|
+
*
|
|
10378
|
+
* A snooze is personal by default — it silences the person who set it. This
|
|
10379
|
+
* opts THIS rule into the "the gardener is here all afternoon" case, where
|
|
10380
|
+
* silencing the camera for the whole household is legitimate. It silences
|
|
10381
|
+
* other people, so it is off unless a rule deliberately allows it.
|
|
10382
|
+
*
|
|
10383
|
+
* `.optional()`, deliberately NOT `.default()`: a Zod default does not run on
|
|
10384
|
+
* the addon cap path (three production failures in one day), so absent is
|
|
10385
|
+
* read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
|
|
10386
|
+
* by this flag — see the scope rules on that function.
|
|
10387
|
+
*/
|
|
10388
|
+
snoozeAllowGlobal: boolean().optional(),
|
|
10389
|
+
/**
|
|
10390
|
+
* Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
|
|
10391
|
+
*
|
|
10392
|
+
* This is what makes the rule set the alarm's trigger set without the alarm
|
|
10393
|
+
* being a special case: arming is
|
|
10394
|
+
* `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
|
|
10395
|
+
* shape as every other actuation.
|
|
10396
|
+
*/
|
|
10397
|
+
actions: NcRuleActionsSchema.optional()
|
|
9959
10398
|
});
|
|
9960
10399
|
/**
|
|
9961
10400
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -10026,7 +10465,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
10026
10465
|
"packagePhase",
|
|
10027
10466
|
"crossingSelect",
|
|
10028
10467
|
"polygonDraw",
|
|
10029
|
-
"occupancy"
|
|
10468
|
+
"occupancy",
|
|
10469
|
+
"deviceState"
|
|
10030
10470
|
]),
|
|
10031
10471
|
operator: _enum([
|
|
10032
10472
|
"in",
|
|
@@ -10040,7 +10480,28 @@ var NcConditionDescriptorSchema = object({
|
|
|
10040
10480
|
/** Which delivery kinds the condition applies to. */
|
|
10041
10481
|
appliesTo: array(NcDeliverySchema),
|
|
10042
10482
|
phase: string(),
|
|
10043
|
-
description: string().optional()
|
|
10483
|
+
description: string().optional(),
|
|
10484
|
+
/**
|
|
10485
|
+
* The CHOICES for a single-choice widget (`sourceSelect`, `crossingSelect`,
|
|
10486
|
+
* `packagePhase`, …), served with the descriptor.
|
|
10487
|
+
*
|
|
10488
|
+
* Before this the descriptor said which widget to render and not what to put
|
|
10489
|
+
* in it, so every option list lived in three places: this file's enums, the
|
|
10490
|
+
* admin's `NC_*_OPTIONS` and the viewer's `NC_*_VALUES`. That triple mirror
|
|
10491
|
+
* is the drift that emptied the viewer's rule editor on 2026-08-04 — the app
|
|
10492
|
+
* mirrors the cap by hand, so it is only ever as current as its last build.
|
|
10493
|
+
*
|
|
10494
|
+
* With the options on the wire, a condition of an EXISTING `valueType` costs
|
|
10495
|
+
* zero client changes. Clients keep a local fallback for an older hub that
|
|
10496
|
+
* does not send them; absent here is "use your own list", not "no choices".
|
|
10497
|
+
*/
|
|
10498
|
+
options: array(object({
|
|
10499
|
+
/** Written to the rule verbatim. `''` means the ABSENT state. */
|
|
10500
|
+
value: string(),
|
|
10501
|
+
label: string(),
|
|
10502
|
+
/** What THIS choice matches — shown one at a time, under the control. */
|
|
10503
|
+
hint: string().optional()
|
|
10504
|
+
})).readonly().optional()
|
|
10044
10505
|
});
|
|
10045
10506
|
/**
|
|
10046
10507
|
* The delivery lifecycle status of a history row — a straight read of the
|
|
@@ -10125,6 +10586,74 @@ var NcHistoryFilterSchema = object({
|
|
|
10125
10586
|
until: number().optional(),
|
|
10126
10587
|
limit: number().int().min(1).max(500).default(100)
|
|
10127
10588
|
});
|
|
10589
|
+
/**
|
|
10590
|
+
* What a snooze covers. Broader scopes win when several overlap, so one window
|
|
10591
|
+
* leaves ONE digest rather than a rule snooze and a whole-feed snooze both
|
|
10592
|
+
* summarising the same silence.
|
|
10593
|
+
*/
|
|
10594
|
+
var NcSnoozeScopeSchema = _enum([
|
|
10595
|
+
"rule",
|
|
10596
|
+
"device",
|
|
10597
|
+
"all"
|
|
10598
|
+
]);
|
|
10599
|
+
/**
|
|
10600
|
+
* Client-authored snooze. The server stamps `userId`, `startedAt` and
|
|
10601
|
+
* `expiresAt` — a DURATION is sent rather than an instant so a client with a
|
|
10602
|
+
* skewed clock cannot author a window that is already over, or never ends.
|
|
10603
|
+
*/
|
|
10604
|
+
var NcSnoozeInputSchema = object({
|
|
10605
|
+
scope: NcSnoozeScopeSchema,
|
|
10606
|
+
/** Required when `scope: 'rule'` — a scoped snooze with no id matches
|
|
10607
|
+
* NOTHING rather than degrading to "everything". */
|
|
10608
|
+
ruleId: string().optional(),
|
|
10609
|
+
/** Required when `scope: 'device'`. */
|
|
10610
|
+
deviceId: number().int().optional(),
|
|
10611
|
+
durationMinutes: number().int().min(1).max(1440),
|
|
10612
|
+
/**
|
|
10613
|
+
* Silence this for EVERY recipient, not just the caller. Permission is
|
|
10614
|
+
* checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
|
|
10615
|
+
* broader scopes). Absent = personal.
|
|
10616
|
+
*/
|
|
10617
|
+
global: boolean().optional(),
|
|
10618
|
+
/**
|
|
10619
|
+
* Deliver a summary of what was suppressed when the window ends. Absent =
|
|
10620
|
+
* ON: someone silencing a nuisance camera wants it off, someone silencing a
|
|
10621
|
+
* SECURITY camera wants to know what they missed, and choosing "off" for
|
|
10622
|
+
* everybody is how a snooze becomes an outage. Resolved to a concrete
|
|
10623
|
+
* boolean by the server at create time — never left to a Zod default, which
|
|
10624
|
+
* does not run on the addon cap path.
|
|
10625
|
+
*/
|
|
10626
|
+
summary: boolean().optional()
|
|
10627
|
+
});
|
|
10628
|
+
/** A persisted snooze window. */
|
|
10629
|
+
var NcSnoozeSchema = object({
|
|
10630
|
+
id: string(),
|
|
10631
|
+
/** Who set it. Also who it silences, unless `global`. */
|
|
10632
|
+
userId: string(),
|
|
10633
|
+
scope: NcSnoozeScopeSchema,
|
|
10634
|
+
ruleId: string().optional(),
|
|
10635
|
+
deviceId: number().int().optional(),
|
|
10636
|
+
startedAt: number(),
|
|
10637
|
+
/** Exclusive: at exactly this instant the snooze is over. Expiry is a
|
|
10638
|
+
* COMPARISON, not a job — no sweeper can leave the operator silenced. */
|
|
10639
|
+
expiresAt: number(),
|
|
10640
|
+
global: boolean(),
|
|
10641
|
+
summary: boolean(),
|
|
10642
|
+
/** When the end-of-window digest went out. Absent = not sent (yet, or the
|
|
10643
|
+
* window has not closed, or `summary` is false). */
|
|
10644
|
+
digestSentAt: number().optional()
|
|
10645
|
+
});
|
|
10646
|
+
object({
|
|
10647
|
+
snoozeId: string(),
|
|
10648
|
+
targetId: string(),
|
|
10649
|
+
ruleId: string(),
|
|
10650
|
+
ruleName: string(),
|
|
10651
|
+
deviceId: number().int(),
|
|
10652
|
+
/** How many notifications this snooze hid for that pair. */
|
|
10653
|
+
count: number().int(),
|
|
10654
|
+
firstAt: number(),
|
|
10655
|
+
lastAt: number()
|
|
10656
|
+
});
|
|
10128
10657
|
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 }), {
|
|
10129
10658
|
kind: "mutation",
|
|
10130
10659
|
auth: "admin",
|
|
@@ -10154,7 +10683,13 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
10154
10683
|
}), method(object({}), object({
|
|
10155
10684
|
catalog: array(NcConditionDescriptorSchema),
|
|
10156
10685
|
taxonomy: NcTaxonomySchema.optional()
|
|
10157
|
-
})), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" })
|
|
10686
|
+
})), 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 }), {
|
|
10687
|
+
kind: "mutation",
|
|
10688
|
+
caller: "required"
|
|
10689
|
+
}), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
|
|
10690
|
+
kind: "mutation",
|
|
10691
|
+
caller: "required"
|
|
10692
|
+
});
|
|
10158
10693
|
/**
|
|
10159
10694
|
* TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
|
|
10160
10695
|
*
|
|
@@ -11174,7 +11709,15 @@ method(object({
|
|
|
11174
11709
|
format: _enum(["gif", "mp4"]).default("gif"),
|
|
11175
11710
|
maxWidth: number().int().min(120).max(1920).default(480),
|
|
11176
11711
|
/** GIF only — MP4 keeps the source cadence. */
|
|
11177
|
-
fps: number().int().min(1).max(15).default(5)
|
|
11712
|
+
fps: number().int().min(1).max(15).default(5),
|
|
11713
|
+
/**
|
|
11714
|
+
* Playback rate. A notification clip is GLANCED at on a lock screen,
|
|
11715
|
+
* not watched, so 2x is the default: the recipient sees the whole
|
|
11716
|
+
* passage in half the time and the GIF is half the bytes. `1` is real
|
|
11717
|
+
* time. Applies to MP4 as well — the operator set a speed, not a GIF
|
|
11718
|
+
* speed.
|
|
11719
|
+
*/
|
|
11720
|
+
speed: number().min(1).max(8).default(2)
|
|
11178
11721
|
}), object({
|
|
11179
11722
|
base64: string(),
|
|
11180
11723
|
mime: string(),
|
|
@@ -18512,6 +19055,210 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
|
|
|
18512
19055
|
kind: "mutation",
|
|
18513
19056
|
auth: "admin"
|
|
18514
19057
|
});
|
|
19058
|
+
/**
|
|
19059
|
+
* Query filter for settings-store collections.
|
|
19060
|
+
*/
|
|
19061
|
+
var QueryFilterSchema = object({
|
|
19062
|
+
where: record(string(), unknown()).optional(),
|
|
19063
|
+
whereIn: record(string(), array(unknown())).optional(),
|
|
19064
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
19065
|
+
orderBy: object({
|
|
19066
|
+
field: string(),
|
|
19067
|
+
direction: _enum(["asc", "desc"])
|
|
19068
|
+
}).optional(),
|
|
19069
|
+
limit: number().optional(),
|
|
19070
|
+
offset: number().optional()
|
|
19071
|
+
});
|
|
19072
|
+
/**
|
|
19073
|
+
* The predicate half of a filter, for BULK MUTATIONS.
|
|
19074
|
+
*
|
|
19075
|
+
* Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
|
|
19076
|
+
* meaning for a statement that rewrites a set, and accepting them would invite
|
|
19077
|
+
* a caller to believe `limit` bounds the damage. Every field is optional here
|
|
19078
|
+
* only so the shape stays composable — the implementation REJECTS a filter
|
|
19079
|
+
* that compiles to no predicate, because that is the whole collection.
|
|
19080
|
+
*/
|
|
19081
|
+
var MutationFilterSchema = object({
|
|
19082
|
+
where: record(string(), unknown()).optional(),
|
|
19083
|
+
whereIn: record(string(), array(unknown())).optional(),
|
|
19084
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
|
|
19085
|
+
});
|
|
19086
|
+
/** A single stored record: `{ id, data }`. */
|
|
19087
|
+
var SettingsRecordSchema = object({
|
|
19088
|
+
id: string(),
|
|
19089
|
+
data: record(string(), unknown())
|
|
19090
|
+
});
|
|
19091
|
+
/**
|
|
19092
|
+
* Column declaration for a structured (SQL-backed) collection.
|
|
19093
|
+
*
|
|
19094
|
+
* Logical types — the backend translates each to the matching SQLite
|
|
19095
|
+
* storage class and handles per-type marshaling:
|
|
19096
|
+
* - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
|
|
19097
|
+
* - `JSON` — TEXT under the hood; serialised on write, parsed on read
|
|
19098
|
+
* - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
|
|
19099
|
+
*/
|
|
19100
|
+
var CollectionColumnSchema = object({
|
|
19101
|
+
name: string(),
|
|
19102
|
+
type: _enum([
|
|
19103
|
+
"TEXT",
|
|
19104
|
+
"INTEGER",
|
|
19105
|
+
"REAL",
|
|
19106
|
+
"JSON",
|
|
19107
|
+
"BOOLEAN"
|
|
19108
|
+
]),
|
|
19109
|
+
primaryKey: boolean().optional(),
|
|
19110
|
+
notNull: boolean().optional(),
|
|
19111
|
+
unique: boolean().optional(),
|
|
19112
|
+
/**
|
|
19113
|
+
* Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
|
|
19114
|
+
* columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
|
|
19115
|
+
* collection surface simply cannot express their existing tables — which is
|
|
19116
|
+
* why they were still on the uncapped `table*` API.
|
|
19117
|
+
*/
|
|
19118
|
+
defaultValue: union([
|
|
19119
|
+
string(),
|
|
19120
|
+
number(),
|
|
19121
|
+
boolean()
|
|
19122
|
+
]).optional()
|
|
19123
|
+
});
|
|
19124
|
+
var CollectionIndexSchema = object({
|
|
19125
|
+
name: string(),
|
|
19126
|
+
columns: array(string()).readonly(),
|
|
19127
|
+
unique: boolean().optional()
|
|
19128
|
+
});
|
|
19129
|
+
method(object({
|
|
19130
|
+
namespace: string().optional(),
|
|
19131
|
+
collection: string(),
|
|
19132
|
+
key: string()
|
|
19133
|
+
}), unknown()), method(object({
|
|
19134
|
+
namespace: string().optional(),
|
|
19135
|
+
collection: string(),
|
|
19136
|
+
key: string(),
|
|
19137
|
+
value: unknown()
|
|
19138
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19139
|
+
namespace: string().optional(),
|
|
19140
|
+
collection: string(),
|
|
19141
|
+
filter: QueryFilterSchema.optional()
|
|
19142
|
+
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
19143
|
+
namespace: string().optional(),
|
|
19144
|
+
collection: string(),
|
|
19145
|
+
record: SettingsRecordSchema
|
|
19146
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19147
|
+
namespace: string().optional(),
|
|
19148
|
+
collection: string(),
|
|
19149
|
+
id: string(),
|
|
19150
|
+
data: record(string(), unknown())
|
|
19151
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19152
|
+
namespace: string().optional(),
|
|
19153
|
+
collection: string(),
|
|
19154
|
+
key: string()
|
|
19155
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19156
|
+
namespace: string().optional(),
|
|
19157
|
+
collection: string(),
|
|
19158
|
+
filter: MutationFilterSchema
|
|
19159
|
+
}), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
|
|
19160
|
+
namespace: string().optional(),
|
|
19161
|
+
collection: string(),
|
|
19162
|
+
filter: MutationFilterSchema,
|
|
19163
|
+
data: record(string(), unknown())
|
|
19164
|
+
}), object({ updated: number().int() }), { kind: "mutation" }), method(object({
|
|
19165
|
+
namespace: string().optional(),
|
|
19166
|
+
collection: string(),
|
|
19167
|
+
filter: QueryFilterSchema.optional()
|
|
19168
|
+
}), number()), method(object({
|
|
19169
|
+
namespace: string().optional(),
|
|
19170
|
+
collection: string(),
|
|
19171
|
+
field: string(),
|
|
19172
|
+
bucketSize: number().int().positive(),
|
|
19173
|
+
origin: number().int(),
|
|
19174
|
+
filter: QueryFilterSchema.optional()
|
|
19175
|
+
}), array(object({
|
|
19176
|
+
bucket: number().int(),
|
|
19177
|
+
count: number().int()
|
|
19178
|
+
})).readonly()), method(object({
|
|
19179
|
+
namespace: string().optional(),
|
|
19180
|
+
collection: string()
|
|
19181
|
+
}), boolean()), method(object({
|
|
19182
|
+
namespace: string().optional(),
|
|
19183
|
+
collection: string(),
|
|
19184
|
+
columns: array(CollectionColumnSchema).readonly(),
|
|
19185
|
+
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
19186
|
+
}), _void(), { kind: "mutation" });
|
|
19187
|
+
/**
|
|
19188
|
+
* What one engine says about itself. The orchestrator uses `kind` to pick
|
|
19189
|
+
* a registrant for a collection; `engineId` is what a log line names when
|
|
19190
|
+
* a call is routed or refused.
|
|
19191
|
+
*/
|
|
19192
|
+
var EngineInfoSchema = object({
|
|
19193
|
+
engineId: string(),
|
|
19194
|
+
/**
|
|
19195
|
+
* `relational` — rows, columns, indexes, the surface `settings-store`
|
|
19196
|
+
* has always described. `vector` — an embedding store answering
|
|
19197
|
+
* similarity queries. A registrant declares exactly one; an engine that
|
|
19198
|
+
* does both registers twice, because "both" would make the routing
|
|
19199
|
+
* decision ambiguous at exactly the point it must not be.
|
|
19200
|
+
*/
|
|
19201
|
+
kind: _enum(["relational", "vector"]),
|
|
19202
|
+
displayName: string()
|
|
19203
|
+
});
|
|
19204
|
+
method(_void(), EngineInfoSchema), method(object({
|
|
19205
|
+
namespace: string().optional(),
|
|
19206
|
+
collection: string(),
|
|
19207
|
+
key: string()
|
|
19208
|
+
}), unknown()), method(object({
|
|
19209
|
+
namespace: string().optional(),
|
|
19210
|
+
collection: string(),
|
|
19211
|
+
key: string(),
|
|
19212
|
+
value: unknown()
|
|
19213
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19214
|
+
namespace: string().optional(),
|
|
19215
|
+
collection: string(),
|
|
19216
|
+
filter: QueryFilterSchema.optional()
|
|
19217
|
+
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
19218
|
+
namespace: string().optional(),
|
|
19219
|
+
collection: string(),
|
|
19220
|
+
record: SettingsRecordSchema
|
|
19221
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19222
|
+
namespace: string().optional(),
|
|
19223
|
+
collection: string(),
|
|
19224
|
+
id: string(),
|
|
19225
|
+
data: record(string(), unknown())
|
|
19226
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19227
|
+
namespace: string().optional(),
|
|
19228
|
+
collection: string(),
|
|
19229
|
+
key: string()
|
|
19230
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
19231
|
+
namespace: string().optional(),
|
|
19232
|
+
collection: string(),
|
|
19233
|
+
filter: MutationFilterSchema
|
|
19234
|
+
}), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
|
|
19235
|
+
namespace: string().optional(),
|
|
19236
|
+
collection: string(),
|
|
19237
|
+
filter: MutationFilterSchema,
|
|
19238
|
+
data: record(string(), unknown())
|
|
19239
|
+
}), object({ updated: number().int() }), { kind: "mutation" }), method(object({
|
|
19240
|
+
namespace: string().optional(),
|
|
19241
|
+
collection: string(),
|
|
19242
|
+
filter: QueryFilterSchema.optional()
|
|
19243
|
+
}), number()), method(object({
|
|
19244
|
+
namespace: string().optional(),
|
|
19245
|
+
collection: string(),
|
|
19246
|
+
field: string(),
|
|
19247
|
+
bucketSize: number().int().positive(),
|
|
19248
|
+
origin: number().int(),
|
|
19249
|
+
filter: QueryFilterSchema.optional()
|
|
19250
|
+
}), array(object({
|
|
19251
|
+
bucket: number().int(),
|
|
19252
|
+
count: number().int()
|
|
19253
|
+
})).readonly()), method(object({
|
|
19254
|
+
namespace: string().optional(),
|
|
19255
|
+
collection: string()
|
|
19256
|
+
}), boolean()), method(object({
|
|
19257
|
+
namespace: string().optional(),
|
|
19258
|
+
collection: string(),
|
|
19259
|
+
columns: array(CollectionColumnSchema).readonly(),
|
|
19260
|
+
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
19261
|
+
}), _void(), { kind: "mutation" });
|
|
18515
19262
|
DeviceType.Camera;
|
|
18516
19263
|
/**
|
|
18517
19264
|
* `device-adoption` — generic discovery + adoption surface,
|
|
@@ -19207,7 +19954,20 @@ method(object({
|
|
|
19207
19954
|
}), _void(), {
|
|
19208
19955
|
kind: "mutation",
|
|
19209
19956
|
auth: "admin"
|
|
19210
|
-
}), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
|
|
19957
|
+
}), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
|
|
19958
|
+
addonId: string().optional(),
|
|
19959
|
+
/**
|
|
19960
|
+
* `slim` omits `config` (returned `{}`), `metadata` (null) and the
|
|
19961
|
+
* `sourceInfo` derived from config — and skips the per-device settings
|
|
19962
|
+
* read that produces them. Everything identifying a device (id, name,
|
|
19963
|
+
* type, online, features, isCamera, parent/link ids) is unchanged.
|
|
19964
|
+
* Do not use it for dispatch routing, which needs `sourceInfo`.
|
|
19965
|
+
*/
|
|
19966
|
+
projection: _enum(["full", "slim"]).optional(),
|
|
19967
|
+
/** Return only camera devices. Filtering server-side instead of
|
|
19968
|
+
* shipping 293 rows to find 12. */
|
|
19969
|
+
isCamera: boolean().optional()
|
|
19970
|
+
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
19211
19971
|
mode: DeviceLinkModeSchema,
|
|
19212
19972
|
devices: array(LinkedDeviceSchema)
|
|
19213
19973
|
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
@@ -19646,14 +20406,14 @@ var LlmProfileSchema = object({
|
|
|
19646
20406
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
19647
20407
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
19648
20408
|
* notification-output.cap.ts:151); the exported TS type re-tightens it. */
|
|
19649
|
-
var ConfigSchemaPassthrough
|
|
20409
|
+
var ConfigSchemaPassthrough = unknown();
|
|
19650
20410
|
var LlmProfileKindDescriptorSchema = object({
|
|
19651
20411
|
kind: LlmProfileKindSchema,
|
|
19652
20412
|
label: string(),
|
|
19653
20413
|
icon: string(),
|
|
19654
20414
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
19655
20415
|
addonId: string(),
|
|
19656
|
-
configSchema: ConfigSchemaPassthrough
|
|
20416
|
+
configSchema: ConfigSchemaPassthrough
|
|
19657
20417
|
});
|
|
19658
20418
|
var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
|
|
19659
20419
|
var LlmDefaultSchema = object({
|
|
@@ -20184,241 +20944,102 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
|
20184
20944
|
});
|
|
20185
20945
|
method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
|
|
20186
20946
|
/**
|
|
20187
|
-
*
|
|
20188
|
-
*
|
|
20189
|
-
*
|
|
20190
|
-
* `docs/superpowers/specs/2026-
|
|
20191
|
-
*
|
|
20192
|
-
*
|
|
20193
|
-
*
|
|
20194
|
-
*
|
|
20195
|
-
*
|
|
20196
|
-
*
|
|
20197
|
-
*
|
|
20198
|
-
*
|
|
20199
|
-
*
|
|
20200
|
-
*
|
|
20201
|
-
*
|
|
20202
|
-
*
|
|
20203
|
-
*
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
*
|
|
20207
|
-
|
|
20208
|
-
|
|
20209
|
-
*
|
|
20210
|
-
|
|
20211
|
-
|
|
20212
|
-
|
|
20213
|
-
|
|
20214
|
-
|
|
20215
|
-
* admin "Integrations" page.
|
|
20216
|
-
*/
|
|
20217
|
-
/**
|
|
20218
|
-
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
20219
|
-
* adapter picks what it supports and the degrade engine filters the rest.
|
|
20220
|
-
*/
|
|
20221
|
-
var AttachmentMediaTypeSchema = _enum([
|
|
20222
|
-
"image",
|
|
20223
|
-
"video",
|
|
20224
|
-
"gif",
|
|
20225
|
-
"audio",
|
|
20226
|
-
"icon"
|
|
20227
|
-
]);
|
|
20228
|
-
/**
|
|
20229
|
-
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
20230
|
-
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
20231
|
-
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
20232
|
-
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
20233
|
-
*/
|
|
20234
|
-
var AttachmentSchema = object({
|
|
20235
|
-
mediaType: AttachmentMediaTypeSchema,
|
|
20236
|
-
url: string().optional(),
|
|
20237
|
-
bytes: _instanceof(Uint8Array).optional(),
|
|
20238
|
-
mime: string().optional(),
|
|
20239
|
-
name: string().optional()
|
|
20240
|
-
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
20241
|
-
var NotificationFormatSchema = _enum([
|
|
20242
|
-
"text",
|
|
20243
|
-
"markdown",
|
|
20244
|
-
"html"
|
|
20947
|
+
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
20948
|
+
* its own process.
|
|
20949
|
+
*
|
|
20950
|
+
* Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
|
|
20951
|
+
*
|
|
20952
|
+
* The first use is **owning devices without being a device provider**: a block
|
|
20953
|
+
* declares devices under a system or custom integration and drives their state,
|
|
20954
|
+
* with the same `ctx` an addon gets. Automations come later; nothing here
|
|
20955
|
+
* models a trigger.
|
|
20956
|
+
*
|
|
20957
|
+
* **Stated plainly, because it does not change by being true:** a block has an
|
|
20958
|
+
* addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
|
|
20959
|
+
* with no review step. What makes that survivable is not a sandbox, it is
|
|
20960
|
+
* PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
|
|
20961
|
+
* so a block that throws or never returns is marked `failed` and visible
|
|
20962
|
+
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
20963
|
+
* must stay so.
|
|
20964
|
+
*/
|
|
20965
|
+
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
20966
|
+
* agent is the reason placement is not fixed to the hub. */
|
|
20967
|
+
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
20968
|
+
/** What a block's process is doing. Mirrors the addon runner's own lifecycle so
|
|
20969
|
+
* a failing block reads the same way a failing addon does. */
|
|
20970
|
+
var CoreBlockStatusSchema = _enum([
|
|
20971
|
+
"stopped",
|
|
20972
|
+
"starting",
|
|
20973
|
+
"running",
|
|
20974
|
+
"failed"
|
|
20245
20975
|
]);
|
|
20246
|
-
/**
|
|
20247
|
-
var
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
20255
|
-
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
20256
|
-
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
20257
|
-
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
20258
|
-
* `priority` for that one target.
|
|
20259
|
-
*/
|
|
20260
|
-
var NotificationSchema = object({
|
|
20261
|
-
body: string(),
|
|
20262
|
-
title: string().optional(),
|
|
20263
|
-
format: NotificationFormatSchema.default("text"),
|
|
20264
|
-
priority: number().int().min(1).max(5).default(3),
|
|
20265
|
-
level: string().optional(),
|
|
20266
|
-
attachments: array(AttachmentSchema).optional(),
|
|
20267
|
-
clickUrl: string().optional(),
|
|
20268
|
-
actions: array(NotificationActionSchema).optional(),
|
|
20269
|
-
sound: string().optional(),
|
|
20270
|
-
ttl: number().optional(),
|
|
20271
|
-
tag: string().optional(),
|
|
20272
|
-
deviceId: number().optional(),
|
|
20273
|
-
eventId: string().optional(),
|
|
20274
|
-
metadata: record(string(), unknown()).optional()
|
|
20275
|
-
});
|
|
20276
|
-
/** One declared native severity/priority level for a kind. */
|
|
20277
|
-
var TargetKindLevelSchema = object({
|
|
20278
|
-
id: string(),
|
|
20279
|
-
label: string(),
|
|
20280
|
-
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
20281
|
-
ordinal: number().int().min(1).max(5).nullable(),
|
|
20282
|
-
flags: object({
|
|
20283
|
-
critical: boolean().optional(),
|
|
20284
|
-
silent: boolean().optional(),
|
|
20285
|
-
noPush: boolean().optional()
|
|
20286
|
-
}).optional(),
|
|
20287
|
-
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
20288
|
-
requires: array(string()).optional(),
|
|
20289
|
-
description: string().optional()
|
|
20290
|
-
});
|
|
20291
|
-
/** The full capability block consulted before dispatch. */
|
|
20292
|
-
var TargetKindCapsSchema = object({
|
|
20293
|
-
attachments: object({
|
|
20294
|
-
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
20295
|
-
mode: _enum([
|
|
20296
|
-
"url",
|
|
20297
|
-
"bytes",
|
|
20298
|
-
"both"
|
|
20299
|
-
]),
|
|
20300
|
-
max: number().int().nonnegative(),
|
|
20301
|
-
maxBytes: number().int().positive().optional()
|
|
20302
|
-
}),
|
|
20303
|
-
/** Max action buttons (0 = none). */
|
|
20304
|
-
actions: number().int().nonnegative(),
|
|
20305
|
-
levels: array(TargetKindLevelSchema),
|
|
20306
|
-
format: array(NotificationFormatSchema),
|
|
20307
|
-
clickUrl: boolean(),
|
|
20308
|
-
sound: boolean(),
|
|
20309
|
-
ttl: boolean(),
|
|
20310
|
-
bodyMaxLen: number().int().positive()
|
|
20311
|
-
});
|
|
20312
|
-
/**
|
|
20313
|
-
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
20314
|
-
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
20315
|
-
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
20316
|
-
* the union is large and not meant for runtime validation here; the exported
|
|
20317
|
-
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
20318
|
-
*/
|
|
20319
|
-
var ConfigSchemaPassthrough = unknown();
|
|
20320
|
-
var TargetKindSchema = object({
|
|
20321
|
-
kind: string(),
|
|
20322
|
-
label: string(),
|
|
20323
|
-
icon: string(),
|
|
20324
|
-
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
20325
|
-
addonId: string(),
|
|
20326
|
-
/**
|
|
20327
|
-
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
20328
|
-
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
20329
|
-
* when the addon bundles no icon for that kind — the client then falls back
|
|
20330
|
-
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
20331
|
-
*
|
|
20332
|
-
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
20333
|
-
* client, and a native client joins it onto its own hub base.
|
|
20334
|
-
*
|
|
20335
|
-
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
20336
|
-
* field that survived only because the runtime cap-router forwards provider
|
|
20337
|
-
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
20338
|
-
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
20339
|
-
* broken silently the moment output validation was tightened anywhere.
|
|
20340
|
-
*/
|
|
20341
|
-
iconUrl: string().optional(),
|
|
20976
|
+
/** Client-authored fields. */
|
|
20977
|
+
var CoreBlockInputSchema = object({
|
|
20978
|
+
name: string().min(1).max(120),
|
|
20979
|
+
/** TypeScript source. Compiled server-side before it is ever stored — a
|
|
20980
|
+
* block that does not compile is a fork failure the operator would meet
|
|
20981
|
+
* minutes later, in a log, instead of in the editor. */
|
|
20982
|
+
code: string().max(2e5),
|
|
20983
|
+
enabled: boolean(),
|
|
20984
|
+
placement: CoreBlockPlacementSchema,
|
|
20342
20985
|
/**
|
|
20343
|
-
*
|
|
20344
|
-
*
|
|
20345
|
-
* The server knows this and therefore says it, because the client cannot
|
|
20346
|
-
* safely guess: a React-Native client renders SVG and raster through two
|
|
20347
|
-
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
20348
|
-
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
20349
|
-
* placeholder glyph for every vector icon while the web build looked fine.
|
|
20350
|
-
*
|
|
20351
|
-
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
20352
|
-
* not been updated — a client that cannot determine the type should prefer
|
|
20353
|
-
* its raster path, which is the safe default for an unknown image.
|
|
20986
|
+
* Integration the block's devices hang from. Absent = the system integration
|
|
20987
|
+
* blocks share. A block may declare its own instead.
|
|
20354
20988
|
*/
|
|
20355
|
-
|
|
20356
|
-
configSchema: ConfigSchemaPassthrough,
|
|
20357
|
-
supportsDiscovery: boolean(),
|
|
20358
|
-
caps: TargetKindCapsSchema
|
|
20989
|
+
integrationId: string().optional()
|
|
20359
20990
|
});
|
|
20360
|
-
/**
|
|
20361
|
-
|
|
20362
|
-
* (return a presence marker only) when serving `listTargets` — never
|
|
20363
|
-
* round-trip a stored secret to the UI.
|
|
20364
|
-
*/
|
|
20365
|
-
var TargetSchema = object({
|
|
20991
|
+
/** A stored block. */
|
|
20992
|
+
var CoreBlockSchema = CoreBlockInputSchema.extend({
|
|
20366
20993
|
id: string(),
|
|
20367
|
-
|
|
20368
|
-
|
|
20369
|
-
|
|
20370
|
-
|
|
20371
|
-
|
|
20372
|
-
|
|
20373
|
-
|
|
20374
|
-
|
|
20375
|
-
|
|
20376
|
-
|
|
20377
|
-
|
|
20378
|
-
|
|
20379
|
-
|
|
20380
|
-
var RenderedAsSchema = object({
|
|
20381
|
-
level: string(),
|
|
20382
|
-
format: NotificationFormatSchema,
|
|
20383
|
-
attachmentsSent: number().int().nonnegative(),
|
|
20384
|
-
actionsSent: number().int().nonnegative(),
|
|
20385
|
-
truncated: boolean(),
|
|
20386
|
-
dropped: array(string())
|
|
20994
|
+
createdAt: number(),
|
|
20995
|
+
updatedAt: number(),
|
|
20996
|
+
/** Server-stamped author. */
|
|
20997
|
+
createdBy: string(),
|
|
20998
|
+
status: CoreBlockStatusSchema,
|
|
20999
|
+
/**
|
|
21000
|
+
* Why the block is not running, when it is not. The operator's ONLY window
|
|
21001
|
+
* into a block that failed at load — a block that is silently absent is the
|
|
21002
|
+
* failure mode this whole feature has to avoid.
|
|
21003
|
+
*/
|
|
21004
|
+
lastError: string().optional(),
|
|
21005
|
+
/** Ms epoch of the last state change. */
|
|
21006
|
+
lastChangedAt: number()
|
|
20387
21007
|
});
|
|
20388
|
-
|
|
20389
|
-
|
|
21008
|
+
/** What a compile attempt produced. */
|
|
21009
|
+
var CoreBlockCompileResultSchema = object({
|
|
21010
|
+
ok: boolean(),
|
|
21011
|
+
/** Present when `ok` is false — the first error, in the author's words. */
|
|
20390
21012
|
error: string().optional(),
|
|
20391
|
-
|
|
21013
|
+
line: number().optional(),
|
|
21014
|
+
column: number().optional()
|
|
20392
21015
|
});
|
|
20393
|
-
|
|
20394
|
-
|
|
20395
|
-
|
|
20396
|
-
|
|
20397
|
-
|
|
20398
|
-
|
|
20399
|
-
|
|
20400
|
-
|
|
20401
|
-
|
|
20402
|
-
|
|
20403
|
-
|
|
20404
|
-
|
|
20405
|
-
|
|
20406
|
-
|
|
20407
|
-
|
|
20408
|
-
|
|
20409
|
-
|
|
20410
|
-
|
|
20411
|
-
|
|
20412
|
-
|
|
20413
|
-
|
|
20414
|
-
|
|
20415
|
-
|
|
20416
|
-
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
}
|
|
20421
|
-
};
|
|
21016
|
+
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 }), {
|
|
21017
|
+
kind: "mutation",
|
|
21018
|
+
auth: "admin",
|
|
21019
|
+
caller: "required"
|
|
21020
|
+
}), method(object({
|
|
21021
|
+
blockId: string(),
|
|
21022
|
+
block: CoreBlockInputSchema.partial()
|
|
21023
|
+
}), object({ block: CoreBlockSchema }), {
|
|
21024
|
+
kind: "mutation",
|
|
21025
|
+
auth: "admin",
|
|
21026
|
+
caller: "required"
|
|
21027
|
+
}), method(object({ blockId: string() }), object({ success: literal(true) }), {
|
|
21028
|
+
kind: "mutation",
|
|
21029
|
+
auth: "admin"
|
|
21030
|
+
}), method(object({
|
|
21031
|
+
blockId: string(),
|
|
21032
|
+
enabled: boolean()
|
|
21033
|
+
}), object({ block: CoreBlockSchema }), {
|
|
21034
|
+
kind: "mutation",
|
|
21035
|
+
auth: "admin"
|
|
21036
|
+
}), method(object({ code: string() }), CoreBlockCompileResultSchema, {
|
|
21037
|
+
kind: "mutation",
|
|
21038
|
+
auth: "admin"
|
|
21039
|
+
}), method(object({}), object({ libs: array(object({
|
|
21040
|
+
filePath: string(),
|
|
21041
|
+
content: string()
|
|
21042
|
+
})) }), { auth: "admin" });
|
|
20422
21043
|
/**
|
|
20423
21044
|
* Zod schemas for persisted record types.
|
|
20424
21045
|
*
|
|
@@ -20666,6 +21287,11 @@ var EventKindDescriptorSchema = object({
|
|
|
20666
21287
|
deviceId: number()
|
|
20667
21288
|
})
|
|
20668
21289
|
});
|
|
21290
|
+
/** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
|
|
21291
|
+
var EventKindsForDeviceSchema = object({
|
|
21292
|
+
deviceId: number(),
|
|
21293
|
+
kinds: array(EventKindDescriptorSchema).readonly()
|
|
21294
|
+
});
|
|
20669
21295
|
var SensorEventSchema = object({
|
|
20670
21296
|
id: string(),
|
|
20671
21297
|
/** The CAMERA the event is attributed to (a sensor linked to N cameras
|
|
@@ -20922,6 +21548,19 @@ var MediaFileSchema = object({
|
|
|
20922
21548
|
sizeBytes: number(),
|
|
20923
21549
|
timestamp: number()
|
|
20924
21550
|
});
|
|
21551
|
+
/**
|
|
21552
|
+
* One media row WITHOUT its bytes.
|
|
21553
|
+
*
|
|
21554
|
+
* A track's media is 5-8 MB of base64 (measured: 7.67 MB across 23 files for a
|
|
21555
|
+
* 140 s track), and a client that renders tiles from the media data plane needs
|
|
21556
|
+
* to know only WHAT EXISTS — the bytes then arrive per tile, lazily, over HTTP
|
|
21557
|
+
* with an immutable cache, instead of all at once inside a tRPC response that
|
|
21558
|
+
* blocks the whole view.
|
|
21559
|
+
*
|
|
21560
|
+
* `sizeBytes` is carried because it is what lets a client decide between the
|
|
21561
|
+
* stored blob and a `?variant=thumb` rendering without fetching either.
|
|
21562
|
+
*/
|
|
21563
|
+
var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
|
|
20925
21564
|
var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
|
|
20926
21565
|
var MAX_EVENT_QUERY_LIMIT = 5e3;
|
|
20927
21566
|
var DeviceEventQueryInput = object({
|
|
@@ -21071,7 +21710,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
21071
21710
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
|
|
21072
21711
|
kind: "mutation",
|
|
21073
21712
|
auth: "admin"
|
|
21074
|
-
}), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
|
|
21713
|
+
}), 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({
|
|
21075
21714
|
deviceId: number(),
|
|
21076
21715
|
since: number().optional(),
|
|
21077
21716
|
until: number().optional(),
|
|
@@ -21145,7 +21784,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
21145
21784
|
}), array(MediaFileSchema).readonly()), method(object({
|
|
21146
21785
|
trackId: string(),
|
|
21147
21786
|
kinds: array(MediaFileKindEnum).optional()
|
|
21148
|
-
}), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
21787
|
+
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
21149
21788
|
deviceId: number(),
|
|
21150
21789
|
timestamp: number(),
|
|
21151
21790
|
frameWidth: number(),
|
|
@@ -21775,6 +22414,28 @@ var ServerUpdateStateSchema = _enum([
|
|
|
21775
22414
|
"pending-restart",
|
|
21776
22415
|
"awaiting-confirmation"
|
|
21777
22416
|
]);
|
|
22417
|
+
var ImageContractSchema = object({
|
|
22418
|
+
state: _enum([
|
|
22419
|
+
"in-sync",
|
|
22420
|
+
"behind-patch",
|
|
22421
|
+
"behind-series",
|
|
22422
|
+
"ahead",
|
|
22423
|
+
"unknown"
|
|
22424
|
+
]),
|
|
22425
|
+
/** The baked seed closure version — the image/app-bundle fingerprint. */
|
|
22426
|
+
seedVersion: string().nullable(),
|
|
22427
|
+
/** Best-known version the deployment contract delivers today. */
|
|
22428
|
+
contractVersion: string().nullable(),
|
|
22429
|
+
/**
|
|
22430
|
+
* Where `contractVersion` came from: a real registry check (`registry`), or
|
|
22431
|
+
* the node's own running version (`running` — a node can never run code
|
|
22432
|
+
* newer than the newest release, so `seed < running` proves image staleness
|
|
22433
|
+
* even before any registry check has run).
|
|
22434
|
+
*/
|
|
22435
|
+
contractSource: _enum(["registry", "running"]).nullable(),
|
|
22436
|
+
/** One operator-grade sentence: this node runs image X; the contract says Y. */
|
|
22437
|
+
message: string()
|
|
22438
|
+
});
|
|
21778
22439
|
var ServerRollbackInfoSchema = object({
|
|
21779
22440
|
/** The version that failed (or was manually rolled back). */
|
|
21780
22441
|
fromVersion: string(),
|
|
@@ -21811,7 +22472,12 @@ var ServerPackageStatusSchema = object({
|
|
|
21811
22472
|
* versions are being IGNORED. Surfaced as a warning in the UI.
|
|
21812
22473
|
*/
|
|
21813
22474
|
stateFileCorrupt: boolean(),
|
|
21814
|
-
lastCheckedAtMs: number().nullable()
|
|
22475
|
+
lastCheckedAtMs: number().nullable(),
|
|
22476
|
+
/**
|
|
22477
|
+
* Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
|
|
22478
|
+
* version skew: an older provider's payload simply omits it.
|
|
22479
|
+
*/
|
|
22480
|
+
imageContract: ImageContractSchema.optional()
|
|
21815
22481
|
});
|
|
21816
22482
|
var ServerUpdateCheckResultSchema = object({
|
|
21817
22483
|
packageName: string(),
|
|
@@ -21846,101 +22512,6 @@ version: string().optional() }), ServerUpdateActionResultSchema, {
|
|
|
21846
22512
|
auth: "admin"
|
|
21847
22513
|
});
|
|
21848
22514
|
/**
|
|
21849
|
-
* Query filter for settings-store collections.
|
|
21850
|
-
*/
|
|
21851
|
-
var QueryFilterSchema = object({
|
|
21852
|
-
where: record(string(), unknown()).optional(),
|
|
21853
|
-
whereIn: record(string(), array(unknown())).optional(),
|
|
21854
|
-
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
21855
|
-
orderBy: object({
|
|
21856
|
-
field: string(),
|
|
21857
|
-
direction: _enum(["asc", "desc"])
|
|
21858
|
-
}).optional(),
|
|
21859
|
-
limit: number().optional(),
|
|
21860
|
-
offset: number().optional()
|
|
21861
|
-
});
|
|
21862
|
-
/** A single stored record: `{ id, data }`. */
|
|
21863
|
-
var SettingsRecordSchema = object({
|
|
21864
|
-
id: string(),
|
|
21865
|
-
data: record(string(), unknown())
|
|
21866
|
-
});
|
|
21867
|
-
/**
|
|
21868
|
-
* Column declaration for a structured (SQL-backed) collection.
|
|
21869
|
-
*
|
|
21870
|
-
* Logical types — the backend translates each to the matching SQLite
|
|
21871
|
-
* storage class and handles per-type marshaling:
|
|
21872
|
-
* - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
|
|
21873
|
-
* - `JSON` — TEXT under the hood; serialised on write, parsed on read
|
|
21874
|
-
* - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
|
|
21875
|
-
*/
|
|
21876
|
-
var CollectionColumnSchema = object({
|
|
21877
|
-
name: string(),
|
|
21878
|
-
type: _enum([
|
|
21879
|
-
"TEXT",
|
|
21880
|
-
"INTEGER",
|
|
21881
|
-
"REAL",
|
|
21882
|
-
"JSON",
|
|
21883
|
-
"BOOLEAN"
|
|
21884
|
-
]),
|
|
21885
|
-
primaryKey: boolean().optional(),
|
|
21886
|
-
notNull: boolean().optional(),
|
|
21887
|
-
unique: boolean().optional()
|
|
21888
|
-
});
|
|
21889
|
-
var CollectionIndexSchema = object({
|
|
21890
|
-
name: string(),
|
|
21891
|
-
columns: array(string()).readonly(),
|
|
21892
|
-
unique: boolean().optional()
|
|
21893
|
-
});
|
|
21894
|
-
method(object({
|
|
21895
|
-
namespace: string().optional(),
|
|
21896
|
-
collection: string(),
|
|
21897
|
-
key: string()
|
|
21898
|
-
}), unknown()), method(object({
|
|
21899
|
-
namespace: string().optional(),
|
|
21900
|
-
collection: string(),
|
|
21901
|
-
key: string(),
|
|
21902
|
-
value: unknown()
|
|
21903
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
21904
|
-
namespace: string().optional(),
|
|
21905
|
-
collection: string(),
|
|
21906
|
-
filter: QueryFilterSchema.optional()
|
|
21907
|
-
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
21908
|
-
namespace: string().optional(),
|
|
21909
|
-
collection: string(),
|
|
21910
|
-
record: SettingsRecordSchema
|
|
21911
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
21912
|
-
namespace: string().optional(),
|
|
21913
|
-
collection: string(),
|
|
21914
|
-
id: string(),
|
|
21915
|
-
data: record(string(), unknown())
|
|
21916
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
21917
|
-
namespace: string().optional(),
|
|
21918
|
-
collection: string(),
|
|
21919
|
-
key: string()
|
|
21920
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
21921
|
-
namespace: string().optional(),
|
|
21922
|
-
collection: string(),
|
|
21923
|
-
filter: QueryFilterSchema.optional()
|
|
21924
|
-
}), number()), method(object({
|
|
21925
|
-
namespace: string().optional(),
|
|
21926
|
-
collection: string(),
|
|
21927
|
-
field: string(),
|
|
21928
|
-
bucketSize: number().int().positive(),
|
|
21929
|
-
origin: number().int(),
|
|
21930
|
-
filter: QueryFilterSchema.optional()
|
|
21931
|
-
}), array(object({
|
|
21932
|
-
bucket: number().int(),
|
|
21933
|
-
count: number().int()
|
|
21934
|
-
})).readonly()), method(object({
|
|
21935
|
-
namespace: string().optional(),
|
|
21936
|
-
collection: string()
|
|
21937
|
-
}), boolean()), method(object({
|
|
21938
|
-
namespace: string().optional(),
|
|
21939
|
-
collection: string(),
|
|
21940
|
-
columns: array(CollectionColumnSchema).readonly(),
|
|
21941
|
-
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
21942
|
-
}), _void(), { kind: "mutation" });
|
|
21943
|
-
/**
|
|
21944
22515
|
* `smtp-provider` — pluggable email delivery surface.
|
|
21945
22516
|
*
|
|
21946
22517
|
* Collection cap: a deployment may install multiple SMTP relays (e.g.
|
|
@@ -25892,6 +26463,54 @@ Object.freeze({
|
|
|
25892
26463
|
addonId: null,
|
|
25893
26464
|
access: "create"
|
|
25894
26465
|
},
|
|
26466
|
+
"coreBlocks.compile": {
|
|
26467
|
+
capName: "core-blocks",
|
|
26468
|
+
capScope: "system",
|
|
26469
|
+
addonId: null,
|
|
26470
|
+
access: "create"
|
|
26471
|
+
},
|
|
26472
|
+
"coreBlocks.create": {
|
|
26473
|
+
capName: "core-blocks",
|
|
26474
|
+
capScope: "system",
|
|
26475
|
+
addonId: null,
|
|
26476
|
+
access: "create"
|
|
26477
|
+
},
|
|
26478
|
+
"coreBlocks.delete": {
|
|
26479
|
+
capName: "core-blocks",
|
|
26480
|
+
capScope: "system",
|
|
26481
|
+
addonId: null,
|
|
26482
|
+
access: "delete"
|
|
26483
|
+
},
|
|
26484
|
+
"coreBlocks.get": {
|
|
26485
|
+
capName: "core-blocks",
|
|
26486
|
+
capScope: "system",
|
|
26487
|
+
addonId: null,
|
|
26488
|
+
access: "view"
|
|
26489
|
+
},
|
|
26490
|
+
"coreBlocks.getTypeDefs": {
|
|
26491
|
+
capName: "core-blocks",
|
|
26492
|
+
capScope: "system",
|
|
26493
|
+
addonId: null,
|
|
26494
|
+
access: "view"
|
|
26495
|
+
},
|
|
26496
|
+
"coreBlocks.list": {
|
|
26497
|
+
capName: "core-blocks",
|
|
26498
|
+
capScope: "system",
|
|
26499
|
+
addonId: null,
|
|
26500
|
+
access: "view"
|
|
26501
|
+
},
|
|
26502
|
+
"coreBlocks.setEnabled": {
|
|
26503
|
+
capName: "core-blocks",
|
|
26504
|
+
capScope: "system",
|
|
26505
|
+
addonId: null,
|
|
26506
|
+
access: "create"
|
|
26507
|
+
},
|
|
26508
|
+
"coreBlocks.update": {
|
|
26509
|
+
capName: "core-blocks",
|
|
26510
|
+
capScope: "system",
|
|
26511
|
+
addonId: null,
|
|
26512
|
+
access: "create"
|
|
26513
|
+
},
|
|
25895
26514
|
"cover.close": {
|
|
25896
26515
|
capName: "cover",
|
|
25897
26516
|
capScope: "device",
|
|
@@ -25928,6 +26547,84 @@ Object.freeze({
|
|
|
25928
26547
|
addonId: null,
|
|
25929
26548
|
access: "view"
|
|
25930
26549
|
},
|
|
26550
|
+
"dataStoreProvider.count": {
|
|
26551
|
+
capName: "data-store-provider",
|
|
26552
|
+
capScope: "system",
|
|
26553
|
+
addonId: null,
|
|
26554
|
+
access: "view"
|
|
26555
|
+
},
|
|
26556
|
+
"dataStoreProvider.declareCollection": {
|
|
26557
|
+
capName: "data-store-provider",
|
|
26558
|
+
capScope: "system",
|
|
26559
|
+
addonId: null,
|
|
26560
|
+
access: "create"
|
|
26561
|
+
},
|
|
26562
|
+
"dataStoreProvider.delete": {
|
|
26563
|
+
capName: "data-store-provider",
|
|
26564
|
+
capScope: "system",
|
|
26565
|
+
addonId: null,
|
|
26566
|
+
access: "delete"
|
|
26567
|
+
},
|
|
26568
|
+
"dataStoreProvider.deleteWhere": {
|
|
26569
|
+
capName: "data-store-provider",
|
|
26570
|
+
capScope: "system",
|
|
26571
|
+
addonId: null,
|
|
26572
|
+
access: "delete"
|
|
26573
|
+
},
|
|
26574
|
+
"dataStoreProvider.get": {
|
|
26575
|
+
capName: "data-store-provider",
|
|
26576
|
+
capScope: "system",
|
|
26577
|
+
addonId: null,
|
|
26578
|
+
access: "view"
|
|
26579
|
+
},
|
|
26580
|
+
"dataStoreProvider.getEngineInfo": {
|
|
26581
|
+
capName: "data-store-provider",
|
|
26582
|
+
capScope: "system",
|
|
26583
|
+
addonId: null,
|
|
26584
|
+
access: "view"
|
|
26585
|
+
},
|
|
26586
|
+
"dataStoreProvider.histogram": {
|
|
26587
|
+
capName: "data-store-provider",
|
|
26588
|
+
capScope: "system",
|
|
26589
|
+
addonId: null,
|
|
26590
|
+
access: "view"
|
|
26591
|
+
},
|
|
26592
|
+
"dataStoreProvider.insert": {
|
|
26593
|
+
capName: "data-store-provider",
|
|
26594
|
+
capScope: "system",
|
|
26595
|
+
addonId: null,
|
|
26596
|
+
access: "create"
|
|
26597
|
+
},
|
|
26598
|
+
"dataStoreProvider.isEmpty": {
|
|
26599
|
+
capName: "data-store-provider",
|
|
26600
|
+
capScope: "system",
|
|
26601
|
+
addonId: null,
|
|
26602
|
+
access: "view"
|
|
26603
|
+
},
|
|
26604
|
+
"dataStoreProvider.query": {
|
|
26605
|
+
capName: "data-store-provider",
|
|
26606
|
+
capScope: "system",
|
|
26607
|
+
addonId: null,
|
|
26608
|
+
access: "view"
|
|
26609
|
+
},
|
|
26610
|
+
"dataStoreProvider.set": {
|
|
26611
|
+
capName: "data-store-provider",
|
|
26612
|
+
capScope: "system",
|
|
26613
|
+
addonId: null,
|
|
26614
|
+
access: "create"
|
|
26615
|
+
},
|
|
26616
|
+
"dataStoreProvider.update": {
|
|
26617
|
+
capName: "data-store-provider",
|
|
26618
|
+
capScope: "system",
|
|
26619
|
+
addonId: null,
|
|
26620
|
+
access: "create"
|
|
26621
|
+
},
|
|
26622
|
+
"dataStoreProvider.updateWhere": {
|
|
26623
|
+
capName: "data-store-provider",
|
|
26624
|
+
capScope: "system",
|
|
26625
|
+
addonId: null,
|
|
26626
|
+
access: "create"
|
|
26627
|
+
},
|
|
25931
26628
|
"dayNight.getOptions": {
|
|
25932
26629
|
capName: "day-night",
|
|
25933
26630
|
capScope: "device",
|
|
@@ -27722,12 +28419,24 @@ Object.freeze({
|
|
|
27722
28419
|
addonId: null,
|
|
27723
28420
|
access: "create"
|
|
27724
28421
|
},
|
|
28422
|
+
"notificationRules.cancelSnooze": {
|
|
28423
|
+
capName: "notification-rules",
|
|
28424
|
+
capScope: "system",
|
|
28425
|
+
addonId: null,
|
|
28426
|
+
access: "create"
|
|
28427
|
+
},
|
|
27725
28428
|
"notificationRules.createRule": {
|
|
27726
28429
|
capName: "notification-rules",
|
|
27727
28430
|
capScope: "system",
|
|
27728
28431
|
addonId: null,
|
|
27729
28432
|
access: "create"
|
|
27730
28433
|
},
|
|
28434
|
+
"notificationRules.createSnooze": {
|
|
28435
|
+
capName: "notification-rules",
|
|
28436
|
+
capScope: "system",
|
|
28437
|
+
addonId: null,
|
|
28438
|
+
access: "create"
|
|
28439
|
+
},
|
|
27731
28440
|
"notificationRules.deleteRule": {
|
|
27732
28441
|
capName: "notification-rules",
|
|
27733
28442
|
capScope: "system",
|
|
@@ -27758,6 +28467,12 @@ Object.freeze({
|
|
|
27758
28467
|
addonId: null,
|
|
27759
28468
|
access: "view"
|
|
27760
28469
|
},
|
|
28470
|
+
"notificationRules.listSnoozes": {
|
|
28471
|
+
capName: "notification-rules",
|
|
28472
|
+
capScope: "system",
|
|
28473
|
+
addonId: null,
|
|
28474
|
+
access: "view"
|
|
28475
|
+
},
|
|
27761
28476
|
"notificationRules.setRuleEnabled": {
|
|
27762
28477
|
capName: "notification-rules",
|
|
27763
28478
|
capScope: "system",
|
|
@@ -27962,6 +28677,12 @@ Object.freeze({
|
|
|
27962
28677
|
addonId: null,
|
|
27963
28678
|
access: "view"
|
|
27964
28679
|
},
|
|
28680
|
+
"pipelineAnalytics.listEventKindsBatch": {
|
|
28681
|
+
capName: "pipeline-analytics",
|
|
28682
|
+
capScope: "device",
|
|
28683
|
+
addonId: null,
|
|
28684
|
+
access: "view"
|
|
28685
|
+
},
|
|
27965
28686
|
"pipelineAnalytics.listOpsLog": {
|
|
27966
28687
|
capName: "pipeline-analytics",
|
|
27967
28688
|
capScope: "device",
|
|
@@ -27974,6 +28695,12 @@ Object.freeze({
|
|
|
27974
28695
|
addonId: null,
|
|
27975
28696
|
access: "view"
|
|
27976
28697
|
},
|
|
28698
|
+
"pipelineAnalytics.listTrackMedia": {
|
|
28699
|
+
capName: "pipeline-analytics",
|
|
28700
|
+
capScope: "device",
|
|
28701
|
+
addonId: null,
|
|
28702
|
+
access: "view"
|
|
28703
|
+
},
|
|
27977
28704
|
"pipelineAnalytics.listTracks": {
|
|
27978
28705
|
capName: "pipeline-analytics",
|
|
27979
28706
|
capScope: "device",
|
|
@@ -29006,6 +29733,12 @@ Object.freeze({
|
|
|
29006
29733
|
addonId: null,
|
|
29007
29734
|
access: "delete"
|
|
29008
29735
|
},
|
|
29736
|
+
"settingsStore.deleteWhere": {
|
|
29737
|
+
capName: "settings-store",
|
|
29738
|
+
capScope: "system",
|
|
29739
|
+
addonId: null,
|
|
29740
|
+
access: "delete"
|
|
29741
|
+
},
|
|
29009
29742
|
"settingsStore.get": {
|
|
29010
29743
|
capName: "settings-store",
|
|
29011
29744
|
capScope: "system",
|
|
@@ -29048,6 +29781,12 @@ Object.freeze({
|
|
|
29048
29781
|
addonId: null,
|
|
29049
29782
|
access: "create"
|
|
29050
29783
|
},
|
|
29784
|
+
"settingsStore.updateWhere": {
|
|
29785
|
+
capName: "settings-store",
|
|
29786
|
+
capScope: "system",
|
|
29787
|
+
addonId: null,
|
|
29788
|
+
access: "create"
|
|
29789
|
+
},
|
|
29051
29790
|
"smtpProvider.getStatus": {
|
|
29052
29791
|
capName: "smtp-provider",
|
|
29053
29792
|
capScope: "system",
|
|
@@ -30432,11 +31171,18 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
|
|
|
30432
31171
|
async onActivate() {
|
|
30433
31172
|
await super.onActivate();
|
|
30434
31173
|
await this.refreshFeatures();
|
|
30435
|
-
await this.subscribeBrokerEntity()
|
|
30436
|
-
|
|
31174
|
+
if (await this.subscribeBrokerEntity()) {
|
|
31175
|
+
this.deviceLogger.info("HA entity ready", { meta: {
|
|
31176
|
+
entityId: this.entityId,
|
|
31177
|
+
type: this.ctx.deviceMeta.type,
|
|
31178
|
+
...this.features.length > 0 ? { features: [...this.features] } : {}
|
|
31179
|
+
} });
|
|
31180
|
+
return;
|
|
31181
|
+
}
|
|
31182
|
+
this.deviceLogger.warn("HA entity registered but NOT subscribed — it will receive nothing", { meta: {
|
|
30437
31183
|
entityId: this.entityId,
|
|
30438
|
-
|
|
30439
|
-
|
|
31184
|
+
brokerId: this.brokerId,
|
|
31185
|
+
type: this.ctx.deviceMeta.type
|
|
30440
31186
|
} });
|
|
30441
31187
|
}
|
|
30442
31188
|
async removeDevice() {
|
|
@@ -30540,6 +31286,8 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
|
|
|
30540
31286
|
* already-attached handler and `handleStatePush` fires with the real
|
|
30541
31287
|
* current state (seeding the cap-state slice at construction time).
|
|
30542
31288
|
*/
|
|
31289
|
+
/** True when the entity is genuinely subscribed. The caller uses it to
|
|
31290
|
+
* decide whether "ready" is an honest thing to log. */
|
|
30543
31291
|
async subscribeBrokerEntity() {
|
|
30544
31292
|
try {
|
|
30545
31293
|
const result = await this.ctx.api.broker.subscribe.mutate({
|
|
@@ -30547,12 +31295,14 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
|
|
|
30547
31295
|
filter: { entityIds: [this.entityId] }
|
|
30548
31296
|
});
|
|
30549
31297
|
this.brokerSubscriptionId = result.subscriptionId;
|
|
31298
|
+
return true;
|
|
30550
31299
|
} catch (err) {
|
|
30551
31300
|
this.deviceLogger.warn("ha-child failed to register broker subscription", { meta: {
|
|
30552
31301
|
entityId: this.entityId,
|
|
30553
31302
|
brokerId: this.brokerId,
|
|
30554
31303
|
error: errMsg(err)
|
|
30555
31304
|
} });
|
|
31305
|
+
return false;
|
|
30556
31306
|
}
|
|
30557
31307
|
}
|
|
30558
31308
|
attachEventBusListener() {
|
|
@@ -36532,7 +37282,7 @@ function createHaIconRouteProvider() {
|
|
|
36532
37282
|
/** The single kind this provider contributes to the shared catalog. */
|
|
36533
37283
|
var HA_NOTIFICATION_KIND = "homeassistant";
|
|
36534
37284
|
/** addonId-namespaced settings-store collection holding HA notify targets. */
|
|
36535
|
-
var HA_TARGETS_COLLECTION = "ha-notification-targets";
|
|
37285
|
+
var HA_TARGETS_COLLECTION = "provider-homeassistant:ha-notification-targets";
|
|
36536
37286
|
function haConfigSchema() {
|
|
36537
37287
|
return { sections: [{
|
|
36538
37288
|
id: "connection",
|
|
@@ -36576,6 +37326,7 @@ function homeassistantTargetKind(addonId) {
|
|
|
36576
37326
|
max: 1
|
|
36577
37327
|
},
|
|
36578
37328
|
actions: 3,
|
|
37329
|
+
actionIcons: true,
|
|
36579
37330
|
levels: [
|
|
36580
37331
|
{
|
|
36581
37332
|
id: "default",
|
|
@@ -36631,6 +37382,21 @@ function applyLevel(data, level) {
|
|
|
36631
37382
|
}
|
|
36632
37383
|
data["push"] = push;
|
|
36633
37384
|
}
|
|
37385
|
+
var HA_ACTION_ICONS = {
|
|
37386
|
+
acknowledge: "mdi:check",
|
|
37387
|
+
dismiss: "mdi:close",
|
|
37388
|
+
silence: "mdi:bell-off",
|
|
37389
|
+
view: "mdi:eye",
|
|
37390
|
+
play: "mdi:play",
|
|
37391
|
+
open: "mdi:door-open",
|
|
37392
|
+
close: "mdi:door-closed",
|
|
37393
|
+
lock: "mdi:lock",
|
|
37394
|
+
unlock: "mdi:lock-open",
|
|
37395
|
+
arm: "mdi:shield-check",
|
|
37396
|
+
disarm: "mdi:shield-off",
|
|
37397
|
+
light: "mdi:lightbulb",
|
|
37398
|
+
alert: "mdi:alert"
|
|
37399
|
+
};
|
|
36634
37400
|
/**
|
|
36635
37401
|
* Build the `notify.<service>` `service_data` from a prepared (degraded)
|
|
36636
37402
|
* notification. PURE — no WS, unit-testable in isolation.
|
|
@@ -36647,7 +37413,8 @@ function buildNotifyServiceData(prepared) {
|
|
|
36647
37413
|
if (prepared.actions.length > 0) data["actions"] = prepared.actions.map((a) => ({
|
|
36648
37414
|
action: a.id,
|
|
36649
37415
|
title: a.label,
|
|
36650
|
-
...a.url !== void 0 ? { uri: a.url } : {}
|
|
37416
|
+
...a.url !== void 0 ? { uri: a.url } : {},
|
|
37417
|
+
...a.icon !== void 0 ? { icon: HA_ACTION_ICONS[a.icon] } : {}
|
|
36651
37418
|
}));
|
|
36652
37419
|
if (prepared.clickUrl !== null) {
|
|
36653
37420
|
data["url"] = prepared.clickUrl;
|
|
@@ -36991,6 +37758,19 @@ async function reconcileBroker(brokerId, deps) {
|
|
|
36991
37758
|
* whose integration no longer exists should be cleaned up. Brokers with no
|
|
36992
37759
|
* integrationId were created manually and are never auto-removed.
|
|
36993
37760
|
*/
|
|
37761
|
+
/**
|
|
37762
|
+
* Brokers whose spawning integration is gone.
|
|
37763
|
+
*
|
|
37764
|
+
* **The caller REPORTS these; it does not delete them.** This function used to
|
|
37765
|
+
* drive a delete, and on 2026-08-05 that erased the operator's Home Assistant
|
|
37766
|
+
* broker — host and token — when `integrations.list` returned an empty array
|
|
37767
|
+
* during a hub restart: nothing "survived", so everything was an orphan. A
|
|
37768
|
+
* broker cannot be rebuilt from an integration record, so the loss was
|
|
37769
|
+
* permanent and the doorbell simply stopped existing.
|
|
37770
|
+
*
|
|
37771
|
+
* Kept as a pure query because naming an orphan is useful; acting on one
|
|
37772
|
+
* automatically is not.
|
|
37773
|
+
*/
|
|
36994
37774
|
function computeBrokerCleanup(brokers, survivingIntegrationIds) {
|
|
36995
37775
|
return brokers.filter((b) => b.integrationId !== void 0 && !survivingIntegrationIds.has(b.integrationId)).map((b) => b.id);
|
|
36996
37776
|
}
|
|
@@ -37578,15 +38358,15 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
37578
38358
|
}
|
|
37579
38359
|
const linkedBrokers = linkBrokersToIntegrations(this.config.brokers, integrationsWithBrokerId);
|
|
37580
38360
|
if (linkedBrokers.some((b, i) => b !== this.config.brokers[i])) await this.updateGlobalSettings({ brokers: linkedBrokers });
|
|
37581
|
-
const
|
|
37582
|
-
if (toRemove.length === 0) return;
|
|
37583
|
-
for (const id of toRemove) await this.requireRegistry().removeEntry(id);
|
|
37584
|
-
const nextBrokers = this.config.brokers.filter((b) => !toRemove.includes(b.id));
|
|
37585
|
-
await this.updateGlobalSettings({ brokers: nextBrokers });
|
|
38361
|
+
const orphans = computeBrokerCleanup(this.config.brokers, survivingIntegrationIds);
|
|
37586
38362
|
this.ctx.logger.info("integration→broker reconcile", { meta: {
|
|
37587
|
-
brokerCount:
|
|
38363
|
+
brokerCount: this.config.brokers.length,
|
|
37588
38364
|
integrationCount: haIntegrations.length,
|
|
37589
|
-
|
|
38365
|
+
orphans: orphans.length
|
|
38366
|
+
} });
|
|
38367
|
+
if (orphans.length > 0) this.ctx.logger.warn("broker entries have no surviving integration — KEPT", { meta: {
|
|
38368
|
+
brokerIds: orphans,
|
|
38369
|
+
integrationCount: haIntegrations.length
|
|
37590
38370
|
} });
|
|
37591
38371
|
} catch (err) {
|
|
37592
38372
|
this.ctx.logger.warn("integration→broker reconcile failed", { meta: { error: errMsg(err) } });
|