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