@camstack/addon-provider-homeassistant 1.2.5 → 1.2.7
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 +1752 -1203
- package/dist/addon.mjs +1752 -1203
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -7318,35 +7318,22 @@ function buildCapturingReply(envelope) {
|
|
|
7318
7318
|
*/
|
|
7319
7319
|
var RecordingWeekdaySchema = number().int().min(0).max(6);
|
|
7320
7320
|
var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
7321
|
-
var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
|
|
7322
|
-
kind: literal("timeOfDay"),
|
|
7323
|
-
start: string().regex(HHMM),
|
|
7324
|
-
end: string().regex(HHMM),
|
|
7325
|
-
/** Restrict to these weekdays; omit = every day. */
|
|
7326
|
-
days: array(RecordingWeekdaySchema).optional()
|
|
7327
|
-
})]);
|
|
7328
|
-
var RecordingModeSchema = _enum([
|
|
7329
|
-
"continuous",
|
|
7330
|
-
"onMotion",
|
|
7331
|
-
"onAudioThreshold"
|
|
7332
|
-
]);
|
|
7333
7321
|
/**
|
|
7334
|
-
*
|
|
7335
|
-
*
|
|
7336
|
-
* - `off` —
|
|
7337
|
-
* - `events` —
|
|
7338
|
-
*
|
|
7339
|
-
* - `continuous` — record 24/7 within the schedule.
|
|
7322
|
+
* DERIVED per-camera storage summary — the single field cheap consumers read
|
|
7323
|
+
* (the viewer's status dot, the camera list) instead of walking `bands`:
|
|
7324
|
+
* - `off` — no band covers the camera (or it is disabled).
|
|
7325
|
+
* - `events` — every band records around triggers only.
|
|
7326
|
+
* - `continuous` — at least one band records continuously.
|
|
7340
7327
|
*
|
|
7341
|
-
*
|
|
7342
|
-
*
|
|
7328
|
+
* NEVER authored: the recorder stamps it from the authoritative `bands` on
|
|
7329
|
+
* every save (`activeModeForConfig`). Writing it has no effect.
|
|
7343
7330
|
*/
|
|
7344
7331
|
var RecordingStorageModeSchema = _enum([
|
|
7345
7332
|
"off",
|
|
7346
7333
|
"events",
|
|
7347
7334
|
"continuous"
|
|
7348
7335
|
]);
|
|
7349
|
-
/** Which detectors trigger an `events`-mode
|
|
7336
|
+
/** Which detectors trigger an `events`-mode band. */
|
|
7350
7337
|
var RecordingTriggersSchema = object({
|
|
7351
7338
|
motion: boolean().optional(),
|
|
7352
7339
|
audioThresholdDbfs: number().optional()
|
|
@@ -7382,18 +7369,6 @@ var RecordingBandSchema = object({
|
|
|
7382
7369
|
preBufferSec: number().min(0).optional(),
|
|
7383
7370
|
postBufferSec: number().min(0).optional()
|
|
7384
7371
|
});
|
|
7385
|
-
var RecordingRuleSchema = object({
|
|
7386
|
-
schedule: RecordingScheduleSchema,
|
|
7387
|
-
mode: RecordingModeSchema,
|
|
7388
|
-
/** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
|
|
7389
|
-
preBufferSec: number().min(0).default(0),
|
|
7390
|
-
/** Keep recording until this many seconds after the last trigger. */
|
|
7391
|
-
postBufferSec: number().min(0).default(0),
|
|
7392
|
-
/** Each new trigger restarts the post-buffer window. */
|
|
7393
|
-
resetTimeoutOnNewEvent: boolean().default(true),
|
|
7394
|
-
/** onAudioThreshold only — dBFS level that counts as a trigger. */
|
|
7395
|
-
thresholdDbfs: number().optional()
|
|
7396
|
-
});
|
|
7397
7372
|
/**
|
|
7398
7373
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7399
7374
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7427,40 +7402,28 @@ var ScrubThumbnailPresetSchema = _enum([
|
|
|
7427
7402
|
/**
|
|
7428
7403
|
* The full per-camera recording intent — the wire shape of a RecordingTarget.
|
|
7429
7404
|
*
|
|
7430
|
-
* `
|
|
7431
|
-
*
|
|
7432
|
-
*
|
|
7433
|
-
*
|
|
7405
|
+
* `bands` is the ONLY authored recording intent: what to record, when, and on
|
|
7406
|
+
* which trigger. `mode` is a derived summary the recorder stamps on save; every
|
|
7407
|
+
* other field is a storage knob (profiles, segment length, retention, scrub).
|
|
7408
|
+
*
|
|
7409
|
+
* STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
|
|
7410
|
+
* `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
|
|
7411
|
+
* A stale caller must fail loudly — silently stripping its legacy intent would
|
|
7412
|
+
* persist a band-less config, i.e. silently stop recording the camera.
|
|
7434
7413
|
*/
|
|
7435
7414
|
var RecordingConfigSchema = object({
|
|
7436
7415
|
enabled: boolean(),
|
|
7437
|
-
/**
|
|
7438
|
-
*
|
|
7416
|
+
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7417
|
+
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7439
7418
|
mode: RecordingStorageModeSchema.optional(),
|
|
7440
7419
|
profiles: array(CamProfileSchema).optional(),
|
|
7441
7420
|
segmentSeconds: number().int().positive().optional(),
|
|
7442
|
-
/** Shared recording time-bands for `events` & `continuous` — record only when
|
|
7443
|
-
* the wall-clock falls inside one of these windows. Omit or empty = always.
|
|
7444
|
-
* `continuous` compiles to one rule per band; `events` to band × trigger. */
|
|
7445
|
-
schedules: array(RecordingScheduleSchema).optional(),
|
|
7446
|
-
/** Legacy single-band predecessor of `schedules`. Read-compat only — it is
|
|
7447
|
-
* normalized into `schedules` on read and never written going forward. (Not
|
|
7448
|
-
* tagged `@deprecated`: the normalization paths must read it cast-free.) */
|
|
7449
|
-
schedule: RecordingScheduleSchema.optional(),
|
|
7450
|
-
/** `events`-mode only — which detectors trigger a recording. */
|
|
7451
|
-
triggers: RecordingTriggersSchema.optional(),
|
|
7452
|
-
/** `events`-mode only — seconds retained before / after a trigger. */
|
|
7453
|
-
preBufferSec: number().min(0).optional(),
|
|
7454
|
-
postBufferSec: number().min(0).optional(),
|
|
7455
|
-
/** DEPRECATED authoring input; retained for migration/transition. */
|
|
7456
|
-
rules: array(RecordingRuleSchema).optional(),
|
|
7457
7421
|
/**
|
|
7458
|
-
* AUTHORITATIVE mode-per-band recording model
|
|
7459
|
-
*
|
|
7460
|
-
*
|
|
7461
|
-
* derived into bands once via `migrateConfigToBands`.
|
|
7422
|
+
* AUTHORITATIVE mode-per-band recording model — the single source of truth
|
|
7423
|
+
* the recorder's band engine consumes. An empty array = record nothing;
|
|
7424
|
+
* "off" is the absence of a covering band, never a band value.
|
|
7462
7425
|
*/
|
|
7463
|
-
bands: array(RecordingBandSchema).
|
|
7426
|
+
bands: array(RecordingBandSchema).default([]),
|
|
7464
7427
|
retention: RecordingRetentionSchema.optional(),
|
|
7465
7428
|
/**
|
|
7466
7429
|
* Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
|
|
@@ -7468,8 +7431,15 @@ var RecordingConfigSchema = object({
|
|
|
7468
7431
|
* windows only — existing sheets are immutable, and each window's index
|
|
7469
7432
|
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7470
7433
|
*/
|
|
7471
|
-
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7472
|
-
|
|
7434
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
|
|
7435
|
+
/**
|
|
7436
|
+
* OPT-IN thumbnail-strip generation for this camera: every keyframe of the
|
|
7437
|
+
* low recording saved as a JPEG (the fast-drag scrub depth), a derived
|
|
7438
|
+
* cache that eviction reclaims with the footage. Absent/false = no strips
|
|
7439
|
+
* are written and scrub reads exact keyframes at every velocity.
|
|
7440
|
+
*/
|
|
7441
|
+
stripsEnabled: boolean().optional()
|
|
7442
|
+
}).strict();
|
|
7473
7443
|
/**
|
|
7474
7444
|
* Ops-log — the durable, append-only operations audit shared by the
|
|
7475
7445
|
* recordings and events management surfaces.
|
|
@@ -7488,7 +7458,8 @@ var OpsLogOpSchema = _enum([
|
|
|
7488
7458
|
"prune",
|
|
7489
7459
|
"manual-delete",
|
|
7490
7460
|
"rescan",
|
|
7491
|
-
"retention-run"
|
|
7461
|
+
"retention-run",
|
|
7462
|
+
"relocate"
|
|
7492
7463
|
]);
|
|
7493
7464
|
/** Why the operation ran. */
|
|
7494
7465
|
var OpsLogReasonSchema = _enum([
|
|
@@ -7527,6 +7498,55 @@ var OpsLogQueryInputSchema = object({
|
|
|
7527
7498
|
limit: number().int().min(1).max(1e3).optional()
|
|
7528
7499
|
});
|
|
7529
7500
|
/**
|
|
7501
|
+
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7502
|
+
*
|
|
7503
|
+
* One shape shared by the recorder's `relocateFootage` (segments + strips) and
|
|
7504
|
+
* pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
|
|
7505
|
+
* page renders both movers with one component. Jobs are in-RAM (a restart
|
|
7506
|
+
* forgets them — re-running is safe by construction: copy-if-absent, delete
|
|
7507
|
+
* after verify) and each completed/failed run also lands one durable ops-log
|
|
7508
|
+
* row on the owning addon's surface.
|
|
7509
|
+
*/
|
|
7510
|
+
var RelocateJobStateSchema = _enum([
|
|
7511
|
+
"running",
|
|
7512
|
+
"done",
|
|
7513
|
+
"failed",
|
|
7514
|
+
"cancelled"
|
|
7515
|
+
]);
|
|
7516
|
+
var RelocateJobSchema = object({
|
|
7517
|
+
jobId: string(),
|
|
7518
|
+
state: RelocateJobStateSchema,
|
|
7519
|
+
/** Source location — for media relocation this is informational ('*': rows
|
|
7520
|
+
* move from wherever they are to the target). */
|
|
7521
|
+
fromLocationId: string(),
|
|
7522
|
+
toLocationId: string(),
|
|
7523
|
+
/** Scoped device, or null = every device. */
|
|
7524
|
+
deviceId: number().nullable(),
|
|
7525
|
+
/** What the job moves (owner-addon specific: segments/strips or media). */
|
|
7526
|
+
entities: array(string()),
|
|
7527
|
+
filesMoved: number().int(),
|
|
7528
|
+
bytesMoved: number().int(),
|
|
7529
|
+
/** Total files discovered up front; null while (or when) unknown. */
|
|
7530
|
+
filesTotal: number().int().nullable(),
|
|
7531
|
+
startedAt: number(),
|
|
7532
|
+
finishedAt: number().nullable(),
|
|
7533
|
+
error: string().nullable()
|
|
7534
|
+
});
|
|
7535
|
+
var RelocateFootageInputSchema = object({
|
|
7536
|
+
deviceId: number().optional(),
|
|
7537
|
+
fromLocationId: string(),
|
|
7538
|
+
toLocationId: string(),
|
|
7539
|
+
entities: array(_enum(["segments", "strips"])).optional(),
|
|
7540
|
+
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7541
|
+
* never allowed to starve live writers. */
|
|
7542
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
7543
|
+
});
|
|
7544
|
+
var RelocateMediaInputSchema = object({
|
|
7545
|
+
deviceId: number().optional(),
|
|
7546
|
+
toLocationId: string(),
|
|
7547
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
7548
|
+
});
|
|
7549
|
+
/**
|
|
7530
7550
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
7531
7551
|
* storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
|
|
7532
7552
|
* so the persisted record schema and the consumer-facing cap can both consume it
|
|
@@ -7576,6 +7596,13 @@ var StorageLocationSchema = object({
|
|
|
7576
7596
|
nodeId: string().optional(),
|
|
7577
7597
|
isDefault: boolean().default(false),
|
|
7578
7598
|
isSystem: boolean().default(false),
|
|
7599
|
+
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
7600
|
+
* for node-local locations it can reach) — never persisted, absent when the
|
|
7601
|
+
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
7602
|
+
capacity: object({
|
|
7603
|
+
totalBytes: number(),
|
|
7604
|
+
availableBytes: number()
|
|
7605
|
+
}).nullable().optional(),
|
|
7579
7606
|
createdAt: number(),
|
|
7580
7607
|
updatedAt: number()
|
|
7581
7608
|
});
|
|
@@ -7637,16 +7664,23 @@ var StorageLocationDeclarationSchema = object({
|
|
|
7637
7664
|
* Which node root the seeded `<id>:default` instance is placed under on a
|
|
7638
7665
|
* FRESH install:
|
|
7639
7666
|
* - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
|
|
7640
|
-
* the appData volume. Right for small/durable data (
|
|
7667
|
+
* the appData volume. Right for small/durable data (logs, models).
|
|
7641
7668
|
* - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
|
|
7642
7669
|
* env is set, else falls back to the data root. Right for bulky, hot media
|
|
7643
7670
|
* (recordings, event media) that should stay off the appData disk.
|
|
7671
|
+
* - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
|
|
7672
|
+
* `/backups` in the image) so archives live on their own mount rather than
|
|
7673
|
+
* filling the appData disk. Falls back to the data root when unset.
|
|
7644
7674
|
*
|
|
7645
7675
|
* Only affects the seeded default's `basePath`; operators can repoint any
|
|
7646
7676
|
* location afterwards, and a `defaultsTo` slot inherits its parent's root
|
|
7647
7677
|
* regardless of this field. Absent (the common case) is treated as `'data'`.
|
|
7648
7678
|
*/
|
|
7649
|
-
defaultRoot: _enum([
|
|
7679
|
+
defaultRoot: _enum([
|
|
7680
|
+
"data",
|
|
7681
|
+
"media",
|
|
7682
|
+
"backup"
|
|
7683
|
+
]).optional()
|
|
7650
7684
|
});
|
|
7651
7685
|
var DecoderStatsSchema = object({
|
|
7652
7686
|
inputFps: number(),
|
|
@@ -8336,7 +8370,8 @@ var NcTaxonomyEntrySchema = object({
|
|
|
8336
8370
|
/** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
|
|
8337
8371
|
parentKind: string().nullable()
|
|
8338
8372
|
});
|
|
8339
|
-
|
|
8373
|
+
/** The complete NC picker taxonomy — three grouped buckets. */
|
|
8374
|
+
var NcTaxonomySchema = object({
|
|
8340
8375
|
videoClasses: array(NcTaxonomyEntrySchema),
|
|
8341
8376
|
audioKinds: array(NcTaxonomyEntrySchema),
|
|
8342
8377
|
labels: array(NcTaxonomyEntrySchema)
|
|
@@ -9302,9 +9337,20 @@ var DEGRADE_ORDER = {
|
|
|
9302
9337
|
"markdown"
|
|
9303
9338
|
]
|
|
9304
9339
|
};
|
|
9305
|
-
/**
|
|
9340
|
+
/**
|
|
9341
|
+
* Pick the best target format the kind supports for a given source format.
|
|
9342
|
+
*
|
|
9343
|
+
* TOLERANT of a source outside the union. `NotificationSchema.format` carries
|
|
9344
|
+
* `.default('text')`, but that default only runs where the schema is PARSED —
|
|
9345
|
+
* the tRPC router. A cap call arriving from another addon (`ctx.api`) skips
|
|
9346
|
+
* that parse, so `format` reaches here `undefined`, and the bare
|
|
9347
|
+
* `DEGRADE_ORDER[source]` threw `is not iterable`. That broke the one promise
|
|
9348
|
+
* the degrade path makes ("never throws — degradation is reported, never
|
|
9349
|
+
* surfaced as an error") and dead-lettered every Notification-Center delivery
|
|
9350
|
+
* for a day (2026-07-30). An unknown source degrades like `text`.
|
|
9351
|
+
*/
|
|
9306
9352
|
function resolveFormat(source, supported) {
|
|
9307
|
-
for (const candidate of DEGRADE_ORDER[source]) if (supported.includes(candidate)) return candidate;
|
|
9353
|
+
for (const candidate of DEGRADE_ORDER[source] ?? DEGRADE_ORDER.text) if (supported.includes(candidate)) return candidate;
|
|
9308
9354
|
return supported[0] ?? "text";
|
|
9309
9355
|
}
|
|
9310
9356
|
/** Transcode a body between formats. */
|
|
@@ -9349,7 +9395,7 @@ function resolveLevel(levels, n) {
|
|
|
9349
9395
|
})[0]);
|
|
9350
9396
|
}
|
|
9351
9397
|
function splitBody(body, maxLen) {
|
|
9352
|
-
if (maxLen <= 0 || body.length <= maxLen) return [body];
|
|
9398
|
+
if (!Number.isFinite(maxLen) || maxLen <= 0 || body.length <= maxLen) return [body];
|
|
9353
9399
|
const parts = [];
|
|
9354
9400
|
for (let i = 0; i < body.length; i += maxLen) parts.push(body.slice(i, i + maxLen));
|
|
9355
9401
|
return parts;
|
|
@@ -9432,147 +9478,849 @@ function prepareNotification(caps, n) {
|
|
|
9432
9478
|
renderedAs
|
|
9433
9479
|
};
|
|
9434
9480
|
}
|
|
9481
|
+
new Set(["devices", "classes"]);
|
|
9435
9482
|
/**
|
|
9436
|
-
*
|
|
9437
|
-
*
|
|
9438
|
-
*
|
|
9439
|
-
*
|
|
9440
|
-
* caps (`battery`, `doorbell`, …) carry their domain-specific state on
|
|
9441
|
-
* their own slices.
|
|
9483
|
+
* Shared geometry vocabulary for on-frame shape caps — privacy-mask,
|
|
9484
|
+
* motion-zones, and the detection zones/lines editor all speak this one
|
|
9485
|
+
* language so a single drawing-plane editor and the providers stay
|
|
9486
|
+
* decoupled from each cap's storage.
|
|
9442
9487
|
*
|
|
9443
|
-
*
|
|
9444
|
-
*
|
|
9445
|
-
* `
|
|
9446
|
-
* `runtimeState.setCapState('device-status', …)`. Cross-process
|
|
9447
|
-
* consumers reach the same data via the `device-state` cap router
|
|
9448
|
-
* (`getCapSlice({deviceId, capName: 'device-status'})`).
|
|
9488
|
+
* All coordinates are normalized 0..1 of the camera frame (top-left
|
|
9489
|
+
* origin). Each cap composes the SUBSET of shape kinds it supports and
|
|
9490
|
+
* advertises it via `supportedShapes` in its `getOptions`.
|
|
9449
9491
|
*/
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9492
|
+
/** A normalized 0..1 point (top-left origin). */
|
|
9493
|
+
var MaskPointSchema = object({
|
|
9494
|
+
x: number(),
|
|
9495
|
+
y: number()
|
|
9496
|
+
});
|
|
9497
|
+
/** Axis-aligned rectangle (normalized 0..1). */
|
|
9498
|
+
var MaskRectShapeSchema = object({
|
|
9499
|
+
kind: literal("rect"),
|
|
9500
|
+
x: number(),
|
|
9501
|
+
y: number(),
|
|
9502
|
+
width: number(),
|
|
9503
|
+
height: number()
|
|
9504
|
+
});
|
|
9505
|
+
/** Free polygon — an ordered list of normalized vertices (≥3). */
|
|
9506
|
+
var MaskPolygonShapeSchema = object({
|
|
9507
|
+
kind: literal("polygon"),
|
|
9508
|
+
points: array(MaskPointSchema)
|
|
9509
|
+
});
|
|
9510
|
+
/** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
|
|
9511
|
+
var MaskGridShapeSchema = object({
|
|
9512
|
+
kind: literal("grid"),
|
|
9513
|
+
gridWidth: number(),
|
|
9514
|
+
gridHeight: number(),
|
|
9515
|
+
cells: array(boolean())
|
|
9516
|
+
});
|
|
9517
|
+
discriminatedUnion("kind", [
|
|
9518
|
+
MaskRectShapeSchema,
|
|
9519
|
+
MaskPolygonShapeSchema,
|
|
9520
|
+
MaskGridShapeSchema,
|
|
9521
|
+
object({
|
|
9522
|
+
kind: literal("line"),
|
|
9523
|
+
points: array(MaskPointSchema)
|
|
9524
|
+
})
|
|
9525
|
+
]);
|
|
9526
|
+
/** Every shape-kind discriminant, for `supportedShapes` advertisement. */
|
|
9527
|
+
var MaskShapeKindSchema = _enum([
|
|
9528
|
+
"rect",
|
|
9529
|
+
"polygon",
|
|
9530
|
+
"grid",
|
|
9531
|
+
"line"
|
|
9532
|
+
]);
|
|
9533
|
+
/** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
|
|
9534
|
+
var MaskPolygonVerticesSchema = object({
|
|
9535
|
+
min: number(),
|
|
9536
|
+
max: number()
|
|
9537
|
+
});
|
|
9538
|
+
/** Grid dimensions when a cap supports 'grid'. */
|
|
9539
|
+
var MaskGridDimsSchema = object({
|
|
9540
|
+
width: number(),
|
|
9541
|
+
height: number()
|
|
9462
9542
|
});
|
|
9463
|
-
var deviceStatusCapability = {
|
|
9464
|
-
name: "device-status",
|
|
9465
|
-
scope: "device",
|
|
9466
|
-
deviceNative: true,
|
|
9467
|
-
mode: "singleton",
|
|
9468
|
-
methods: {},
|
|
9469
|
-
events: {
|
|
9470
|
-
/** Emitted when `online` transitions. Mirrors the semantics of
|
|
9471
|
-
* `battery.onStatusChanged`. */
|
|
9472
|
-
onStatusChanged: { data: object({
|
|
9473
|
-
deviceId: number(),
|
|
9474
|
-
status: DeviceStatusSchema
|
|
9475
|
-
}) } },
|
|
9476
|
-
status: {
|
|
9477
|
-
schema: DeviceStatusSchema,
|
|
9478
|
-
kind: "push"
|
|
9479
|
-
},
|
|
9480
|
-
runtimeState: DeviceStatusSchema
|
|
9481
|
-
};
|
|
9482
9543
|
/**
|
|
9483
|
-
*
|
|
9484
|
-
* truth about what a device CAN do — which the kernel uses to:
|
|
9485
|
-
* 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
|
|
9486
|
-
* based on what the firmware actually advertises).
|
|
9487
|
-
* 2. Compute the public `features: DeviceFeature[]` array surfaced via
|
|
9488
|
-
* `device-manager.listAll`.
|
|
9489
|
-
* 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
|
|
9490
|
-
* to register on the device's capability surface.
|
|
9544
|
+
* notification-rules — the Notification Center rule surface (P1 core).
|
|
9491
9545
|
*
|
|
9492
|
-
*
|
|
9493
|
-
*
|
|
9494
|
-
* accessory reconciliation). Consumers read via:
|
|
9495
|
-
* `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
|
|
9546
|
+
* Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
|
|
9547
|
+
* (operator decisions D-1/D-2/D-3 are binding):
|
|
9496
9548
|
*
|
|
9497
|
-
*
|
|
9498
|
-
*
|
|
9499
|
-
*
|
|
9549
|
+
* - D-2: rule EVALUATION lives in `addon-post-analysis` (the
|
|
9550
|
+
* `notification-center` module), hooked on the durable persistence
|
|
9551
|
+
* moments (object-event insert, TrackCloser.closeExpired) with a
|
|
9552
|
+
* persisted outbox + retry — never the lossy telemetry bus (D8).
|
|
9553
|
+
* - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
|
|
9554
|
+
* FIRST persisted detection matching the conditions (per-track dedup,
|
|
9555
|
+
* `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
|
|
9556
|
+
* `delivery: 'track-end'` evaluates the finalized track record at close.
|
|
9557
|
+
* - DISPATCH stays behind `notification-output` (rules reference targets
|
|
9558
|
+
* by id; per-backend params are a passthrough blob capped by the
|
|
9559
|
+
* target kind's own caps/degrade engine).
|
|
9500
9560
|
*
|
|
9501
|
-
*
|
|
9502
|
-
*
|
|
9503
|
-
*
|
|
9504
|
-
*
|
|
9561
|
+
* P1 scope: admin-authored rules only (`createdBy` stamped from the
|
|
9562
|
+
* server-injected caller identity — the first `caller: 'required'`
|
|
9563
|
+
* adopter). The P1 condition subset is: devices, classes(+exclude),
|
|
9564
|
+
* minConfidence, admin zones (any/all + exclude), weekly schedule
|
|
9565
|
+
* windows, and the optional label/identity/plate matchers. User rules,
|
|
9566
|
+
* private zones, per-recipient fan-out and the wider condition table are
|
|
9567
|
+
* P2+ (see spec §7).
|
|
9568
|
+
*
|
|
9569
|
+
* All schemas here are the single source of truth — `NcRule` etc. are
|
|
9570
|
+
* `z.infer` exports; no duplicate interfaces (the advanced-notifier
|
|
9571
|
+
* schema/interface drift is explicitly not repeated).
|
|
9505
9572
|
*/
|
|
9506
|
-
var FeatureProbeStatusSchema = object({
|
|
9507
|
-
/**
|
|
9508
|
-
* Driver-specific flag bag. Each driver picks its own key names — the
|
|
9509
|
-
* cap deliberately does NOT enforce a closed enum here. Reolink keys:
|
|
9510
|
-
* `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
|
|
9511
|
-
* `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
|
|
9512
|
-
* `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
|
|
9513
|
-
*/
|
|
9514
|
-
flags: record(string(), unknown()),
|
|
9515
|
-
/**
|
|
9516
|
-
* Coarse driver-classification — lets cross-process consumers tell apart
|
|
9517
|
-
* cameras / battery-cams / NVRs without re-running the probe. `null`
|
|
9518
|
-
* before the first probe completes.
|
|
9519
|
-
*/
|
|
9520
|
-
deviceType: string().nullable(),
|
|
9521
|
-
/** Camera/firmware model string. `null` when the firmware doesn't expose it. */
|
|
9522
|
-
model: string().nullable(),
|
|
9523
|
-
/** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
|
|
9524
|
-
channelCount: number().nullable(),
|
|
9525
|
-
/**
|
|
9526
|
-
* Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
|
|
9527
|
-
* completes — drivers' `getAccessoryChildren()` should treat zero as
|
|
9528
|
-
* "probe not done yet, return empty" so accessories aren't spawned
|
|
9529
|
-
* before the firmware is queried.
|
|
9530
|
-
*/
|
|
9531
|
-
lastProbedAt: number(),
|
|
9532
|
-
/**
|
|
9533
|
-
* Framework convention: every runtime-state slice carries this for the
|
|
9534
|
-
* createRuntimeStateBridge stale-check helper. We keep it in sync with
|
|
9535
|
-
* `lastProbedAt` on every write.
|
|
9536
|
-
*/
|
|
9537
|
-
lastFetchedAt: number()
|
|
9538
|
-
});
|
|
9539
|
-
var featureProbeCapability = {
|
|
9540
|
-
name: "feature-probe",
|
|
9541
|
-
scope: "device",
|
|
9542
|
-
deviceNative: true,
|
|
9543
|
-
mode: "singleton",
|
|
9544
|
-
methods: {},
|
|
9545
|
-
events: {
|
|
9546
|
-
/** Fires whenever a fresh probe completes (kernel-driven `reprobe()`
|
|
9547
|
-
* or driver-initiated re-detect after a state change). */
|
|
9548
|
-
onProbeChanged: { data: object({
|
|
9549
|
-
deviceId: number(),
|
|
9550
|
-
status: FeatureProbeStatusSchema
|
|
9551
|
-
}) } },
|
|
9552
|
-
status: {
|
|
9553
|
-
schema: FeatureProbeStatusSchema,
|
|
9554
|
-
kind: "push"
|
|
9555
|
-
},
|
|
9556
|
-
runtimeState: FeatureProbeStatusSchema
|
|
9557
|
-
};
|
|
9558
9573
|
/**
|
|
9559
|
-
*
|
|
9560
|
-
*
|
|
9561
|
-
*
|
|
9562
|
-
*
|
|
9563
|
-
* `
|
|
9564
|
-
*
|
|
9565
|
-
*
|
|
9574
|
+
* D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
|
|
9575
|
+
* The value maps 1:1 onto the evaluated record kind:
|
|
9576
|
+
* - `immediate` ↔ object-event persist (lowest-latency detection burst)
|
|
9577
|
+
* - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
|
|
9578
|
+
* - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
|
|
9579
|
+
* change of a LINKED device, one row per linked camera)
|
|
9580
|
+
* - `package-event` ↔ PackageDropDetector object-event insert (a `package`
|
|
9581
|
+
* delivery / pick-up)
|
|
9582
|
+
*
|
|
9583
|
+
* `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
|
|
9584
|
+
* `package-event` are pure trigger kinds (no urgency dimension). Extending
|
|
9585
|
+
* this one field keeps the schema additive — a rule still declares exactly
|
|
9586
|
+
* one trigger.
|
|
9566
9587
|
*/
|
|
9567
|
-
var
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9588
|
+
var NcDeliverySchema = _enum([
|
|
9589
|
+
"immediate",
|
|
9590
|
+
"track-end",
|
|
9591
|
+
"device-event",
|
|
9592
|
+
"package-event"
|
|
9593
|
+
]);
|
|
9594
|
+
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
9595
|
+
var NcScheduleSchema = object({
|
|
9596
|
+
windows: array(object({
|
|
9597
|
+
/** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
|
|
9598
|
+
days: array(number().int().min(0).max(6)).min(1),
|
|
9599
|
+
startMinute: number().int().min(0).max(1439),
|
|
9600
|
+
endMinute: number().int().min(0).max(1439)
|
|
9601
|
+
})).min(1),
|
|
9602
|
+
/** IANA timezone; default = hub host timezone. */
|
|
9603
|
+
timezone: string().optional(),
|
|
9604
|
+
/** Active OUTSIDE the windows (e.g. "only outside business hours"). */
|
|
9605
|
+
invert: boolean().optional()
|
|
9606
|
+
});
|
|
9607
|
+
/** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
|
|
9608
|
+
var NcPlateMatcherSchema = object({
|
|
9609
|
+
values: array(string().min(1)).min(1),
|
|
9610
|
+
/** Max Levenshtein distance after normalization (uppercase alphanumeric). */
|
|
9611
|
+
maxDistance: number().int().min(0).max(3).default(1)
|
|
9612
|
+
});
|
|
9613
|
+
/**
|
|
9614
|
+
* Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
|
|
9615
|
+
* occupancy edge for a device — optionally narrowed to a single admin
|
|
9616
|
+
* `zoneId` and/or object `className`. `op` selects the edge/threshold:
|
|
9617
|
+
* - `became-occupied` (default) — count crossed 0 → ≥ `count`
|
|
9618
|
+
* - `became-free` — count crossed ≥ `count` → below it
|
|
9619
|
+
* - `>=` / `<=` — count is at/over or at/under `count`
|
|
9620
|
+
* `sustainSeconds` requires the condition hold continuously that long
|
|
9621
|
+
* before firing (debounces flicker; 0 = fire on the first matching edge).
|
|
9622
|
+
* Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
|
|
9623
|
+
* the condition never matches. Confirmed edge-state survives addon restarts
|
|
9624
|
+
* (declared SQLite collection, reseeded on boot).
|
|
9625
|
+
*/
|
|
9626
|
+
var NcOccupancyConditionSchema = object({
|
|
9627
|
+
/** Admin zone id to scope the count to; absent = whole-frame occupancy. */
|
|
9628
|
+
zoneId: string().optional(),
|
|
9629
|
+
/** Object class to count; absent = any class. */
|
|
9630
|
+
className: string().optional(),
|
|
9631
|
+
op: _enum([
|
|
9632
|
+
"became-occupied",
|
|
9633
|
+
"became-free",
|
|
9634
|
+
">=",
|
|
9635
|
+
"<="
|
|
9636
|
+
]).default("became-occupied"),
|
|
9637
|
+
count: number().int().min(0).default(1),
|
|
9638
|
+
sustainSeconds: number().int().min(0).max(3600).default(15)
|
|
9639
|
+
});
|
|
9640
|
+
/** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
|
|
9641
|
+
var NcZoneConditionSchema = object({
|
|
9642
|
+
ids: array(string().min(1)).min(1),
|
|
9643
|
+
/** Quantifier over `ids` — at least one / every one visited. */
|
|
9644
|
+
match: _enum(["any", "all"]).default("any")
|
|
9645
|
+
});
|
|
9646
|
+
/**
|
|
9647
|
+
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9648
|
+
* membership lists are OR within the list (spec §2.3).
|
|
9649
|
+
*/
|
|
9650
|
+
var NcConditionsSchema = object({
|
|
9651
|
+
/** Device scope — absent = all devices. */
|
|
9652
|
+
devices: array(number()).optional(),
|
|
9653
|
+
/** Detector class names (any overlap with the record's class set). */
|
|
9654
|
+
classes: array(string().min(1)).optional(),
|
|
9655
|
+
/** Veto classes — any overlap fails the rule. */
|
|
9656
|
+
classesExclude: array(string().min(1)).optional(),
|
|
9657
|
+
/** Minimum detection confidence 0–1 (fails when the record has none). */
|
|
9658
|
+
minConfidence: number().min(0).max(1).optional(),
|
|
9659
|
+
/** Admin zone membership over event `zones` / track `zonesVisited`. */
|
|
9660
|
+
zones: NcZoneConditionSchema.optional(),
|
|
9661
|
+
/** Veto zones — any hit fails the rule. */
|
|
9662
|
+
zonesExclude: array(string().min(1)).optional(),
|
|
9663
|
+
/**
|
|
9664
|
+
* Exact (case-insensitive) match on the record's collapsed `label`
|
|
9665
|
+
* (identity name / plate text / subclass).
|
|
9666
|
+
*/
|
|
9667
|
+
labelEquals: array(string().min(1)).optional(),
|
|
9668
|
+
/**
|
|
9669
|
+
* Identity matcher. P1 boundary: matched against the record's collapsed
|
|
9670
|
+
* `label` (the identity display name propagated by the face pipeline) —
|
|
9671
|
+
* identity-ID matching rides in P2 when identity ids reach the record.
|
|
9672
|
+
*/
|
|
9673
|
+
identities: array(string().min(1)).optional(),
|
|
9674
|
+
/** Fuzzy plate matcher against the record's `label` (plate text). */
|
|
9675
|
+
plates: NcPlateMatcherSchema.optional(),
|
|
9676
|
+
/**
|
|
9677
|
+
* Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
|
|
9678
|
+
* Same P1 boundary: matched against the record's collapsed `label` (the
|
|
9679
|
+
* identity display name). A record with NO label passes (nothing to
|
|
9680
|
+
* exclude), unlike the include variant which fails on an absent label.
|
|
9681
|
+
*/
|
|
9682
|
+
identitiesExclude: array(string().min(1)).optional(),
|
|
9683
|
+
/**
|
|
9684
|
+
* Minimum server-computed key-event importance in [0,1] (`Track.importance`).
|
|
9685
|
+
* TRACK-END only: importance is scored at track close, so it does not exist
|
|
9686
|
+
* at immediate / object-event evaluation time (see catalog `appliesTo`). At
|
|
9687
|
+
* close the value is threaded via the close-time info (the `Track` clone is
|
|
9688
|
+
* captured before the DB row is updated, so it would otherwise read stale).
|
|
9689
|
+
* Fails when the record carries no importance (never guess quality — the
|
|
9690
|
+
* `minConfidence` precedent). MVP cut: a single scalar threshold.
|
|
9691
|
+
*/
|
|
9692
|
+
minImportance: number().min(0).max(1).optional(),
|
|
9693
|
+
/**
|
|
9694
|
+
* Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
|
|
9695
|
+
* TRACK-END only: an `immediate` / object-event subject has no closed
|
|
9696
|
+
* lifespan, so a dwell condition never matches immediate delivery
|
|
9697
|
+
* (documented choice — the object-event record carries no `firstSeen`,
|
|
9698
|
+
* so dwell cannot be computed from what the subject actually carries).
|
|
9699
|
+
*/
|
|
9700
|
+
minDwellSeconds: number().min(0).optional(),
|
|
9701
|
+
/**
|
|
9702
|
+
* Detection provenance filter. `any` (default / absent) matches every
|
|
9703
|
+
* source; otherwise the subject's source must equal it. Legacy records
|
|
9704
|
+
* with no stamped source are treated as `pipeline`. The union spans both
|
|
9705
|
+
* record kinds — object events carry `pipeline` | `onboard`, synthetic
|
|
9706
|
+
* tracks carry `sensor`.
|
|
9707
|
+
*/
|
|
9708
|
+
source: _enum([
|
|
9709
|
+
"pipeline",
|
|
9710
|
+
"onboard",
|
|
9711
|
+
"sensor",
|
|
9712
|
+
"any"
|
|
9713
|
+
]).optional(),
|
|
9714
|
+
/**
|
|
9715
|
+
* Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
|
|
9716
|
+
* detector `minConfidence` (that gates the object-detection score; this
|
|
9717
|
+
* gates the recognition/OCR match score). Fails when the subject carries
|
|
9718
|
+
* no label-match confidence (never guess). TRACK-END only: the confidence
|
|
9719
|
+
* lives on the recognition result and reaches the subject at track close.
|
|
9720
|
+
*
|
|
9721
|
+
* What it measures precisely (plumbed at track close — the closer threads
|
|
9722
|
+
* the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
|
|
9723
|
+
* `importance`): the BEST recognition match confidence observed for the
|
|
9724
|
+
* label the track carries at close — for a face, the peak cosine similarity
|
|
9725
|
+
* of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
|
|
9726
|
+
* for a plate, the peak OCR read score of the best-held plate
|
|
9727
|
+
* (`plateText.confidence`). When BOTH a face and a plate were recognized on
|
|
9728
|
+
* one track the higher of the two is used. A track that ended with no
|
|
9729
|
+
* confident identity/plate match carries no value, so the condition fails
|
|
9730
|
+
* closed for it (an un-recognized subject).
|
|
9731
|
+
*/
|
|
9732
|
+
minLabelConfidence: number().min(0).max(1).optional(),
|
|
9733
|
+
/**
|
|
9734
|
+
* DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
|
|
9735
|
+
* e.g. a doorbell `press` / `press_long`) — matched case-insensitively
|
|
9736
|
+
* against the token carried on the device-event subject (extracted from the
|
|
9737
|
+
* event-emitter runtime slice's `lastEvent.eventType`). Fails when the
|
|
9738
|
+
* subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
|
|
9739
|
+
* eventType, so gate those with {@link sensorKinds} instead.
|
|
9740
|
+
*/
|
|
9741
|
+
eventTypeTokens: array(string().min(1)).optional(),
|
|
9742
|
+
/**
|
|
9743
|
+
* DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
|
|
9744
|
+
* `contact`, `button`, `device-event`) — matched against the persisted
|
|
9745
|
+
* `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
|
|
9746
|
+
*/
|
|
9747
|
+
sensorKinds: array(string().min(1)).optional(),
|
|
9748
|
+
/**
|
|
9749
|
+
* PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
|
|
9750
|
+
* (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
|
|
9751
|
+
* when the subject's phase does not match (a subject always carries a phase
|
|
9752
|
+
* on the package-event trigger).
|
|
9753
|
+
*/
|
|
9754
|
+
packagePhase: _enum([
|
|
9755
|
+
"delivered",
|
|
9756
|
+
"picked-up",
|
|
9757
|
+
"both"
|
|
9758
|
+
]).optional(),
|
|
9759
|
+
/**
|
|
9760
|
+
* PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
|
|
9761
|
+
* (MaskShape vocabulary). A record passes when its bbox overlaps ANY
|
|
9762
|
+
* listed polygon (ZoneEngine membership semantics). Evaluated only when
|
|
9763
|
+
* the subject carries a bbox; absent bbox ⇒ the condition FAILS.
|
|
9764
|
+
*/
|
|
9765
|
+
customZones: array(MaskPolygonShapeSchema).optional(),
|
|
9766
|
+
/**
|
|
9767
|
+
* DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
|
|
9768
|
+
* (optionally zone/class-scoped) occupancy count crosses the configured
|
|
9769
|
+
* threshold and holds for `sustainSeconds`. Fail-closed on missing
|
|
9770
|
+
* substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
|
|
9771
|
+
*/
|
|
9772
|
+
occupancy: NcOccupancyConditionSchema.optional()
|
|
9773
|
+
});
|
|
9774
|
+
/** One delivery target: a `notification-output` Target ref + passthrough params. */
|
|
9775
|
+
var NcRuleTargetSchema = object({
|
|
9776
|
+
/** `notification-output` Target id. */
|
|
9777
|
+
targetId: string().min(1),
|
|
9778
|
+
/**
|
|
9779
|
+
* Per-backend passthrough. Recognized keys are mapped onto the canonical
|
|
9780
|
+
* Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
|
|
9781
|
+
* degrade engine drops what the backend can't render.
|
|
9782
|
+
*/
|
|
9783
|
+
params: record(string(), unknown()).optional()
|
|
9784
|
+
});
|
|
9785
|
+
/**
|
|
9786
|
+
* Media attachment policy (P1 still-image subset).
|
|
9787
|
+
* - `best` — the best AVAILABLE subject image at dispatch time (D-3).
|
|
9788
|
+
* - `best-matching` — the media that explains WHY the rule fired: a rule
|
|
9789
|
+
* matched on identities attaches the subject's `faceCrop`, one matched on
|
|
9790
|
+
* plates attaches the `plateCrop`; a rule with no identity/plate condition
|
|
9791
|
+
* (or when the specific crop is missing) degrades to `best`, then
|
|
9792
|
+
* `keyFrame`, then no attachment — never delaying the send. The matched
|
|
9793
|
+
* condition summary is frozen on the outbox row at enqueue (like the rule
|
|
9794
|
+
* name), so the choice never drifts from the record that fired it.
|
|
9795
|
+
* - `keyFrame` — the clean scene frame (no subject box).
|
|
9796
|
+
* - `none` — no attachment.
|
|
9797
|
+
*/
|
|
9798
|
+
/**
|
|
9799
|
+
* WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
|
|
9800
|
+
* conflate it with the selection strategy and betray the request: asking for
|
|
9801
|
+
* the clean scene frame on an object-event owner used to start at
|
|
9802
|
+
* `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
|
|
9803
|
+
* - `cropped` — the subject, tight. What "who is at the door" wants.
|
|
9804
|
+
* - `full` — the clean scene, no annotation. What "what is going on" wants.
|
|
9805
|
+
* - `boxed` — the scene WITH the detection boxes drawn, for verifying what
|
|
9806
|
+
* the pipeline actually saw.
|
|
9807
|
+
*/
|
|
9808
|
+
var NcMediaFrameSchema = _enum([
|
|
9809
|
+
"cropped",
|
|
9810
|
+
"full",
|
|
9811
|
+
"boxed"
|
|
9812
|
+
]);
|
|
9813
|
+
var NcMediaPolicySchema = object({
|
|
9814
|
+
attach: _enum([
|
|
9815
|
+
"best",
|
|
9816
|
+
"best-matching",
|
|
9817
|
+
"keyFrame",
|
|
9818
|
+
"none"
|
|
9819
|
+
]).default("best"),
|
|
9820
|
+
/** What the still shows. Absent = `cropped` (the historical `best` shape). */
|
|
9821
|
+
frame: NcMediaFrameSchema.optional(),
|
|
9822
|
+
/** Crop the attached still to the rule's condition-zone bbox (padded) —
|
|
9823
|
+
* "show me the ZONE", not the whole scene or the subject crop. */
|
|
9824
|
+
zoneCrop: boolean().optional(),
|
|
9825
|
+
/**
|
|
9826
|
+
* Also attach a short GIF cut from the stream broker's clip ring around the
|
|
9827
|
+
* event — NOT from the recording, so the camera does not have to be
|
|
9828
|
+
* recording, and the window sits AROUND the moment instead of a segment
|
|
9829
|
+
* behind it. Fail-closed: a window the ring does not cover contributes no
|
|
9830
|
+
* gif, never a failed notification.
|
|
9831
|
+
*/
|
|
9832
|
+
gif: boolean().optional(),
|
|
9833
|
+
/**
|
|
9834
|
+
* Also attach a short MP4 CLIP of the same cut. Same source and the same
|
|
9835
|
+
* fail-closed rule as `gif`. Prefer it where the backend takes video
|
|
9836
|
+
* (telegram, discord, zentik, webhook); backends that do not are handled by
|
|
9837
|
+
* the degrade engine, which drops the video and keeps the still.
|
|
9838
|
+
*/
|
|
9839
|
+
clip: boolean().optional(),
|
|
9840
|
+
/** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
|
|
9841
|
+
clipPreRollSec: number().int().min(0).max(30).optional(),
|
|
9842
|
+
clipPostRollSec: number().int().min(0).max(30).optional(),
|
|
9843
|
+
/**
|
|
9844
|
+
* Which stream profile the footage is cut from. Absent = the CHEAPEST
|
|
9845
|
+
* assigned profile: a notification is watched on a phone, so the 4K
|
|
9846
|
+
* rendition would burn CPU to produce a file the client downscales anyway.
|
|
9847
|
+
* A profile that is not assigned falls back to the cheapest, and the render
|
|
9848
|
+
* reports which one actually ran.
|
|
9849
|
+
*/
|
|
9850
|
+
profile: CamProfileSchema.optional()
|
|
9851
|
+
});
|
|
9852
|
+
/** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
|
|
9853
|
+
var NcThrottleSchema = object({
|
|
9854
|
+
cooldownSec: number().int().min(0).max(86400).default(60),
|
|
9855
|
+
/** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
|
|
9856
|
+
scope: _enum(["rule", "rule-device"]).default("rule-device")
|
|
9857
|
+
});
|
|
9858
|
+
/** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
|
|
9859
|
+
var NcRuleInputSchema = object({
|
|
9860
|
+
name: string().min(1).max(200),
|
|
9861
|
+
enabled: boolean().default(true),
|
|
9862
|
+
delivery: NcDeliverySchema,
|
|
9863
|
+
conditions: NcConditionsSchema.default({}),
|
|
9864
|
+
schedule: NcScheduleSchema.optional(),
|
|
9865
|
+
/** May be empty when `targetUsers` addresses at least one user — the
|
|
9866
|
+
* "at least one addressee" invariant is enforced by the provider, because
|
|
9867
|
+
* a cross-field refine here would break `NcRulePatchSchema.partial()`. */
|
|
9868
|
+
targets: array(NcRuleTargetSchema),
|
|
9869
|
+
/**
|
|
9870
|
+
* USERS this rule addresses in addition to `targets` (Phase 4). At fire
|
|
9871
|
+
* time each user fans out to the personal targets they own
|
|
9872
|
+
* (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
|
|
9873
|
+
* firing camera — a user is never notified about a device they cannot open.
|
|
9874
|
+
* Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
|
|
9875
|
+
* targets.
|
|
9876
|
+
*/
|
|
9877
|
+
targetUsers: array(string()).optional(),
|
|
9878
|
+
media: NcMediaPolicySchema.default({ attach: "best" }),
|
|
9879
|
+
throttle: NcThrottleSchema.default({
|
|
9880
|
+
cooldownSec: 60,
|
|
9881
|
+
scope: "rule-device"
|
|
9882
|
+
}),
|
|
9883
|
+
/** `{{var}}` templating over camera/class/label/zones/confidence/time. */
|
|
9884
|
+
template: object({
|
|
9885
|
+
title: string().max(500).optional(),
|
|
9886
|
+
body: string().max(2e3).optional()
|
|
9887
|
+
}).optional(),
|
|
9888
|
+
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
9889
|
+
priority: number().int().min(1).max(5).default(3),
|
|
9890
|
+
/**
|
|
9891
|
+
* Ownership/visibility key. Absent = admin/global rule (unchanged legacy
|
|
9892
|
+
* behaviour, visible to all, read-only in the viewer). Present = personal
|
|
9893
|
+
* rule owned by this userId. Server-stamped; never trusted from a client.
|
|
9894
|
+
*/
|
|
9895
|
+
ownerUserId: string().optional()
|
|
9896
|
+
});
|
|
9897
|
+
/**
|
|
9898
|
+
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
9899
|
+
* persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
|
|
9900
|
+
* NOT a client-authored input field (it lives on the persisted rule, not the
|
|
9901
|
+
* input), so it is added here explicitly to let the store's per-target opt-out
|
|
9902
|
+
* toggle round-trip through the shared `update` path. Viewer opt-out mutations
|
|
9903
|
+
* still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
|
|
9904
|
+
* `updateRule` patch.
|
|
9905
|
+
*/
|
|
9906
|
+
var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
|
|
9907
|
+
/** A persisted rule. */
|
|
9908
|
+
var NcRuleSchema = NcRuleInputSchema.extend({
|
|
9909
|
+
id: string(),
|
|
9910
|
+
/** userId of the admin who created the rule (server-stamped caller). */
|
|
9911
|
+
createdBy: string(),
|
|
9912
|
+
createdAt: number(),
|
|
9913
|
+
updatedAt: number(),
|
|
9914
|
+
/**
|
|
9915
|
+
* Per-target opt-out set. A targetId here is suppressed for THIS rule at
|
|
9916
|
+
* send time. Only a target's OWNER may add/remove its id (server-checked
|
|
9917
|
+
* in `nc.setRuleTargetEnabled`). Defaults to empty.
|
|
9918
|
+
*/
|
|
9919
|
+
disabledTargetIds: array(string()).default([])
|
|
9920
|
+
});
|
|
9921
|
+
var NcTestResultSchema = object({
|
|
9922
|
+
recordId: string(),
|
|
9923
|
+
recordKind: _enum([
|
|
9924
|
+
"object-event",
|
|
9925
|
+
"track",
|
|
9926
|
+
"device-event",
|
|
9927
|
+
"package-event"
|
|
9928
|
+
]),
|
|
9929
|
+
deviceId: number(),
|
|
9930
|
+
timestamp: number(),
|
|
9931
|
+
wouldFire: boolean(),
|
|
9932
|
+
/** Condition id that failed (first failing group), when `wouldFire` is false. */
|
|
9933
|
+
failedCondition: string().optional(),
|
|
9934
|
+
className: string().optional(),
|
|
9935
|
+
label: string().optional()
|
|
9936
|
+
});
|
|
9937
|
+
var NcConditionDescriptorSchema = object({
|
|
9938
|
+
/** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
|
|
9939
|
+
id: string(),
|
|
9940
|
+
group: _enum([
|
|
9941
|
+
"scope",
|
|
9942
|
+
"class",
|
|
9943
|
+
"zones",
|
|
9944
|
+
"quality",
|
|
9945
|
+
"label",
|
|
9946
|
+
"schedule",
|
|
9947
|
+
"device",
|
|
9948
|
+
"package",
|
|
9949
|
+
"occupancy"
|
|
9950
|
+
]),
|
|
9951
|
+
label: string(),
|
|
9952
|
+
/** Editor widget the UI renders — never hardcode per-condition forms. */
|
|
9953
|
+
valueType: _enum([
|
|
9954
|
+
"deviceIdList",
|
|
9955
|
+
"stringList",
|
|
9956
|
+
"number01",
|
|
9957
|
+
"number",
|
|
9958
|
+
"sourceSelect",
|
|
9959
|
+
"zoneSelection",
|
|
9960
|
+
"zoneIdList",
|
|
9961
|
+
"schedule",
|
|
9962
|
+
"plateMatcher",
|
|
9963
|
+
"packagePhase",
|
|
9964
|
+
"polygonDraw",
|
|
9965
|
+
"occupancy"
|
|
9966
|
+
]),
|
|
9967
|
+
operator: _enum([
|
|
9968
|
+
"in",
|
|
9969
|
+
"notIn",
|
|
9970
|
+
"anyOf",
|
|
9971
|
+
"allOf",
|
|
9972
|
+
"gte",
|
|
9973
|
+
"fuzzyIn",
|
|
9974
|
+
"withinSchedule"
|
|
9975
|
+
]),
|
|
9976
|
+
/** Which delivery kinds the condition applies to. */
|
|
9977
|
+
appliesTo: array(NcDeliverySchema),
|
|
9978
|
+
phase: string(),
|
|
9979
|
+
description: string().optional()
|
|
9980
|
+
});
|
|
9981
|
+
/**
|
|
9982
|
+
* The delivery lifecycle status of a history row — a straight read of the
|
|
9983
|
+
* durable outbox row's own status (single source of truth):
|
|
9984
|
+
* - `pending` — enqueued, in-flight or retrying with backoff
|
|
9985
|
+
* - `sent` — delivered (terminal)
|
|
9986
|
+
* - `dead` — dead-lettered after exhausting retries / a permanent
|
|
9987
|
+
* backend rejection / a deleted target (terminal; carries
|
|
9988
|
+
* the failure `error`)
|
|
9989
|
+
*
|
|
9990
|
+
* P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
|
|
9991
|
+
* user dimension (quiet hours / snooze) and are additive when they land.
|
|
9992
|
+
*/
|
|
9993
|
+
var NcHistoryStatusSchema = _enum([
|
|
9994
|
+
"pending",
|
|
9995
|
+
"sent",
|
|
9996
|
+
"dead"
|
|
9997
|
+
]);
|
|
9998
|
+
/** The evaluated record kind a history row descends from (one per trigger). */
|
|
9999
|
+
var NcHistoryRecordKindSchema = _enum([
|
|
10000
|
+
"object-event",
|
|
10001
|
+
"track-end",
|
|
10002
|
+
"device-event",
|
|
10003
|
+
"package-event"
|
|
10004
|
+
]);
|
|
10005
|
+
/** Subject summary frozen on the row at fire time (survives rule/record edits). */
|
|
10006
|
+
var NcHistorySubjectSchema = object({
|
|
10007
|
+
className: string(),
|
|
10008
|
+
label: string().optional(),
|
|
10009
|
+
confidence: number().optional(),
|
|
10010
|
+
zones: array(string()),
|
|
10011
|
+
timestamp: number()
|
|
10012
|
+
});
|
|
10013
|
+
/**
|
|
10014
|
+
* One delivery-history row. This is a read-only VIEW over the durable
|
|
10015
|
+
* outbox row (single source of truth — the same row the drain loop drives;
|
|
10016
|
+
* NO second write path, so history can never drift from delivery state).
|
|
10017
|
+
* The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
|
|
10018
|
+
* `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
|
|
10019
|
+
* (fire) / `updatedAt` (last transition), `status` + `error` are the
|
|
10020
|
+
* lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
|
|
10021
|
+
* enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
|
|
10022
|
+
* P1 (admin scope only).
|
|
10023
|
+
*/
|
|
10024
|
+
var NcHistoryEntrySchema = object({
|
|
10025
|
+
/** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
|
|
10026
|
+
id: string(),
|
|
10027
|
+
ruleId: string(),
|
|
10028
|
+
/** Rule name frozen at fire time (outlives a later rename / delete). */
|
|
10029
|
+
ruleName: string(),
|
|
10030
|
+
/** The rule urgency/trigger that produced this delivery. */
|
|
10031
|
+
delivery: NcDeliverySchema,
|
|
10032
|
+
targetId: string(),
|
|
10033
|
+
deviceId: number(),
|
|
10034
|
+
recordKind: NcHistoryRecordKindSchema,
|
|
10035
|
+
/** Event / track ref of the evaluated record (§3.2 `eventRef`). */
|
|
10036
|
+
recordId: string(),
|
|
10037
|
+
/** Present for track-scoped deliveries (object-event / track-end). */
|
|
10038
|
+
trackId: string().optional(),
|
|
10039
|
+
status: NcHistoryStatusSchema,
|
|
10040
|
+
/** Delivery attempts made so far. */
|
|
10041
|
+
attempts: number().int(),
|
|
10042
|
+
/** Fire time (outbox enqueue). */
|
|
10043
|
+
createdAt: number(),
|
|
10044
|
+
/** Last transition time (terminal for sent / dead). */
|
|
10045
|
+
updatedAt: number(),
|
|
10046
|
+
/** Failure detail — present on a `dead` row. */
|
|
10047
|
+
error: string().optional(),
|
|
10048
|
+
subject: NcHistorySubjectSchema
|
|
10049
|
+
});
|
|
10050
|
+
/**
|
|
10051
|
+
* Query filter for `getHistory` (spec §4.2). Every field is a narrowing
|
|
10052
|
+
* AND; absent = unbounded on that axis. `since`/`until` bound the fire time
|
|
10053
|
+
* (`createdAt`, epoch ms, inclusive). `limit` is clamped to
|
|
10054
|
+
* {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
|
|
10055
|
+
*/
|
|
10056
|
+
var NcHistoryFilterSchema = object({
|
|
10057
|
+
ruleId: string().optional(),
|
|
10058
|
+
deviceId: number().optional(),
|
|
10059
|
+
status: NcHistoryStatusSchema.optional(),
|
|
10060
|
+
since: number().optional(),
|
|
10061
|
+
until: number().optional(),
|
|
10062
|
+
limit: number().int().min(1).max(500).default(100)
|
|
10063
|
+
});
|
|
10064
|
+
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 }), {
|
|
10065
|
+
kind: "mutation",
|
|
10066
|
+
auth: "admin",
|
|
10067
|
+
caller: "required"
|
|
10068
|
+
}), method(object({
|
|
10069
|
+
ruleId: string(),
|
|
10070
|
+
patch: NcRulePatchSchema
|
|
10071
|
+
}), object({ rule: NcRuleSchema }), {
|
|
10072
|
+
kind: "mutation",
|
|
10073
|
+
auth: "admin",
|
|
10074
|
+
caller: "required"
|
|
10075
|
+
}), method(object({ ruleId: string() }), object({ success: literal(true) }), {
|
|
10076
|
+
kind: "mutation",
|
|
10077
|
+
auth: "admin"
|
|
10078
|
+
}), method(object({
|
|
10079
|
+
ruleId: string(),
|
|
10080
|
+
enabled: boolean()
|
|
10081
|
+
}), object({ success: literal(true) }), {
|
|
10082
|
+
kind: "mutation",
|
|
10083
|
+
auth: "admin"
|
|
10084
|
+
}), method(object({
|
|
10085
|
+
rule: NcRuleInputSchema,
|
|
10086
|
+
lookbackMinutes: number().int().min(1).max(1440).default(60)
|
|
10087
|
+
}), object({ results: array(NcTestResultSchema) }), {
|
|
10088
|
+
kind: "mutation",
|
|
10089
|
+
auth: "admin"
|
|
10090
|
+
}), method(object({}), object({
|
|
10091
|
+
catalog: array(NcConditionDescriptorSchema),
|
|
10092
|
+
taxonomy: NcTaxonomySchema.optional()
|
|
10093
|
+
})), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
|
|
10094
|
+
/**
|
|
10095
|
+
* TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
|
|
10096
|
+
*
|
|
10097
|
+
* Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
|
|
10098
|
+
* §3.2/§3.3.
|
|
10099
|
+
*
|
|
10100
|
+
* Deliberately NOT a capability definition and NOT an `NcRule`:
|
|
10101
|
+
* - Every `NcDelivery` member is a *persisted-pipeline-record* trigger. A
|
|
10102
|
+
* timelapse fires on a SCHEDULE WINDOW BOUNDARY, evaluates no pipeline
|
|
10103
|
+
* record, and produces a video it assembled itself — so it rides no
|
|
10104
|
+
* delivery-enum member (the enum is frozen) and no cap method. This file is
|
|
10105
|
+
* a plain typed schema; it does NOT go through `npm run codegen`.
|
|
10106
|
+
* - It shares only the delivery leg (`notification-output.send`) and the
|
|
10107
|
+
* persistence/ownership patterns with the Notification Center, reusing
|
|
10108
|
+
* {@link NcScheduleSchema} (weekly windows, midnight-crossing, invertible)
|
|
10109
|
+
* and {@link NcRuleTargetSchema} (target ref + passthrough params).
|
|
10110
|
+
*
|
|
10111
|
+
* Ownership is SERVER-DERIVED. `ownerUserId` / `createdBy` / `createdAt` /
|
|
10112
|
+
* `updatedAt` / `id` / `lastGeneratedAt` live on the PERSISTED rule only —
|
|
10113
|
+
* {@link TimelapseRuleInputSchema} and {@link TimelapseRulePatchSchema} do not
|
|
10114
|
+
* carry them, so a forged client payload can never claim or re-own a rule
|
|
10115
|
+
* (Zod strips unknown keys). The store stamps them from the resolved caller.
|
|
10116
|
+
*/
|
|
10117
|
+
/** `{{var}}` templating over camera/rule/time — same vocabulary as `NcRule`. */
|
|
10118
|
+
var TimelapseTemplateSchema = object({
|
|
10119
|
+
title: string().max(500).optional(),
|
|
10120
|
+
body: string().max(2e3).optional()
|
|
10121
|
+
});
|
|
10122
|
+
var NameField = string().min(1).max(200);
|
|
10123
|
+
var DeviceIdsField = array(number()).min(1);
|
|
10124
|
+
var CadenceSecField = number().int().min(2).max(3600);
|
|
10125
|
+
var FramerateField = number().int().min(1).max(60);
|
|
10126
|
+
var TargetsField = array(NcRuleTargetSchema).min(1);
|
|
10127
|
+
var PriorityField = number().int().min(1).max(5);
|
|
10128
|
+
/**
|
|
10129
|
+
* Client-supplied timelapse-rule fields. The server stamps id / createdBy /
|
|
10130
|
+
* createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
|
|
10131
|
+
* here (see the ownership note above).
|
|
10132
|
+
*/
|
|
10133
|
+
var TimelapseRuleInputSchema = object({
|
|
10134
|
+
name: NameField,
|
|
10135
|
+
enabled: boolean().default(true),
|
|
10136
|
+
/** Cameras sampled by this rule — one scratch dir + one artifact per device. */
|
|
10137
|
+
deviceIds: DeviceIdsField,
|
|
10138
|
+
/**
|
|
10139
|
+
* Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
|
|
10140
|
+
* means "always active"): a timelapse is defined by its window boundaries —
|
|
10141
|
+
* open clears the scratch, close assembles and delivers.
|
|
10142
|
+
*/
|
|
10143
|
+
schedule: NcScheduleSchema,
|
|
10144
|
+
/** Force-snapshot cadence inside the window, seconds (predecessor parity). */
|
|
10145
|
+
cadenceSec: CadenceSecField.default(15),
|
|
10146
|
+
/** Output frames per second of the assembled mp4 (predecessor parity). */
|
|
10147
|
+
framerate: FramerateField.default(10),
|
|
10148
|
+
/** `notification-output` targets the finished video/thumbnail is sent to. */
|
|
10149
|
+
targets: TargetsField,
|
|
10150
|
+
template: TimelapseTemplateSchema.optional(),
|
|
10151
|
+
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
10152
|
+
priority: PriorityField.default(3)
|
|
10153
|
+
});
|
|
10154
|
+
object({
|
|
10155
|
+
name: NameField.optional(),
|
|
10156
|
+
enabled: boolean().optional(),
|
|
10157
|
+
deviceIds: DeviceIdsField.optional(),
|
|
10158
|
+
schedule: NcScheduleSchema.optional(),
|
|
10159
|
+
cadenceSec: CadenceSecField.optional(),
|
|
10160
|
+
framerate: FramerateField.optional(),
|
|
10161
|
+
targets: TargetsField.optional(),
|
|
10162
|
+
template: TimelapseTemplateSchema.nullable().optional(),
|
|
10163
|
+
priority: PriorityField.optional()
|
|
10164
|
+
});
|
|
10165
|
+
TimelapseRuleInputSchema.extend({
|
|
10166
|
+
id: string(),
|
|
10167
|
+
/**
|
|
10168
|
+
* Ownership/visibility key. Absent = admin/global rule (visible to all).
|
|
10169
|
+
* Present = personal rule owned by this userId. Server-stamped from the
|
|
10170
|
+
* resolved caller; never trusted from a client payload.
|
|
10171
|
+
*/
|
|
10172
|
+
ownerUserId: string().optional(),
|
|
10173
|
+
/**
|
|
10174
|
+
* Epoch-ms of the last successful generation — the 1-hour re-generation
|
|
10175
|
+
* guard's durable state (predecessor parity). Absent = never generated.
|
|
10176
|
+
*/
|
|
10177
|
+
lastGeneratedAt: number().optional(),
|
|
10178
|
+
/** userId of the caller who created the rule (server-stamped). */
|
|
10179
|
+
createdBy: string(),
|
|
10180
|
+
createdAt: number(),
|
|
10181
|
+
updatedAt: number()
|
|
10182
|
+
});
|
|
10183
|
+
/**
|
|
10184
|
+
* Generic device-level status snapshot. Auto-registered by `BaseDevice`
|
|
10185
|
+
* for every device, regardless of provider — the kernel needs a uniform
|
|
10186
|
+
* cap-keyed slice for the basic device flags every consumer expects to
|
|
10187
|
+
* read across processes (the `online` flag in particular). Driver-specific
|
|
10188
|
+
* caps (`battery`, `doorbell`, …) carry their domain-specific state on
|
|
10189
|
+
* their own slices.
|
|
10190
|
+
*
|
|
10191
|
+
* Pattern is identical to `battery`: schema-bearing `runtimeState`,
|
|
10192
|
+
* empty `methods`, single change event. Reads land at
|
|
10193
|
+
* `runtimeState.getCapState('device-status')`; writes at
|
|
10194
|
+
* `runtimeState.setCapState('device-status', …)`. Cross-process
|
|
10195
|
+
* consumers reach the same data via the `device-state` cap router
|
|
10196
|
+
* (`getCapSlice({deviceId, capName: 'device-status'})`).
|
|
10197
|
+
*/
|
|
10198
|
+
var DeviceStatusSchema = object({
|
|
10199
|
+
/**
|
|
10200
|
+
* Device-level liveness. Drivers flip via `markOnline(boolean)` on
|
|
10201
|
+
* `BaseDevice`. Provider semantics vary — RTSP aggregates broker
|
|
10202
|
+
* stream-health, Reolink reads firmware push events, ONVIF tracks
|
|
10203
|
+
* ping responses. This cap intentionally does NOT prescribe which
|
|
10204
|
+
* signal drives the flag.
|
|
10205
|
+
*/
|
|
10206
|
+
online: boolean(),
|
|
10207
|
+
/** Ms epoch of the last `online` transition. Lets consumers tell
|
|
10208
|
+
* apart "just came online" from "still online". */
|
|
10209
|
+
lastChangedAt: number()
|
|
10210
|
+
});
|
|
10211
|
+
var deviceStatusCapability = {
|
|
10212
|
+
name: "device-status",
|
|
10213
|
+
scope: "device",
|
|
10214
|
+
deviceNative: true,
|
|
10215
|
+
mode: "singleton",
|
|
10216
|
+
methods: {},
|
|
10217
|
+
events: {
|
|
10218
|
+
/** Emitted when `online` transitions. Mirrors the semantics of
|
|
10219
|
+
* `battery.onStatusChanged`. */
|
|
10220
|
+
onStatusChanged: { data: object({
|
|
10221
|
+
deviceId: number(),
|
|
10222
|
+
status: DeviceStatusSchema
|
|
10223
|
+
}) } },
|
|
10224
|
+
status: {
|
|
10225
|
+
schema: DeviceStatusSchema,
|
|
10226
|
+
kind: "push"
|
|
10227
|
+
},
|
|
10228
|
+
runtimeState: DeviceStatusSchema
|
|
10229
|
+
};
|
|
10230
|
+
/**
|
|
10231
|
+
* Per-device feature/identity probe slice. Holds the runtime-resolved
|
|
10232
|
+
* truth about what a device CAN do — which the kernel uses to:
|
|
10233
|
+
* 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
|
|
10234
|
+
* based on what the firmware actually advertises).
|
|
10235
|
+
* 2. Compute the public `features: DeviceFeature[]` array surfaced via
|
|
10236
|
+
* `device-manager.listAll`.
|
|
10237
|
+
* 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
|
|
10238
|
+
* to register on the device's capability surface.
|
|
10239
|
+
*
|
|
10240
|
+
* Auto-registered by `BaseDevice` for every device. Drivers populate the
|
|
10241
|
+
* slice from `onProbe()` (kernel calls it once after register, before
|
|
10242
|
+
* accessory reconciliation). Consumers read via:
|
|
10243
|
+
* `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
|
|
10244
|
+
*
|
|
10245
|
+
* `flags` is an open record so each driver carries its own keys without
|
|
10246
|
+
* a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
|
|
10247
|
+
* Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
|
|
10248
|
+
*
|
|
10249
|
+
* Replaces the older driver-local `deviceCache.has*` blob: the per-device
|
|
10250
|
+
* config is for operator-edited overrides + UI snapshots; runtime probe
|
|
10251
|
+
* results belong in runtime-state where the kernel handles persistence,
|
|
10252
|
+
* cross-process mirroring, and reactive updates.
|
|
10253
|
+
*/
|
|
10254
|
+
var FeatureProbeStatusSchema = object({
|
|
10255
|
+
/**
|
|
10256
|
+
* Driver-specific flag bag. Each driver picks its own key names — the
|
|
10257
|
+
* cap deliberately does NOT enforce a closed enum here. Reolink keys:
|
|
10258
|
+
* `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
|
|
10259
|
+
* `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
|
|
10260
|
+
* `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
|
|
10261
|
+
*/
|
|
10262
|
+
flags: record(string(), unknown()),
|
|
10263
|
+
/**
|
|
10264
|
+
* Coarse driver-classification — lets cross-process consumers tell apart
|
|
10265
|
+
* cameras / battery-cams / NVRs without re-running the probe. `null`
|
|
10266
|
+
* before the first probe completes.
|
|
10267
|
+
*/
|
|
10268
|
+
deviceType: string().nullable(),
|
|
10269
|
+
/** Camera/firmware model string. `null` when the firmware doesn't expose it. */
|
|
10270
|
+
model: string().nullable(),
|
|
10271
|
+
/** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
|
|
10272
|
+
channelCount: number().nullable(),
|
|
10273
|
+
/**
|
|
10274
|
+
* Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
|
|
10275
|
+
* completes — drivers' `getAccessoryChildren()` should treat zero as
|
|
10276
|
+
* "probe not done yet, return empty" so accessories aren't spawned
|
|
10277
|
+
* before the firmware is queried.
|
|
10278
|
+
*/
|
|
10279
|
+
lastProbedAt: number(),
|
|
10280
|
+
/**
|
|
10281
|
+
* Framework convention: every runtime-state slice carries this for the
|
|
10282
|
+
* createRuntimeStateBridge stale-check helper. We keep it in sync with
|
|
10283
|
+
* `lastProbedAt` on every write.
|
|
10284
|
+
*/
|
|
10285
|
+
lastFetchedAt: number()
|
|
10286
|
+
});
|
|
10287
|
+
var featureProbeCapability = {
|
|
10288
|
+
name: "feature-probe",
|
|
10289
|
+
scope: "device",
|
|
10290
|
+
deviceNative: true,
|
|
10291
|
+
mode: "singleton",
|
|
10292
|
+
methods: {},
|
|
10293
|
+
events: {
|
|
10294
|
+
/** Fires whenever a fresh probe completes (kernel-driven `reprobe()`
|
|
10295
|
+
* or driver-initiated re-detect after a state change). */
|
|
10296
|
+
onProbeChanged: { data: object({
|
|
10297
|
+
deviceId: number(),
|
|
10298
|
+
status: FeatureProbeStatusSchema
|
|
10299
|
+
}) } },
|
|
10300
|
+
status: {
|
|
10301
|
+
schema: FeatureProbeStatusSchema,
|
|
10302
|
+
kind: "push"
|
|
10303
|
+
},
|
|
10304
|
+
runtimeState: FeatureProbeStatusSchema
|
|
10305
|
+
};
|
|
10306
|
+
/**
|
|
10307
|
+
* Multi-metric air-quality slice. Covers CO₂, total VOCs, particulate
|
|
10308
|
+
* matter at PM2.5 / PM10, and a derived AQI index — all optional so
|
|
10309
|
+
* a single-metric source populates only what it observes. Mirrors
|
|
10310
|
+
* the HA `sensor` device_class set (`co2`, `volatile_organic_compounds`,
|
|
10311
|
+
* `pm25`, `pm10`, `aqi`) collapsed into one cap because a typical
|
|
10312
|
+
* air-quality node reports several of these together; modelling them
|
|
10313
|
+
* as siblings keeps a single timestamp + one slice subscription.
|
|
10314
|
+
*/
|
|
10315
|
+
var AirQualitySensorStatusSchema = object({
|
|
10316
|
+
/** Carbon dioxide concentration in ppm. */
|
|
10317
|
+
co2Ppm: number().min(0).optional(),
|
|
10318
|
+
/** Total volatile organic compounds in ppb. */
|
|
10319
|
+
vocPpb: number().min(0).optional(),
|
|
10320
|
+
/** Particulate matter ≤ 2.5 μm in µg/m³. */
|
|
10321
|
+
pm25: number().min(0).optional(),
|
|
10322
|
+
/** Particulate matter ≤ 10 μm in µg/m³. */
|
|
10323
|
+
pm10: number().min(0).optional(),
|
|
9576
10324
|
/** Composite AQI value (typically 0..500). */
|
|
9577
10325
|
aqi: number().optional(),
|
|
9578
10326
|
/** Ms epoch when the slice was last updated. */
|
|
@@ -10351,6 +11099,28 @@ method(object({
|
|
|
10351
11099
|
}), object({ success: literal(true) }), {
|
|
10352
11100
|
kind: "mutation",
|
|
10353
11101
|
auth: "admin"
|
|
11102
|
+
}), method(object({
|
|
11103
|
+
deviceId: number(),
|
|
11104
|
+
/** Absent = the LOWEST assigned profile — a notification attachment is
|
|
11105
|
+
* watched on a phone, and the cheap rendition is the right default. */
|
|
11106
|
+
profile: CamProfileSchema.optional(),
|
|
11107
|
+
aroundMs: number(),
|
|
11108
|
+
preRollSec: number().min(0).max(20).default(3),
|
|
11109
|
+
postRollSec: number().min(0).max(20).default(5),
|
|
11110
|
+
format: _enum(["gif", "mp4"]).default("gif"),
|
|
11111
|
+
maxWidth: number().int().min(120).max(1920).default(480),
|
|
11112
|
+
/** GIF only — MP4 keeps the source cadence. */
|
|
11113
|
+
fps: number().int().min(1).max(15).default(5)
|
|
11114
|
+
}), object({
|
|
11115
|
+
base64: string(),
|
|
11116
|
+
mime: string(),
|
|
11117
|
+
bytes: number().int(),
|
|
11118
|
+
/** The profile actually rendered (what the default resolved to). */
|
|
11119
|
+
profile: CamProfileSchema,
|
|
11120
|
+
durationMs: number()
|
|
11121
|
+
}), {
|
|
11122
|
+
kind: "mutation",
|
|
11123
|
+
auth: "admin"
|
|
10354
11124
|
}), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
|
|
10355
11125
|
probed: boolean(),
|
|
10356
11126
|
summary: string()
|
|
@@ -13629,67 +14399,6 @@ var motionTriggerCapability = {
|
|
|
13629
14399
|
runtimeState: MotionTriggerRuntimeStateSchema
|
|
13630
14400
|
};
|
|
13631
14401
|
/**
|
|
13632
|
-
* Shared geometry vocabulary for on-frame shape caps — privacy-mask,
|
|
13633
|
-
* motion-zones, and the detection zones/lines editor all speak this one
|
|
13634
|
-
* language so a single drawing-plane editor and the providers stay
|
|
13635
|
-
* decoupled from each cap's storage.
|
|
13636
|
-
*
|
|
13637
|
-
* All coordinates are normalized 0..1 of the camera frame (top-left
|
|
13638
|
-
* origin). Each cap composes the SUBSET of shape kinds it supports and
|
|
13639
|
-
* advertises it via `supportedShapes` in its `getOptions`.
|
|
13640
|
-
*/
|
|
13641
|
-
/** A normalized 0..1 point (top-left origin). */
|
|
13642
|
-
var MaskPointSchema = object({
|
|
13643
|
-
x: number(),
|
|
13644
|
-
y: number()
|
|
13645
|
-
});
|
|
13646
|
-
/** Axis-aligned rectangle (normalized 0..1). */
|
|
13647
|
-
var MaskRectShapeSchema = object({
|
|
13648
|
-
kind: literal("rect"),
|
|
13649
|
-
x: number(),
|
|
13650
|
-
y: number(),
|
|
13651
|
-
width: number(),
|
|
13652
|
-
height: number()
|
|
13653
|
-
});
|
|
13654
|
-
/** Free polygon — an ordered list of normalized vertices (≥3). */
|
|
13655
|
-
var MaskPolygonShapeSchema = object({
|
|
13656
|
-
kind: literal("polygon"),
|
|
13657
|
-
points: array(MaskPointSchema)
|
|
13658
|
-
});
|
|
13659
|
-
/** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
|
|
13660
|
-
var MaskGridShapeSchema = object({
|
|
13661
|
-
kind: literal("grid"),
|
|
13662
|
-
gridWidth: number(),
|
|
13663
|
-
gridHeight: number(),
|
|
13664
|
-
cells: array(boolean())
|
|
13665
|
-
});
|
|
13666
|
-
discriminatedUnion("kind", [
|
|
13667
|
-
MaskRectShapeSchema,
|
|
13668
|
-
MaskPolygonShapeSchema,
|
|
13669
|
-
MaskGridShapeSchema,
|
|
13670
|
-
object({
|
|
13671
|
-
kind: literal("line"),
|
|
13672
|
-
points: array(MaskPointSchema)
|
|
13673
|
-
})
|
|
13674
|
-
]);
|
|
13675
|
-
/** Every shape-kind discriminant, for `supportedShapes` advertisement. */
|
|
13676
|
-
var MaskShapeKindSchema = _enum([
|
|
13677
|
-
"rect",
|
|
13678
|
-
"polygon",
|
|
13679
|
-
"grid",
|
|
13680
|
-
"line"
|
|
13681
|
-
]);
|
|
13682
|
-
/** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
|
|
13683
|
-
var MaskPolygonVerticesSchema = object({
|
|
13684
|
-
min: number(),
|
|
13685
|
-
max: number()
|
|
13686
|
-
});
|
|
13687
|
-
/** Grid dimensions when a cap supports 'grid'. */
|
|
13688
|
-
var MaskGridDimsSchema = object({
|
|
13689
|
-
width: number(),
|
|
13690
|
-
height: number()
|
|
13691
|
-
});
|
|
13692
|
-
/**
|
|
13693
14402
|
* Motion-zones share the same MaskShape vocabulary as privacy-mask — the
|
|
13694
14403
|
* on-camera motion-detection mask is a single `grid` region (a row-major
|
|
13695
14404
|
* boolean cell lattice the camera's onboard VMD evaluates). Composing it as
|
|
@@ -17145,6 +17854,55 @@ method(object({
|
|
|
17145
17854
|
password: string()
|
|
17146
17855
|
}), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
|
|
17147
17856
|
/**
|
|
17857
|
+
* A live terminal session hosted by the provider addon. Output and input do
|
|
17858
|
+
* NOT flow through the capability — they use the addon data plane
|
|
17859
|
+
* (`GET /addon/terminal/<id>/out` SSE, `POST /addon/terminal/<id>/in`) because
|
|
17860
|
+
* terminal output must be ordered and lossless. The event bus is telemetry and
|
|
17861
|
+
* may drop chunks ([D8]), and a dropped chunk desynchronises the vt parser
|
|
17862
|
+
* permanently until a full repaint. The capability owns only lifecycle.
|
|
17863
|
+
*/
|
|
17864
|
+
var TerminalSessionInfoSchema = object({
|
|
17865
|
+
/** Opaque session id minted by the provider on `openSession`. */
|
|
17866
|
+
sessionId: string(),
|
|
17867
|
+
/** The pre-declared profile this session runs (never a free-form command). */
|
|
17868
|
+
profileId: string(),
|
|
17869
|
+
/** Human-readable profile label for the UI session list. */
|
|
17870
|
+
label: string(),
|
|
17871
|
+
cols: number().int().positive(),
|
|
17872
|
+
rows: number().int().positive(),
|
|
17873
|
+
/** ms-epoch the session's pty was spawned. */
|
|
17874
|
+
startedAt: number()
|
|
17875
|
+
});
|
|
17876
|
+
/**
|
|
17877
|
+
* A profile the operator may open — a pre-declared, allowlisted program
|
|
17878
|
+
* (`monitor` → `btm`). The capability accepts only these ids; a free-form
|
|
17879
|
+
* command string would be remote code execution as the server's user, so it is
|
|
17880
|
+
* deliberately not part of the contract.
|
|
17881
|
+
*/
|
|
17882
|
+
var TerminalProfileInfoSchema = object({
|
|
17883
|
+
profileId: string(),
|
|
17884
|
+
label: string(),
|
|
17885
|
+
description: string().optional()
|
|
17886
|
+
});
|
|
17887
|
+
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
17888
|
+
profileId: string(),
|
|
17889
|
+
cols: number().int().positive(),
|
|
17890
|
+
rows: number().int().positive()
|
|
17891
|
+
}), TerminalSessionInfoSchema, {
|
|
17892
|
+
kind: "mutation",
|
|
17893
|
+
auth: "admin"
|
|
17894
|
+
}), method(object({
|
|
17895
|
+
sessionId: string(),
|
|
17896
|
+
cols: number().int().positive(),
|
|
17897
|
+
rows: number().int().positive()
|
|
17898
|
+
}), _void(), {
|
|
17899
|
+
kind: "mutation",
|
|
17900
|
+
auth: "admin"
|
|
17901
|
+
}), method(object({ sessionId: string() }), _void(), {
|
|
17902
|
+
kind: "mutation",
|
|
17903
|
+
auth: "admin"
|
|
17904
|
+
});
|
|
17905
|
+
/**
|
|
17148
17906
|
* Orchestrator-side destination metadata. The orchestrator computes
|
|
17149
17907
|
* `id = <addonId>:<subId>` from its provider lookup so consumers
|
|
17150
17908
|
* (admin UI, restore flow) see one canonical key.
|
|
@@ -17245,11 +18003,53 @@ var LocationStatSchema = object({
|
|
|
17245
18003
|
fileCount: number(),
|
|
17246
18004
|
present: boolean()
|
|
17247
18005
|
});
|
|
18006
|
+
/**
|
|
18007
|
+
* A backup schedule — the N:M "entry" that binds one cron cadence to a
|
|
18008
|
+
* SET of destination locations. Supersedes the per-location cron on
|
|
18009
|
+
* `BackupDestinationPolicy`: an operator creates a schedule, picks the
|
|
18010
|
+
* `backups` locations it should write to, and the orchestrator fans a
|
|
18011
|
+
* single archive out to all of them when the cron fires.
|
|
18012
|
+
*
|
|
18013
|
+
* `retentionCount` is per-schedule (D-decision 2026-07-28): every
|
|
18014
|
+
* location targeted by this schedule keeps this many archives from
|
|
18015
|
+
* this schedule's runs.
|
|
18016
|
+
*
|
|
18017
|
+
* `dataSources` optionally narrows which top-level state locations
|
|
18018
|
+
* (db, addons, tls, …) are archived; omitted = the orchestrator's
|
|
18019
|
+
* default full set.
|
|
18020
|
+
*/
|
|
18021
|
+
var BackupScheduleSchema = object({
|
|
18022
|
+
/** Stable id. Generated by the orchestrator on first upsert if absent. */
|
|
18023
|
+
id: string(),
|
|
18024
|
+
/** Operator-facing display name. */
|
|
18025
|
+
label: string(),
|
|
18026
|
+
/** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
|
|
18027
|
+
cron: string(),
|
|
18028
|
+
/** Master on/off toggle for the whole schedule. */
|
|
18029
|
+
enabled: boolean(),
|
|
18030
|
+
/** `backups`-location ids this schedule writes to (fan-out set). */
|
|
18031
|
+
locationIds: array(string()).readonly(),
|
|
18032
|
+
/** Archives kept per targeted location for this schedule. */
|
|
18033
|
+
retentionCount: number().int().min(1).max(1e3),
|
|
18034
|
+
/** Optional subset of source locations to include; omitted = all. */
|
|
18035
|
+
dataSources: array(string()).readonly().optional(),
|
|
18036
|
+
/** ms-epoch of last successful run. */
|
|
18037
|
+
lastRunAt: number().optional(),
|
|
18038
|
+
/** ms-epoch of next computed firing (read-only, filled on list). */
|
|
18039
|
+
nextRunAt: number().optional()
|
|
18040
|
+
});
|
|
17248
18041
|
method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
17249
18042
|
/** Subset of registered `backup-destination` addon ids to write to. */
|
|
17250
18043
|
destinations: array(string()).optional(),
|
|
17251
18044
|
locations: array(string()).optional(),
|
|
17252
|
-
label: string().optional()
|
|
18045
|
+
label: string().optional(),
|
|
18046
|
+
/**
|
|
18047
|
+
* Per-run retention override applied to every targeted
|
|
18048
|
+
* destination. Used by schedule-driven runs (per-entry
|
|
18049
|
+
* retention). Omitted = each destination's own policy
|
|
18050
|
+
* retention (manual runs).
|
|
18051
|
+
*/
|
|
18052
|
+
retentionCount: number().int().min(1).max(1e3).optional()
|
|
17253
18053
|
}).optional(), array(BackupEntrySchema).readonly(), {
|
|
17254
18054
|
kind: "mutation",
|
|
17255
18055
|
auth: "admin"
|
|
@@ -17298,7 +18098,21 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
|
|
|
17298
18098
|
ok: boolean(),
|
|
17299
18099
|
error: string().optional(),
|
|
17300
18100
|
nextRuns: array(number()).readonly()
|
|
17301
|
-
}))
|
|
18101
|
+
})), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
|
|
18102
|
+
id: string().optional(),
|
|
18103
|
+
label: string(),
|
|
18104
|
+
cron: string(),
|
|
18105
|
+
enabled: boolean(),
|
|
18106
|
+
locationIds: array(string()).readonly(),
|
|
18107
|
+
retentionCount: number().int().min(1).max(1e3),
|
|
18108
|
+
dataSources: array(string()).readonly().optional()
|
|
18109
|
+
}), BackupScheduleSchema, {
|
|
18110
|
+
kind: "mutation",
|
|
18111
|
+
auth: "admin"
|
|
18112
|
+
}), method(object({ id: string() }), _void(), {
|
|
18113
|
+
kind: "mutation",
|
|
18114
|
+
auth: "admin"
|
|
18115
|
+
});
|
|
17302
18116
|
/**
|
|
17303
18117
|
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
17304
18118
|
*
|
|
@@ -18993,997 +19807,539 @@ var MetricsGpuInfoSchema = object({
|
|
|
18993
19807
|
model: string(),
|
|
18994
19808
|
memoryUsedBytes: number(),
|
|
18995
19809
|
memoryTotalBytes: number(),
|
|
18996
|
-
temperature: number().nullable()
|
|
18997
|
-
});
|
|
18998
|
-
var ProcessResourceInfoSchema = object({
|
|
18999
|
-
openFds: number(),
|
|
19000
|
-
threadCount: number(),
|
|
19001
|
-
activeHandles: number(),
|
|
19002
|
-
activeRequests: number()
|
|
19003
|
-
});
|
|
19004
|
-
var PressureAvgsSchema = object({
|
|
19005
|
-
avg10: number(),
|
|
19006
|
-
avg60: number(),
|
|
19007
|
-
avg300: number()
|
|
19008
|
-
});
|
|
19009
|
-
var PressureInfoSchema = object({
|
|
19010
|
-
some: PressureAvgsSchema,
|
|
19011
|
-
full: PressureAvgsSchema.nullable()
|
|
19012
|
-
});
|
|
19013
|
-
var SystemResourceSnapshotSchema = object({
|
|
19014
|
-
cpu: CpuBreakdownSchema,
|
|
19015
|
-
memory: MemoryInfoSchema,
|
|
19016
|
-
gpu: MetricsGpuInfoSchema.nullable(),
|
|
19017
|
-
network: NetworkIoSnapshotSchema,
|
|
19018
|
-
disk: DiskIoSnapshotSchema,
|
|
19019
|
-
pressure: object({
|
|
19020
|
-
cpu: PressureInfoSchema.nullable(),
|
|
19021
|
-
memory: PressureInfoSchema.nullable(),
|
|
19022
|
-
io: PressureInfoSchema.nullable()
|
|
19023
|
-
}),
|
|
19024
|
-
process: ProcessResourceInfoSchema,
|
|
19025
|
-
cpuTemperature: number().nullable(),
|
|
19026
|
-
timestampMs: number()
|
|
19027
|
-
});
|
|
19028
|
-
var DiskSpaceInfoSchema = object({
|
|
19029
|
-
path: string(),
|
|
19030
|
-
totalBytes: number(),
|
|
19031
|
-
usedBytes: number(),
|
|
19032
|
-
availableBytes: number(),
|
|
19033
|
-
percent: number()
|
|
19034
|
-
});
|
|
19035
|
-
var PidResourceStatsSchema = object({
|
|
19036
|
-
pid: number(),
|
|
19037
|
-
cpu: number(),
|
|
19038
|
-
memory: number(),
|
|
19039
|
-
/**
|
|
19040
|
-
* Private (anonymous) resident bytes — the per-process V8 heap + native
|
|
19041
|
-
* allocations NOT shared with other processes (Linux RssAnon). This is the
|
|
19042
|
-
* "real" per-runner cost; summing it across runners is meaningful, unlike
|
|
19043
|
-
* `memory` (RSS), which double-counts the shared mmap'd framework code.
|
|
19044
|
-
* Undefined where /proc is unavailable (e.g. macOS).
|
|
19045
|
-
*/
|
|
19046
|
-
privateBytes: number().optional(),
|
|
19047
|
-
/**
|
|
19048
|
-
* Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
|
|
19049
|
-
* code shared copy-on-write across runners. Undefined on macOS.
|
|
19050
|
-
*/
|
|
19051
|
-
sharedBytes: number().optional()
|
|
19052
|
-
});
|
|
19053
|
-
var AddonInstanceSchema = object({
|
|
19054
|
-
addonId: string(),
|
|
19055
|
-
nodeId: string(),
|
|
19056
|
-
role: _enum(["hub", "worker"]),
|
|
19057
|
-
pid: number(),
|
|
19058
|
-
state: _enum([
|
|
19059
|
-
"starting",
|
|
19060
|
-
"running",
|
|
19061
|
-
"stopping",
|
|
19062
|
-
"stopped",
|
|
19063
|
-
"crashed"
|
|
19064
|
-
]),
|
|
19065
|
-
uptimeSec: number()
|
|
19066
|
-
});
|
|
19067
|
-
var NodeProcessSchema = object({
|
|
19068
|
-
pid: number(),
|
|
19069
|
-
ppid: number(),
|
|
19070
|
-
pgid: number(),
|
|
19071
|
-
classification: _enum([
|
|
19072
|
-
"root",
|
|
19073
|
-
"managed",
|
|
19074
|
-
"system",
|
|
19075
|
-
"ghost"
|
|
19076
|
-
]),
|
|
19077
|
-
/** `$process` addon binding when `managed`, else null. */
|
|
19078
|
-
addonId: string().nullable(),
|
|
19079
|
-
/** Kernel-reported nodeId when the process is a known agent/worker. */
|
|
19080
|
-
nodeId: string().nullable(),
|
|
19081
|
-
/** Truncated command line. */
|
|
19082
|
-
command: string(),
|
|
19083
|
-
cpuPercent: number(),
|
|
19084
|
-
memoryRssBytes: number(),
|
|
19085
|
-
/** Wall-clock uptime (seconds). Parsed from `ps etime`. */
|
|
19086
|
-
uptimeSec: number(),
|
|
19087
|
-
/** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
|
|
19088
|
-
orphaned: boolean()
|
|
19089
|
-
});
|
|
19090
|
-
var KillProcessInputSchema = object({
|
|
19091
|
-
pid: number(),
|
|
19092
|
-
/** Force = SIGKILL. Default is SIGTERM. */
|
|
19093
|
-
force: boolean().optional()
|
|
19094
|
-
});
|
|
19095
|
-
var KillProcessResultSchema = object({
|
|
19096
|
-
success: boolean(),
|
|
19097
|
-
reason: string().optional(),
|
|
19098
|
-
signal: _enum(["SIGTERM", "SIGKILL"]).optional()
|
|
19099
|
-
});
|
|
19100
|
-
var DumpHeapSnapshotInputSchema = object({
|
|
19101
|
-
/** The addon whose runner should dump a heap snapshot. */
|
|
19102
|
-
addonId: string() });
|
|
19103
|
-
var DumpHeapSnapshotResultSchema = object({
|
|
19104
|
-
success: boolean(),
|
|
19105
|
-
/** Path of the written .heapsnapshot inside the runner's container/host. */
|
|
19106
|
-
path: string().optional(),
|
|
19107
|
-
/** Process pid that was signalled. */
|
|
19108
|
-
pid: number().optional(),
|
|
19109
|
-
reason: string().optional()
|
|
19110
|
-
});
|
|
19111
|
-
var SystemMetricsSchema = object({
|
|
19112
|
-
cpuPercent: number(),
|
|
19113
|
-
memoryPercent: number(),
|
|
19114
|
-
memoryUsedMB: number(),
|
|
19115
|
-
memoryTotalMB: number(),
|
|
19116
|
-
diskPercent: number().optional(),
|
|
19117
|
-
temperature: number().optional(),
|
|
19118
|
-
gpuPercent: number().optional(),
|
|
19119
|
-
gpuMemoryPercent: number().optional()
|
|
19120
|
-
});
|
|
19121
|
-
method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(KillProcessInputSchema, KillProcessResultSchema, {
|
|
19122
|
-
kind: "mutation",
|
|
19123
|
-
auth: "admin"
|
|
19124
|
-
}), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
|
|
19125
|
-
kind: "mutation",
|
|
19126
|
-
auth: "admin"
|
|
19127
|
-
});
|
|
19128
|
-
method(object({
|
|
19129
|
-
sourceUrl: string(),
|
|
19130
|
-
metadata: ModelConvertMetadataSchema,
|
|
19131
|
-
targets: array(ConvertTargetSchema).min(1).readonly(),
|
|
19132
|
-
calibrationRef: string().optional(),
|
|
19133
|
-
sessionId: string().optional()
|
|
19134
|
-
}), ConvertResultSchema, {
|
|
19135
|
-
kind: "mutation",
|
|
19136
|
-
auth: "admin",
|
|
19137
|
-
timeoutMs: 6e5
|
|
19138
|
-
});
|
|
19139
|
-
method(object({
|
|
19140
|
-
nodeId: string(),
|
|
19141
|
-
modelId: string(),
|
|
19142
|
-
format: _enum(MODEL_FORMATS),
|
|
19143
|
-
entry: ModelCatalogEntrySchema
|
|
19144
|
-
}), object({
|
|
19145
|
-
ok: boolean(),
|
|
19146
|
-
/** sha256 of the staged tarball (empty for a hub-local no-op). */
|
|
19147
|
-
sha256: string(),
|
|
19148
|
-
bytes: number(),
|
|
19149
|
-
/** The target node's modelsDir the artifact landed in. */
|
|
19150
|
-
path: string()
|
|
19151
|
-
}), {
|
|
19152
|
-
kind: "mutation",
|
|
19153
|
-
auth: "admin"
|
|
19154
|
-
});
|
|
19155
|
-
/**
|
|
19156
|
-
* `mqtt-broker` — broker-registry cap.
|
|
19157
|
-
*
|
|
19158
|
-
* NOT a pub/sub proxy. The cap exposes (a) a registry of configured
|
|
19159
|
-
* MQTT brokers (external + optionally an embedded `aedes`-backed one)
|
|
19160
|
-
* and (b) the connection details a consumer addon needs to spin up
|
|
19161
|
-
* its OWN `mqtt.js` client.
|
|
19162
|
-
*
|
|
19163
|
-
* Why: pub/sub routing over the system event-bus loses fidelity
|
|
19164
|
-
* (callback shape, QoS guarantees, will/retain semantics) and adds
|
|
19165
|
-
* refcount bookkeeping that addons would rather own themselves. The
|
|
19166
|
-
* canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
|
|
19167
|
-
* features anyway — give it the connection config, get out of the way.
|
|
19168
|
-
*
|
|
19169
|
-
* Consumer flow:
|
|
19170
|
-
* const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
|
|
19171
|
-
* const client = mqtt.connect(cfg.url, { username: cfg.username, … })
|
|
19172
|
-
* client.subscribe('zigbee2mqtt/+')
|
|
19173
|
-
*
|
|
19174
|
-
* Collection mode: multiple brokers (e.g. one local mosquitto + one
|
|
19175
|
-
* cloud bridge). The "embedded" entry (when present) is just another
|
|
19176
|
-
* broker in the registry — its lifecycle is owned by the addon that
|
|
19177
|
-
* spawned it.
|
|
19178
|
-
*/
|
|
19179
|
-
var BrokerKindSchema = _enum(["external", "embedded"]);
|
|
19180
|
-
/**
|
|
19181
|
-
* Broker live-probe status.
|
|
19182
|
-
*
|
|
19183
|
-
* - `connected` — last probe completed a clean CONNACK
|
|
19184
|
-
* - `disconnected` — no probe has run yet (cold cache)
|
|
19185
|
-
* - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
|
|
19186
|
-
* - `unreachable` — TCP connect timed out / refused
|
|
19187
|
-
* - `tls-error` — TLS handshake failed (cert / SNI / cipher)
|
|
19188
|
-
*/
|
|
19189
|
-
var BrokerStatusSchema$1 = _enum([
|
|
19190
|
-
"connected",
|
|
19191
|
-
"disconnected",
|
|
19192
|
-
"auth-failed",
|
|
19193
|
-
"unreachable",
|
|
19194
|
-
"tls-error"
|
|
19195
|
-
]);
|
|
19196
|
-
var BrokerInfoSchema = object({
|
|
19197
|
-
id: string(),
|
|
19198
|
-
name: string(),
|
|
19199
|
-
url: string(),
|
|
19200
|
-
kind: BrokerKindSchema,
|
|
19201
|
-
status: BrokerStatusSchema$1,
|
|
19202
|
-
latencyMs: number().nullable(),
|
|
19203
|
-
error: string().optional(),
|
|
19204
|
-
/** Embedded brokers only: number of MQTT clients currently connected. */
|
|
19205
|
-
connectedClients: number().int().nonnegative().optional(),
|
|
19206
|
-
/** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
|
|
19207
|
-
lastCheckedAt: number().optional()
|
|
19208
|
-
});
|
|
19209
|
-
/**
|
|
19210
|
-
* Connection details — what a consumer needs to call
|
|
19211
|
-
* `mqtt.connect(url, options)`. We split URL + credentials so the
|
|
19212
|
-
* consumer can pass them as `mqtt.connect(url, { username, password })`
|
|
19213
|
-
* instead of stuffing creds into the URL (which leaks them into logs).
|
|
19214
|
-
*/
|
|
19215
|
-
var BrokerConnectionDetailsSchema = object({
|
|
19216
|
-
url: string(),
|
|
19217
|
-
username: string().optional(),
|
|
19218
|
-
password: string().optional(),
|
|
19219
|
-
/**
|
|
19220
|
-
* Suggested prefix for `clientId`. Each consumer should suffix this
|
|
19221
|
-
* with its own discriminator (addon id, instance id) so reconnects
|
|
19222
|
-
* don't kick each other off (MQTT spec: clientId must be unique per
|
|
19223
|
-
* broker).
|
|
19224
|
-
*/
|
|
19225
|
-
clientIdPrefix: string().optional()
|
|
19226
|
-
});
|
|
19227
|
-
var AddBrokerInputSchema = object({
|
|
19228
|
-
name: string().min(1),
|
|
19229
|
-
url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
|
|
19230
|
-
username: string().optional(),
|
|
19231
|
-
password: string().optional(),
|
|
19232
|
-
clientIdPrefix: string().optional()
|
|
19810
|
+
temperature: number().nullable()
|
|
19233
19811
|
});
|
|
19234
|
-
var
|
|
19235
|
-
|
|
19236
|
-
|
|
19237
|
-
|
|
19238
|
-
|
|
19239
|
-
}), object({
|
|
19240
|
-
ok: literal(false),
|
|
19241
|
-
error: string()
|
|
19242
|
-
})]);
|
|
19243
|
-
var StartEmbeddedInputSchema = object({
|
|
19244
|
-
port: number().int().min(1).max(65535).default(1883),
|
|
19245
|
-
/** Allow anonymous connect (no username/password). Default: false. */
|
|
19246
|
-
allowAnonymous: boolean().default(false),
|
|
19247
|
-
/** Optional shared username/password for clients. */
|
|
19248
|
-
username: string().optional(),
|
|
19249
|
-
password: string().optional()
|
|
19812
|
+
var ProcessResourceInfoSchema = object({
|
|
19813
|
+
openFds: number(),
|
|
19814
|
+
threadCount: number(),
|
|
19815
|
+
activeHandles: number(),
|
|
19816
|
+
activeRequests: number()
|
|
19250
19817
|
});
|
|
19251
|
-
var
|
|
19252
|
-
|
|
19253
|
-
|
|
19818
|
+
var PressureAvgsSchema = object({
|
|
19819
|
+
avg10: number(),
|
|
19820
|
+
avg60: number(),
|
|
19821
|
+
avg300: number()
|
|
19254
19822
|
});
|
|
19255
|
-
var
|
|
19256
|
-
|
|
19257
|
-
|
|
19823
|
+
var PressureInfoSchema = object({
|
|
19824
|
+
some: PressureAvgsSchema,
|
|
19825
|
+
full: PressureAvgsSchema.nullable()
|
|
19258
19826
|
});
|
|
19259
|
-
|
|
19260
|
-
|
|
19261
|
-
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19827
|
+
var SystemResourceSnapshotSchema = object({
|
|
19828
|
+
cpu: CpuBreakdownSchema,
|
|
19829
|
+
memory: MemoryInfoSchema,
|
|
19830
|
+
gpu: MetricsGpuInfoSchema.nullable(),
|
|
19831
|
+
network: NetworkIoSnapshotSchema,
|
|
19832
|
+
disk: DiskIoSnapshotSchema,
|
|
19833
|
+
pressure: object({
|
|
19834
|
+
cpu: PressureInfoSchema.nullable(),
|
|
19835
|
+
memory: PressureInfoSchema.nullable(),
|
|
19836
|
+
io: PressureInfoSchema.nullable()
|
|
19837
|
+
}),
|
|
19838
|
+
process: ProcessResourceInfoSchema,
|
|
19839
|
+
cpuTemperature: number().nullable(),
|
|
19840
|
+
timestampMs: number()
|
|
19265
19841
|
});
|
|
19266
|
-
var
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19842
|
+
var DiskSpaceInfoSchema = object({
|
|
19843
|
+
path: string(),
|
|
19844
|
+
totalBytes: number(),
|
|
19845
|
+
usedBytes: number(),
|
|
19846
|
+
availableBytes: number(),
|
|
19847
|
+
percent: number()
|
|
19270
19848
|
});
|
|
19271
|
-
|
|
19272
|
-
|
|
19273
|
-
|
|
19274
|
-
|
|
19275
|
-
* the originating provider config (mode + sourcePort) so the
|
|
19276
|
-
* orchestrator UI can label rows distinctly. Providers that expose only
|
|
19277
|
-
* one endpoint just omit `listEndpoints` from their provider impl.
|
|
19278
|
-
*/
|
|
19279
|
-
var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
19849
|
+
var PidResourceStatsSchema = object({
|
|
19850
|
+
pid: number(),
|
|
19851
|
+
cpu: number(),
|
|
19852
|
+
memory: number(),
|
|
19280
19853
|
/**
|
|
19281
|
-
*
|
|
19282
|
-
*
|
|
19854
|
+
* Private (anonymous) resident bytes — the per-process V8 heap + native
|
|
19855
|
+
* allocations NOT shared with other processes (Linux RssAnon). This is the
|
|
19856
|
+
* "real" per-runner cost; summing it across runners is meaningful, unlike
|
|
19857
|
+
* `memory` (RSS), which double-counts the shared mmap'd framework code.
|
|
19858
|
+
* Undefined where /proc is unavailable (e.g. macOS).
|
|
19283
19859
|
*/
|
|
19284
|
-
|
|
19285
|
-
/**
|
|
19286
|
-
|
|
19287
|
-
|
|
19288
|
-
|
|
19289
|
-
|
|
19290
|
-
sourcePort: number().optional()
|
|
19291
|
-
});
|
|
19292
|
-
method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
|
|
19293
|
-
/**
|
|
19294
|
-
* notification-output — canonical, capability-gated notification delivery.
|
|
19295
|
-
*
|
|
19296
|
-
* Apprise-derived model (see
|
|
19297
|
-
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
19298
|
-
* callers emit ONE canonical `Notification`; each provider declares a
|
|
19299
|
-
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
19300
|
-
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
19301
|
-
* message to what the kind supports — callers never special-case a service.
|
|
19302
|
-
*
|
|
19303
|
-
* DESIGN DECISIONS (locked):
|
|
19304
|
-
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
19305
|
-
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
19306
|
-
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
19307
|
-
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
19308
|
-
* alternative would fork the UI per addon and cannot host the
|
|
19309
|
-
* discovery→adopt flow.
|
|
19310
|
-
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
19311
|
-
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
19312
|
-
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
19313
|
-
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
19314
|
-
* `addonId` the generated collection router extracts from the call input.
|
|
19315
|
-
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
19316
|
-
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
19317
|
-
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
19318
|
-
* base64 fallback needed.
|
|
19319
|
-
*
|
|
19320
|
-
* TODO (deferred, closed-set change — separate decision): add
|
|
19321
|
-
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
19322
|
-
* admin "Integrations" page.
|
|
19323
|
-
*/
|
|
19324
|
-
/**
|
|
19325
|
-
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
19326
|
-
* adapter picks what it supports and the degrade engine filters the rest.
|
|
19327
|
-
*/
|
|
19328
|
-
var AttachmentMediaTypeSchema = _enum([
|
|
19329
|
-
"image",
|
|
19330
|
-
"video",
|
|
19331
|
-
"gif",
|
|
19332
|
-
"audio",
|
|
19333
|
-
"icon"
|
|
19334
|
-
]);
|
|
19335
|
-
/**
|
|
19336
|
-
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
19337
|
-
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
19338
|
-
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
19339
|
-
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
19340
|
-
*/
|
|
19341
|
-
var AttachmentSchema = object({
|
|
19342
|
-
mediaType: AttachmentMediaTypeSchema,
|
|
19343
|
-
url: string().optional(),
|
|
19344
|
-
bytes: _instanceof(Uint8Array).optional(),
|
|
19345
|
-
mime: string().optional(),
|
|
19346
|
-
name: string().optional()
|
|
19347
|
-
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
19348
|
-
var NotificationFormatSchema = _enum([
|
|
19349
|
-
"text",
|
|
19350
|
-
"markdown",
|
|
19351
|
-
"html"
|
|
19352
|
-
]);
|
|
19353
|
-
/** A single tap-through action button. */
|
|
19354
|
-
var NotificationActionSchema = object({
|
|
19355
|
-
id: string(),
|
|
19356
|
-
label: string(),
|
|
19357
|
-
url: string().optional()
|
|
19358
|
-
});
|
|
19359
|
-
/**
|
|
19360
|
-
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
19361
|
-
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
19362
|
-
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
19363
|
-
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
19364
|
-
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
19365
|
-
* `priority` for that one target.
|
|
19366
|
-
*/
|
|
19367
|
-
var NotificationSchema = object({
|
|
19368
|
-
body: string(),
|
|
19369
|
-
title: string().optional(),
|
|
19370
|
-
format: NotificationFormatSchema.default("text"),
|
|
19371
|
-
priority: number().int().min(1).max(5).default(3),
|
|
19372
|
-
level: string().optional(),
|
|
19373
|
-
attachments: array(AttachmentSchema).optional(),
|
|
19374
|
-
clickUrl: string().optional(),
|
|
19375
|
-
actions: array(NotificationActionSchema).optional(),
|
|
19376
|
-
sound: string().optional(),
|
|
19377
|
-
ttl: number().optional(),
|
|
19378
|
-
tag: string().optional(),
|
|
19379
|
-
deviceId: number().optional(),
|
|
19380
|
-
eventId: string().optional(),
|
|
19381
|
-
metadata: record(string(), unknown()).optional()
|
|
19860
|
+
privateBytes: number().optional(),
|
|
19861
|
+
/**
|
|
19862
|
+
* Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
|
|
19863
|
+
* code shared copy-on-write across runners. Undefined on macOS.
|
|
19864
|
+
*/
|
|
19865
|
+
sharedBytes: number().optional()
|
|
19382
19866
|
});
|
|
19383
|
-
|
|
19384
|
-
|
|
19385
|
-
|
|
19386
|
-
|
|
19387
|
-
|
|
19388
|
-
|
|
19389
|
-
|
|
19390
|
-
|
|
19391
|
-
|
|
19392
|
-
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
|
|
19396
|
-
description: string().optional()
|
|
19867
|
+
var AddonInstanceSchema = object({
|
|
19868
|
+
addonId: string(),
|
|
19869
|
+
nodeId: string(),
|
|
19870
|
+
role: _enum(["hub", "worker"]),
|
|
19871
|
+
pid: number(),
|
|
19872
|
+
state: _enum([
|
|
19873
|
+
"starting",
|
|
19874
|
+
"running",
|
|
19875
|
+
"stopping",
|
|
19876
|
+
"stopped",
|
|
19877
|
+
"crashed"
|
|
19878
|
+
]),
|
|
19879
|
+
uptimeSec: number()
|
|
19397
19880
|
});
|
|
19398
|
-
|
|
19399
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
/**
|
|
19411
|
-
|
|
19412
|
-
|
|
19413
|
-
|
|
19414
|
-
|
|
19415
|
-
|
|
19416
|
-
|
|
19417
|
-
|
|
19881
|
+
var NodeProcessSchema = object({
|
|
19882
|
+
pid: number(),
|
|
19883
|
+
ppid: number(),
|
|
19884
|
+
pgid: number(),
|
|
19885
|
+
classification: _enum([
|
|
19886
|
+
"root",
|
|
19887
|
+
"managed",
|
|
19888
|
+
"system",
|
|
19889
|
+
"ghost"
|
|
19890
|
+
]),
|
|
19891
|
+
/** `$process` addon binding when `managed`, else null. */
|
|
19892
|
+
addonId: string().nullable(),
|
|
19893
|
+
/** Kernel-reported nodeId when the process is a known agent/worker. */
|
|
19894
|
+
nodeId: string().nullable(),
|
|
19895
|
+
/** Truncated command line. */
|
|
19896
|
+
command: string(),
|
|
19897
|
+
cpuPercent: number(),
|
|
19898
|
+
memoryRssBytes: number(),
|
|
19899
|
+
/** Wall-clock uptime (seconds). Parsed from `ps etime`. */
|
|
19900
|
+
uptimeSec: number(),
|
|
19901
|
+
/** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
|
|
19902
|
+
orphaned: boolean()
|
|
19418
19903
|
});
|
|
19419
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
19423
|
-
* the union is large and not meant for runtime validation here; the exported
|
|
19424
|
-
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
19425
|
-
*/
|
|
19426
|
-
var ConfigSchemaPassthrough = unknown();
|
|
19427
|
-
var TargetKindSchema = object({
|
|
19428
|
-
kind: string(),
|
|
19429
|
-
label: string(),
|
|
19430
|
-
icon: string(),
|
|
19431
|
-
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
19432
|
-
addonId: string(),
|
|
19433
|
-
configSchema: ConfigSchemaPassthrough,
|
|
19434
|
-
supportsDiscovery: boolean(),
|
|
19435
|
-
caps: TargetKindCapsSchema
|
|
19904
|
+
var KillProcessInputSchema = object({
|
|
19905
|
+
pid: number(),
|
|
19906
|
+
/** Force = SIGKILL. Default is SIGTERM. */
|
|
19907
|
+
force: boolean().optional()
|
|
19436
19908
|
});
|
|
19437
|
-
|
|
19438
|
-
|
|
19439
|
-
|
|
19440
|
-
|
|
19441
|
-
*/
|
|
19442
|
-
var TargetSchema = object({
|
|
19443
|
-
id: string(),
|
|
19444
|
-
name: string(),
|
|
19445
|
-
kind: string(),
|
|
19446
|
-
addonId: string(),
|
|
19447
|
-
enabled: boolean(),
|
|
19448
|
-
config: record(string(), unknown())
|
|
19909
|
+
var KillProcessResultSchema = object({
|
|
19910
|
+
success: boolean(),
|
|
19911
|
+
reason: string().optional(),
|
|
19912
|
+
signal: _enum(["SIGTERM", "SIGKILL"]).optional()
|
|
19449
19913
|
});
|
|
19450
|
-
|
|
19451
|
-
|
|
19452
|
-
|
|
19453
|
-
|
|
19454
|
-
|
|
19914
|
+
var DumpHeapSnapshotInputSchema = object({
|
|
19915
|
+
/** The addon whose runner should dump a heap snapshot. */
|
|
19916
|
+
addonId: string() });
|
|
19917
|
+
var DumpHeapSnapshotResultSchema = object({
|
|
19918
|
+
success: boolean(),
|
|
19919
|
+
/** Path of the written .heapsnapshot inside the runner's container/host. */
|
|
19920
|
+
path: string().optional(),
|
|
19921
|
+
/** Process pid that was signalled. */
|
|
19922
|
+
pid: number().optional(),
|
|
19923
|
+
reason: string().optional()
|
|
19455
19924
|
});
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19461
|
-
|
|
19462
|
-
|
|
19463
|
-
|
|
19925
|
+
var SystemMetricsSchema = object({
|
|
19926
|
+
cpuPercent: number(),
|
|
19927
|
+
memoryPercent: number(),
|
|
19928
|
+
memoryUsedMB: number(),
|
|
19929
|
+
memoryTotalMB: number(),
|
|
19930
|
+
diskPercent: number().optional(),
|
|
19931
|
+
temperature: number().optional(),
|
|
19932
|
+
gpuPercent: number().optional(),
|
|
19933
|
+
gpuMemoryPercent: number().optional()
|
|
19464
19934
|
});
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19935
|
+
method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(KillProcessInputSchema, KillProcessResultSchema, {
|
|
19936
|
+
kind: "mutation",
|
|
19937
|
+
auth: "admin"
|
|
19938
|
+
}), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
|
|
19939
|
+
kind: "mutation",
|
|
19940
|
+
auth: "admin"
|
|
19941
|
+
});
|
|
19942
|
+
method(object({
|
|
19943
|
+
sourceUrl: string(),
|
|
19944
|
+
metadata: ModelConvertMetadataSchema,
|
|
19945
|
+
targets: array(ConvertTargetSchema).min(1).readonly(),
|
|
19946
|
+
calibrationRef: string().optional(),
|
|
19947
|
+
sessionId: string().optional()
|
|
19948
|
+
}), ConvertResultSchema, {
|
|
19949
|
+
kind: "mutation",
|
|
19950
|
+
auth: "admin",
|
|
19951
|
+
timeoutMs: 6e5
|
|
19952
|
+
});
|
|
19953
|
+
method(object({
|
|
19954
|
+
nodeId: string(),
|
|
19955
|
+
modelId: string(),
|
|
19956
|
+
format: _enum(MODEL_FORMATS),
|
|
19957
|
+
entry: ModelCatalogEntrySchema
|
|
19958
|
+
}), object({
|
|
19959
|
+
ok: boolean(),
|
|
19960
|
+
/** sha256 of the staged tarball (empty for a hub-local no-op). */
|
|
19961
|
+
sha256: string(),
|
|
19962
|
+
bytes: number(),
|
|
19963
|
+
/** The target node's modelsDir the artifact landed in. */
|
|
19964
|
+
path: string()
|
|
19965
|
+
}), {
|
|
19966
|
+
kind: "mutation",
|
|
19967
|
+
auth: "admin"
|
|
19469
19968
|
});
|
|
19470
|
-
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
19471
|
-
var TestResultSchema = SendResultSchema;
|
|
19472
|
-
var notificationOutputCapability = {
|
|
19473
|
-
name: "notification-output",
|
|
19474
|
-
scope: "system",
|
|
19475
|
-
mode: "collection",
|
|
19476
|
-
methods: {
|
|
19477
|
-
listTargetKinds: method(object({}), array(TargetKindSchema)),
|
|
19478
|
-
listTargets: method(object({}), array(TargetSchema)),
|
|
19479
|
-
discoverTargets: method(object({
|
|
19480
|
-
kind: string(),
|
|
19481
|
-
config: record(string(), unknown()).optional()
|
|
19482
|
-
}), array(DiscoveredTargetSchema)),
|
|
19483
|
-
send: method(object({
|
|
19484
|
-
targetId: string(),
|
|
19485
|
-
notification: NotificationSchema
|
|
19486
|
-
}), SendResultSchema, { kind: "mutation" }),
|
|
19487
|
-
testTarget: method(object({
|
|
19488
|
-
targetId: string(),
|
|
19489
|
-
sample: NotificationSchema.optional()
|
|
19490
|
-
}), TestResultSchema, { kind: "mutation" }),
|
|
19491
|
-
upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
|
|
19492
|
-
deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
|
|
19493
|
-
setTargetEnabled: method(object({
|
|
19494
|
-
targetId: string(),
|
|
19495
|
-
enabled: boolean()
|
|
19496
|
-
}), _void(), { kind: "mutation" })
|
|
19497
|
-
}
|
|
19498
|
-
};
|
|
19499
19969
|
/**
|
|
19500
|
-
*
|
|
19970
|
+
* `mqtt-broker` — broker-registry cap.
|
|
19501
19971
|
*
|
|
19502
|
-
*
|
|
19503
|
-
* (
|
|
19972
|
+
* NOT a pub/sub proxy. The cap exposes (a) a registry of configured
|
|
19973
|
+
* MQTT brokers (external + optionally an embedded `aedes`-backed one)
|
|
19974
|
+
* and (b) the connection details a consumer addon needs to spin up
|
|
19975
|
+
* its OWN `mqtt.js` client.
|
|
19504
19976
|
*
|
|
19505
|
-
*
|
|
19506
|
-
*
|
|
19507
|
-
*
|
|
19508
|
-
*
|
|
19509
|
-
*
|
|
19510
|
-
* FIRST persisted detection matching the conditions (per-track dedup,
|
|
19511
|
-
* `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
|
|
19512
|
-
* `delivery: 'track-end'` evaluates the finalized track record at close.
|
|
19513
|
-
* - DISPATCH stays behind `notification-output` (rules reference targets
|
|
19514
|
-
* by id; per-backend params are a passthrough blob capped by the
|
|
19515
|
-
* target kind's own caps/degrade engine).
|
|
19977
|
+
* Why: pub/sub routing over the system event-bus loses fidelity
|
|
19978
|
+
* (callback shape, QoS guarantees, will/retain semantics) and adds
|
|
19979
|
+
* refcount bookkeeping that addons would rather own themselves. The
|
|
19980
|
+
* canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
|
|
19981
|
+
* features anyway — give it the connection config, get out of the way.
|
|
19516
19982
|
*
|
|
19517
|
-
*
|
|
19518
|
-
*
|
|
19519
|
-
*
|
|
19520
|
-
*
|
|
19521
|
-
* windows, and the optional label/identity/plate matchers. User rules,
|
|
19522
|
-
* private zones, per-recipient fan-out and the wider condition table are
|
|
19523
|
-
* P2+ (see spec §7).
|
|
19983
|
+
* Consumer flow:
|
|
19984
|
+
* const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
|
|
19985
|
+
* const client = mqtt.connect(cfg.url, { username: cfg.username, … })
|
|
19986
|
+
* client.subscribe('zigbee2mqtt/+')
|
|
19524
19987
|
*
|
|
19525
|
-
*
|
|
19526
|
-
*
|
|
19527
|
-
*
|
|
19988
|
+
* Collection mode: multiple brokers (e.g. one local mosquitto + one
|
|
19989
|
+
* cloud bridge). The "embedded" entry (when present) is just another
|
|
19990
|
+
* broker in the registry — its lifecycle is owned by the addon that
|
|
19991
|
+
* spawned it.
|
|
19528
19992
|
*/
|
|
19993
|
+
var BrokerKindSchema = _enum(["external", "embedded"]);
|
|
19529
19994
|
/**
|
|
19530
|
-
*
|
|
19531
|
-
* The value maps 1:1 onto the evaluated record kind:
|
|
19532
|
-
* - `immediate` ↔ object-event persist (lowest-latency detection burst)
|
|
19533
|
-
* - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
|
|
19534
|
-
* - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
|
|
19535
|
-
* change of a LINKED device, one row per linked camera)
|
|
19536
|
-
* - `package-event` ↔ PackageDropDetector object-event insert (a `package`
|
|
19537
|
-
* delivery / pick-up)
|
|
19995
|
+
* Broker live-probe status.
|
|
19538
19996
|
*
|
|
19539
|
-
*
|
|
19540
|
-
*
|
|
19541
|
-
*
|
|
19542
|
-
*
|
|
19543
|
-
|
|
19544
|
-
var NcDeliverySchema = _enum([
|
|
19545
|
-
"immediate",
|
|
19546
|
-
"track-end",
|
|
19547
|
-
"device-event",
|
|
19548
|
-
"package-event"
|
|
19549
|
-
]);
|
|
19550
|
-
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
19551
|
-
var NcScheduleSchema = object({
|
|
19552
|
-
windows: array(object({
|
|
19553
|
-
/** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
|
|
19554
|
-
days: array(number().int().min(0).max(6)).min(1),
|
|
19555
|
-
startMinute: number().int().min(0).max(1439),
|
|
19556
|
-
endMinute: number().int().min(0).max(1439)
|
|
19557
|
-
})).min(1),
|
|
19558
|
-
/** IANA timezone; default = hub host timezone. */
|
|
19559
|
-
timezone: string().optional(),
|
|
19560
|
-
/** Active OUTSIDE the windows (e.g. "only outside business hours"). */
|
|
19561
|
-
invert: boolean().optional()
|
|
19562
|
-
});
|
|
19563
|
-
/** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
|
|
19564
|
-
var NcPlateMatcherSchema = object({
|
|
19565
|
-
values: array(string().min(1)).min(1),
|
|
19566
|
-
/** Max Levenshtein distance after normalization (uppercase alphanumeric). */
|
|
19567
|
-
maxDistance: number().int().min(0).max(3).default(1)
|
|
19568
|
-
});
|
|
19569
|
-
/**
|
|
19570
|
-
* Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
|
|
19571
|
-
* occupancy edge for a device — optionally narrowed to a single admin
|
|
19572
|
-
* `zoneId` and/or object `className`. `op` selects the edge/threshold:
|
|
19573
|
-
* - `became-occupied` (default) — count crossed 0 → ≥ `count`
|
|
19574
|
-
* - `became-free` — count crossed ≥ `count` → below it
|
|
19575
|
-
* - `>=` / `<=` — count is at/over or at/under `count`
|
|
19576
|
-
* `sustainSeconds` requires the condition hold continuously that long
|
|
19577
|
-
* before firing (debounces flicker; 0 = fire on the first matching edge).
|
|
19578
|
-
* Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
|
|
19579
|
-
* the condition never matches. Confirmed edge-state survives addon restarts
|
|
19580
|
-
* (declared SQLite collection, reseeded on boot).
|
|
19997
|
+
* - `connected` — last probe completed a clean CONNACK
|
|
19998
|
+
* - `disconnected` — no probe has run yet (cold cache)
|
|
19999
|
+
* - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
|
|
20000
|
+
* - `unreachable` — TCP connect timed out / refused
|
|
20001
|
+
* - `tls-error` — TLS handshake failed (cert / SNI / cipher)
|
|
19581
20002
|
*/
|
|
19582
|
-
var
|
|
19583
|
-
|
|
19584
|
-
|
|
19585
|
-
|
|
19586
|
-
|
|
19587
|
-
|
|
19588
|
-
|
|
19589
|
-
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
/**
|
|
19600
|
-
|
|
20003
|
+
var BrokerStatusSchema$1 = _enum([
|
|
20004
|
+
"connected",
|
|
20005
|
+
"disconnected",
|
|
20006
|
+
"auth-failed",
|
|
20007
|
+
"unreachable",
|
|
20008
|
+
"tls-error"
|
|
20009
|
+
]);
|
|
20010
|
+
var BrokerInfoSchema = object({
|
|
20011
|
+
id: string(),
|
|
20012
|
+
name: string(),
|
|
20013
|
+
url: string(),
|
|
20014
|
+
kind: BrokerKindSchema,
|
|
20015
|
+
status: BrokerStatusSchema$1,
|
|
20016
|
+
latencyMs: number().nullable(),
|
|
20017
|
+
error: string().optional(),
|
|
20018
|
+
/** Embedded brokers only: number of MQTT clients currently connected. */
|
|
20019
|
+
connectedClients: number().int().nonnegative().optional(),
|
|
20020
|
+
/** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
|
|
20021
|
+
lastCheckedAt: number().optional()
|
|
19601
20022
|
});
|
|
19602
20023
|
/**
|
|
19603
|
-
*
|
|
19604
|
-
*
|
|
20024
|
+
* Connection details — what a consumer needs to call
|
|
20025
|
+
* `mqtt.connect(url, options)`. We split URL + credentials so the
|
|
20026
|
+
* consumer can pass them as `mqtt.connect(url, { username, password })`
|
|
20027
|
+
* instead of stuffing creds into the URL (which leaks them into logs).
|
|
19605
20028
|
*/
|
|
19606
|
-
var
|
|
19607
|
-
|
|
19608
|
-
|
|
19609
|
-
|
|
19610
|
-
classes: array(string().min(1)).optional(),
|
|
19611
|
-
/** Veto classes — any overlap fails the rule. */
|
|
19612
|
-
classesExclude: array(string().min(1)).optional(),
|
|
19613
|
-
/** Minimum detection confidence 0–1 (fails when the record has none). */
|
|
19614
|
-
minConfidence: number().min(0).max(1).optional(),
|
|
19615
|
-
/** Admin zone membership over event `zones` / track `zonesVisited`. */
|
|
19616
|
-
zones: NcZoneConditionSchema.optional(),
|
|
19617
|
-
/** Veto zones — any hit fails the rule. */
|
|
19618
|
-
zonesExclude: array(string().min(1)).optional(),
|
|
19619
|
-
/**
|
|
19620
|
-
* Exact (case-insensitive) match on the record's collapsed `label`
|
|
19621
|
-
* (identity name / plate text / subclass).
|
|
19622
|
-
*/
|
|
19623
|
-
labelEquals: array(string().min(1)).optional(),
|
|
19624
|
-
/**
|
|
19625
|
-
* Identity matcher. P1 boundary: matched against the record's collapsed
|
|
19626
|
-
* `label` (the identity display name propagated by the face pipeline) —
|
|
19627
|
-
* identity-ID matching rides in P2 when identity ids reach the record.
|
|
19628
|
-
*/
|
|
19629
|
-
identities: array(string().min(1)).optional(),
|
|
19630
|
-
/** Fuzzy plate matcher against the record's `label` (plate text). */
|
|
19631
|
-
plates: NcPlateMatcherSchema.optional(),
|
|
19632
|
-
/**
|
|
19633
|
-
* Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
|
|
19634
|
-
* Same P1 boundary: matched against the record's collapsed `label` (the
|
|
19635
|
-
* identity display name). A record with NO label passes (nothing to
|
|
19636
|
-
* exclude), unlike the include variant which fails on an absent label.
|
|
19637
|
-
*/
|
|
19638
|
-
identitiesExclude: array(string().min(1)).optional(),
|
|
19639
|
-
/**
|
|
19640
|
-
* Minimum server-computed key-event importance in [0,1] (`Track.importance`).
|
|
19641
|
-
* TRACK-END only: importance is scored at track close, so it does not exist
|
|
19642
|
-
* at immediate / object-event evaluation time (see catalog `appliesTo`). At
|
|
19643
|
-
* close the value is threaded via the close-time info (the `Track` clone is
|
|
19644
|
-
* captured before the DB row is updated, so it would otherwise read stale).
|
|
19645
|
-
* Fails when the record carries no importance (never guess quality — the
|
|
19646
|
-
* `minConfidence` precedent). MVP cut: a single scalar threshold.
|
|
19647
|
-
*/
|
|
19648
|
-
minImportance: number().min(0).max(1).optional(),
|
|
19649
|
-
/**
|
|
19650
|
-
* Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
|
|
19651
|
-
* TRACK-END only: an `immediate` / object-event subject has no closed
|
|
19652
|
-
* lifespan, so a dwell condition never matches immediate delivery
|
|
19653
|
-
* (documented choice — the object-event record carries no `firstSeen`,
|
|
19654
|
-
* so dwell cannot be computed from what the subject actually carries).
|
|
19655
|
-
*/
|
|
19656
|
-
minDwellSeconds: number().min(0).optional(),
|
|
19657
|
-
/**
|
|
19658
|
-
* Detection provenance filter. `any` (default / absent) matches every
|
|
19659
|
-
* source; otherwise the subject's source must equal it. Legacy records
|
|
19660
|
-
* with no stamped source are treated as `pipeline`. The union spans both
|
|
19661
|
-
* record kinds — object events carry `pipeline` | `onboard`, synthetic
|
|
19662
|
-
* tracks carry `sensor`.
|
|
19663
|
-
*/
|
|
19664
|
-
source: _enum([
|
|
19665
|
-
"pipeline",
|
|
19666
|
-
"onboard",
|
|
19667
|
-
"sensor",
|
|
19668
|
-
"any"
|
|
19669
|
-
]).optional(),
|
|
19670
|
-
/**
|
|
19671
|
-
* Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
|
|
19672
|
-
* detector `minConfidence` (that gates the object-detection score; this
|
|
19673
|
-
* gates the recognition/OCR match score). Fails when the subject carries
|
|
19674
|
-
* no label-match confidence (never guess). TRACK-END only: the confidence
|
|
19675
|
-
* lives on the recognition result and reaches the subject at track close.
|
|
19676
|
-
*
|
|
19677
|
-
* What it measures precisely (plumbed at track close — the closer threads
|
|
19678
|
-
* the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
|
|
19679
|
-
* `importance`): the BEST recognition match confidence observed for the
|
|
19680
|
-
* label the track carries at close — for a face, the peak cosine similarity
|
|
19681
|
-
* of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
|
|
19682
|
-
* for a plate, the peak OCR read score of the best-held plate
|
|
19683
|
-
* (`plateText.confidence`). When BOTH a face and a plate were recognized on
|
|
19684
|
-
* one track the higher of the two is used. A track that ended with no
|
|
19685
|
-
* confident identity/plate match carries no value, so the condition fails
|
|
19686
|
-
* closed for it (an un-recognized subject).
|
|
19687
|
-
*/
|
|
19688
|
-
minLabelConfidence: number().min(0).max(1).optional(),
|
|
19689
|
-
/**
|
|
19690
|
-
* DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
|
|
19691
|
-
* e.g. a doorbell `press` / `press_long`) — matched case-insensitively
|
|
19692
|
-
* against the token carried on the device-event subject (extracted from the
|
|
19693
|
-
* event-emitter runtime slice's `lastEvent.eventType`). Fails when the
|
|
19694
|
-
* subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
|
|
19695
|
-
* eventType, so gate those with {@link sensorKinds} instead.
|
|
19696
|
-
*/
|
|
19697
|
-
eventTypeTokens: array(string().min(1)).optional(),
|
|
19698
|
-
/**
|
|
19699
|
-
* DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
|
|
19700
|
-
* `contact`, `button`, `device-event`) — matched against the persisted
|
|
19701
|
-
* `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
|
|
19702
|
-
*/
|
|
19703
|
-
sensorKinds: array(string().min(1)).optional(),
|
|
19704
|
-
/**
|
|
19705
|
-
* PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
|
|
19706
|
-
* (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
|
|
19707
|
-
* when the subject's phase does not match (a subject always carries a phase
|
|
19708
|
-
* on the package-event trigger).
|
|
19709
|
-
*/
|
|
19710
|
-
packagePhase: _enum([
|
|
19711
|
-
"delivered",
|
|
19712
|
-
"picked-up",
|
|
19713
|
-
"both"
|
|
19714
|
-
]).optional(),
|
|
19715
|
-
/**
|
|
19716
|
-
* PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
|
|
19717
|
-
* (MaskShape vocabulary). A record passes when its bbox overlaps ANY
|
|
19718
|
-
* listed polygon (ZoneEngine membership semantics). Evaluated only when
|
|
19719
|
-
* the subject carries a bbox; absent bbox ⇒ the condition FAILS.
|
|
19720
|
-
*/
|
|
19721
|
-
customZones: array(MaskPolygonShapeSchema).optional(),
|
|
20029
|
+
var BrokerConnectionDetailsSchema = object({
|
|
20030
|
+
url: string(),
|
|
20031
|
+
username: string().optional(),
|
|
20032
|
+
password: string().optional(),
|
|
19722
20033
|
/**
|
|
19723
|
-
*
|
|
19724
|
-
* (
|
|
19725
|
-
*
|
|
19726
|
-
*
|
|
20034
|
+
* Suggested prefix for `clientId`. Each consumer should suffix this
|
|
20035
|
+
* with its own discriminator (addon id, instance id) so reconnects
|
|
20036
|
+
* don't kick each other off (MQTT spec: clientId must be unique per
|
|
20037
|
+
* broker).
|
|
19727
20038
|
*/
|
|
19728
|
-
|
|
20039
|
+
clientIdPrefix: string().optional()
|
|
19729
20040
|
});
|
|
19730
|
-
|
|
19731
|
-
|
|
19732
|
-
|
|
19733
|
-
|
|
19734
|
-
|
|
19735
|
-
|
|
19736
|
-
|
|
19737
|
-
|
|
19738
|
-
|
|
19739
|
-
|
|
20041
|
+
var AddBrokerInputSchema = object({
|
|
20042
|
+
name: string().min(1),
|
|
20043
|
+
url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
|
|
20044
|
+
username: string().optional(),
|
|
20045
|
+
password: string().optional(),
|
|
20046
|
+
clientIdPrefix: string().optional()
|
|
20047
|
+
});
|
|
20048
|
+
var AddBrokerResultSchema = object({ id: string() });
|
|
20049
|
+
var IdInputSchema = object({ id: string() });
|
|
20050
|
+
var TestResultSchema$1 = discriminatedUnion("ok", [object({
|
|
20051
|
+
ok: literal(true),
|
|
20052
|
+
latencyMs: number()
|
|
20053
|
+
}), object({
|
|
20054
|
+
ok: literal(false),
|
|
20055
|
+
error: string()
|
|
20056
|
+
})]);
|
|
20057
|
+
var StartEmbeddedInputSchema = object({
|
|
20058
|
+
port: number().int().min(1).max(65535).default(1883),
|
|
20059
|
+
/** Allow anonymous connect (no username/password). Default: false. */
|
|
20060
|
+
allowAnonymous: boolean().default(false),
|
|
20061
|
+
/** Optional shared username/password for clients. */
|
|
20062
|
+
username: string().optional(),
|
|
20063
|
+
password: string().optional()
|
|
20064
|
+
});
|
|
20065
|
+
var StartEmbeddedResultSchema = object({
|
|
20066
|
+
id: string(),
|
|
20067
|
+
url: string()
|
|
20068
|
+
});
|
|
20069
|
+
var StatusSchema = object({
|
|
20070
|
+
brokerCount: number(),
|
|
20071
|
+
embeddedRunning: boolean()
|
|
20072
|
+
});
|
|
20073
|
+
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);
|
|
20074
|
+
var NetworkEndpointSchema = object({
|
|
20075
|
+
url: string(),
|
|
20076
|
+
hostname: string(),
|
|
20077
|
+
port: number(),
|
|
20078
|
+
protocol: _enum(["http", "https"])
|
|
20079
|
+
});
|
|
20080
|
+
var NetworkAccessStatusSchema = object({
|
|
20081
|
+
connected: boolean(),
|
|
20082
|
+
endpoint: NetworkEndpointSchema.nullable(),
|
|
20083
|
+
error: string().optional()
|
|
19740
20084
|
});
|
|
19741
20085
|
/**
|
|
19742
|
-
*
|
|
19743
|
-
*
|
|
19744
|
-
*
|
|
19745
|
-
*
|
|
19746
|
-
*
|
|
19747
|
-
*
|
|
19748
|
-
* `keyFrame`, then no attachment — never delaying the send. The matched
|
|
19749
|
-
* condition summary is frozen on the outbox row at enqueue (like the rule
|
|
19750
|
-
* name), so the choice never drifts from the record that fired it.
|
|
19751
|
-
* - `keyFrame` — the clean scene frame (no subject box).
|
|
19752
|
-
* - `none` — no attachment.
|
|
20086
|
+
* Optional, richer endpoint shape returned by providers that expose
|
|
20087
|
+
* MORE than one ingress concurrently (Tailscale Ingress with mixed
|
|
20088
|
+
* serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
|
|
20089
|
+
* the originating provider config (mode + sourcePort) so the
|
|
20090
|
+
* orchestrator UI can label rows distinctly. Providers that expose only
|
|
20091
|
+
* one endpoint just omit `listEndpoints` from their provider impl.
|
|
19753
20092
|
*/
|
|
19754
|
-
var
|
|
19755
|
-
|
|
19756
|
-
|
|
19757
|
-
|
|
19758
|
-
"none"
|
|
19759
|
-
]).default("best") });
|
|
19760
|
-
/** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
|
|
19761
|
-
var NcThrottleSchema = object({
|
|
19762
|
-
cooldownSec: number().int().min(0).max(86400).default(60),
|
|
19763
|
-
/** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
|
|
19764
|
-
scope: _enum(["rule", "rule-device"]).default("rule-device")
|
|
19765
|
-
});
|
|
19766
|
-
/** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
|
|
19767
|
-
var NcRuleInputSchema = object({
|
|
19768
|
-
name: string().min(1).max(200),
|
|
19769
|
-
enabled: boolean().default(true),
|
|
19770
|
-
delivery: NcDeliverySchema,
|
|
19771
|
-
conditions: NcConditionsSchema.default({}),
|
|
19772
|
-
schedule: NcScheduleSchema.optional(),
|
|
19773
|
-
targets: array(NcRuleTargetSchema).min(1),
|
|
19774
|
-
media: NcMediaPolicySchema.default({ attach: "best" }),
|
|
19775
|
-
throttle: NcThrottleSchema.default({
|
|
19776
|
-
cooldownSec: 60,
|
|
19777
|
-
scope: "rule-device"
|
|
19778
|
-
}),
|
|
19779
|
-
/** `{{var}}` templating over camera/class/label/zones/confidence/time. */
|
|
19780
|
-
template: object({
|
|
19781
|
-
title: string().max(500).optional(),
|
|
19782
|
-
body: string().max(2e3).optional()
|
|
19783
|
-
}).optional(),
|
|
19784
|
-
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
19785
|
-
priority: number().int().min(1).max(5).default(3),
|
|
19786
|
-
/**
|
|
19787
|
-
* Ownership/visibility key. Absent = admin/global rule (unchanged legacy
|
|
19788
|
-
* behaviour, visible to all, read-only in the viewer). Present = personal
|
|
19789
|
-
* rule owned by this userId. Server-stamped; never trusted from a client.
|
|
20093
|
+
var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
20094
|
+
/**
|
|
20095
|
+
* Stable id within the provider — typically `<mode>-<sourcePort>` so
|
|
20096
|
+
* the orchestrator can dedupe across `listEndpoints` polls.
|
|
19790
20097
|
*/
|
|
19791
|
-
|
|
20098
|
+
id: string(),
|
|
20099
|
+
/** Operator-facing label (mirrors `MeshEndpoint.label`). */
|
|
20100
|
+
label: string(),
|
|
20101
|
+
/** Optional provider-specific mode tag, used for icon/colour in admin UI. */
|
|
20102
|
+
mode: string().optional(),
|
|
20103
|
+
/** Originating local port the ingress fronts (informational). */
|
|
20104
|
+
sourcePort: number().optional()
|
|
19792
20105
|
});
|
|
20106
|
+
method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
|
|
19793
20107
|
/**
|
|
19794
|
-
*
|
|
19795
|
-
*
|
|
19796
|
-
*
|
|
19797
|
-
*
|
|
19798
|
-
*
|
|
19799
|
-
*
|
|
19800
|
-
* `
|
|
20108
|
+
* notification-output — canonical, capability-gated notification delivery.
|
|
20109
|
+
*
|
|
20110
|
+
* Apprise-derived model (see
|
|
20111
|
+
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
20112
|
+
* callers emit ONE canonical `Notification`; each provider declares a
|
|
20113
|
+
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
20114
|
+
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
20115
|
+
* message to what the kind supports — callers never special-case a service.
|
|
20116
|
+
*
|
|
20117
|
+
* DESIGN DECISIONS (locked):
|
|
20118
|
+
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
20119
|
+
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
20120
|
+
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
20121
|
+
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
20122
|
+
* alternative would fork the UI per addon and cannot host the
|
|
20123
|
+
* discovery→adopt flow.
|
|
20124
|
+
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
20125
|
+
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
20126
|
+
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
20127
|
+
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
20128
|
+
* `addonId` the generated collection router extracts from the call input.
|
|
20129
|
+
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
20130
|
+
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
20131
|
+
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
20132
|
+
* base64 fallback needed.
|
|
20133
|
+
*
|
|
20134
|
+
* TODO (deferred, closed-set change — separate decision): add
|
|
20135
|
+
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
20136
|
+
* admin "Integrations" page.
|
|
19801
20137
|
*/
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
20138
|
+
/**
|
|
20139
|
+
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
20140
|
+
* adapter picks what it supports and the degrade engine filters the rest.
|
|
20141
|
+
*/
|
|
20142
|
+
var AttachmentMediaTypeSchema = _enum([
|
|
20143
|
+
"image",
|
|
20144
|
+
"video",
|
|
20145
|
+
"gif",
|
|
20146
|
+
"audio",
|
|
20147
|
+
"icon"
|
|
20148
|
+
]);
|
|
20149
|
+
/**
|
|
20150
|
+
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
20151
|
+
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
20152
|
+
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
20153
|
+
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
20154
|
+
*/
|
|
20155
|
+
var AttachmentSchema = object({
|
|
20156
|
+
mediaType: AttachmentMediaTypeSchema,
|
|
20157
|
+
url: string().optional(),
|
|
20158
|
+
bytes: _instanceof(Uint8Array).optional(),
|
|
20159
|
+
mime: string().optional(),
|
|
20160
|
+
name: string().optional()
|
|
20161
|
+
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
20162
|
+
var NotificationFormatSchema = _enum([
|
|
20163
|
+
"text",
|
|
20164
|
+
"markdown",
|
|
20165
|
+
"html"
|
|
20166
|
+
]);
|
|
20167
|
+
/** A single tap-through action button. */
|
|
20168
|
+
var NotificationActionSchema = object({
|
|
19805
20169
|
id: string(),
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
createdAt: number(),
|
|
19809
|
-
updatedAt: number(),
|
|
19810
|
-
/**
|
|
19811
|
-
* Per-target opt-out set. A targetId here is suppressed for THIS rule at
|
|
19812
|
-
* send time. Only a target's OWNER may add/remove its id (server-checked
|
|
19813
|
-
* in `nc.setRuleTargetEnabled`). Defaults to empty.
|
|
19814
|
-
*/
|
|
19815
|
-
disabledTargetIds: array(string()).default([])
|
|
20170
|
+
label: string(),
|
|
20171
|
+
url: string().optional()
|
|
19816
20172
|
});
|
|
19817
|
-
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
|
|
19827
|
-
|
|
19828
|
-
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
20173
|
+
/**
|
|
20174
|
+
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
20175
|
+
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
20176
|
+
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
20177
|
+
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
20178
|
+
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
20179
|
+
* `priority` for that one target.
|
|
20180
|
+
*/
|
|
20181
|
+
var NotificationSchema = object({
|
|
20182
|
+
body: string(),
|
|
20183
|
+
title: string().optional(),
|
|
20184
|
+
format: NotificationFormatSchema.default("text"),
|
|
20185
|
+
priority: number().int().min(1).max(5).default(3),
|
|
20186
|
+
level: string().optional(),
|
|
20187
|
+
attachments: array(AttachmentSchema).optional(),
|
|
20188
|
+
clickUrl: string().optional(),
|
|
20189
|
+
actions: array(NotificationActionSchema).optional(),
|
|
20190
|
+
sound: string().optional(),
|
|
20191
|
+
ttl: number().optional(),
|
|
20192
|
+
tag: string().optional(),
|
|
20193
|
+
deviceId: number().optional(),
|
|
20194
|
+
eventId: string().optional(),
|
|
20195
|
+
metadata: record(string(), unknown()).optional()
|
|
19832
20196
|
});
|
|
19833
|
-
|
|
19834
|
-
|
|
20197
|
+
/** One declared native severity/priority level for a kind. */
|
|
20198
|
+
var TargetKindLevelSchema = object({
|
|
19835
20199
|
id: string(),
|
|
19836
|
-
group: _enum([
|
|
19837
|
-
"scope",
|
|
19838
|
-
"class",
|
|
19839
|
-
"zones",
|
|
19840
|
-
"quality",
|
|
19841
|
-
"label",
|
|
19842
|
-
"schedule",
|
|
19843
|
-
"device",
|
|
19844
|
-
"package",
|
|
19845
|
-
"occupancy"
|
|
19846
|
-
]),
|
|
19847
20200
|
label: string(),
|
|
19848
|
-
/**
|
|
19849
|
-
|
|
19850
|
-
|
|
19851
|
-
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
"schedule",
|
|
19858
|
-
"plateMatcher",
|
|
19859
|
-
"packagePhase",
|
|
19860
|
-
"polygonDraw",
|
|
19861
|
-
"occupancy"
|
|
19862
|
-
]),
|
|
19863
|
-
operator: _enum([
|
|
19864
|
-
"in",
|
|
19865
|
-
"notIn",
|
|
19866
|
-
"anyOf",
|
|
19867
|
-
"allOf",
|
|
19868
|
-
"gte",
|
|
19869
|
-
"fuzzyIn",
|
|
19870
|
-
"withinSchedule"
|
|
19871
|
-
]),
|
|
19872
|
-
/** Which delivery kinds the condition applies to. */
|
|
19873
|
-
appliesTo: array(NcDeliverySchema),
|
|
19874
|
-
phase: string(),
|
|
20201
|
+
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
20202
|
+
ordinal: number().int().min(1).max(5).nullable(),
|
|
20203
|
+
flags: object({
|
|
20204
|
+
critical: boolean().optional(),
|
|
20205
|
+
silent: boolean().optional(),
|
|
20206
|
+
noPush: boolean().optional()
|
|
20207
|
+
}).optional(),
|
|
20208
|
+
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
20209
|
+
requires: array(string()).optional(),
|
|
19875
20210
|
description: string().optional()
|
|
19876
20211
|
});
|
|
20212
|
+
/** The full capability block consulted before dispatch. */
|
|
20213
|
+
var TargetKindCapsSchema = object({
|
|
20214
|
+
attachments: object({
|
|
20215
|
+
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
20216
|
+
mode: _enum([
|
|
20217
|
+
"url",
|
|
20218
|
+
"bytes",
|
|
20219
|
+
"both"
|
|
20220
|
+
]),
|
|
20221
|
+
max: number().int().nonnegative(),
|
|
20222
|
+
maxBytes: number().int().positive().optional()
|
|
20223
|
+
}),
|
|
20224
|
+
/** Max action buttons (0 = none). */
|
|
20225
|
+
actions: number().int().nonnegative(),
|
|
20226
|
+
levels: array(TargetKindLevelSchema),
|
|
20227
|
+
format: array(NotificationFormatSchema),
|
|
20228
|
+
clickUrl: boolean(),
|
|
20229
|
+
sound: boolean(),
|
|
20230
|
+
ttl: boolean(),
|
|
20231
|
+
bodyMaxLen: number().int().positive()
|
|
20232
|
+
});
|
|
19877
20233
|
/**
|
|
19878
|
-
*
|
|
19879
|
-
*
|
|
19880
|
-
*
|
|
19881
|
-
*
|
|
19882
|
-
*
|
|
19883
|
-
* backend rejection / a deleted target (terminal; carries
|
|
19884
|
-
* the failure `error`)
|
|
19885
|
-
*
|
|
19886
|
-
* P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
|
|
19887
|
-
* user dimension (quiet hours / snooze) and are additive when they land.
|
|
20234
|
+
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
20235
|
+
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
20236
|
+
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
20237
|
+
* the union is large and not meant for runtime validation here; the exported
|
|
20238
|
+
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
19888
20239
|
*/
|
|
19889
|
-
var
|
|
19890
|
-
|
|
19891
|
-
|
|
19892
|
-
|
|
19893
|
-
|
|
19894
|
-
/**
|
|
19895
|
-
|
|
19896
|
-
|
|
19897
|
-
|
|
19898
|
-
|
|
19899
|
-
|
|
19900
|
-
|
|
19901
|
-
|
|
19902
|
-
|
|
19903
|
-
|
|
19904
|
-
|
|
19905
|
-
|
|
19906
|
-
|
|
19907
|
-
|
|
20240
|
+
var ConfigSchemaPassthrough = unknown();
|
|
20241
|
+
var TargetKindSchema = object({
|
|
20242
|
+
kind: string(),
|
|
20243
|
+
label: string(),
|
|
20244
|
+
icon: string(),
|
|
20245
|
+
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
20246
|
+
addonId: string(),
|
|
20247
|
+
/**
|
|
20248
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
20249
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
20250
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
20251
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
20252
|
+
*
|
|
20253
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
20254
|
+
* client, and a native client joins it onto its own hub base.
|
|
20255
|
+
*
|
|
20256
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
20257
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
20258
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
20259
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
20260
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
20261
|
+
*/
|
|
20262
|
+
iconUrl: string().optional(),
|
|
20263
|
+
/**
|
|
20264
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
20265
|
+
*
|
|
20266
|
+
* The server knows this and therefore says it, because the client cannot
|
|
20267
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
20268
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
20269
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
20270
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
20271
|
+
*
|
|
20272
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
20273
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
20274
|
+
* its raster path, which is the safe default for an unknown image.
|
|
20275
|
+
*/
|
|
20276
|
+
iconMediaType: string().optional(),
|
|
20277
|
+
configSchema: ConfigSchemaPassthrough,
|
|
20278
|
+
supportsDiscovery: boolean(),
|
|
20279
|
+
caps: TargetKindCapsSchema
|
|
19908
20280
|
});
|
|
19909
20281
|
/**
|
|
19910
|
-
*
|
|
19911
|
-
*
|
|
19912
|
-
*
|
|
19913
|
-
* The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
|
|
19914
|
-
* `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
|
|
19915
|
-
* (fire) / `updatedAt` (last transition), `status` + `error` are the
|
|
19916
|
-
* lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
|
|
19917
|
-
* enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
|
|
19918
|
-
* P1 (admin scope only).
|
|
20282
|
+
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
20283
|
+
* (return a presence marker only) when serving `listTargets` — never
|
|
20284
|
+
* round-trip a stored secret to the UI.
|
|
19919
20285
|
*/
|
|
19920
|
-
var
|
|
19921
|
-
/** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
|
|
20286
|
+
var TargetSchema = object({
|
|
19922
20287
|
id: string(),
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
|
|
19928
|
-
targetId: string(),
|
|
19929
|
-
deviceId: number(),
|
|
19930
|
-
recordKind: NcHistoryRecordKindSchema,
|
|
19931
|
-
/** Event / track ref of the evaluated record (§3.2 `eventRef`). */
|
|
19932
|
-
recordId: string(),
|
|
19933
|
-
/** Present for track-scoped deliveries (object-event / track-end). */
|
|
19934
|
-
trackId: string().optional(),
|
|
19935
|
-
status: NcHistoryStatusSchema,
|
|
19936
|
-
/** Delivery attempts made so far. */
|
|
19937
|
-
attempts: number().int(),
|
|
19938
|
-
/** Fire time (outbox enqueue). */
|
|
19939
|
-
createdAt: number(),
|
|
19940
|
-
/** Last transition time (terminal for sent / dead). */
|
|
19941
|
-
updatedAt: number(),
|
|
19942
|
-
/** Failure detail — present on a `dead` row. */
|
|
19943
|
-
error: string().optional(),
|
|
19944
|
-
subject: NcHistorySubjectSchema
|
|
20288
|
+
name: string(),
|
|
20289
|
+
kind: string(),
|
|
20290
|
+
addonId: string(),
|
|
20291
|
+
enabled: boolean(),
|
|
20292
|
+
config: record(string(), unknown())
|
|
19945
20293
|
});
|
|
19946
|
-
/**
|
|
19947
|
-
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
19951
|
-
*/
|
|
19952
|
-
var NcHistoryFilterSchema = object({
|
|
19953
|
-
ruleId: string().optional(),
|
|
19954
|
-
deviceId: number().optional(),
|
|
19955
|
-
status: NcHistoryStatusSchema.optional(),
|
|
19956
|
-
since: number().optional(),
|
|
19957
|
-
until: number().optional(),
|
|
19958
|
-
limit: number().int().min(1).max(500).default(100)
|
|
20294
|
+
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
20295
|
+
var DiscoveredTargetSchema = object({
|
|
20296
|
+
kind: string(),
|
|
20297
|
+
suggestedName: string(),
|
|
20298
|
+
config: record(string(), unknown())
|
|
19959
20299
|
});
|
|
19960
|
-
|
|
19961
|
-
|
|
19962
|
-
|
|
19963
|
-
|
|
19964
|
-
|
|
19965
|
-
|
|
19966
|
-
|
|
19967
|
-
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
19971
|
-
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19975
|
-
|
|
19976
|
-
|
|
19977
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
}),
|
|
20300
|
+
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
20301
|
+
var RenderedAsSchema = object({
|
|
20302
|
+
level: string(),
|
|
20303
|
+
format: NotificationFormatSchema,
|
|
20304
|
+
attachmentsSent: number().int().nonnegative(),
|
|
20305
|
+
actionsSent: number().int().nonnegative(),
|
|
20306
|
+
truncated: boolean(),
|
|
20307
|
+
dropped: array(string())
|
|
20308
|
+
});
|
|
20309
|
+
var SendResultSchema = object({
|
|
20310
|
+
success: boolean(),
|
|
20311
|
+
error: string().optional(),
|
|
20312
|
+
renderedAs: RenderedAsSchema.optional()
|
|
20313
|
+
});
|
|
20314
|
+
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
20315
|
+
var TestResultSchema = SendResultSchema;
|
|
20316
|
+
var notificationOutputCapability = {
|
|
20317
|
+
name: "notification-output",
|
|
20318
|
+
scope: "system",
|
|
20319
|
+
mode: "collection",
|
|
20320
|
+
methods: {
|
|
20321
|
+
listTargetKinds: method(object({}), array(TargetKindSchema)),
|
|
20322
|
+
listTargets: method(object({}), array(TargetSchema)),
|
|
20323
|
+
discoverTargets: method(object({
|
|
20324
|
+
kind: string(),
|
|
20325
|
+
config: record(string(), unknown()).optional()
|
|
20326
|
+
}), array(DiscoveredTargetSchema)),
|
|
20327
|
+
send: method(object({
|
|
20328
|
+
targetId: string(),
|
|
20329
|
+
notification: NotificationSchema
|
|
20330
|
+
}), SendResultSchema, { kind: "mutation" }),
|
|
20331
|
+
testTarget: method(object({
|
|
20332
|
+
targetId: string(),
|
|
20333
|
+
sample: NotificationSchema.optional()
|
|
20334
|
+
}), TestResultSchema, { kind: "mutation" }),
|
|
20335
|
+
upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
|
|
20336
|
+
deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
|
|
20337
|
+
setTargetEnabled: method(object({
|
|
20338
|
+
targetId: string(),
|
|
20339
|
+
enabled: boolean()
|
|
20340
|
+
}), _void(), { kind: "mutation" })
|
|
20341
|
+
}
|
|
20342
|
+
};
|
|
19987
20343
|
/**
|
|
19988
20344
|
* Zod schemas for persisted record types.
|
|
19989
20345
|
*
|
|
@@ -20663,6 +21019,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
20663
21019
|
}), method(object({ deviceId: number() }), EventPruneCountsSchema, {
|
|
20664
21020
|
kind: "mutation",
|
|
20665
21021
|
auth: "admin"
|
|
21022
|
+
}), method(RelocateMediaInputSchema, object({ jobId: string() }), {
|
|
21023
|
+
kind: "mutation",
|
|
21024
|
+
auth: "admin"
|
|
21025
|
+
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
21026
|
+
kind: "query",
|
|
21027
|
+
auth: "admin"
|
|
21028
|
+
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
21029
|
+
kind: "mutation",
|
|
21030
|
+
auth: "admin"
|
|
20666
21031
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
20667
21032
|
kind: "query",
|
|
20668
21033
|
auth: "admin"
|
|
@@ -23231,6 +23596,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
23231
23596
|
*/
|
|
23232
23597
|
priority: number()
|
|
23233
23598
|
})).readonly() });
|
|
23599
|
+
/**
|
|
23600
|
+
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
23601
|
+
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
23602
|
+
* what AUTO currently picks, so the UI can show the effective value either way.
|
|
23603
|
+
*/
|
|
23604
|
+
var NotificationEndpointSchema = object({
|
|
23605
|
+
/** The operator's explicit choice, or null for AUTO. */
|
|
23606
|
+
baseUrl: string().nullable(),
|
|
23607
|
+
/** What the ranking currently resolves to (null when nothing is reachable). */
|
|
23608
|
+
resolved: string().nullable()
|
|
23609
|
+
});
|
|
23234
23610
|
var AllowedAddressesSchema = object({
|
|
23235
23611
|
/**
|
|
23236
23612
|
* Allowlist of interface addresses operators have explicitly opted
|
|
@@ -23253,7 +23629,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
23253
23629
|
* to avoid mixed-content blocks in the browser. The public
|
|
23254
23630
|
* tunnel always emits `https://` regardless. */
|
|
23255
23631
|
scheme: _enum(["http", "https"]).optional()
|
|
23256
|
-
}), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
|
|
23632
|
+
}), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
|
|
23257
23633
|
/**
|
|
23258
23634
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
23259
23635
|
*
|
|
@@ -24052,7 +24428,12 @@ var RecordingDeviceUsageSchema = object({
|
|
|
24052
24428
|
var RecordingLocationUsageSchema = object({
|
|
24053
24429
|
/** StorageLocation id; null for the legacy/degraded single-root fallback. */
|
|
24054
24430
|
locationId: string().nullable(),
|
|
24055
|
-
/**
|
|
24431
|
+
/** Every location id sharing this row's PHYSICAL volume (aliases). One row
|
|
24432
|
+
* is emitted per physical disk (2026-07-29): two locations on one root
|
|
24433
|
+
* previously rendered as two identical "disks" with a nonsensical used
|
|
24434
|
+
* split — hydrate attribution across aliases is arbitrary by nature. */
|
|
24435
|
+
locationIds: array(string()).optional(),
|
|
24436
|
+
/** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
|
|
24056
24437
|
usedBytes: number(),
|
|
24057
24438
|
/** Free bytes on the location's volume; null when capacity is unknown (remote). */
|
|
24058
24439
|
availableBytes: number().nullable(),
|
|
@@ -24164,6 +24545,44 @@ method(object({
|
|
|
24164
24545
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
24165
24546
|
kind: "query",
|
|
24166
24547
|
auth: "admin"
|
|
24548
|
+
}), method(object({
|
|
24549
|
+
deviceId: number(),
|
|
24550
|
+
aroundMs: number(),
|
|
24551
|
+
preRollSec: number().min(0).max(30).default(2),
|
|
24552
|
+
postRollSec: number().min(0).max(30).default(5),
|
|
24553
|
+
maxWidth: number().int().min(120).max(1280).default(480),
|
|
24554
|
+
fps: number().int().min(1).max(15).default(5)
|
|
24555
|
+
}), object({
|
|
24556
|
+
gifBase64: string(),
|
|
24557
|
+
fromMs: number(),
|
|
24558
|
+
toMs: number()
|
|
24559
|
+
}), {
|
|
24560
|
+
kind: "mutation",
|
|
24561
|
+
auth: "admin"
|
|
24562
|
+
}), method(object({
|
|
24563
|
+
deviceId: number(),
|
|
24564
|
+
aroundMs: number(),
|
|
24565
|
+
preRollSec: number().min(0).max(30).default(3),
|
|
24566
|
+
postRollSec: number().min(0).max(30).default(7),
|
|
24567
|
+
maxWidth: number().int().min(160).max(1920).default(640)
|
|
24568
|
+
}), object({
|
|
24569
|
+
clipBase64: string(),
|
|
24570
|
+
mime: string(),
|
|
24571
|
+
fromMs: number(),
|
|
24572
|
+
toMs: number(),
|
|
24573
|
+
bytes: number().int()
|
|
24574
|
+
}), {
|
|
24575
|
+
kind: "mutation",
|
|
24576
|
+
auth: "admin"
|
|
24577
|
+
}), method(RelocateFootageInputSchema, object({ jobId: string() }), {
|
|
24578
|
+
kind: "mutation",
|
|
24579
|
+
auth: "admin"
|
|
24580
|
+
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
24581
|
+
kind: "query",
|
|
24582
|
+
auth: "admin"
|
|
24583
|
+
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
24584
|
+
kind: "mutation",
|
|
24585
|
+
auth: "admin"
|
|
24167
24586
|
});
|
|
24168
24587
|
/**
|
|
24169
24588
|
* `recordingExport` cap — render a footage time range into a single downloadable
|
|
@@ -25077,6 +25496,12 @@ Object.freeze({
|
|
|
25077
25496
|
addonId: null,
|
|
25078
25497
|
access: "delete"
|
|
25079
25498
|
},
|
|
25499
|
+
"backup.deleteSchedule": {
|
|
25500
|
+
capName: "backup",
|
|
25501
|
+
capScope: "system",
|
|
25502
|
+
addonId: null,
|
|
25503
|
+
access: "delete"
|
|
25504
|
+
},
|
|
25080
25505
|
"backup.getEntries": {
|
|
25081
25506
|
capName: "backup",
|
|
25082
25507
|
capScope: "system",
|
|
@@ -25107,6 +25532,12 @@ Object.freeze({
|
|
|
25107
25532
|
addonId: null,
|
|
25108
25533
|
access: "view"
|
|
25109
25534
|
},
|
|
25535
|
+
"backup.listSchedules": {
|
|
25536
|
+
capName: "backup",
|
|
25537
|
+
capScope: "system",
|
|
25538
|
+
addonId: null,
|
|
25539
|
+
access: "view"
|
|
25540
|
+
},
|
|
25110
25541
|
"backup.previewSchedule": {
|
|
25111
25542
|
capName: "backup",
|
|
25112
25543
|
capScope: "system",
|
|
@@ -25131,6 +25562,12 @@ Object.freeze({
|
|
|
25131
25562
|
addonId: null,
|
|
25132
25563
|
access: "create"
|
|
25133
25564
|
},
|
|
25565
|
+
"backup.upsertSchedule": {
|
|
25566
|
+
capName: "backup",
|
|
25567
|
+
capScope: "system",
|
|
25568
|
+
addonId: null,
|
|
25569
|
+
access: "create"
|
|
25570
|
+
},
|
|
25134
25571
|
"battery.wakeForStream": {
|
|
25135
25572
|
capName: "battery",
|
|
25136
25573
|
capScope: "device",
|
|
@@ -26631,6 +27068,12 @@ Object.freeze({
|
|
|
26631
27068
|
addonId: null,
|
|
26632
27069
|
access: "view"
|
|
26633
27070
|
},
|
|
27071
|
+
"localNetwork.getNotificationEndpoint": {
|
|
27072
|
+
capName: "local-network",
|
|
27073
|
+
capScope: "system",
|
|
27074
|
+
addonId: null,
|
|
27075
|
+
access: "view"
|
|
27076
|
+
},
|
|
26634
27077
|
"localNetwork.getPreferred": {
|
|
26635
27078
|
capName: "local-network",
|
|
26636
27079
|
capScope: "system",
|
|
@@ -26655,6 +27098,12 @@ Object.freeze({
|
|
|
26655
27098
|
addonId: null,
|
|
26656
27099
|
access: "create"
|
|
26657
27100
|
},
|
|
27101
|
+
"localNetwork.setNotificationEndpoint": {
|
|
27102
|
+
capName: "local-network",
|
|
27103
|
+
capScope: "system",
|
|
27104
|
+
addonId: null,
|
|
27105
|
+
access: "create"
|
|
27106
|
+
},
|
|
26658
27107
|
"lockControl.lock": {
|
|
26659
27108
|
capName: "lock-control",
|
|
26660
27109
|
capScope: "device",
|
|
@@ -27297,6 +27746,12 @@ Object.freeze({
|
|
|
27297
27746
|
addonId: null,
|
|
27298
27747
|
access: "create"
|
|
27299
27748
|
},
|
|
27749
|
+
"pipelineAnalytics.cancelMediaRelocate": {
|
|
27750
|
+
capName: "pipeline-analytics",
|
|
27751
|
+
capScope: "device",
|
|
27752
|
+
addonId: null,
|
|
27753
|
+
access: "create"
|
|
27754
|
+
},
|
|
27300
27755
|
"pipelineAnalytics.clearTracks": {
|
|
27301
27756
|
capName: "pipeline-analytics",
|
|
27302
27757
|
capScope: "device",
|
|
@@ -27351,6 +27806,12 @@ Object.freeze({
|
|
|
27351
27806
|
addonId: null,
|
|
27352
27807
|
access: "view"
|
|
27353
27808
|
},
|
|
27809
|
+
"pipelineAnalytics.getMediaRelocateStatus": {
|
|
27810
|
+
capName: "pipeline-analytics",
|
|
27811
|
+
capScope: "device",
|
|
27812
|
+
addonId: null,
|
|
27813
|
+
access: "view"
|
|
27814
|
+
},
|
|
27354
27815
|
"pipelineAnalytics.getMotionEvents": {
|
|
27355
27816
|
capName: "pipeline-analytics",
|
|
27356
27817
|
capScope: "device",
|
|
@@ -27423,6 +27884,12 @@ Object.freeze({
|
|
|
27423
27884
|
addonId: null,
|
|
27424
27885
|
access: "create"
|
|
27425
27886
|
},
|
|
27887
|
+
"pipelineAnalytics.relocateMedia": {
|
|
27888
|
+
capName: "pipeline-analytics",
|
|
27889
|
+
capScope: "device",
|
|
27890
|
+
addonId: null,
|
|
27891
|
+
access: "create"
|
|
27892
|
+
},
|
|
27426
27893
|
"pipelineAnalytics.searchObjectEvents": {
|
|
27427
27894
|
capName: "pipeline-analytics",
|
|
27428
27895
|
capScope: "device",
|
|
@@ -28185,6 +28652,12 @@ Object.freeze({
|
|
|
28185
28652
|
addonId: null,
|
|
28186
28653
|
access: "create"
|
|
28187
28654
|
},
|
|
28655
|
+
"recording.cancelRelocate": {
|
|
28656
|
+
capName: "recording",
|
|
28657
|
+
capScope: "system",
|
|
28658
|
+
addonId: null,
|
|
28659
|
+
access: "create"
|
|
28660
|
+
},
|
|
28188
28661
|
"recording.deleteFootprint": {
|
|
28189
28662
|
capName: "recording",
|
|
28190
28663
|
capScope: "system",
|
|
@@ -28215,6 +28688,12 @@ Object.freeze({
|
|
|
28215
28688
|
addonId: null,
|
|
28216
28689
|
access: "view"
|
|
28217
28690
|
},
|
|
28691
|
+
"recording.getRelocateStatus": {
|
|
28692
|
+
capName: "recording",
|
|
28693
|
+
capScope: "system",
|
|
28694
|
+
addonId: null,
|
|
28695
|
+
access: "view"
|
|
28696
|
+
},
|
|
28218
28697
|
"recording.getStorageUsage": {
|
|
28219
28698
|
capName: "recording",
|
|
28220
28699
|
capScope: "system",
|
|
@@ -28245,6 +28724,24 @@ Object.freeze({
|
|
|
28245
28724
|
addonId: null,
|
|
28246
28725
|
access: "view"
|
|
28247
28726
|
},
|
|
28727
|
+
"recording.relocateFootage": {
|
|
28728
|
+
capName: "recording",
|
|
28729
|
+
capScope: "system",
|
|
28730
|
+
addonId: null,
|
|
28731
|
+
access: "create"
|
|
28732
|
+
},
|
|
28733
|
+
"recording.renderClip": {
|
|
28734
|
+
capName: "recording",
|
|
28735
|
+
capScope: "system",
|
|
28736
|
+
addonId: null,
|
|
28737
|
+
access: "create"
|
|
28738
|
+
},
|
|
28739
|
+
"recording.renderGif": {
|
|
28740
|
+
capName: "recording",
|
|
28741
|
+
capScope: "system",
|
|
28742
|
+
addonId: null,
|
|
28743
|
+
access: "create"
|
|
28744
|
+
},
|
|
28248
28745
|
"recording.rescanStorage": {
|
|
28249
28746
|
capName: "recording",
|
|
28250
28747
|
capScope: "system",
|
|
@@ -28839,6 +29336,12 @@ Object.freeze({
|
|
|
28839
29336
|
addonId: null,
|
|
28840
29337
|
access: "create"
|
|
28841
29338
|
},
|
|
29339
|
+
"streamBroker.renderPreBufferClip": {
|
|
29340
|
+
capName: "stream-broker",
|
|
29341
|
+
capScope: "system",
|
|
29342
|
+
addonId: null,
|
|
29343
|
+
access: "create"
|
|
29344
|
+
},
|
|
28842
29345
|
"streamBroker.restartProfile": {
|
|
28843
29346
|
capName: "stream-broker",
|
|
28844
29347
|
capScope: "system",
|
|
@@ -28965,6 +29468,36 @@ Object.freeze({
|
|
|
28965
29468
|
addonId: null,
|
|
28966
29469
|
access: "create"
|
|
28967
29470
|
},
|
|
29471
|
+
"terminalSession.close": {
|
|
29472
|
+
capName: "terminal-session",
|
|
29473
|
+
capScope: "system",
|
|
29474
|
+
addonId: null,
|
|
29475
|
+
access: "create"
|
|
29476
|
+
},
|
|
29477
|
+
"terminalSession.listProfiles": {
|
|
29478
|
+
capName: "terminal-session",
|
|
29479
|
+
capScope: "system",
|
|
29480
|
+
addonId: null,
|
|
29481
|
+
access: "view"
|
|
29482
|
+
},
|
|
29483
|
+
"terminalSession.listSessions": {
|
|
29484
|
+
capName: "terminal-session",
|
|
29485
|
+
capScope: "system",
|
|
29486
|
+
addonId: null,
|
|
29487
|
+
access: "view"
|
|
29488
|
+
},
|
|
29489
|
+
"terminalSession.openSession": {
|
|
29490
|
+
capName: "terminal-session",
|
|
29491
|
+
capScope: "system",
|
|
29492
|
+
addonId: null,
|
|
29493
|
+
access: "create"
|
|
29494
|
+
},
|
|
29495
|
+
"terminalSession.resize": {
|
|
29496
|
+
capName: "terminal-session",
|
|
29497
|
+
capScope: "system",
|
|
29498
|
+
addonId: null,
|
|
29499
|
+
access: "create"
|
|
29500
|
+
},
|
|
28968
29501
|
"toast.onToast": {
|
|
28969
29502
|
capName: "toast",
|
|
28970
29503
|
capScope: "system",
|
|
@@ -35804,6 +36337,17 @@ var HOME_ASSISTANT_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0
|
|
|
35804
36337
|
* the admin UI is served from.
|
|
35805
36338
|
*/
|
|
35806
36339
|
var HA_NOTIFICATION_ICON_URL = `/addon/${HA_ROUTE_ID}/icons/${HA_ICON_KIND}`;
|
|
36340
|
+
/**
|
|
36341
|
+
* Media type of {@link HA_NOTIFICATION_ICON_URL}, stamped onto the kind
|
|
36342
|
+
* descriptor beside the URL.
|
|
36343
|
+
*
|
|
36344
|
+
* Not decoration: a React-Native client renders SVG and raster through
|
|
36345
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image`, and expo-image does
|
|
36346
|
+
* not decode SVG on iOS/Android), and it cannot infer the type from the URL
|
|
36347
|
+
* because the route path carries no file extension. Without this the viewer
|
|
36348
|
+
* treats the icon as raster and falls back to a placeholder glyph.
|
|
36349
|
+
*/
|
|
36350
|
+
var HA_NOTIFICATION_ICON_MEDIA_TYPE = "image/svg+xml";
|
|
35807
36351
|
/** The icon's identity bytes plus every content coding we may serve, computed
|
|
35808
36352
|
* once at module load (the icon is immutable). */
|
|
35809
36353
|
var HA_ICON_IDENTITY = Buffer.from(HOME_ASSISTANT_SVG, "utf8");
|
|
@@ -36240,10 +36784,14 @@ function createHaNotificationOutputProvider(deps) {
|
|
|
36240
36784
|
}
|
|
36241
36785
|
return {
|
|
36242
36786
|
listTargetKinds: async () => {
|
|
36243
|
-
return [deps.iconUrl !== void 0 ? {
|
|
36787
|
+
return [deps.iconUrl === void 0 ? descriptor : deps.iconMediaType !== void 0 ? {
|
|
36788
|
+
...descriptor,
|
|
36789
|
+
iconUrl: deps.iconUrl,
|
|
36790
|
+
iconMediaType: deps.iconMediaType
|
|
36791
|
+
} : {
|
|
36244
36792
|
...descriptor,
|
|
36245
36793
|
iconUrl: deps.iconUrl
|
|
36246
|
-
}
|
|
36794
|
+
}];
|
|
36247
36795
|
},
|
|
36248
36796
|
listTargets: async () => {
|
|
36249
36797
|
return (await store.list()).map((target) => ({
|
|
@@ -36827,6 +37375,7 @@ var HaProviderAddon = class HaProviderAddon extends BaseDeviceProvider {
|
|
|
36827
37375
|
return createHaNotificationOutputProvider({
|
|
36828
37376
|
addonId: this.ctx.id,
|
|
36829
37377
|
iconUrl: HA_NOTIFICATION_ICON_URL,
|
|
37378
|
+
iconMediaType: HA_NOTIFICATION_ICON_MEDIA_TYPE,
|
|
36830
37379
|
store,
|
|
36831
37380
|
publish: async (brokerId, service, serviceData) => {
|
|
36832
37381
|
await this.requireRegistry().publish(brokerId, {
|