@camstack/addon-notifiers 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.
Files changed (3) hide show
  1. package/dist/addon.js +1644 -1395
  2. package/dist/addon.mjs +1644 -1395
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7623,16 +7623,23 @@ var StorageLocationDeclarationSchema = object({
7623
7623
  * Which node root the seeded `<id>:default` instance is placed under on a
7624
7624
  * FRESH install:
7625
7625
  * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
7626
- * the appData volume. Right for small/durable data (backups, logs, models).
7626
+ * the appData volume. Right for small/durable data (logs, models).
7627
7627
  * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
7628
7628
  * env is set, else falls back to the data root. Right for bulky, hot media
7629
7629
  * (recordings, event media) that should stay off the appData disk.
7630
+ * - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
7631
+ * `/backups` in the image) so archives live on their own mount rather than
7632
+ * filling the appData disk. Falls back to the data root when unset.
7630
7633
  *
7631
7634
  * Only affects the seeded default's `basePath`; operators can repoint any
7632
7635
  * location afterwards, and a `defaultsTo` slot inherits its parent's root
7633
7636
  * regardless of this field. Absent (the common case) is treated as `'data'`.
7634
7637
  */
7635
- defaultRoot: _enum(["data", "media"]).optional()
7638
+ defaultRoot: _enum([
7639
+ "data",
7640
+ "media",
7641
+ "backup"
7642
+ ]).optional()
7636
7643
  });
7637
7644
  var DecoderStatsSchema = object({
7638
7645
  inputFps: number(),
@@ -9176,401 +9183,1039 @@ function prepareNotification(caps, n) {
9176
9183
  };
9177
9184
  }
9178
9185
  /**
9179
- * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9180
- * for every device, regardless of provider the kernel needs a uniform
9181
- * cap-keyed slice for the basic device flags every consumer expects to
9182
- * read across processes (the `online` flag in particular). Driver-specific
9183
- * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9184
- * their own slices.
9186
+ * Shared geometry vocabulary for on-frame shape caps — privacy-mask,
9187
+ * motion-zones, and the detection zones/lines editor all speak this one
9188
+ * language so a single drawing-plane editor and the providers stay
9189
+ * decoupled from each cap's storage.
9185
9190
  *
9186
- * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9187
- * empty `methods`, single change event. Reads land at
9188
- * `runtimeState.getCapState('device-status')`; writes at
9189
- * `runtimeState.setCapState('device-status', …)`. Cross-process
9190
- * consumers reach the same data via the `device-state` cap router
9191
- * (`getCapSlice({deviceId, capName: 'device-status'})`).
9191
+ * All coordinates are normalized 0..1 of the camera frame (top-left
9192
+ * origin). Each cap composes the SUBSET of shape kinds it supports and
9193
+ * advertises it via `supportedShapes` in its `getOptions`.
9192
9194
  */
9193
- var DeviceStatusSchema = object({
9194
- /**
9195
- * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9196
- * `BaseDevice`. Provider semantics vary — RTSP aggregates broker
9197
- * stream-health, Reolink reads firmware push events, ONVIF tracks
9198
- * ping responses. This cap intentionally does NOT prescribe which
9199
- * signal drives the flag.
9200
- */
9201
- online: boolean(),
9202
- /** Ms epoch of the last `online` transition. Lets consumers tell
9203
- * apart "just came online" from "still online". */
9204
- lastChangedAt: number()
9195
+ /** A normalized 0..1 point (top-left origin). */
9196
+ var MaskPointSchema = object({
9197
+ x: number(),
9198
+ y: number()
9205
9199
  });
9206
- object({
9207
- deviceId: number(),
9208
- status: DeviceStatusSchema
9200
+ /** Axis-aligned rectangle (normalized 0..1). */
9201
+ var MaskRectShapeSchema = object({
9202
+ kind: literal("rect"),
9203
+ x: number(),
9204
+ y: number(),
9205
+ width: number(),
9206
+ height: number()
9207
+ });
9208
+ /** Free polygon — an ordered list of normalized vertices (≥3). */
9209
+ var MaskPolygonShapeSchema = object({
9210
+ kind: literal("polygon"),
9211
+ points: array(MaskPointSchema)
9212
+ });
9213
+ /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
9214
+ var MaskGridShapeSchema = object({
9215
+ kind: literal("grid"),
9216
+ gridWidth: number(),
9217
+ gridHeight: number(),
9218
+ cells: array(boolean())
9219
+ });
9220
+ discriminatedUnion("kind", [
9221
+ MaskRectShapeSchema,
9222
+ MaskPolygonShapeSchema,
9223
+ MaskGridShapeSchema,
9224
+ object({
9225
+ kind: literal("line"),
9226
+ points: array(MaskPointSchema)
9227
+ })
9228
+ ]);
9229
+ /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
9230
+ var MaskShapeKindSchema = _enum([
9231
+ "rect",
9232
+ "polygon",
9233
+ "grid",
9234
+ "line"
9235
+ ]);
9236
+ /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
9237
+ var MaskPolygonVerticesSchema = object({
9238
+ min: number(),
9239
+ max: number()
9240
+ });
9241
+ /** Grid dimensions when a cap supports 'grid'. */
9242
+ var MaskGridDimsSchema = object({
9243
+ width: number(),
9244
+ height: number()
9209
9245
  });
9210
9246
  /**
9211
- * Per-device feature/identity probe slice. Holds the runtime-resolved
9212
- * truth about what a device CAN do — which the kernel uses to:
9213
- * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9214
- * based on what the firmware actually advertises).
9215
- * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9216
- * `device-manager.listAll`.
9217
- * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9218
- * to register on the device's capability surface.
9247
+ * notification-rules the Notification Center rule surface (P1 core).
9219
9248
  *
9220
- * Auto-registered by `BaseDevice` for every device. Drivers populate the
9221
- * slice from `onProbe()` (kernel calls it once after register, before
9222
- * accessory reconciliation). Consumers read via:
9223
- * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9249
+ * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
9250
+ * (operator decisions D-1/D-2/D-3 are binding):
9224
9251
  *
9225
- * `flags` is an open record so each driver carries its own keys without
9226
- * a centralized schema bottleneck Reolink writes `hasPtz/hasIntercom`,
9227
- * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9252
+ * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
9253
+ * `notification-center` module), hooked on the durable persistence
9254
+ * moments (object-event insert, TrackCloser.closeExpired) with a
9255
+ * persisted outbox + retry — never the lossy telemetry bus (D8).
9256
+ * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
9257
+ * FIRST persisted detection matching the conditions (per-track dedup,
9258
+ * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
9259
+ * `delivery: 'track-end'` evaluates the finalized track record at close.
9260
+ * - DISPATCH stays behind `notification-output` (rules reference targets
9261
+ * by id; per-backend params are a passthrough blob capped by the
9262
+ * target kind's own caps/degrade engine).
9228
9263
  *
9229
- * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9230
- * config is for operator-edited overrides + UI snapshots; runtime probe
9231
- * results belong in runtime-state where the kernel handles persistence,
9232
- * cross-process mirroring, and reactive updates.
9264
+ * P1 scope: admin-authored rules only (`createdBy` stamped from the
9265
+ * server-injected caller identity the first `caller: 'required'`
9266
+ * adopter). The P1 condition subset is: devices, classes(+exclude),
9267
+ * minConfidence, admin zones (any/all + exclude), weekly schedule
9268
+ * windows, and the optional label/identity/plate matchers. User rules,
9269
+ * private zones, per-recipient fan-out and the wider condition table are
9270
+ * P2+ (see spec §7).
9271
+ *
9272
+ * All schemas here are the single source of truth — `NcRule` etc. are
9273
+ * `z.infer` exports; no duplicate interfaces (the advanced-notifier
9274
+ * schema/interface drift is explicitly not repeated).
9233
9275
  */
9234
- var FeatureProbeStatusSchema = object({
9276
+ /**
9277
+ * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
9278
+ * The value maps 1:1 onto the evaluated record kind:
9279
+ * - `immediate` ↔ object-event persist (lowest-latency detection burst)
9280
+ * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
9281
+ * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
9282
+ * change of a LINKED device, one row per linked camera)
9283
+ * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
9284
+ * delivery / pick-up)
9285
+ *
9286
+ * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
9287
+ * `package-event` are pure trigger kinds (no urgency dimension). Extending
9288
+ * this one field keeps the schema additive — a rule still declares exactly
9289
+ * one trigger.
9290
+ */
9291
+ var NcDeliverySchema = _enum([
9292
+ "immediate",
9293
+ "track-end",
9294
+ "device-event",
9295
+ "package-event"
9296
+ ]);
9297
+ /** Weekly schedule — OR of windows; absence on the rule = always active. */
9298
+ var NcScheduleSchema = object({
9299
+ windows: array(object({
9300
+ /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
9301
+ days: array(number().int().min(0).max(6)).min(1),
9302
+ startMinute: number().int().min(0).max(1439),
9303
+ endMinute: number().int().min(0).max(1439)
9304
+ })).min(1),
9305
+ /** IANA timezone; default = hub host timezone. */
9306
+ timezone: string().optional(),
9307
+ /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
9308
+ invert: boolean().optional()
9309
+ });
9310
+ /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
9311
+ var NcPlateMatcherSchema = object({
9312
+ values: array(string().min(1)).min(1),
9313
+ /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
9314
+ maxDistance: number().int().min(0).max(3).default(1)
9315
+ });
9316
+ /**
9317
+ * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
9318
+ * occupancy edge for a device — optionally narrowed to a single admin
9319
+ * `zoneId` and/or object `className`. `op` selects the edge/threshold:
9320
+ * - `became-occupied` (default) — count crossed 0 → ≥ `count`
9321
+ * - `became-free` — count crossed ≥ `count` → below it
9322
+ * - `>=` / `<=` — count is at/over or at/under `count`
9323
+ * `sustainSeconds` requires the condition hold continuously that long
9324
+ * before firing (debounces flicker; 0 = fire on the first matching edge).
9325
+ * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
9326
+ * the condition never matches. Confirmed edge-state survives addon restarts
9327
+ * (declared SQLite collection, reseeded on boot).
9328
+ */
9329
+ var NcOccupancyConditionSchema = object({
9330
+ /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
9331
+ zoneId: string().optional(),
9332
+ /** Object class to count; absent = any class. */
9333
+ className: string().optional(),
9334
+ op: _enum([
9335
+ "became-occupied",
9336
+ "became-free",
9337
+ ">=",
9338
+ "<="
9339
+ ]).default("became-occupied"),
9340
+ count: number().int().min(0).default(1),
9341
+ sustainSeconds: number().int().min(0).max(3600).default(15)
9342
+ });
9343
+ /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9344
+ var NcZoneConditionSchema = object({
9345
+ ids: array(string().min(1)).min(1),
9346
+ /** Quantifier over `ids` — at least one / every one visited. */
9347
+ match: _enum(["any", "all"]).default("any")
9348
+ });
9349
+ /**
9350
+ * The P1 condition set — a flat AND of groups; absent group = pass;
9351
+ * membership lists are OR within the list (spec §2.3).
9352
+ */
9353
+ var NcConditionsSchema = object({
9354
+ /** Device scope — absent = all devices. */
9355
+ devices: array(number()).optional(),
9356
+ /** Detector class names (any overlap with the record's class set). */
9357
+ classes: array(string().min(1)).optional(),
9358
+ /** Veto classes — any overlap fails the rule. */
9359
+ classesExclude: array(string().min(1)).optional(),
9360
+ /** Minimum detection confidence 0–1 (fails when the record has none). */
9361
+ minConfidence: number().min(0).max(1).optional(),
9362
+ /** Admin zone membership over event `zones` / track `zonesVisited`. */
9363
+ zones: NcZoneConditionSchema.optional(),
9364
+ /** Veto zones — any hit fails the rule. */
9365
+ zonesExclude: array(string().min(1)).optional(),
9235
9366
  /**
9236
- * Driver-specific flag bag. Each driver picks its own key names — the
9237
- * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9238
- * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9239
- * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9240
- * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9367
+ * Exact (case-insensitive) match on the record's collapsed `label`
9368
+ * (identity name / plate text / subclass).
9241
9369
  */
9242
- flags: record(string(), unknown()),
9370
+ labelEquals: array(string().min(1)).optional(),
9243
9371
  /**
9244
- * Coarse driver-classification lets cross-process consumers tell apart
9245
- * cameras / battery-cams / NVRs without re-running the probe. `null`
9246
- * before the first probe completes.
9372
+ * Identity matcher. P1 boundary: matched against the record's collapsed
9373
+ * `label` (the identity display name propagated by the face pipeline) —
9374
+ * identity-ID matching rides in P2 when identity ids reach the record.
9247
9375
  */
9248
- deviceType: string().nullable(),
9249
- /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9250
- model: string().nullable(),
9251
- /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9252
- channelCount: number().nullable(),
9376
+ identities: array(string().min(1)).optional(),
9377
+ /** Fuzzy plate matcher against the record's `label` (plate text). */
9378
+ plates: NcPlateMatcherSchema.optional(),
9253
9379
  /**
9254
- * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9255
- * completes drivers' `getAccessoryChildren()` should treat zero as
9256
- * "probe not done yet, return empty" so accessories aren't spawned
9257
- * before the firmware is queried.
9380
+ * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
9381
+ * Same P1 boundary: matched against the record's collapsed `label` (the
9382
+ * identity display name). A record with NO label passes (nothing to
9383
+ * exclude), unlike the include variant which fails on an absent label.
9258
9384
  */
9259
- lastProbedAt: number(),
9385
+ identitiesExclude: array(string().min(1)).optional(),
9260
9386
  /**
9261
- * Framework convention: every runtime-state slice carries this for the
9262
- * createRuntimeStateBridge stale-check helper. We keep it in sync with
9263
- * `lastProbedAt` on every write.
9264
- */
9265
- lastFetchedAt: number()
9266
- });
9267
- object({
9268
- deviceId: number(),
9269
- status: FeatureProbeStatusSchema
9387
+ * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
9388
+ * TRACK-END only: importance is scored at track close, so it does not exist
9389
+ * at immediate / object-event evaluation time (see catalog `appliesTo`). At
9390
+ * close the value is threaded via the close-time info (the `Track` clone is
9391
+ * captured before the DB row is updated, so it would otherwise read stale).
9392
+ * Fails when the record carries no importance (never guess quality — the
9393
+ * `minConfidence` precedent). MVP cut: a single scalar threshold.
9394
+ */
9395
+ minImportance: number().min(0).max(1).optional(),
9396
+ /**
9397
+ * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
9398
+ * TRACK-END only: an `immediate` / object-event subject has no closed
9399
+ * lifespan, so a dwell condition never matches immediate delivery
9400
+ * (documented choice — the object-event record carries no `firstSeen`,
9401
+ * so dwell cannot be computed from what the subject actually carries).
9402
+ */
9403
+ minDwellSeconds: number().min(0).optional(),
9404
+ /**
9405
+ * Detection provenance filter. `any` (default / absent) matches every
9406
+ * source; otherwise the subject's source must equal it. Legacy records
9407
+ * with no stamped source are treated as `pipeline`. The union spans both
9408
+ * record kinds — object events carry `pipeline` | `onboard`, synthetic
9409
+ * tracks carry `sensor`.
9410
+ */
9411
+ source: _enum([
9412
+ "pipeline",
9413
+ "onboard",
9414
+ "sensor",
9415
+ "any"
9416
+ ]).optional(),
9417
+ /**
9418
+ * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
9419
+ * detector `minConfidence` (that gates the object-detection score; this
9420
+ * gates the recognition/OCR match score). Fails when the subject carries
9421
+ * no label-match confidence (never guess). TRACK-END only: the confidence
9422
+ * lives on the recognition result and reaches the subject at track close.
9423
+ *
9424
+ * What it measures precisely (plumbed at track close — the closer threads
9425
+ * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
9426
+ * `importance`): the BEST recognition match confidence observed for the
9427
+ * label the track carries at close — for a face, the peak cosine similarity
9428
+ * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
9429
+ * for a plate, the peak OCR read score of the best-held plate
9430
+ * (`plateText.confidence`). When BOTH a face and a plate were recognized on
9431
+ * one track the higher of the two is used. A track that ended with no
9432
+ * confident identity/plate match carries no value, so the condition fails
9433
+ * closed for it (an un-recognized subject).
9434
+ */
9435
+ minLabelConfidence: number().min(0).max(1).optional(),
9436
+ /**
9437
+ * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
9438
+ * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
9439
+ * against the token carried on the device-event subject (extracted from the
9440
+ * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
9441
+ * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
9442
+ * eventType, so gate those with {@link sensorKinds} instead.
9443
+ */
9444
+ eventTypeTokens: array(string().min(1)).optional(),
9445
+ /**
9446
+ * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
9447
+ * `contact`, `button`, `device-event`) — matched against the persisted
9448
+ * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
9449
+ */
9450
+ sensorKinds: array(string().min(1)).optional(),
9451
+ /**
9452
+ * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
9453
+ * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
9454
+ * when the subject's phase does not match (a subject always carries a phase
9455
+ * on the package-event trigger).
9456
+ */
9457
+ packagePhase: _enum([
9458
+ "delivered",
9459
+ "picked-up",
9460
+ "both"
9461
+ ]).optional(),
9462
+ /**
9463
+ * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
9464
+ * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
9465
+ * listed polygon (ZoneEngine membership semantics). Evaluated only when
9466
+ * the subject carries a bbox; absent bbox ⇒ the condition FAILS.
9467
+ */
9468
+ customZones: array(MaskPolygonShapeSchema).optional(),
9469
+ /**
9470
+ * DEVICE-EVENT only. ZoneAnalytics occupancy edge — fires when a device's
9471
+ * (optionally zone/class-scoped) occupancy count crosses the configured
9472
+ * threshold and holds for `sustainSeconds`. Fail-closed on missing
9473
+ * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
9474
+ */
9475
+ occupancy: NcOccupancyConditionSchema.optional()
9270
9476
  });
9271
- object({
9272
- /** Carbon dioxide concentration in ppm. */
9273
- co2Ppm: number().min(0).optional(),
9274
- /** Total volatile organic compounds in ppb. */
9275
- vocPpb: number().min(0).optional(),
9276
- /** Particulate matter 2.5 μm in µg/m³. */
9277
- pm25: number().min(0).optional(),
9278
- /** Particulate matter 10 μm in µg/m³. */
9279
- pm10: number().min(0).optional(),
9280
- /** Composite AQI value (typically 0..500). */
9281
- aqi: number().optional(),
9282
- /** Ms epoch when the slice was last updated. */
9283
- lastFetchedAt: number(),
9284
- /** Live display unit of the single metric this slice carries (e.g. HA
9285
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
9286
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
9287
- * per slice is unambiguous. */
9288
- unit: string().optional(),
9289
- /** Suggested decimal places for numeric display.
9290
- * Populated live from the upstream source when provided (e.g. HA
9291
- * `attributes.suggested_display_precision`). Falls back to
9292
- * auto-formatting when absent. */
9293
- precision: number().int().min(0).max(10).optional()
9477
+ /** One delivery target: a `notification-output` Target ref + passthrough params. */
9478
+ var NcRuleTargetSchema = object({
9479
+ /** `notification-output` Target id. */
9480
+ targetId: string().min(1),
9481
+ /**
9482
+ * Per-backend passthrough. Recognized keys are mapped onto the canonical
9483
+ * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
9484
+ * degrade engine drops what the backend can't render.
9485
+ */
9486
+ params: record(string(), unknown()).optional()
9294
9487
  });
9295
- DeviceType.Sensor;
9296
9488
  /**
9297
- * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9298
- * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9299
- * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9300
- * arming / pending / triggered / disarming.
9301
- *
9302
- * Many panels require a PIN code on arm / disarm — the optional
9303
- * `code` field on the methods passes it through to the upstream
9304
- * service; it's NEVER persisted in the runtime slice or any event
9305
- * payload. The presence of a required code is signalled by
9306
- * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9307
- * field without a slice fetch.
9308
- *
9309
- * `availableModes` mirrors HA's `supported_features`-derived arm
9310
- * mode list — the UI renders only the buttons the panel accepts.
9489
+ * Media attachment policy (P1 still-image subset).
9490
+ * - `best` the best AVAILABLE subject image at dispatch time (D-3).
9491
+ * - `best-matching` the media that explains WHY the rule fired: a rule
9492
+ * matched on identities attaches the subject's `faceCrop`, one matched on
9493
+ * plates attaches the `plateCrop`; a rule with no identity/plate condition
9494
+ * (or when the specific crop is missing) degrades to `best`, then
9495
+ * `keyFrame`, then no attachment never delaying the send. The matched
9496
+ * condition summary is frozen on the outbox row at enqueue (like the rule
9497
+ * name), so the choice never drifts from the record that fired it.
9498
+ * - `keyFrame` the clean scene frame (no subject box).
9499
+ * - `none` no attachment.
9311
9500
  */
9312
- var AlarmStateSchema = _enum([
9313
- "disarmed",
9314
- "armed_home",
9315
- "armed_away",
9316
- "armed_night",
9317
- "armed_vacation",
9318
- "armed_custom_bypass",
9319
- "arming",
9320
- "disarming",
9321
- "pending",
9322
- "triggered"
9323
- ]);
9324
- var AlarmArmModeSchema = _enum([
9325
- "home",
9326
- "away",
9327
- "night",
9328
- "vacation",
9329
- "custom_bypass"
9330
- ]);
9331
- object({
9332
- /** Current lifecycle state. */
9333
- state: AlarmStateSchema,
9334
- /** Subset of arm modes the panel accepts. UI renders one button per
9335
- * mode in this list. */
9336
- availableModes: array(AlarmArmModeSchema),
9337
- /** Whether the panel requires a PIN on arm / disarm. Mirrors
9338
- * `DeviceFeature.AlarmPinRequired` for slice consumers. */
9339
- requiresCode: boolean(),
9340
- /** Ms epoch when the slice was last updated. */
9341
- lastChangedAt: number()
9342
- });
9343
- DeviceType.AlarmPanel, method(object({
9344
- deviceId: number().int().nonnegative(),
9345
- mode: AlarmArmModeSchema,
9346
- /** Optional PIN code. Required when `requiresCode === true`.
9347
- * Passed through to the upstream service; never persisted. */
9348
- code: string().min(1).optional()
9349
- }), _void(), {
9350
- kind: "mutation",
9351
- auth: "admin"
9352
- }), method(object({
9353
- deviceId: number().int().nonnegative(),
9354
- code: string().min(1).optional()
9355
- }), _void(), {
9356
- kind: "mutation",
9357
- auth: "admin"
9358
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
9359
- kind: "mutation",
9360
- auth: "admin"
9501
+ var NcMediaPolicySchema = object({ attach: _enum([
9502
+ "best",
9503
+ "best-matching",
9504
+ "keyFrame",
9505
+ "none"
9506
+ ]).default("best") });
9507
+ /** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
9508
+ var NcThrottleSchema = object({
9509
+ cooldownSec: number().int().min(0).max(86400).default(60),
9510
+ /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
9511
+ scope: _enum(["rule", "rule-device"]).default("rule-device")
9361
9512
  });
9362
- object({
9363
- /** Current illuminance in lux (lx). */
9364
- lux: number().min(0),
9365
- /** Ms epoch when the slice was last updated. */
9366
- lastFetchedAt: number(),
9367
- /** Live display unit from the upstream source (e.g. HA
9368
- * `attributes.unit_of_measurement`). The UI prefers this over the
9369
- * role's canonical unit. Absent → fall back to the canonical unit. */
9370
- unit: string().optional(),
9371
- /** Suggested decimal places for numeric display.
9372
- * Populated live from the upstream source when provided (e.g. HA
9373
- * `attributes.suggested_display_precision`). Falls back to
9374
- * auto-formatting when absent. */
9375
- precision: number().int().min(0).max(10).optional()
9513
+ /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
9514
+ var NcRuleInputSchema = object({
9515
+ name: string().min(1).max(200),
9516
+ enabled: boolean().default(true),
9517
+ delivery: NcDeliverySchema,
9518
+ conditions: NcConditionsSchema.default({}),
9519
+ schedule: NcScheduleSchema.optional(),
9520
+ targets: array(NcRuleTargetSchema).min(1),
9521
+ media: NcMediaPolicySchema.default({ attach: "best" }),
9522
+ throttle: NcThrottleSchema.default({
9523
+ cooldownSec: 60,
9524
+ scope: "rule-device"
9525
+ }),
9526
+ /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
9527
+ template: object({
9528
+ title: string().max(500).optional(),
9529
+ body: string().max(2e3).optional()
9530
+ }).optional(),
9531
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9532
+ priority: number().int().min(1).max(5).default(3),
9533
+ /**
9534
+ * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
9535
+ * behaviour, visible to all, read-only in the viewer). Present = personal
9536
+ * rule owned by this userId. Server-stamped; never trusted from a client.
9537
+ */
9538
+ ownerUserId: string().optional()
9376
9539
  });
9377
- DeviceType.Sensor;
9378
9540
  /**
9379
- * Per-class audio metrics aggregated over a sliding window.
9380
- */
9381
- var AudioClassSummarySchema = object({
9382
- className: string(),
9383
- /** Number of windows (chunks) where this class was the top hit. */
9384
- hits: number().int().nonnegative(),
9385
- /** Mean score across those hits, clamped to [0,1]. */
9386
- avgScore: number().min(0).max(1),
9387
- /** Peak score in the window. */
9388
- peakScore: number().min(0).max(1)
9541
+ * Partial patch for `updateRule` any subset of the input fields, plus the
9542
+ * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
9543
+ * NOT a client-authored input field (it lives on the persisted rule, not the
9544
+ * input), so it is added here explicitly to let the store's per-target opt-out
9545
+ * toggle round-trip through the shared `update` path. Viewer opt-out mutations
9546
+ * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
9547
+ * `updateRule` patch.
9548
+ */
9549
+ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
9550
+ /** A persisted rule. */
9551
+ var NcRuleSchema = NcRuleInputSchema.extend({
9552
+ id: string(),
9553
+ /** userId of the admin who created the rule (server-stamped caller). */
9554
+ createdBy: string(),
9555
+ createdAt: number(),
9556
+ updatedAt: number(),
9557
+ /**
9558
+ * Per-target opt-out set. A targetId here is suppressed for THIS rule at
9559
+ * send time. Only a target's OWNER may add/remove its id (server-checked
9560
+ * in `nc.setRuleTargetEnabled`). Defaults to empty.
9561
+ */
9562
+ disabledTargetIds: array(string()).default([])
9563
+ });
9564
+ var NcTestResultSchema = object({
9565
+ recordId: string(),
9566
+ recordKind: _enum([
9567
+ "object-event",
9568
+ "track",
9569
+ "device-event",
9570
+ "package-event"
9571
+ ]),
9572
+ deviceId: number(),
9573
+ timestamp: number(),
9574
+ wouldFire: boolean(),
9575
+ /** Condition id that failed (first failing group), when `wouldFire` is false. */
9576
+ failedCondition: string().optional(),
9577
+ className: string().optional(),
9578
+ label: string().optional()
9579
+ });
9580
+ var NcConditionDescriptorSchema = object({
9581
+ /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
9582
+ id: string(),
9583
+ group: _enum([
9584
+ "scope",
9585
+ "class",
9586
+ "zones",
9587
+ "quality",
9588
+ "label",
9589
+ "schedule",
9590
+ "device",
9591
+ "package",
9592
+ "occupancy"
9593
+ ]),
9594
+ label: string(),
9595
+ /** Editor widget the UI renders — never hardcode per-condition forms. */
9596
+ valueType: _enum([
9597
+ "deviceIdList",
9598
+ "stringList",
9599
+ "number01",
9600
+ "number",
9601
+ "sourceSelect",
9602
+ "zoneSelection",
9603
+ "zoneIdList",
9604
+ "schedule",
9605
+ "plateMatcher",
9606
+ "packagePhase",
9607
+ "polygonDraw",
9608
+ "occupancy"
9609
+ ]),
9610
+ operator: _enum([
9611
+ "in",
9612
+ "notIn",
9613
+ "anyOf",
9614
+ "allOf",
9615
+ "gte",
9616
+ "fuzzyIn",
9617
+ "withinSchedule"
9618
+ ]),
9619
+ /** Which delivery kinds the condition applies to. */
9620
+ appliesTo: array(NcDeliverySchema),
9621
+ phase: string(),
9622
+ description: string().optional()
9389
9623
  });
9390
9624
  /**
9391
- * Per-camera audio metrics snapshotemitted by the analytics frame
9392
- * handler on every `pipeline.audio-inference-result` event and
9393
- * mirrored into the `audio-metrics` device-state slice. Symmetric
9394
- * with `zone-analytics` snapshots for video every consumer
9395
- * (admin UI panel, automations, alert rules) reads via the
9396
- * canonical `device.state.audioMetrics.value` reactive handle.
9625
+ * The delivery lifecycle status of a history row a straight read of the
9626
+ * durable outbox row's own status (single source of truth):
9627
+ * - `pending` enqueued, in-flight or retrying with backoff
9628
+ * - `sent` delivered (terminal)
9629
+ * - `dead` — dead-lettered after exhausting retries / a permanent
9630
+ * backend rejection / a deleted target (terminal; carries
9631
+ * the failure `error`)
9397
9632
  *
9398
- * Aggregates are computed over a rolling `windowSec` window
9399
- * (default 60s). Past that window, classes drop out of `byClass`
9400
- * and the level history shifts forward.
9633
+ * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
9634
+ * user dimension (quiet hours / snooze) and are additive when they land.
9401
9635
  */
9402
- var AudioMetricsSnapshotSchema = object({
9403
- /** Wall-clock timestamp (ms) of the most recent audio window. */
9404
- ts: number().int(),
9405
- /** Sliding-window length (seconds) used for aggregation. */
9406
- windowSec: number().int().positive(),
9407
- /** Latest level reading from the most recent window. */
9408
- level: object({
9409
- rms: number(),
9410
- dbfs: number()
9411
- }),
9412
- /** Peak dBFS observed across the rolling window. */
9413
- peakDbfs: number(),
9414
- /** Mean dBFS across the rolling window. */
9415
- avgDbfs: number(),
9416
- /** Most recent above-threshold classification, or null on silence. */
9417
- current: object({
9418
- className: string(),
9419
- score: number().min(0).max(1),
9420
- timestamp: number().int()
9421
- }).nullable(),
9422
- /** Per-class summary across the rolling window — keys are
9423
- * `macroClass` strings (e.g. `dog_bark`, `speech`, `glass_break`). */
9424
- byClass: array(AudioClassSummarySchema).readonly()
9636
+ var NcHistoryStatusSchema = _enum([
9637
+ "pending",
9638
+ "sent",
9639
+ "dead"
9640
+ ]);
9641
+ /** The evaluated record kind a history row descends from (one per trigger). */
9642
+ var NcHistoryRecordKindSchema = _enum([
9643
+ "object-event",
9644
+ "track-end",
9645
+ "device-event",
9646
+ "package-event"
9647
+ ]);
9648
+ /** Subject summary frozen on the row at fire time (survives rule/record edits). */
9649
+ var NcHistorySubjectSchema = object({
9650
+ className: string(),
9651
+ label: string().optional(),
9652
+ confidence: number().optional(),
9653
+ zones: array(string()),
9654
+ timestamp: number()
9425
9655
  });
9426
9656
  /**
9427
- * Audio-metrics history payload a series of `AudioMetricsHistoryPoint`
9428
- * samples capped at `maxPoints` (default 1024). When the requested
9429
- * `windowSec / sampleEveryMs` would exceed the cap, the provider
9430
- * subsamples by bucketed averaging and reports the effective sample
9431
- * spacing on `effectiveSampleEveryMs` so the UI can label the x-axis.
9657
+ * One delivery-history row. This is a read-only VIEW over the durable
9658
+ * outbox row (single source of truth the same row the drain loop drives;
9659
+ * NO second write path, so history can never drift from delivery state).
9660
+ * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
9661
+ * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
9662
+ * (fire) / `updatedAt` (last transition), `status` + `error` are the
9663
+ * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
9664
+ * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
9665
+ * P1 (admin scope only).
9432
9666
  */
9433
- var AudioMetricsHistorySchema = object({
9434
- points: array(object({
9435
- /** Wall-clock ms when this sample was recorded. */
9436
- ts: number().int(),
9437
- /** Instantaneous dBFS level at sample time. `null` for windows where
9438
- * the source had no level reading (rare; happens at decode startup). */
9439
- dbfs: number().nullable(),
9440
- /** Rolling-window peak dBFS at sample time. Same window the live
9441
- * snapshot reports. */
9442
- peakDbfs: number(),
9443
- /** Rolling-window mean dBFS at sample time. */
9444
- avgDbfs: number(),
9445
- /** Dominant above-threshold class at sample time, or null on silence. */
9446
- topClass: string().nullable(),
9447
- /** Score of the dominant class (`null` whenever `topClass` is null). */
9448
- topScore: number().min(0).max(1).nullable()
9449
- })).readonly(),
9450
- /** Actual ms between adjacent samples after any subsampling. */
9451
- effectiveSampleEveryMs: number().int().positive(),
9452
- /** Wall-clock window covered by `points` (`points[N-1].ts - points[0].ts`),
9453
- * or `0` when there's fewer than 2 samples. */
9454
- windowMsActual: number().int().nonnegative()
9455
- });
9456
- DeviceType.Camera, method(object({ deviceId: number() }), AudioMetricsSnapshotSchema.nullable()), method(object({
9667
+ var NcHistoryEntrySchema = object({
9668
+ /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
9669
+ id: string(),
9670
+ ruleId: string(),
9671
+ /** Rule name frozen at fire time (outlives a later rename / delete). */
9672
+ ruleName: string(),
9673
+ /** The rule urgency/trigger that produced this delivery. */
9674
+ delivery: NcDeliverySchema,
9675
+ targetId: string(),
9457
9676
  deviceId: number(),
9458
- /** History window in seconds. Default 300 (5 minutes).
9459
- * Provider clamps to its retention cap if larger. */
9460
- windowSec: number().int().positive().optional(),
9461
- /** Target sample interval in ms. Default 1000 (1 sample/second).
9462
- * Provider clamps to natural sample rate if smaller, and
9463
- * bucket-averages when bigger than the requested window
9464
- * would produce more than `maxPoints` samples. */
9465
- sampleEveryMs: number().int().positive().optional()
9466
- }), AudioMetricsHistorySchema);
9467
- object({
9468
- /** Whether the automation is currently enabled. Disabled automations
9469
- * ignore their trigger block — manual `trigger` still works. */
9470
- enabled: boolean(),
9471
- /** Whether the automation is currently executing its action block. */
9472
- isRunning: boolean(),
9473
- /** Ms epoch of the last successful run. 0 when never run. */
9474
- lastTriggeredAt: number(),
9475
- /** Failure description from the last completed run. Null on success
9476
- * or when never run. */
9477
- lastError: string().nullable(),
9478
- /** Ms epoch when the slice was last updated. */
9479
- lastChangedAt: number()
9677
+ recordKind: NcHistoryRecordKindSchema,
9678
+ /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
9679
+ recordId: string(),
9680
+ /** Present for track-scoped deliveries (object-event / track-end). */
9681
+ trackId: string().optional(),
9682
+ status: NcHistoryStatusSchema,
9683
+ /** Delivery attempts made so far. */
9684
+ attempts: number().int(),
9685
+ /** Fire time (outbox enqueue). */
9686
+ createdAt: number(),
9687
+ /** Last transition time (terminal for sent / dead). */
9688
+ updatedAt: number(),
9689
+ /** Failure detail — present on a `dead` row. */
9690
+ error: string().optional(),
9691
+ subject: NcHistorySubjectSchema
9480
9692
  });
9481
- DeviceType.Automation, method(object({ deviceId: number().int().nonnegative() }), _void(), {
9482
- kind: "mutation",
9483
- auth: "admin"
9484
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
9693
+ /**
9694
+ * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
9695
+ * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
9696
+ * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
9697
+ * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
9698
+ */
9699
+ var NcHistoryFilterSchema = object({
9700
+ ruleId: string().optional(),
9701
+ deviceId: number().optional(),
9702
+ status: NcHistoryStatusSchema.optional(),
9703
+ since: number().optional(),
9704
+ until: number().optional(),
9705
+ limit: number().int().min(1).max(500).default(100)
9706
+ });
9707
+ 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 }), {
9485
9708
  kind: "mutation",
9486
- auth: "admin"
9709
+ auth: "admin",
9710
+ caller: "required"
9487
9711
  }), method(object({
9488
- deviceId: number().int().nonnegative(),
9489
- /** When true, fires the action block while bypassing the
9490
- * automation's condition evaluation. Gated by
9491
- * `DeviceFeature.AutomationSkipCondition`. */
9492
- skipCondition: boolean().optional()
9493
- }), _void(), {
9712
+ ruleId: string(),
9713
+ patch: NcRulePatchSchema
9714
+ }), object({ rule: NcRuleSchema }), {
9715
+ kind: "mutation",
9716
+ auth: "admin",
9717
+ caller: "required"
9718
+ }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
9719
+ kind: "mutation",
9720
+ auth: "admin"
9721
+ }), method(object({
9722
+ ruleId: string(),
9723
+ enabled: boolean()
9724
+ }), object({ success: literal(true) }), {
9725
+ kind: "mutation",
9726
+ auth: "admin"
9727
+ }), method(object({
9728
+ rule: NcRuleInputSchema,
9729
+ lookbackMinutes: number().int().min(1).max(1440).default(60)
9730
+ }), object({ results: array(NcTestResultSchema) }), {
9494
9731
  kind: "mutation",
9495
9732
  auth: "admin"
9733
+ }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
9734
+ /**
9735
+ * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9736
+ *
9737
+ * Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
9738
+ * §3.2/§3.3.
9739
+ *
9740
+ * Deliberately NOT a capability definition and NOT an `NcRule`:
9741
+ * - Every `NcDelivery` member is a *persisted-pipeline-record* trigger. A
9742
+ * timelapse fires on a SCHEDULE WINDOW BOUNDARY, evaluates no pipeline
9743
+ * record, and produces a video it assembled itself — so it rides no
9744
+ * delivery-enum member (the enum is frozen) and no cap method. This file is
9745
+ * a plain typed schema; it does NOT go through `npm run codegen`.
9746
+ * - It shares only the delivery leg (`notification-output.send`) and the
9747
+ * persistence/ownership patterns with the Notification Center, reusing
9748
+ * {@link NcScheduleSchema} (weekly windows, midnight-crossing, invertible)
9749
+ * and {@link NcRuleTargetSchema} (target ref + passthrough params).
9750
+ *
9751
+ * Ownership is SERVER-DERIVED. `ownerUserId` / `createdBy` / `createdAt` /
9752
+ * `updatedAt` / `id` / `lastGeneratedAt` live on the PERSISTED rule only —
9753
+ * {@link TimelapseRuleInputSchema} and {@link TimelapseRulePatchSchema} do not
9754
+ * carry them, so a forged client payload can never claim or re-own a rule
9755
+ * (Zod strips unknown keys). The store stamps them from the resolved caller.
9756
+ */
9757
+ /** `{{var}}` templating over camera/rule/time — same vocabulary as `NcRule`. */
9758
+ var TimelapseTemplateSchema = object({
9759
+ title: string().max(500).optional(),
9760
+ body: string().max(2e3).optional()
9761
+ });
9762
+ var NameField = string().min(1).max(200);
9763
+ var DeviceIdsField = array(number()).min(1);
9764
+ var CadenceSecField = number().int().min(2).max(3600);
9765
+ var FramerateField = number().int().min(1).max(60);
9766
+ var TargetsField = array(NcRuleTargetSchema).min(1);
9767
+ var PriorityField = number().int().min(1).max(5);
9768
+ /**
9769
+ * Client-supplied timelapse-rule fields. The server stamps id / createdBy /
9770
+ * createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
9771
+ * here (see the ownership note above).
9772
+ */
9773
+ var TimelapseRuleInputSchema = object({
9774
+ name: NameField,
9775
+ enabled: boolean().default(true),
9776
+ /** Cameras sampled by this rule — one scratch dir + one artifact per device. */
9777
+ deviceIds: DeviceIdsField,
9778
+ /**
9779
+ * Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
9780
+ * means "always active"): a timelapse is defined by its window boundaries —
9781
+ * open clears the scratch, close assembles and delivers.
9782
+ */
9783
+ schedule: NcScheduleSchema,
9784
+ /** Force-snapshot cadence inside the window, seconds (predecessor parity). */
9785
+ cadenceSec: CadenceSecField.default(15),
9786
+ /** Output frames per second of the assembled mp4 (predecessor parity). */
9787
+ framerate: FramerateField.default(10),
9788
+ /** `notification-output` targets the finished video/thumbnail is sent to. */
9789
+ targets: TargetsField,
9790
+ template: TimelapseTemplateSchema.optional(),
9791
+ /** Canonical notification priority ordinal (1..5); per-target overridable. */
9792
+ priority: PriorityField.default(3)
9793
+ });
9794
+ object({
9795
+ name: NameField.optional(),
9796
+ enabled: boolean().optional(),
9797
+ deviceIds: DeviceIdsField.optional(),
9798
+ schedule: NcScheduleSchema.optional(),
9799
+ cadenceSec: CadenceSecField.optional(),
9800
+ framerate: FramerateField.optional(),
9801
+ targets: TargetsField.optional(),
9802
+ template: TimelapseTemplateSchema.nullable().optional(),
9803
+ priority: PriorityField.optional()
9804
+ });
9805
+ TimelapseRuleInputSchema.extend({
9806
+ id: string(),
9807
+ /**
9808
+ * Ownership/visibility key. Absent = admin/global rule (visible to all).
9809
+ * Present = personal rule owned by this userId. Server-stamped from the
9810
+ * resolved caller; never trusted from a client payload.
9811
+ */
9812
+ ownerUserId: string().optional(),
9813
+ /**
9814
+ * Epoch-ms of the last successful generation — the 1-hour re-generation
9815
+ * guard's durable state (predecessor parity). Absent = never generated.
9816
+ */
9817
+ lastGeneratedAt: number().optional(),
9818
+ /** userId of the caller who created the rule (server-stamped). */
9819
+ createdBy: string(),
9820
+ createdAt: number(),
9821
+ updatedAt: number()
9496
9822
  });
9497
9823
  /**
9498
- * Battery status snapshot. Emitted by providers whose device is
9499
- * battery-operated (cameras with `DeviceFeature.BatteryOperated`,
9500
- * future sensor/button accessories). Consumers build their own "low
9501
- * battery" alerting on top — the cap deliberately does NOT enforce a
9502
- * threshold.
9824
+ * Generic device-level status snapshot. Auto-registered by `BaseDevice`
9825
+ * for every device, regardless of provider — the kernel needs a uniform
9826
+ * cap-keyed slice for the basic device flags every consumer expects to
9827
+ * read across processes (the `online` flag in particular). Driver-specific
9828
+ * caps (`battery`, `doorbell`, …) carry their domain-specific state on
9829
+ * their own slices.
9830
+ *
9831
+ * Pattern is identical to `battery`: schema-bearing `runtimeState`,
9832
+ * empty `methods`, single change event. Reads land at
9833
+ * `runtimeState.getCapState('device-status')`; writes at
9834
+ * `runtimeState.setCapState('device-status', …)`. Cross-process
9835
+ * consumers reach the same data via the `device-state` cap router
9836
+ * (`getCapSlice({deviceId, capName: 'device-status'})`).
9503
9837
  */
9504
- var BatteryStatusSchema = object({
9505
- /** 0..100 inclusive. Firmware-reported. */
9506
- percentage: number().min(0).max(100),
9838
+ var DeviceStatusSchema = object({
9507
9839
  /**
9508
- * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
9509
- * Reolink-specific for the Solar Panel 2 accessory (will become
9510
- * common on other battery cams). `'none'` means running on battery
9511
- * alone.
9840
+ * Device-level liveness. Drivers flip via `markOnline(boolean)` on
9841
+ * `BaseDevice`. Provider semantics vary RTSP aggregates broker
9842
+ * stream-health, Reolink reads firmware push events, ONVIF tracks
9843
+ * ping responses. This cap intentionally does NOT prescribe which
9844
+ * signal drives the flag.
9512
9845
  */
9513
- charging: _enum([
9514
- "dc",
9515
- "solar",
9516
- "none"
9517
- ]),
9846
+ online: boolean(),
9847
+ /** Ms epoch of the last `online` transition. Lets consumers tell
9848
+ * apart "just came online" from "still online". */
9849
+ lastChangedAt: number()
9850
+ });
9851
+ object({
9852
+ deviceId: number(),
9853
+ status: DeviceStatusSchema
9854
+ });
9855
+ /**
9856
+ * Per-device feature/identity probe slice. Holds the runtime-resolved
9857
+ * truth about what a device CAN do — which the kernel uses to:
9858
+ * 1. Reconcile accessory children (hub-children spawn siren/floodlight/PIR
9859
+ * based on what the firmware actually advertises).
9860
+ * 2. Compute the public `features: DeviceFeature[]` array surfaced via
9861
+ * `device-manager.listAll`.
9862
+ * 3. Decide which optional caps (PTZ, intercom, doorbell, battery, …)
9863
+ * to register on the device's capability surface.
9864
+ *
9865
+ * Auto-registered by `BaseDevice` for every device. Drivers populate the
9866
+ * slice from `onProbe()` (kernel calls it once after register, before
9867
+ * accessory reconciliation). Consumers read via:
9868
+ * `runtimeState.getCapState<FeatureProbeStatus>('feature-probe')`
9869
+ *
9870
+ * `flags` is an open record so each driver carries its own keys without
9871
+ * a centralized schema bottleneck — Reolink writes `hasPtz/hasIntercom`,
9872
+ * Hikvision writes `hasSupplementalLight/hasAlarmIo`, etc.
9873
+ *
9874
+ * Replaces the older driver-local `deviceCache.has*` blob: the per-device
9875
+ * config is for operator-edited overrides + UI snapshots; runtime probe
9876
+ * results belong in runtime-state where the kernel handles persistence,
9877
+ * cross-process mirroring, and reactive updates.
9878
+ */
9879
+ var FeatureProbeStatusSchema = object({
9518
9880
  /**
9519
- * True when the camera firmware has gone into low-power mode. Battery
9520
- * providers MUST avoid polling during sleep reading the battery
9521
- * wakes the camera up and drains charge.
9881
+ * Driver-specific flag bag. Each driver picks its own key names — the
9882
+ * cap deliberately does NOT enforce a closed enum here. Reolink keys:
9883
+ * `hasPtz`, `hasIntercom`, `hasDoorbell`, `hasFloodlight`, `hasSiren`,
9884
+ * `hasPirSensor`, `hasAutotrack`, `hasBattery`. Hikvision keys:
9885
+ * `hasSupplementalLight`, `lightHasWhiteLight`, `hasAlarmIo`, `hasPtz`.
9522
9886
  */
9523
- sleeping: boolean(),
9524
- /** Ms epoch of the last observation. Lets consumers reason about freshness. */
9525
- lastUpdated: number(),
9887
+ flags: record(string(), unknown()),
9526
9888
  /**
9527
- * True when the source is a BINARY low-battery indicator (HA
9528
- * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
9529
- * charge level `percentage` is then a coarse stand-in (100 = normal,
9530
- * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
9531
- * misleading exact percentage. Absent/false → genuine 0–100 % reading.
9889
+ * Coarse driver-classification lets cross-process consumers tell apart
9890
+ * cameras / battery-cams / NVRs without re-running the probe. `null`
9891
+ * before the first probe completes.
9532
9892
  */
9533
- binary: boolean().optional()
9893
+ deviceType: string().nullable(),
9894
+ /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
9895
+ model: string().nullable(),
9896
+ /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
9897
+ channelCount: number().nullable(),
9898
+ /**
9899
+ * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
9900
+ * completes — drivers' `getAccessoryChildren()` should treat zero as
9901
+ * "probe not done yet, return empty" so accessories aren't spawned
9902
+ * before the firmware is queried.
9903
+ */
9904
+ lastProbedAt: number(),
9905
+ /**
9906
+ * Framework convention: every runtime-state slice carries this for the
9907
+ * createRuntimeStateBridge stale-check helper. We keep it in sync with
9908
+ * `lastProbedAt` on every write.
9909
+ */
9910
+ lastFetchedAt: number()
9534
9911
  });
9535
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, method(object({
9536
- deviceId: number(),
9537
- /** Bound on the wait. Sensible range 3000–10000ms. */
9538
- timeoutMs: number().int().min(500).max(3e4).default(8e3)
9539
- }), object({
9540
- awoke: boolean(),
9541
- durationMs: number()
9542
- }), { kind: "mutation" }), object({
9912
+ object({
9543
9913
  deviceId: number(),
9544
- status: BatteryStatusSchema
9914
+ status: FeatureProbeStatusSchema
9545
9915
  });
9546
9916
  object({
9547
- on: boolean(),
9548
- /** Ms epoch of the last transition. 0 if never observed. */
9549
- lastChangedAt: number()
9917
+ /** Carbon dioxide concentration in ppm. */
9918
+ co2Ppm: number().min(0).optional(),
9919
+ /** Total volatile organic compounds in ppb. */
9920
+ vocPpb: number().min(0).optional(),
9921
+ /** Particulate matter ≤ 2.5 μm in µg/m³. */
9922
+ pm25: number().min(0).optional(),
9923
+ /** Particulate matter ≤ 10 μm in µg/m³. */
9924
+ pm10: number().min(0).optional(),
9925
+ /** Composite AQI value (typically 0..500). */
9926
+ aqi: number().optional(),
9927
+ /** Ms epoch when the slice was last updated. */
9928
+ lastFetchedAt: number(),
9929
+ /** Live display unit of the single metric this slice carries (e.g. HA
9930
+ * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
9931
+ * upstream `sensor.*` entity surfaces ONE device_class, so one unit
9932
+ * per slice is unambiguous. */
9933
+ unit: string().optional(),
9934
+ /** Suggested decimal places for numeric display.
9935
+ * Populated live from the upstream source when provided (e.g. HA
9936
+ * `attributes.suggested_display_precision`). Falls back to
9937
+ * auto-formatting when absent. */
9938
+ precision: number().int().min(0).max(10).optional()
9550
9939
  });
9551
9940
  DeviceType.Sensor;
9941
+ /**
9942
+ * Alarm-panel cap. Models HA `alarm_control_panel.*` on
9943
+ * `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
9944
+ * across disarmed / armed_(home|away|night|vacation|custom_bypass) /
9945
+ * arming / pending / triggered / disarming.
9946
+ *
9947
+ * Many panels require a PIN code on arm / disarm — the optional
9948
+ * `code` field on the methods passes it through to the upstream
9949
+ * service; it's NEVER persisted in the runtime slice or any event
9950
+ * payload. The presence of a required code is signalled by
9951
+ * `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
9952
+ * field without a slice fetch.
9953
+ *
9954
+ * `availableModes` mirrors HA's `supported_features`-derived arm
9955
+ * mode list — the UI renders only the buttons the panel accepts.
9956
+ */
9957
+ var AlarmStateSchema = _enum([
9958
+ "disarmed",
9959
+ "armed_home",
9960
+ "armed_away",
9961
+ "armed_night",
9962
+ "armed_vacation",
9963
+ "armed_custom_bypass",
9964
+ "arming",
9965
+ "disarming",
9966
+ "pending",
9967
+ "triggered"
9968
+ ]);
9969
+ var AlarmArmModeSchema = _enum([
9970
+ "home",
9971
+ "away",
9972
+ "night",
9973
+ "vacation",
9974
+ "custom_bypass"
9975
+ ]);
9552
9976
  object({
9553
- /** Current level as 0..100 inclusive. Firmware-reported. */
9554
- percentage: number().min(0).max(100),
9555
- /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
9977
+ /** Current lifecycle state. */
9978
+ state: AlarmStateSchema,
9979
+ /** Subset of arm modes the panel accepts. UI renders one button per
9980
+ * mode in this list. */
9981
+ availableModes: array(AlarmArmModeSchema),
9982
+ /** Whether the panel requires a PIN on arm / disarm. Mirrors
9983
+ * `DeviceFeature.AlarmPinRequired` for slice consumers. */
9984
+ requiresCode: boolean(),
9985
+ /** Ms epoch when the slice was last updated. */
9556
9986
  lastChangedAt: number()
9557
9987
  });
9558
- DeviceType.Light, method(object({
9988
+ DeviceType.AlarmPanel, method(object({
9559
9989
  deviceId: number().int().nonnegative(),
9560
- percentage: number().min(0).max(100)
9990
+ mode: AlarmArmModeSchema,
9991
+ /** Optional PIN code. Required when `requiresCode === true`.
9992
+ * Passed through to the upstream service; never persisted. */
9993
+ code: string().min(1).optional()
9561
9994
  }), _void(), {
9562
9995
  kind: "mutation",
9563
9996
  auth: "admin"
9564
- }), object({
9565
- deviceId: number(),
9566
- percentage: number().min(0).max(100),
9567
- lastChangedAt: number()
9568
- });
9569
- /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
9570
- var StreamFormatSchema = _enum([
9571
- "webrtc",
9572
- "hls",
9573
- "mjpeg",
9997
+ }), method(object({
9998
+ deviceId: number().int().nonnegative(),
9999
+ code: string().min(1).optional()
10000
+ }), _void(), {
10001
+ kind: "mutation",
10002
+ auth: "admin"
10003
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
10004
+ kind: "mutation",
10005
+ auth: "admin"
10006
+ });
10007
+ object({
10008
+ /** Current illuminance in lux (lx). */
10009
+ lux: number().min(0),
10010
+ /** Ms epoch when the slice was last updated. */
10011
+ lastFetchedAt: number(),
10012
+ /** Live display unit from the upstream source (e.g. HA
10013
+ * `attributes.unit_of_measurement`). The UI prefers this over the
10014
+ * role's canonical unit. Absent → fall back to the canonical unit. */
10015
+ unit: string().optional(),
10016
+ /** Suggested decimal places for numeric display.
10017
+ * Populated live from the upstream source when provided (e.g. HA
10018
+ * `attributes.suggested_display_precision`). Falls back to
10019
+ * auto-formatting when absent. */
10020
+ precision: number().int().min(0).max(10).optional()
10021
+ });
10022
+ DeviceType.Sensor;
10023
+ /**
10024
+ * Per-class audio metrics aggregated over a sliding window.
10025
+ */
10026
+ var AudioClassSummarySchema = object({
10027
+ className: string(),
10028
+ /** Number of windows (chunks) where this class was the top hit. */
10029
+ hits: number().int().nonnegative(),
10030
+ /** Mean score across those hits, clamped to [0,1]. */
10031
+ avgScore: number().min(0).max(1),
10032
+ /** Peak score in the window. */
10033
+ peakScore: number().min(0).max(1)
10034
+ });
10035
+ /**
10036
+ * Per-camera audio metrics snapshot — emitted by the analytics frame
10037
+ * handler on every `pipeline.audio-inference-result` event and
10038
+ * mirrored into the `audio-metrics` device-state slice. Symmetric
10039
+ * with `zone-analytics` snapshots for video — every consumer
10040
+ * (admin UI panel, automations, alert rules) reads via the
10041
+ * canonical `device.state.audioMetrics.value` reactive handle.
10042
+ *
10043
+ * Aggregates are computed over a rolling `windowSec` window
10044
+ * (default 60s). Past that window, classes drop out of `byClass`
10045
+ * and the level history shifts forward.
10046
+ */
10047
+ var AudioMetricsSnapshotSchema = object({
10048
+ /** Wall-clock timestamp (ms) of the most recent audio window. */
10049
+ ts: number().int(),
10050
+ /** Sliding-window length (seconds) used for aggregation. */
10051
+ windowSec: number().int().positive(),
10052
+ /** Latest level reading from the most recent window. */
10053
+ level: object({
10054
+ rms: number(),
10055
+ dbfs: number()
10056
+ }),
10057
+ /** Peak dBFS observed across the rolling window. */
10058
+ peakDbfs: number(),
10059
+ /** Mean dBFS across the rolling window. */
10060
+ avgDbfs: number(),
10061
+ /** Most recent above-threshold classification, or null on silence. */
10062
+ current: object({
10063
+ className: string(),
10064
+ score: number().min(0).max(1),
10065
+ timestamp: number().int()
10066
+ }).nullable(),
10067
+ /** Per-class summary across the rolling window — keys are
10068
+ * `macroClass` strings (e.g. `dog_bark`, `speech`, `glass_break`). */
10069
+ byClass: array(AudioClassSummarySchema).readonly()
10070
+ });
10071
+ /**
10072
+ * Audio-metrics history payload — a series of `AudioMetricsHistoryPoint`
10073
+ * samples capped at `maxPoints` (default 1024). When the requested
10074
+ * `windowSec / sampleEveryMs` would exceed the cap, the provider
10075
+ * subsamples by bucketed averaging and reports the effective sample
10076
+ * spacing on `effectiveSampleEveryMs` so the UI can label the x-axis.
10077
+ */
10078
+ var AudioMetricsHistorySchema = object({
10079
+ points: array(object({
10080
+ /** Wall-clock ms when this sample was recorded. */
10081
+ ts: number().int(),
10082
+ /** Instantaneous dBFS level at sample time. `null` for windows where
10083
+ * the source had no level reading (rare; happens at decode startup). */
10084
+ dbfs: number().nullable(),
10085
+ /** Rolling-window peak dBFS at sample time. Same window the live
10086
+ * snapshot reports. */
10087
+ peakDbfs: number(),
10088
+ /** Rolling-window mean dBFS at sample time. */
10089
+ avgDbfs: number(),
10090
+ /** Dominant above-threshold class at sample time, or null on silence. */
10091
+ topClass: string().nullable(),
10092
+ /** Score of the dominant class (`null` whenever `topClass` is null). */
10093
+ topScore: number().min(0).max(1).nullable()
10094
+ })).readonly(),
10095
+ /** Actual ms between adjacent samples after any subsampling. */
10096
+ effectiveSampleEveryMs: number().int().positive(),
10097
+ /** Wall-clock window covered by `points` (`points[N-1].ts - points[0].ts`),
10098
+ * or `0` when there's fewer than 2 samples. */
10099
+ windowMsActual: number().int().nonnegative()
10100
+ });
10101
+ DeviceType.Camera, method(object({ deviceId: number() }), AudioMetricsSnapshotSchema.nullable()), method(object({
10102
+ deviceId: number(),
10103
+ /** History window in seconds. Default 300 (5 minutes).
10104
+ * Provider clamps to its retention cap if larger. */
10105
+ windowSec: number().int().positive().optional(),
10106
+ /** Target sample interval in ms. Default 1000 (1 sample/second).
10107
+ * Provider clamps to natural sample rate if smaller, and
10108
+ * bucket-averages when bigger than the requested window
10109
+ * would produce more than `maxPoints` samples. */
10110
+ sampleEveryMs: number().int().positive().optional()
10111
+ }), AudioMetricsHistorySchema);
10112
+ object({
10113
+ /** Whether the automation is currently enabled. Disabled automations
10114
+ * ignore their trigger block — manual `trigger` still works. */
10115
+ enabled: boolean(),
10116
+ /** Whether the automation is currently executing its action block. */
10117
+ isRunning: boolean(),
10118
+ /** Ms epoch of the last successful run. 0 when never run. */
10119
+ lastTriggeredAt: number(),
10120
+ /** Failure description from the last completed run. Null on success
10121
+ * or when never run. */
10122
+ lastError: string().nullable(),
10123
+ /** Ms epoch when the slice was last updated. */
10124
+ lastChangedAt: number()
10125
+ });
10126
+ DeviceType.Automation, method(object({ deviceId: number().int().nonnegative() }), _void(), {
10127
+ kind: "mutation",
10128
+ auth: "admin"
10129
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
10130
+ kind: "mutation",
10131
+ auth: "admin"
10132
+ }), method(object({
10133
+ deviceId: number().int().nonnegative(),
10134
+ /** When true, fires the action block while bypassing the
10135
+ * automation's condition evaluation. Gated by
10136
+ * `DeviceFeature.AutomationSkipCondition`. */
10137
+ skipCondition: boolean().optional()
10138
+ }), _void(), {
10139
+ kind: "mutation",
10140
+ auth: "admin"
10141
+ });
10142
+ /**
10143
+ * Battery status snapshot. Emitted by providers whose device is
10144
+ * battery-operated (cameras with `DeviceFeature.BatteryOperated`,
10145
+ * future sensor/button accessories). Consumers build their own "low
10146
+ * battery" alerting on top — the cap deliberately does NOT enforce a
10147
+ * threshold.
10148
+ */
10149
+ var BatteryStatusSchema = object({
10150
+ /** 0..100 inclusive. Firmware-reported. */
10151
+ percentage: number().min(0).max(100),
10152
+ /**
10153
+ * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
10154
+ * Reolink-specific for the Solar Panel 2 accessory (will become
10155
+ * common on other battery cams). `'none'` means running on battery
10156
+ * alone.
10157
+ */
10158
+ charging: _enum([
10159
+ "dc",
10160
+ "solar",
10161
+ "none"
10162
+ ]),
10163
+ /**
10164
+ * True when the camera firmware has gone into low-power mode. Battery
10165
+ * providers MUST avoid polling during sleep — reading the battery
10166
+ * wakes the camera up and drains charge.
10167
+ */
10168
+ sleeping: boolean(),
10169
+ /** Ms epoch of the last observation. Lets consumers reason about freshness. */
10170
+ lastUpdated: number(),
10171
+ /**
10172
+ * True when the source is a BINARY low-battery indicator (HA
10173
+ * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
10174
+ * charge level — `percentage` is then a coarse stand-in (100 = normal,
10175
+ * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
10176
+ * misleading exact percentage. Absent/false → genuine 0–100 % reading.
10177
+ */
10178
+ binary: boolean().optional()
10179
+ });
10180
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, method(object({
10181
+ deviceId: number(),
10182
+ /** Bound on the wait. Sensible range 3000–10000ms. */
10183
+ timeoutMs: number().int().min(500).max(3e4).default(8e3)
10184
+ }), object({
10185
+ awoke: boolean(),
10186
+ durationMs: number()
10187
+ }), { kind: "mutation" }), object({
10188
+ deviceId: number(),
10189
+ status: BatteryStatusSchema
10190
+ });
10191
+ object({
10192
+ on: boolean(),
10193
+ /** Ms epoch of the last transition. 0 if never observed. */
10194
+ lastChangedAt: number()
10195
+ });
10196
+ DeviceType.Sensor;
10197
+ object({
10198
+ /** Current level as 0..100 inclusive. Firmware-reported. */
10199
+ percentage: number().min(0).max(100),
10200
+ /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
10201
+ lastChangedAt: number()
10202
+ });
10203
+ DeviceType.Light, method(object({
10204
+ deviceId: number().int().nonnegative(),
10205
+ percentage: number().min(0).max(100)
10206
+ }), _void(), {
10207
+ kind: "mutation",
10208
+ auth: "admin"
10209
+ }), object({
10210
+ deviceId: number(),
10211
+ percentage: number().min(0).max(100),
10212
+ lastChangedAt: number()
10213
+ });
10214
+ /** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
10215
+ var StreamFormatSchema = _enum([
10216
+ "webrtc",
10217
+ "hls",
10218
+ "mjpeg",
9574
10219
  "rtsp"
9575
10220
  ]);
9576
10221
  var RtspRestreamEntrySchema = object({
@@ -12155,84 +12800,23 @@ DeviceType.Light, DeviceType.Siren, DeviceType.Switch, method(object({
12155
12800
  lastChangedAt: number()
12156
12801
  });
12157
12802
  /**
12158
- * Shared geometry vocabulary for on-frame shape caps privacy-mask,
12159
- * motion-zones, and the detection zones/lines editor all speak this one
12160
- * language so a single drawing-plane editor and the providers stay
12161
- * decoupled from each cap's storage.
12162
- *
12163
- * All coordinates are normalized 0..1 of the camera frame (top-left
12164
- * origin). Each cap composes the SUBSET of shape kinds it supports and
12165
- * advertises it via `supportedShapes` in its `getOptions`.
12803
+ * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
12804
+ * on-camera motion-detection mask is a single `grid` region (a row-major
12805
+ * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
12806
+ * a region keeps one drawing-plane model across all geometry caps.
12166
12807
  */
12167
- /** A normalized 0..1 point (top-left origin). */
12168
- var MaskPointSchema = object({
12169
- x: number(),
12170
- y: number()
12808
+ /** A motion-zone region exactly one boolean cell grid today. */
12809
+ var MotionZoneRegionSchema = object({
12810
+ id: number(),
12811
+ enabled: boolean(),
12812
+ shape: MaskGridShapeSchema
12171
12813
  });
12172
- /** Axis-aligned rectangle (normalized 0..1). */
12173
- var MaskRectShapeSchema = object({
12174
- kind: literal("rect"),
12175
- x: number(),
12176
- y: number(),
12177
- width: number(),
12178
- height: number()
12179
- });
12180
- /** Free polygon — an ordered list of normalized vertices (≥3). */
12181
- var MaskPolygonShapeSchema = object({
12182
- kind: literal("polygon"),
12183
- points: array(MaskPointSchema)
12184
- });
12185
- /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
12186
- var MaskGridShapeSchema = object({
12187
- kind: literal("grid"),
12188
- gridWidth: number(),
12189
- gridHeight: number(),
12190
- cells: array(boolean())
12191
- });
12192
- discriminatedUnion("kind", [
12193
- MaskRectShapeSchema,
12194
- MaskPolygonShapeSchema,
12195
- MaskGridShapeSchema,
12196
- object({
12197
- kind: literal("line"),
12198
- points: array(MaskPointSchema)
12199
- })
12200
- ]);
12201
- /** Every shape-kind discriminant, for `supportedShapes` advertisement. */
12202
- var MaskShapeKindSchema = _enum([
12203
- "rect",
12204
- "polygon",
12205
- "grid",
12206
- "line"
12207
- ]);
12208
- /** Polygon vertex bounds when a cap supports 'polygon' (e.g. Hikvision {min:4,max:4}). */
12209
- var MaskPolygonVerticesSchema = object({
12210
- min: number(),
12211
- max: number()
12212
- });
12213
- /** Grid dimensions when a cap supports 'grid'. */
12214
- var MaskGridDimsSchema = object({
12215
- width: number(),
12216
- height: number()
12217
- });
12218
- /**
12219
- * Motion-zones share the same MaskShape vocabulary as privacy-mask — the
12220
- * on-camera motion-detection mask is a single `grid` region (a row-major
12221
- * boolean cell lattice the camera's onboard VMD evaluates). Composing it as
12222
- * a region keeps one drawing-plane model across all geometry caps.
12223
- */
12224
- /** A motion-zone region — exactly one boolean cell grid today. */
12225
- var MotionZoneRegionSchema = object({
12226
- id: number(),
12227
- enabled: boolean(),
12228
- shape: MaskGridShapeSchema
12229
- });
12230
- object({
12231
- enabled: boolean(),
12232
- sensitivity: number(),
12233
- /** Grid region(s). Today exactly one `grid` shape. */
12234
- regions: array(MotionZoneRegionSchema),
12235
- lastFetchedAt: number()
12814
+ object({
12815
+ enabled: boolean(),
12816
+ sensitivity: number(),
12817
+ /** Grid region(s). Today exactly one `grid` shape. */
12818
+ regions: array(MotionZoneRegionSchema),
12819
+ lastFetchedAt: number()
12236
12820
  });
12237
12821
  /** Per-camera availability — grid dims are fixed per camera model; the UI
12238
12822
  * sizes its editor from `grid`. */
@@ -14103,6 +14687,55 @@ method(object({
14103
14687
  password: string()
14104
14688
  }), AuthResultSchema.nullable(), { kind: "mutation" }), method(object({ state: string() }), string()), method(record(string(), string()), AuthResultSchema, { kind: "mutation" }), method(object({ token: string() }), AuthResultSchema.nullable());
14105
14689
  /**
14690
+ * A live terminal session hosted by the provider addon. Output and input do
14691
+ * NOT flow through the capability — they use the addon data plane
14692
+ * (`GET /addon/terminal/<id>/out` SSE, `POST /addon/terminal/<id>/in`) because
14693
+ * terminal output must be ordered and lossless. The event bus is telemetry and
14694
+ * may drop chunks ([D8]), and a dropped chunk desynchronises the vt parser
14695
+ * permanently until a full repaint. The capability owns only lifecycle.
14696
+ */
14697
+ var TerminalSessionInfoSchema = object({
14698
+ /** Opaque session id minted by the provider on `openSession`. */
14699
+ sessionId: string(),
14700
+ /** The pre-declared profile this session runs (never a free-form command). */
14701
+ profileId: string(),
14702
+ /** Human-readable profile label for the UI session list. */
14703
+ label: string(),
14704
+ cols: number().int().positive(),
14705
+ rows: number().int().positive(),
14706
+ /** ms-epoch the session's pty was spawned. */
14707
+ startedAt: number()
14708
+ });
14709
+ /**
14710
+ * A profile the operator may open — a pre-declared, allowlisted program
14711
+ * (`monitor` → `btm`). The capability accepts only these ids; a free-form
14712
+ * command string would be remote code execution as the server's user, so it is
14713
+ * deliberately not part of the contract.
14714
+ */
14715
+ var TerminalProfileInfoSchema = object({
14716
+ profileId: string(),
14717
+ label: string(),
14718
+ description: string().optional()
14719
+ });
14720
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
14721
+ profileId: string(),
14722
+ cols: number().int().positive(),
14723
+ rows: number().int().positive()
14724
+ }), TerminalSessionInfoSchema, {
14725
+ kind: "mutation",
14726
+ auth: "admin"
14727
+ }), method(object({
14728
+ sessionId: string(),
14729
+ cols: number().int().positive(),
14730
+ rows: number().int().positive()
14731
+ }), _void(), {
14732
+ kind: "mutation",
14733
+ auth: "admin"
14734
+ }), method(object({ sessionId: string() }), _void(), {
14735
+ kind: "mutation",
14736
+ auth: "admin"
14737
+ });
14738
+ /**
14106
14739
  * Orchestrator-side destination metadata. The orchestrator computes
14107
14740
  * `id = <addonId>:<subId>` from its provider lookup so consumers
14108
14741
  * (admin UI, restore flow) see one canonical key.
@@ -14203,11 +14836,53 @@ var LocationStatSchema = object({
14203
14836
  fileCount: number(),
14204
14837
  present: boolean()
14205
14838
  });
14839
+ /**
14840
+ * A backup schedule — the N:M "entry" that binds one cron cadence to a
14841
+ * SET of destination locations. Supersedes the per-location cron on
14842
+ * `BackupDestinationPolicy`: an operator creates a schedule, picks the
14843
+ * `backups` locations it should write to, and the orchestrator fans a
14844
+ * single archive out to all of them when the cron fires.
14845
+ *
14846
+ * `retentionCount` is per-schedule (D-decision 2026-07-28): every
14847
+ * location targeted by this schedule keeps this many archives from
14848
+ * this schedule's runs.
14849
+ *
14850
+ * `dataSources` optionally narrows which top-level state locations
14851
+ * (db, addons, tls, …) are archived; omitted = the orchestrator's
14852
+ * default full set.
14853
+ */
14854
+ var BackupScheduleSchema = object({
14855
+ /** Stable id. Generated by the orchestrator on first upsert if absent. */
14856
+ id: string(),
14857
+ /** Operator-facing display name. */
14858
+ label: string(),
14859
+ /** 5-field POSIX cron. Empty = disabled cadence (kept for editing). */
14860
+ cron: string(),
14861
+ /** Master on/off toggle for the whole schedule. */
14862
+ enabled: boolean(),
14863
+ /** `backups`-location ids this schedule writes to (fan-out set). */
14864
+ locationIds: array(string()).readonly(),
14865
+ /** Archives kept per targeted location for this schedule. */
14866
+ retentionCount: number().int().min(1).max(1e3),
14867
+ /** Optional subset of source locations to include; omitted = all. */
14868
+ dataSources: array(string()).readonly().optional(),
14869
+ /** ms-epoch of last successful run. */
14870
+ lastRunAt: number().optional(),
14871
+ /** ms-epoch of next computed firing (read-only, filled on list). */
14872
+ nextRunAt: number().optional()
14873
+ });
14206
14874
  method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }), method(object({
14207
14875
  /** Subset of registered `backup-destination` addon ids to write to. */
14208
14876
  destinations: array(string()).optional(),
14209
14877
  locations: array(string()).optional(),
14210
- label: string().optional()
14878
+ label: string().optional(),
14879
+ /**
14880
+ * Per-run retention override applied to every targeted
14881
+ * destination. Used by schedule-driven runs (per-entry
14882
+ * retention). Omitted = each destination's own policy
14883
+ * retention (manual runs).
14884
+ */
14885
+ retentionCount: number().int().min(1).max(1e3).optional()
14211
14886
  }).optional(), array(BackupEntrySchema).readonly(), {
14212
14887
  kind: "mutation",
14213
14888
  auth: "admin"
@@ -14256,7 +14931,21 @@ method(_void(), array(BackupDestinationInfoSchema).readonly(), { auth: "admin" }
14256
14931
  ok: boolean(),
14257
14932
  error: string().optional(),
14258
14933
  nextRuns: array(number()).readonly()
14259
- }));
14934
+ })), method(_void(), array(BackupScheduleSchema).readonly(), { auth: "admin" }), method(object({
14935
+ id: string().optional(),
14936
+ label: string(),
14937
+ cron: string(),
14938
+ enabled: boolean(),
14939
+ locationIds: array(string()).readonly(),
14940
+ retentionCount: number().int().min(1).max(1e3),
14941
+ dataSources: array(string()).readonly().optional()
14942
+ }), BackupScheduleSchema, {
14943
+ kind: "mutation",
14944
+ auth: "admin"
14945
+ }), method(object({ id: string() }), _void(), {
14946
+ kind: "mutation",
14947
+ auth: "admin"
14948
+ });
14260
14949
  /**
14261
14950
  * `broker` — unified pub/sub broker registry, system-scoped collection.
14262
14951
  *
@@ -15891,1013 +16580,525 @@ var DiskIoSnapshotSchema = object({
15891
16580
  writeBytes: number(),
15892
16581
  readOps: number(),
15893
16582
  writeOps: number(),
15894
- timestampMs: number()
15895
- });
15896
- var NetworkIoSnapshotSchema = object({
15897
- rxBytes: number(),
15898
- txBytes: number(),
15899
- rxPackets: number(),
15900
- txPackets: number(),
15901
- rxErrors: number(),
15902
- txErrors: number(),
15903
- timestampMs: number()
15904
- });
15905
- var MetricsGpuInfoSchema = object({
15906
- utilization: number(),
15907
- model: string(),
15908
- memoryUsedBytes: number(),
15909
- memoryTotalBytes: number(),
15910
- temperature: number().nullable()
15911
- });
15912
- var ProcessResourceInfoSchema = object({
15913
- openFds: number(),
15914
- threadCount: number(),
15915
- activeHandles: number(),
15916
- activeRequests: number()
15917
- });
15918
- var PressureAvgsSchema = object({
15919
- avg10: number(),
15920
- avg60: number(),
15921
- avg300: number()
15922
- });
15923
- var PressureInfoSchema = object({
15924
- some: PressureAvgsSchema,
15925
- full: PressureAvgsSchema.nullable()
15926
- });
15927
- var SystemResourceSnapshotSchema = object({
15928
- cpu: CpuBreakdownSchema,
15929
- memory: MemoryInfoSchema,
15930
- gpu: MetricsGpuInfoSchema.nullable(),
15931
- network: NetworkIoSnapshotSchema,
15932
- disk: DiskIoSnapshotSchema,
15933
- pressure: object({
15934
- cpu: PressureInfoSchema.nullable(),
15935
- memory: PressureInfoSchema.nullable(),
15936
- io: PressureInfoSchema.nullable()
15937
- }),
15938
- process: ProcessResourceInfoSchema,
15939
- cpuTemperature: number().nullable(),
15940
- timestampMs: number()
15941
- });
15942
- var DiskSpaceInfoSchema = object({
15943
- path: string(),
15944
- totalBytes: number(),
15945
- usedBytes: number(),
15946
- availableBytes: number(),
15947
- percent: number()
15948
- });
15949
- var PidResourceStatsSchema = object({
15950
- pid: number(),
15951
- cpu: number(),
15952
- memory: number(),
15953
- /**
15954
- * Private (anonymous) resident bytes — the per-process V8 heap + native
15955
- * allocations NOT shared with other processes (Linux RssAnon). This is the
15956
- * "real" per-runner cost; summing it across runners is meaningful, unlike
15957
- * `memory` (RSS), which double-counts the shared mmap'd framework code.
15958
- * Undefined where /proc is unavailable (e.g. macOS).
15959
- */
15960
- privateBytes: number().optional(),
15961
- /**
15962
- * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
15963
- * code shared copy-on-write across runners. Undefined on macOS.
15964
- */
15965
- sharedBytes: number().optional()
15966
- });
15967
- var AddonInstanceSchema = object({
15968
- addonId: string(),
15969
- nodeId: string(),
15970
- role: _enum(["hub", "worker"]),
15971
- pid: number(),
15972
- state: _enum([
15973
- "starting",
15974
- "running",
15975
- "stopping",
15976
- "stopped",
15977
- "crashed"
15978
- ]),
15979
- uptimeSec: number()
15980
- });
15981
- var NodeProcessSchema = object({
15982
- pid: number(),
15983
- ppid: number(),
15984
- pgid: number(),
15985
- classification: _enum([
15986
- "root",
15987
- "managed",
15988
- "system",
15989
- "ghost"
15990
- ]),
15991
- /** `$process` addon binding when `managed`, else null. */
15992
- addonId: string().nullable(),
15993
- /** Kernel-reported nodeId when the process is a known agent/worker. */
15994
- nodeId: string().nullable(),
15995
- /** Truncated command line. */
15996
- command: string(),
15997
- cpuPercent: number(),
15998
- memoryRssBytes: number(),
15999
- /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
16000
- uptimeSec: number(),
16001
- /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
16002
- orphaned: boolean()
16003
- });
16004
- var KillProcessInputSchema = object({
16005
- pid: number(),
16006
- /** Force = SIGKILL. Default is SIGTERM. */
16007
- force: boolean().optional()
16008
- });
16009
- var KillProcessResultSchema = object({
16010
- success: boolean(),
16011
- reason: string().optional(),
16012
- signal: _enum(["SIGTERM", "SIGKILL"]).optional()
16013
- });
16014
- var DumpHeapSnapshotInputSchema = object({
16015
- /** The addon whose runner should dump a heap snapshot. */
16016
- addonId: string() });
16017
- var DumpHeapSnapshotResultSchema = object({
16018
- success: boolean(),
16019
- /** Path of the written .heapsnapshot inside the runner's container/host. */
16020
- path: string().optional(),
16021
- /** Process pid that was signalled. */
16022
- pid: number().optional(),
16023
- reason: string().optional()
16024
- });
16025
- var SystemMetricsSchema = object({
16026
- cpuPercent: number(),
16027
- memoryPercent: number(),
16028
- memoryUsedMB: number(),
16029
- memoryTotalMB: number(),
16030
- diskPercent: number().optional(),
16031
- temperature: number().optional(),
16032
- gpuPercent: number().optional(),
16033
- gpuMemoryPercent: number().optional()
16034
- });
16035
- 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, {
16036
- kind: "mutation",
16037
- auth: "admin"
16038
- }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
16039
- kind: "mutation",
16040
- auth: "admin"
16041
- });
16042
- method(object({
16043
- sourceUrl: string(),
16044
- metadata: ModelConvertMetadataSchema,
16045
- targets: array(ConvertTargetSchema).min(1).readonly(),
16046
- calibrationRef: string().optional(),
16047
- sessionId: string().optional()
16048
- }), ConvertResultSchema, {
16049
- kind: "mutation",
16050
- auth: "admin",
16051
- timeoutMs: 6e5
16052
- });
16053
- method(object({
16054
- nodeId: string(),
16055
- modelId: string(),
16056
- format: _enum(MODEL_FORMATS),
16057
- entry: ModelCatalogEntrySchema
16058
- }), object({
16059
- ok: boolean(),
16060
- /** sha256 of the staged tarball (empty for a hub-local no-op). */
16061
- sha256: string(),
16062
- bytes: number(),
16063
- /** The target node's modelsDir the artifact landed in. */
16064
- path: string()
16065
- }), {
16066
- kind: "mutation",
16067
- auth: "admin"
16068
- });
16069
- /**
16070
- * `mqtt-broker` — broker-registry cap.
16071
- *
16072
- * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
16073
- * MQTT brokers (external + optionally an embedded `aedes`-backed one)
16074
- * and (b) the connection details a consumer addon needs to spin up
16075
- * its OWN `mqtt.js` client.
16076
- *
16077
- * Why: pub/sub routing over the system event-bus loses fidelity
16078
- * (callback shape, QoS guarantees, will/retain semantics) and adds
16079
- * refcount bookkeeping that addons would rather own themselves. The
16080
- * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
16081
- * features anyway — give it the connection config, get out of the way.
16082
- *
16083
- * Consumer flow:
16084
- * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
16085
- * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
16086
- * client.subscribe('zigbee2mqtt/+')
16087
- *
16088
- * Collection mode: multiple brokers (e.g. one local mosquitto + one
16089
- * cloud bridge). The "embedded" entry (when present) is just another
16090
- * broker in the registry — its lifecycle is owned by the addon that
16091
- * spawned it.
16092
- */
16093
- var BrokerKindSchema = _enum(["external", "embedded"]);
16094
- /**
16095
- * Broker live-probe status.
16096
- *
16097
- * - `connected` — last probe completed a clean CONNACK
16098
- * - `disconnected` — no probe has run yet (cold cache)
16099
- * - `auth-failed` — CONNACK refused with auth error (RC 4 / 5)
16100
- * - `unreachable` — TCP connect timed out / refused
16101
- * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
16102
- */
16103
- var BrokerStatusSchema$1 = _enum([
16104
- "connected",
16105
- "disconnected",
16106
- "auth-failed",
16107
- "unreachable",
16108
- "tls-error"
16109
- ]);
16110
- var BrokerInfoSchema = object({
16111
- id: string(),
16112
- name: string(),
16113
- url: string(),
16114
- kind: BrokerKindSchema,
16115
- status: BrokerStatusSchema$1,
16116
- latencyMs: number().nullable(),
16117
- error: string().optional(),
16118
- /** Embedded brokers only: number of MQTT clients currently connected. */
16119
- connectedClients: number().int().nonnegative().optional(),
16120
- /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
16121
- lastCheckedAt: number().optional()
16122
- });
16123
- /**
16124
- * Connection details — what a consumer needs to call
16125
- * `mqtt.connect(url, options)`. We split URL + credentials so the
16126
- * consumer can pass them as `mqtt.connect(url, { username, password })`
16127
- * instead of stuffing creds into the URL (which leaks them into logs).
16128
- */
16129
- var BrokerConnectionDetailsSchema = object({
16130
- url: string(),
16131
- username: string().optional(),
16132
- password: string().optional(),
16133
- /**
16134
- * Suggested prefix for `clientId`. Each consumer should suffix this
16135
- * with its own discriminator (addon id, instance id) so reconnects
16136
- * don't kick each other off (MQTT spec: clientId must be unique per
16137
- * broker).
16138
- */
16139
- clientIdPrefix: string().optional()
16140
- });
16141
- var AddBrokerInputSchema = object({
16142
- name: string().min(1),
16143
- url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
16144
- username: string().optional(),
16145
- password: string().optional(),
16146
- clientIdPrefix: string().optional()
16147
- });
16148
- var AddBrokerResultSchema = object({ id: string() });
16149
- var IdInputSchema = object({ id: string() });
16150
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
16151
- ok: literal(true),
16152
- latencyMs: number()
16153
- }), object({
16154
- ok: literal(false),
16155
- error: string()
16156
- })]);
16157
- var StartEmbeddedInputSchema = object({
16158
- port: number().int().min(1).max(65535).default(1883),
16159
- /** Allow anonymous connect (no username/password). Default: false. */
16160
- allowAnonymous: boolean().default(false),
16161
- /** Optional shared username/password for clients. */
16162
- username: string().optional(),
16163
- password: string().optional()
16164
- });
16165
- var StartEmbeddedResultSchema = object({
16166
- id: string(),
16167
- url: string()
16168
- });
16169
- var StatusSchema = object({
16170
- brokerCount: number(),
16171
- embeddedRunning: boolean()
16172
- });
16173
- 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);
16174
- var NetworkEndpointSchema = object({
16175
- url: string(),
16176
- hostname: string(),
16177
- port: number(),
16178
- protocol: _enum(["http", "https"])
16179
- });
16180
- var NetworkAccessStatusSchema = object({
16181
- connected: boolean(),
16182
- endpoint: NetworkEndpointSchema.nullable(),
16183
- error: string().optional()
16184
- });
16185
- /**
16186
- * Optional, richer endpoint shape returned by providers that expose
16187
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
16188
- * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
16189
- * the originating provider config (mode + sourcePort) so the
16190
- * orchestrator UI can label rows distinctly. Providers that expose only
16191
- * one endpoint just omit `listEndpoints` from their provider impl.
16192
- */
16193
- var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16194
- /**
16195
- * Stable id within the provider — typically `<mode>-<sourcePort>` so
16196
- * the orchestrator can dedupe across `listEndpoints` polls.
16197
- */
16198
- id: string(),
16199
- /** Operator-facing label (mirrors `MeshEndpoint.label`). */
16200
- label: string(),
16201
- /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
16202
- mode: string().optional(),
16203
- /** Originating local port the ingress fronts (informational). */
16204
- sourcePort: number().optional()
16205
- });
16206
- method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16207
- /**
16208
- * notification-output — canonical, capability-gated notification delivery.
16209
- *
16210
- * Apprise-derived model (see
16211
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16212
- * callers emit ONE canonical `Notification`; each provider declares a
16213
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
16214
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16215
- * message to what the kind supports — callers never special-case a service.
16216
- *
16217
- * DESIGN DECISIONS (locked):
16218
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16219
- * `setTargetEnabled`), each provider persisting via the `settings-store`
16220
- * cap. Rationale: the admin UI needs one uniform surface across the
16221
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16222
- * alternative would fork the UI per addon and cannot host the
16223
- * discovery→adopt flow.
16224
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16225
- * the generated cap-mount auto-`concatCollection`-fans them across every
16226
- * registered provider (notifiers addon + HA addon) so one catalog is
16227
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16228
- * `addonId` the generated collection router extracts from the call input.
16229
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16230
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16231
- * `storage` / `storage-provider` / `recording` caps over the same path. No
16232
- * base64 fallback needed.
16233
- *
16234
- * TODO (deferred, closed-set change — separate decision): add
16235
- * `providerKind: 'notify'` so notification providers surface on the unified
16236
- * admin "Integrations" page.
16237
- */
16238
- /**
16239
- * Zentik-derived typed-media enum — the superset across every kind. Each
16240
- * adapter picks what it supports and the degrade engine filters the rest.
16241
- */
16242
- var AttachmentMediaTypeSchema = _enum([
16243
- "image",
16244
- "video",
16245
- "gif",
16246
- "audio",
16247
- "icon"
16248
- ]);
16249
- /**
16250
- * A single attachment. Exactly one of `url` (remote source, most adapters
16251
- * prefer this) or `bytes` (inline source; required for Pushover-style
16252
- * bytes-only kinds) MUST be present — the degrade engine expresses a
16253
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
16254
- */
16255
- var AttachmentSchema = object({
16256
- mediaType: AttachmentMediaTypeSchema,
16257
- url: string().optional(),
16258
- bytes: _instanceof(Uint8Array).optional(),
16259
- mime: string().optional(),
16260
- name: string().optional()
16261
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16262
- var NotificationFormatSchema = _enum([
16263
- "text",
16264
- "markdown",
16265
- "html"
16266
- ]);
16267
- /** A single tap-through action button. */
16268
- var NotificationActionSchema = object({
16269
- id: string(),
16270
- label: string(),
16271
- url: string().optional()
16272
- });
16273
- /**
16274
- * The canonical notification. `body` is the only hard field (Apprise model).
16275
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16276
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16277
- * the adapter maps this ordinal onto its native level. `level?` is an
16278
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
16279
- * `priority` for that one target.
16280
- */
16281
- var NotificationSchema = object({
16282
- body: string(),
16283
- title: string().optional(),
16284
- format: NotificationFormatSchema.default("text"),
16285
- priority: number().int().min(1).max(5).default(3),
16286
- level: string().optional(),
16287
- attachments: array(AttachmentSchema).optional(),
16288
- clickUrl: string().optional(),
16289
- actions: array(NotificationActionSchema).optional(),
16290
- sound: string().optional(),
16291
- ttl: number().optional(),
16292
- tag: string().optional(),
16293
- deviceId: number().optional(),
16294
- eventId: string().optional(),
16295
- metadata: record(string(), unknown()).optional()
16296
- });
16297
- /** One declared native severity/priority level for a kind. */
16298
- var TargetKindLevelSchema = object({
16299
- id: string(),
16300
- label: string(),
16301
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16302
- ordinal: number().int().min(1).max(5).nullable(),
16303
- flags: object({
16304
- critical: boolean().optional(),
16305
- silent: boolean().optional(),
16306
- noPush: boolean().optional()
16307
- }).optional(),
16308
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16309
- requires: array(string()).optional(),
16310
- description: string().optional()
16311
- });
16312
- /** The full capability block consulted before dispatch. */
16313
- var TargetKindCapsSchema = object({
16314
- attachments: object({
16315
- mediaTypes: array(AttachmentMediaTypeSchema),
16316
- mode: _enum([
16317
- "url",
16318
- "bytes",
16319
- "both"
16320
- ]),
16321
- max: number().int().nonnegative(),
16322
- maxBytes: number().int().positive().optional()
16323
- }),
16324
- /** Max action buttons (0 = none). */
16325
- actions: number().int().nonnegative(),
16326
- levels: array(TargetKindLevelSchema),
16327
- format: array(NotificationFormatSchema),
16328
- clickUrl: boolean(),
16329
- sound: boolean(),
16330
- ttl: boolean(),
16331
- bodyMaxLen: number().int().positive()
16332
- });
16333
- /**
16334
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
16335
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
16336
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
16337
- * the union is large and not meant for runtime validation here; the exported
16338
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16339
- */
16340
- var ConfigSchemaPassthrough = unknown();
16341
- var TargetKindSchema = object({
16342
- kind: string(),
16343
- label: string(),
16344
- icon: string(),
16345
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
16346
- addonId: string(),
16347
- configSchema: ConfigSchemaPassthrough,
16348
- supportsDiscovery: boolean(),
16349
- caps: TargetKindCapsSchema
16350
- });
16351
- /**
16352
- * A persisted target. `config` holds secrets; providers REDACT secret fields
16353
- * (return a presence marker only) when serving `listTargets` — never
16354
- * round-trip a stored secret to the UI.
16355
- */
16356
- var TargetSchema = object({
16357
- id: string(),
16358
- name: string(),
16359
- kind: string(),
16360
- addonId: string(),
16361
- enabled: boolean(),
16362
- config: record(string(), unknown())
16363
- });
16364
- /** A discovery-surfaced candidate (config is partial + non-secret). */
16365
- var DiscoveredTargetSchema = object({
16366
- kind: string(),
16367
- suggestedName: string(),
16368
- config: record(string(), unknown())
16369
- });
16370
- /** The degrade engine's report — what was resolved / dropped / degraded. */
16371
- var RenderedAsSchema = object({
16372
- level: string(),
16373
- format: NotificationFormatSchema,
16374
- attachmentsSent: number().int().nonnegative(),
16375
- actionsSent: number().int().nonnegative(),
16376
- truncated: boolean(),
16377
- dropped: array(string())
16378
- });
16379
- var SendResultSchema = object({
16380
- success: boolean(),
16381
- error: string().optional(),
16382
- renderedAs: RenderedAsSchema.optional()
16383
- });
16384
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
16385
- var TestResultSchema = SendResultSchema;
16386
- var notificationOutputCapability = {
16387
- name: "notification-output",
16388
- scope: "system",
16389
- mode: "collection",
16390
- methods: {
16391
- listTargetKinds: method(object({}), array(TargetKindSchema)),
16392
- listTargets: method(object({}), array(TargetSchema)),
16393
- discoverTargets: method(object({
16394
- kind: string(),
16395
- config: record(string(), unknown()).optional()
16396
- }), array(DiscoveredTargetSchema)),
16397
- send: method(object({
16398
- targetId: string(),
16399
- notification: NotificationSchema
16400
- }), SendResultSchema, { kind: "mutation" }),
16401
- testTarget: method(object({
16402
- targetId: string(),
16403
- sample: NotificationSchema.optional()
16404
- }), TestResultSchema, { kind: "mutation" }),
16405
- upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
16406
- deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
16407
- setTargetEnabled: method(object({
16408
- targetId: string(),
16409
- enabled: boolean()
16410
- }), _void(), { kind: "mutation" })
16411
- }
16412
- };
16413
- /**
16414
- * notification-rules — the Notification Center rule surface (P1 core).
16415
- *
16416
- * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
16417
- * (operator decisions D-1/D-2/D-3 are binding):
16418
- *
16419
- * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
16420
- * `notification-center` module), hooked on the durable persistence
16421
- * moments (object-event insert, TrackCloser.closeExpired) with a
16422
- * persisted outbox + retry — never the lossy telemetry bus (D8).
16423
- * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
16424
- * FIRST persisted detection matching the conditions (per-track dedup,
16425
- * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
16426
- * `delivery: 'track-end'` evaluates the finalized track record at close.
16427
- * - DISPATCH stays behind `notification-output` (rules reference targets
16428
- * by id; per-backend params are a passthrough blob capped by the
16429
- * target kind's own caps/degrade engine).
16430
- *
16431
- * P1 scope: admin-authored rules only (`createdBy` stamped from the
16432
- * server-injected caller identity — the first `caller: 'required'`
16433
- * adopter). The P1 condition subset is: devices, classes(+exclude),
16434
- * minConfidence, admin zones (any/all + exclude), weekly schedule
16435
- * windows, and the optional label/identity/plate matchers. User rules,
16436
- * private zones, per-recipient fan-out and the wider condition table are
16437
- * P2+ (see spec §7).
16438
- *
16439
- * All schemas here are the single source of truth — `NcRule` etc. are
16440
- * `z.infer` exports; no duplicate interfaces (the advanced-notifier
16441
- * schema/interface drift is explicitly not repeated).
16442
- */
16443
- /**
16444
- * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
16445
- * The value maps 1:1 onto the evaluated record kind:
16446
- * - `immediate` ↔ object-event persist (lowest-latency detection burst)
16447
- * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
16448
- * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
16449
- * change of a LINKED device, one row per linked camera)
16450
- * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
16451
- * delivery / pick-up)
16452
- *
16453
- * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
16454
- * `package-event` are pure trigger kinds (no urgency dimension). Extending
16455
- * this one field keeps the schema additive — a rule still declares exactly
16456
- * one trigger.
16457
- */
16458
- var NcDeliverySchema = _enum([
16459
- "immediate",
16460
- "track-end",
16461
- "device-event",
16462
- "package-event"
16463
- ]);
16464
- /** Weekly schedule — OR of windows; absence on the rule = always active. */
16465
- var NcScheduleSchema = object({
16466
- windows: array(object({
16467
- /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
16468
- days: array(number().int().min(0).max(6)).min(1),
16469
- startMinute: number().int().min(0).max(1439),
16470
- endMinute: number().int().min(0).max(1439)
16471
- })).min(1),
16472
- /** IANA timezone; default = hub host timezone. */
16473
- timezone: string().optional(),
16474
- /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
16475
- invert: boolean().optional()
16476
- });
16477
- /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
16478
- var NcPlateMatcherSchema = object({
16479
- values: array(string().min(1)).min(1),
16480
- /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
16481
- maxDistance: number().int().min(0).max(3).default(1)
16482
- });
16483
- /**
16484
- * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
16485
- * occupancy edge for a device — optionally narrowed to a single admin
16486
- * `zoneId` and/or object `className`. `op` selects the edge/threshold:
16487
- * - `became-occupied` (default) — count crossed 0 → ≥ `count`
16488
- * - `became-free` — count crossed ≥ `count` → below it
16489
- * - `>=` / `<=` — count is at/over or at/under `count`
16490
- * `sustainSeconds` requires the condition hold continuously that long
16491
- * before firing (debounces flicker; 0 = fire on the first matching edge).
16492
- * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
16493
- * the condition never matches. Confirmed edge-state survives addon restarts
16494
- * (declared SQLite collection, reseeded on boot).
16495
- */
16496
- var NcOccupancyConditionSchema = object({
16497
- /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
16498
- zoneId: string().optional(),
16499
- /** Object class to count; absent = any class. */
16500
- className: string().optional(),
16501
- op: _enum([
16502
- "became-occupied",
16503
- "became-free",
16504
- ">=",
16505
- "<="
16506
- ]).default("became-occupied"),
16507
- count: number().int().min(0).default(1),
16508
- sustainSeconds: number().int().min(0).max(3600).default(15)
16509
- });
16510
- /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
16511
- var NcZoneConditionSchema = object({
16512
- ids: array(string().min(1)).min(1),
16513
- /** Quantifier over `ids` — at least one / every one visited. */
16514
- match: _enum(["any", "all"]).default("any")
16515
- });
16516
- /**
16517
- * The P1 condition set — a flat AND of groups; absent group = pass;
16518
- * membership lists are OR within the list (spec §2.3).
16519
- */
16520
- var NcConditionsSchema = object({
16521
- /** Device scope — absent = all devices. */
16522
- devices: array(number()).optional(),
16523
- /** Detector class names (any overlap with the record's class set). */
16524
- classes: array(string().min(1)).optional(),
16525
- /** Veto classes — any overlap fails the rule. */
16526
- classesExclude: array(string().min(1)).optional(),
16527
- /** Minimum detection confidence 0–1 (fails when the record has none). */
16528
- minConfidence: number().min(0).max(1).optional(),
16529
- /** Admin zone membership over event `zones` / track `zonesVisited`. */
16530
- zones: NcZoneConditionSchema.optional(),
16531
- /** Veto zones — any hit fails the rule. */
16532
- zonesExclude: array(string().min(1)).optional(),
16533
- /**
16534
- * Exact (case-insensitive) match on the record's collapsed `label`
16535
- * (identity name / plate text / subclass).
16536
- */
16537
- labelEquals: array(string().min(1)).optional(),
16538
- /**
16539
- * Identity matcher. P1 boundary: matched against the record's collapsed
16540
- * `label` (the identity display name propagated by the face pipeline) —
16541
- * identity-ID matching rides in P2 when identity ids reach the record.
16542
- */
16543
- identities: array(string().min(1)).optional(),
16544
- /** Fuzzy plate matcher against the record's `label` (plate text). */
16545
- plates: NcPlateMatcherSchema.optional(),
16546
- /**
16547
- * Identity EXCLUDE — mirror of {@link identities} with `notIn` semantics.
16548
- * Same P1 boundary: matched against the record's collapsed `label` (the
16549
- * identity display name). A record with NO label passes (nothing to
16550
- * exclude), unlike the include variant which fails on an absent label.
16551
- */
16552
- identitiesExclude: array(string().min(1)).optional(),
16553
- /**
16554
- * Minimum server-computed key-event importance in [0,1] (`Track.importance`).
16555
- * TRACK-END only: importance is scored at track close, so it does not exist
16556
- * at immediate / object-event evaluation time (see catalog `appliesTo`). At
16557
- * close the value is threaded via the close-time info (the `Track` clone is
16558
- * captured before the DB row is updated, so it would otherwise read stale).
16559
- * Fails when the record carries no importance (never guess quality — the
16560
- * `minConfidence` precedent). MVP cut: a single scalar threshold.
16561
- */
16562
- minImportance: number().min(0).max(1).optional(),
16563
- /**
16564
- * Minimum track dwell in SECONDS — `(lastSeen − firstSeen) / 1000`.
16565
- * TRACK-END only: an `immediate` / object-event subject has no closed
16566
- * lifespan, so a dwell condition never matches immediate delivery
16567
- * (documented choice — the object-event record carries no `firstSeen`,
16568
- * so dwell cannot be computed from what the subject actually carries).
16569
- */
16570
- minDwellSeconds: number().min(0).optional(),
16571
- /**
16572
- * Detection provenance filter. `any` (default / absent) matches every
16573
- * source; otherwise the subject's source must equal it. Legacy records
16574
- * with no stamped source are treated as `pipeline`. The union spans both
16575
- * record kinds — object events carry `pipeline` | `onboard`, synthetic
16576
- * tracks carry `sensor`.
16577
- */
16578
- source: _enum([
16579
- "pipeline",
16580
- "onboard",
16581
- "sensor",
16582
- "any"
16583
- ]).optional(),
16584
- /**
16585
- * Minimum identity / plate MATCH confidence in [0,1] — DISTINCT from the
16586
- * detector `minConfidence` (that gates the object-detection score; this
16587
- * gates the recognition/OCR match score). Fails when the subject carries
16588
- * no label-match confidence (never guess). TRACK-END only: the confidence
16589
- * lives on the recognition result and reaches the subject at track close.
16590
- *
16591
- * What it measures precisely (plumbed at track close — the closer threads
16592
- * the value into `NcTrackClosedInfo.labelConfidence`, the same seam as
16593
- * `importance`): the BEST recognition match confidence observed for the
16594
- * label the track carries at close — for a face, the peak cosine similarity
16595
- * of the ASSIGNED identity (`FaceMatch.score`, reset on an identity switch);
16596
- * for a plate, the peak OCR read score of the best-held plate
16597
- * (`plateText.confidence`). When BOTH a face and a plate were recognized on
16598
- * one track the higher of the two is used. A track that ended with no
16599
- * confident identity/plate match carries no value, so the condition fails
16600
- * closed for it (an un-recognized subject).
16601
- */
16602
- minLabelConfidence: number().min(0).max(1).optional(),
16603
- /**
16604
- * DEVICE-EVENT only. Raw device event-type tokens (`EventFire.eventType`,
16605
- * e.g. a doorbell `press` / `press_long`) — matched case-insensitively
16606
- * against the token carried on the device-event subject (extracted from the
16607
- * event-emitter runtime slice's `lastEvent.eventType`). Fails when the
16608
- * subject carries no token. Doorbell-pulse / passive-sensor kinds emit no
16609
- * eventType, so gate those with {@link sensorKinds} instead.
16610
- */
16611
- eventTypeTokens: array(string().min(1)).optional(),
16612
- /**
16613
- * DEVICE-EVENT only. Sensor/control taxonomy kinds (e.g. `doorbell`,
16614
- * `contact`, `button`, `device-event`) — matched against the persisted
16615
- * `SensorEvent.kind` (see `sensor-event-kinds.ts`). Membership is OR.
16616
- */
16617
- sensorKinds: array(string().min(1)).optional(),
16618
- /**
16619
- * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
16620
- * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
16621
- * when the subject's phase does not match (a subject always carries a phase
16622
- * on the package-event trigger).
16623
- */
16624
- packagePhase: _enum([
16625
- "delivered",
16626
- "picked-up",
16627
- "both"
16628
- ]).optional(),
16583
+ timestampMs: number()
16584
+ });
16585
+ var NetworkIoSnapshotSchema = object({
16586
+ rxBytes: number(),
16587
+ txBytes: number(),
16588
+ rxPackets: number(),
16589
+ txPackets: number(),
16590
+ rxErrors: number(),
16591
+ txErrors: number(),
16592
+ timestampMs: number()
16593
+ });
16594
+ var MetricsGpuInfoSchema = object({
16595
+ utilization: number(),
16596
+ model: string(),
16597
+ memoryUsedBytes: number(),
16598
+ memoryTotalBytes: number(),
16599
+ temperature: number().nullable()
16600
+ });
16601
+ var ProcessResourceInfoSchema = object({
16602
+ openFds: number(),
16603
+ threadCount: number(),
16604
+ activeHandles: number(),
16605
+ activeRequests: number()
16606
+ });
16607
+ var PressureAvgsSchema = object({
16608
+ avg10: number(),
16609
+ avg60: number(),
16610
+ avg300: number()
16611
+ });
16612
+ var PressureInfoSchema = object({
16613
+ some: PressureAvgsSchema,
16614
+ full: PressureAvgsSchema.nullable()
16615
+ });
16616
+ var SystemResourceSnapshotSchema = object({
16617
+ cpu: CpuBreakdownSchema,
16618
+ memory: MemoryInfoSchema,
16619
+ gpu: MetricsGpuInfoSchema.nullable(),
16620
+ network: NetworkIoSnapshotSchema,
16621
+ disk: DiskIoSnapshotSchema,
16622
+ pressure: object({
16623
+ cpu: PressureInfoSchema.nullable(),
16624
+ memory: PressureInfoSchema.nullable(),
16625
+ io: PressureInfoSchema.nullable()
16626
+ }),
16627
+ process: ProcessResourceInfoSchema,
16628
+ cpuTemperature: number().nullable(),
16629
+ timestampMs: number()
16630
+ });
16631
+ var DiskSpaceInfoSchema = object({
16632
+ path: string(),
16633
+ totalBytes: number(),
16634
+ usedBytes: number(),
16635
+ availableBytes: number(),
16636
+ percent: number()
16637
+ });
16638
+ var PidResourceStatsSchema = object({
16639
+ pid: number(),
16640
+ cpu: number(),
16641
+ memory: number(),
16629
16642
  /**
16630
- * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
16631
- * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
16632
- * listed polygon (ZoneEngine membership semantics). Evaluated only when
16633
- * the subject carries a bbox; absent bbox the condition FAILS.
16643
+ * Private (anonymous) resident bytes — the per-process V8 heap + native
16644
+ * allocations NOT shared with other processes (Linux RssAnon). This is the
16645
+ * "real" per-runner cost; summing it across runners is meaningful, unlike
16646
+ * `memory` (RSS), which double-counts the shared mmap'd framework code.
16647
+ * Undefined where /proc is unavailable (e.g. macOS).
16634
16648
  */
16635
- customZones: array(MaskPolygonShapeSchema).optional(),
16649
+ privateBytes: number().optional(),
16636
16650
  /**
16637
- * DEVICE-EVENT only. ZoneAnalytics occupancy edgefires when a device's
16638
- * (optionally zone/class-scoped) occupancy count crosses the configured
16639
- * threshold and holds for `sustainSeconds`. Fail-closed on missing
16640
- * substrate (no snapshot / missing zone). See {@link NcOccupancyCondition}.
16651
+ * Shared file-backed resident bytes (Linux RssFile)mmap'd framework/lib
16652
+ * code shared copy-on-write across runners. Undefined on macOS.
16641
16653
  */
16642
- occupancy: NcOccupancyConditionSchema.optional()
16654
+ sharedBytes: number().optional()
16643
16655
  });
16644
- /** One delivery target: a `notification-output` Target ref + passthrough params. */
16645
- var NcRuleTargetSchema = object({
16646
- /** `notification-output` Target id. */
16647
- targetId: string().min(1),
16648
- /**
16649
- * Per-backend passthrough. Recognized keys are mapped onto the canonical
16650
- * Notification (`priority`, `level`, `sound`, `clickUrl`, `ttl`); the
16651
- * degrade engine drops what the backend can't render.
16652
- */
16653
- params: record(string(), unknown()).optional()
16656
+ var AddonInstanceSchema = object({
16657
+ addonId: string(),
16658
+ nodeId: string(),
16659
+ role: _enum(["hub", "worker"]),
16660
+ pid: number(),
16661
+ state: _enum([
16662
+ "starting",
16663
+ "running",
16664
+ "stopping",
16665
+ "stopped",
16666
+ "crashed"
16667
+ ]),
16668
+ uptimeSec: number()
16669
+ });
16670
+ var NodeProcessSchema = object({
16671
+ pid: number(),
16672
+ ppid: number(),
16673
+ pgid: number(),
16674
+ classification: _enum([
16675
+ "root",
16676
+ "managed",
16677
+ "system",
16678
+ "ghost"
16679
+ ]),
16680
+ /** `$process` addon binding when `managed`, else null. */
16681
+ addonId: string().nullable(),
16682
+ /** Kernel-reported nodeId when the process is a known agent/worker. */
16683
+ nodeId: string().nullable(),
16684
+ /** Truncated command line. */
16685
+ command: string(),
16686
+ cpuPercent: number(),
16687
+ memoryRssBytes: number(),
16688
+ /** Wall-clock uptime (seconds). Parsed from `ps etime`. */
16689
+ uptimeSec: number(),
16690
+ /** True when ancestor walk reaches `ppid=1` (reparented to init/launchd). */
16691
+ orphaned: boolean()
16692
+ });
16693
+ var KillProcessInputSchema = object({
16694
+ pid: number(),
16695
+ /** Force = SIGKILL. Default is SIGTERM. */
16696
+ force: boolean().optional()
16697
+ });
16698
+ var KillProcessResultSchema = object({
16699
+ success: boolean(),
16700
+ reason: string().optional(),
16701
+ signal: _enum(["SIGTERM", "SIGKILL"]).optional()
16702
+ });
16703
+ var DumpHeapSnapshotInputSchema = object({
16704
+ /** The addon whose runner should dump a heap snapshot. */
16705
+ addonId: string() });
16706
+ var DumpHeapSnapshotResultSchema = object({
16707
+ success: boolean(),
16708
+ /** Path of the written .heapsnapshot inside the runner's container/host. */
16709
+ path: string().optional(),
16710
+ /** Process pid that was signalled. */
16711
+ pid: number().optional(),
16712
+ reason: string().optional()
16713
+ });
16714
+ var SystemMetricsSchema = object({
16715
+ cpuPercent: number(),
16716
+ memoryPercent: number(),
16717
+ memoryUsedMB: number(),
16718
+ memoryTotalMB: number(),
16719
+ diskPercent: number().optional(),
16720
+ temperature: number().optional(),
16721
+ gpuPercent: number().optional(),
16722
+ gpuMemoryPercent: number().optional()
16723
+ });
16724
+ 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, {
16725
+ kind: "mutation",
16726
+ auth: "admin"
16727
+ }), method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
16728
+ kind: "mutation",
16729
+ auth: "admin"
16730
+ });
16731
+ method(object({
16732
+ sourceUrl: string(),
16733
+ metadata: ModelConvertMetadataSchema,
16734
+ targets: array(ConvertTargetSchema).min(1).readonly(),
16735
+ calibrationRef: string().optional(),
16736
+ sessionId: string().optional()
16737
+ }), ConvertResultSchema, {
16738
+ kind: "mutation",
16739
+ auth: "admin",
16740
+ timeoutMs: 6e5
16741
+ });
16742
+ method(object({
16743
+ nodeId: string(),
16744
+ modelId: string(),
16745
+ format: _enum(MODEL_FORMATS),
16746
+ entry: ModelCatalogEntrySchema
16747
+ }), object({
16748
+ ok: boolean(),
16749
+ /** sha256 of the staged tarball (empty for a hub-local no-op). */
16750
+ sha256: string(),
16751
+ bytes: number(),
16752
+ /** The target node's modelsDir the artifact landed in. */
16753
+ path: string()
16754
+ }), {
16755
+ kind: "mutation",
16756
+ auth: "admin"
16654
16757
  });
16655
16758
  /**
16656
- * Media attachment policy (P1 still-image subset).
16657
- * - `best` — the best AVAILABLE subject image at dispatch time (D-3).
16658
- * - `best-matching` the media that explains WHY the rule fired: a rule
16659
- * matched on identities attaches the subject's `faceCrop`, one matched on
16660
- * plates attaches the `plateCrop`; a rule with no identity/plate condition
16661
- * (or when the specific crop is missing) degrades to `best`, then
16662
- * `keyFrame`, then no attachment — never delaying the send. The matched
16663
- * condition summary is frozen on the outbox row at enqueue (like the rule
16664
- * name), so the choice never drifts from the record that fired it.
16665
- * - `keyFrame` the clean scene frame (no subject box).
16666
- * - `none` no attachment.
16759
+ * `mqtt-broker` broker-registry cap.
16760
+ *
16761
+ * NOT a pub/sub proxy. The cap exposes (a) a registry of configured
16762
+ * MQTT brokers (external + optionally an embedded `aedes`-backed one)
16763
+ * and (b) the connection details a consumer addon needs to spin up
16764
+ * its OWN `mqtt.js` client.
16765
+ *
16766
+ * Why: pub/sub routing over the system event-bus loses fidelity
16767
+ * (callback shape, QoS guarantees, will/retain semantics) and adds
16768
+ * refcount bookkeeping that addons would rather own themselves. The
16769
+ * canonical consumer (`addon-export-ha-mqtt`) needs raw `mqtt.js`
16770
+ * features anyway — give it the connection config, get out of the way.
16771
+ *
16772
+ * Consumer flow:
16773
+ * const cfg = await ctx.api.mqttBroker.getBrokerConfig({ id })
16774
+ * const client = mqtt.connect(cfg.url, { username: cfg.username, … })
16775
+ * client.subscribe('zigbee2mqtt/+')
16776
+ *
16777
+ * Collection mode: multiple brokers (e.g. one local mosquitto + one
16778
+ * cloud bridge). The "embedded" entry (when present) is just another
16779
+ * broker in the registry — its lifecycle is owned by the addon that
16780
+ * spawned it.
16667
16781
  */
16668
- var NcMediaPolicySchema = object({ attach: _enum([
16669
- "best",
16670
- "best-matching",
16671
- "keyFrame",
16672
- "none"
16673
- ]).default("best") });
16674
- /** Throttlecooldown survives restarts (rebuilt from the outbox on boot). */
16675
- var NcThrottleSchema = object({
16676
- cooldownSec: number().int().min(0).max(86400).default(60),
16677
- /** `rule` = one shared cooldown; `rule-device` = per-camera cooldown. */
16678
- scope: _enum(["rule", "rule-device"]).default("rule-device")
16782
+ var BrokerKindSchema = _enum(["external", "embedded"]);
16783
+ /**
16784
+ * Broker live-probe status.
16785
+ *
16786
+ * - `connected` — last probe completed a clean CONNACK
16787
+ * - `disconnected` — no probe has run yet (cold cache)
16788
+ * - `auth-failed` CONNACK refused with auth error (RC 4 / 5)
16789
+ * - `unreachable` — TCP connect timed out / refused
16790
+ * - `tls-error` — TLS handshake failed (cert / SNI / cipher)
16791
+ */
16792
+ var BrokerStatusSchema$1 = _enum([
16793
+ "connected",
16794
+ "disconnected",
16795
+ "auth-failed",
16796
+ "unreachable",
16797
+ "tls-error"
16798
+ ]);
16799
+ var BrokerInfoSchema = object({
16800
+ id: string(),
16801
+ name: string(),
16802
+ url: string(),
16803
+ kind: BrokerKindSchema,
16804
+ status: BrokerStatusSchema$1,
16805
+ latencyMs: number().nullable(),
16806
+ error: string().optional(),
16807
+ /** Embedded brokers only: number of MQTT clients currently connected. */
16808
+ connectedClients: number().int().nonnegative().optional(),
16809
+ /** Epoch ms of the last live probe (external) or aedes snapshot (embedded). */
16810
+ lastCheckedAt: number().optional()
16679
16811
  });
16680
- /** Client-supplied rule fields (server stamps id/createdBy/createdAt/updatedAt). */
16681
- var NcRuleInputSchema = object({
16682
- name: string().min(1).max(200),
16683
- enabled: boolean().default(true),
16684
- delivery: NcDeliverySchema,
16685
- conditions: NcConditionsSchema.default({}),
16686
- schedule: NcScheduleSchema.optional(),
16687
- targets: array(NcRuleTargetSchema).min(1),
16688
- media: NcMediaPolicySchema.default({ attach: "best" }),
16689
- throttle: NcThrottleSchema.default({
16690
- cooldownSec: 60,
16691
- scope: "rule-device"
16692
- }),
16693
- /** `{{var}}` templating over camera/class/label/zones/confidence/time. */
16694
- template: object({
16695
- title: string().max(500).optional(),
16696
- body: string().max(2e3).optional()
16697
- }).optional(),
16698
- /** Canonical notification priority ordinal (1..5); per-target overridable. */
16699
- priority: number().int().min(1).max(5).default(3),
16812
+ /**
16813
+ * Connection details — what a consumer needs to call
16814
+ * `mqtt.connect(url, options)`. We split URL + credentials so the
16815
+ * consumer can pass them as `mqtt.connect(url, { username, password })`
16816
+ * instead of stuffing creds into the URL (which leaks them into logs).
16817
+ */
16818
+ var BrokerConnectionDetailsSchema = object({
16819
+ url: string(),
16820
+ username: string().optional(),
16821
+ password: string().optional(),
16700
16822
  /**
16701
- * Ownership/visibility key. Absent = admin/global rule (unchanged legacy
16702
- * behaviour, visible to all, read-only in the viewer). Present = personal
16703
- * rule owned by this userId. Server-stamped; never trusted from a client.
16823
+ * Suggested prefix for `clientId`. Each consumer should suffix this
16824
+ * with its own discriminator (addon id, instance id) so reconnects
16825
+ * don't kick each other off (MQTT spec: clientId must be unique per
16826
+ * broker).
16704
16827
  */
16705
- ownerUserId: string().optional()
16828
+ clientIdPrefix: string().optional()
16829
+ });
16830
+ var AddBrokerInputSchema = object({
16831
+ name: string().min(1),
16832
+ url: string().regex(/^(mqtt|mqtts|ws|wss):\/\//, "URL must start with mqtt(s):// or ws(s)://"),
16833
+ username: string().optional(),
16834
+ password: string().optional(),
16835
+ clientIdPrefix: string().optional()
16836
+ });
16837
+ var AddBrokerResultSchema = object({ id: string() });
16838
+ var IdInputSchema = object({ id: string() });
16839
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
16840
+ ok: literal(true),
16841
+ latencyMs: number()
16842
+ }), object({
16843
+ ok: literal(false),
16844
+ error: string()
16845
+ })]);
16846
+ var StartEmbeddedInputSchema = object({
16847
+ port: number().int().min(1).max(65535).default(1883),
16848
+ /** Allow anonymous connect (no username/password). Default: false. */
16849
+ allowAnonymous: boolean().default(false),
16850
+ /** Optional shared username/password for clients. */
16851
+ username: string().optional(),
16852
+ password: string().optional()
16853
+ });
16854
+ var StartEmbeddedResultSchema = object({
16855
+ id: string(),
16856
+ url: string()
16857
+ });
16858
+ var StatusSchema = object({
16859
+ brokerCount: number(),
16860
+ embeddedRunning: boolean()
16861
+ });
16862
+ 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);
16863
+ var NetworkEndpointSchema = object({
16864
+ url: string(),
16865
+ hostname: string(),
16866
+ port: number(),
16867
+ protocol: _enum(["http", "https"])
16868
+ });
16869
+ var NetworkAccessStatusSchema = object({
16870
+ connected: boolean(),
16871
+ endpoint: NetworkEndpointSchema.nullable(),
16872
+ error: string().optional()
16706
16873
  });
16707
16874
  /**
16708
- * Partial patch for `updateRule` any subset of the input fields, plus the
16709
- * persisted-only {@link NcRuleSchema} `disabledTargetIds` set. The latter is
16710
- * NOT a client-authored input field (it lives on the persisted rule, not the
16711
- * input), so it is added here explicitly to let the store's per-target opt-out
16712
- * toggle round-trip through the shared `update` path. Viewer opt-out mutations
16713
- * still flow through `nc.setRuleTargetEnabled` (owner-checked), never a raw
16714
- * `updateRule` patch.
16875
+ * Optional, richer endpoint shape returned by providers that expose
16876
+ * MORE than one ingress concurrently (Tailscale Ingress with mixed
16877
+ * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
16878
+ * the originating provider config (mode + sourcePort) so the
16879
+ * orchestrator UI can label rows distinctly. Providers that expose only
16880
+ * one endpoint just omit `listEndpoints` from their provider impl.
16715
16881
  */
16716
- var NcRulePatchSchema = NcRuleInputSchema.partial().extend({ disabledTargetIds: array(string()).optional() });
16717
- /** A persisted rule. */
16718
- var NcRuleSchema = NcRuleInputSchema.extend({
16719
- id: string(),
16720
- /** userId of the admin who created the rule (server-stamped caller). */
16721
- createdBy: string(),
16722
- createdAt: number(),
16723
- updatedAt: number(),
16882
+ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
16724
16883
  /**
16725
- * Per-target opt-out set. A targetId here is suppressed for THIS rule at
16726
- * send time. Only a target's OWNER may add/remove its id (server-checked
16727
- * in `nc.setRuleTargetEnabled`). Defaults to empty.
16884
+ * Stable id within the provider typically `<mode>-<sourcePort>` so
16885
+ * the orchestrator can dedupe across `listEndpoints` polls.
16728
16886
  */
16729
- disabledTargetIds: array(string()).default([])
16730
- });
16731
- var NcTestResultSchema = object({
16732
- recordId: string(),
16733
- recordKind: _enum([
16734
- "object-event",
16735
- "track",
16736
- "device-event",
16737
- "package-event"
16738
- ]),
16739
- deviceId: number(),
16740
- timestamp: number(),
16741
- wouldFire: boolean(),
16742
- /** Condition id that failed (first failing group), when `wouldFire` is false. */
16743
- failedCondition: string().optional(),
16744
- className: string().optional(),
16745
- label: string().optional()
16887
+ id: string(),
16888
+ /** Operator-facing label (mirrors `MeshEndpoint.label`). */
16889
+ label: string(),
16890
+ /** Optional provider-specific mode tag, used for icon/colour in admin UI. */
16891
+ mode: string().optional(),
16892
+ /** Originating local port the ingress fronts (informational). */
16893
+ sourcePort: number().optional()
16746
16894
  });
16747
- var NcConditionDescriptorSchema = object({
16748
- /** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
16895
+ method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
16896
+ /**
16897
+ * notification-output — canonical, capability-gated notification delivery.
16898
+ *
16899
+ * Apprise-derived model (see
16900
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
16901
+ * callers emit ONE canonical `Notification`; each provider declares a
16902
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
16903
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
16904
+ * message to what the kind supports — callers never special-case a service.
16905
+ *
16906
+ * DESIGN DECISIONS (locked):
16907
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
16908
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
16909
+ * cap. Rationale: the admin UI needs one uniform surface across the
16910
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
16911
+ * alternative would fork the UI per addon and cannot host the
16912
+ * discovery→adopt flow.
16913
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
16914
+ * the generated cap-mount auto-`concatCollection`-fans them across every
16915
+ * registered provider (notifiers addon + HA addon) so one catalog is
16916
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
16917
+ * `addonId` the generated collection router extracts from the call input.
16918
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
16919
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
16920
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
16921
+ * base64 fallback needed.
16922
+ *
16923
+ * TODO (deferred, closed-set change — separate decision): add
16924
+ * `providerKind: 'notify'` so notification providers surface on the unified
16925
+ * admin "Integrations" page.
16926
+ */
16927
+ /**
16928
+ * Zentik-derived typed-media enum — the superset across every kind. Each
16929
+ * adapter picks what it supports and the degrade engine filters the rest.
16930
+ */
16931
+ var AttachmentMediaTypeSchema = _enum([
16932
+ "image",
16933
+ "video",
16934
+ "gif",
16935
+ "audio",
16936
+ "icon"
16937
+ ]);
16938
+ /**
16939
+ * A single attachment. Exactly one of `url` (remote source, most adapters
16940
+ * prefer this) or `bytes` (inline source; required for Pushover-style
16941
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
16942
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
16943
+ */
16944
+ var AttachmentSchema = object({
16945
+ mediaType: AttachmentMediaTypeSchema,
16946
+ url: string().optional(),
16947
+ bytes: _instanceof(Uint8Array).optional(),
16948
+ mime: string().optional(),
16949
+ name: string().optional()
16950
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
16951
+ var NotificationFormatSchema = _enum([
16952
+ "text",
16953
+ "markdown",
16954
+ "html"
16955
+ ]);
16956
+ /** A single tap-through action button. */
16957
+ var NotificationActionSchema = object({
16749
16958
  id: string(),
16750
- group: _enum([
16751
- "scope",
16752
- "class",
16753
- "zones",
16754
- "quality",
16755
- "label",
16756
- "schedule",
16757
- "device",
16758
- "package",
16759
- "occupancy"
16760
- ]),
16761
16959
  label: string(),
16762
- /** Editor widget the UI renders — never hardcode per-condition forms. */
16763
- valueType: _enum([
16764
- "deviceIdList",
16765
- "stringList",
16766
- "number01",
16767
- "number",
16768
- "sourceSelect",
16769
- "zoneSelection",
16770
- "zoneIdList",
16771
- "schedule",
16772
- "plateMatcher",
16773
- "packagePhase",
16774
- "polygonDraw",
16775
- "occupancy"
16776
- ]),
16777
- operator: _enum([
16778
- "in",
16779
- "notIn",
16780
- "anyOf",
16781
- "allOf",
16782
- "gte",
16783
- "fuzzyIn",
16784
- "withinSchedule"
16785
- ]),
16786
- /** Which delivery kinds the condition applies to. */
16787
- appliesTo: array(NcDeliverySchema),
16788
- phase: string(),
16960
+ url: string().optional()
16961
+ });
16962
+ /**
16963
+ * The canonical notification. `body` is the only hard field (Apprise model).
16964
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
16965
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
16966
+ * the adapter maps this ordinal onto its native level. `level?` is an
16967
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
16968
+ * `priority` for that one target.
16969
+ */
16970
+ var NotificationSchema = object({
16971
+ body: string(),
16972
+ title: string().optional(),
16973
+ format: NotificationFormatSchema.default("text"),
16974
+ priority: number().int().min(1).max(5).default(3),
16975
+ level: string().optional(),
16976
+ attachments: array(AttachmentSchema).optional(),
16977
+ clickUrl: string().optional(),
16978
+ actions: array(NotificationActionSchema).optional(),
16979
+ sound: string().optional(),
16980
+ ttl: number().optional(),
16981
+ tag: string().optional(),
16982
+ deviceId: number().optional(),
16983
+ eventId: string().optional(),
16984
+ metadata: record(string(), unknown()).optional()
16985
+ });
16986
+ /** One declared native severity/priority level for a kind. */
16987
+ var TargetKindLevelSchema = object({
16988
+ id: string(),
16989
+ label: string(),
16990
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
16991
+ ordinal: number().int().min(1).max(5).nullable(),
16992
+ flags: object({
16993
+ critical: boolean().optional(),
16994
+ silent: boolean().optional(),
16995
+ noPush: boolean().optional()
16996
+ }).optional(),
16997
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
16998
+ requires: array(string()).optional(),
16789
16999
  description: string().optional()
16790
17000
  });
17001
+ /** The full capability block consulted before dispatch. */
17002
+ var TargetKindCapsSchema = object({
17003
+ attachments: object({
17004
+ mediaTypes: array(AttachmentMediaTypeSchema),
17005
+ mode: _enum([
17006
+ "url",
17007
+ "bytes",
17008
+ "both"
17009
+ ]),
17010
+ max: number().int().nonnegative(),
17011
+ maxBytes: number().int().positive().optional()
17012
+ }),
17013
+ /** Max action buttons (0 = none). */
17014
+ actions: number().int().nonnegative(),
17015
+ levels: array(TargetKindLevelSchema),
17016
+ format: array(NotificationFormatSchema),
17017
+ clickUrl: boolean(),
17018
+ sound: boolean(),
17019
+ ttl: boolean(),
17020
+ bodyMaxLen: number().int().positive()
17021
+ });
16791
17022
  /**
16792
- * The delivery lifecycle status of a history row a straight read of the
16793
- * durable outbox row's own status (single source of truth):
16794
- * - `pending` — enqueued, in-flight or retrying with backoff
16795
- * - `sent` — delivered (terminal)
16796
- * - `dead` dead-lettered after exhausting retries / a permanent
16797
- * backend rejection / a deleted target (terminal; carries
16798
- * the failure `error`)
16799
- *
16800
- * P1 has no `suppressed-quiet-hours` / `snoozed` states — those ride the P2
16801
- * user dimension (quiet hours / snooze) and are additive when they land.
17023
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17024
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17025
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`)
17026
+ * the union is large and not meant for runtime validation here; the exported
17027
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
16802
17028
  */
16803
- var NcHistoryStatusSchema = _enum([
16804
- "pending",
16805
- "sent",
16806
- "dead"
16807
- ]);
16808
- /** The evaluated record kind a history row descends from (one per trigger). */
16809
- var NcHistoryRecordKindSchema = _enum([
16810
- "object-event",
16811
- "track-end",
16812
- "device-event",
16813
- "package-event"
16814
- ]);
16815
- /** Subject summary frozen on the row at fire time (survives rule/record edits). */
16816
- var NcHistorySubjectSchema = object({
16817
- className: string(),
16818
- label: string().optional(),
16819
- confidence: number().optional(),
16820
- zones: array(string()),
16821
- timestamp: number()
17029
+ var ConfigSchemaPassthrough = unknown();
17030
+ var TargetKindSchema = object({
17031
+ kind: string(),
17032
+ label: string(),
17033
+ icon: string(),
17034
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
17035
+ addonId: string(),
17036
+ configSchema: ConfigSchemaPassthrough,
17037
+ supportsDiscovery: boolean(),
17038
+ caps: TargetKindCapsSchema
16822
17039
  });
16823
17040
  /**
16824
- * One delivery-history row. This is a read-only VIEW over the durable
16825
- * outbox row (single source of truth the same row the drain loop drives;
16826
- * NO second write path, so history can never drift from delivery state).
16827
- * The §3.2 fields map directly: `ruleId`/`targetId`/`deviceId` are columns,
16828
- * `eventRef` is `recordKind`+`recordId`, `timestamps` are `createdAt`
16829
- * (fire) / `updatedAt` (last transition), `status` + `error` are the
16830
- * lifecycle. `ruleName` + `subject` are the intent snapshot frozen at
16831
- * enqueue. `userId?` (per-recipient history) is P2 — no user dimension in
16832
- * P1 (admin scope only).
17041
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
17042
+ * (return a presence marker only) when serving `listTargets` never
17043
+ * round-trip a stored secret to the UI.
16833
17044
  */
16834
- var NcHistoryEntrySchema = object({
16835
- /** Outbox row id — the stable dedup id `ruleId:dedupRef:targetId`. */
17045
+ var TargetSchema = object({
16836
17046
  id: string(),
16837
- ruleId: string(),
16838
- /** Rule name frozen at fire time (outlives a later rename / delete). */
16839
- ruleName: string(),
16840
- /** The rule urgency/trigger that produced this delivery. */
16841
- delivery: NcDeliverySchema,
16842
- targetId: string(),
16843
- deviceId: number(),
16844
- recordKind: NcHistoryRecordKindSchema,
16845
- /** Event / track ref of the evaluated record (§3.2 `eventRef`). */
16846
- recordId: string(),
16847
- /** Present for track-scoped deliveries (object-event / track-end). */
16848
- trackId: string().optional(),
16849
- status: NcHistoryStatusSchema,
16850
- /** Delivery attempts made so far. */
16851
- attempts: number().int(),
16852
- /** Fire time (outbox enqueue). */
16853
- createdAt: number(),
16854
- /** Last transition time (terminal for sent / dead). */
16855
- updatedAt: number(),
16856
- /** Failure detail — present on a `dead` row. */
16857
- error: string().optional(),
16858
- subject: NcHistorySubjectSchema
17047
+ name: string(),
17048
+ kind: string(),
17049
+ addonId: string(),
17050
+ enabled: boolean(),
17051
+ config: record(string(), unknown())
16859
17052
  });
16860
- /**
16861
- * Query filter for `getHistory` (spec §4.2). Every field is a narrowing
16862
- * AND; absent = unbounded on that axis. `since`/`until` bound the fire time
16863
- * (`createdAt`, epoch ms, inclusive). `limit` is clamped to
16864
- * {@link NC_HISTORY_LIMIT_MAX}. `userId` (per-recipient filtering) is P2.
16865
- */
16866
- var NcHistoryFilterSchema = object({
16867
- ruleId: string().optional(),
16868
- deviceId: number().optional(),
16869
- status: NcHistoryStatusSchema.optional(),
16870
- since: number().optional(),
16871
- until: number().optional(),
16872
- limit: number().int().min(1).max(500).default(100)
17053
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
17054
+ var DiscoveredTargetSchema = object({
17055
+ kind: string(),
17056
+ suggestedName: string(),
17057
+ config: record(string(), unknown())
16873
17058
  });
16874
- 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 }), {
16875
- kind: "mutation",
16876
- auth: "admin",
16877
- caller: "required"
16878
- }), method(object({
16879
- ruleId: string(),
16880
- patch: NcRulePatchSchema
16881
- }), object({ rule: NcRuleSchema }), {
16882
- kind: "mutation",
16883
- auth: "admin",
16884
- caller: "required"
16885
- }), method(object({ ruleId: string() }), object({ success: literal(true) }), {
16886
- kind: "mutation",
16887
- auth: "admin"
16888
- }), method(object({
16889
- ruleId: string(),
16890
- enabled: boolean()
16891
- }), object({ success: literal(true) }), {
16892
- kind: "mutation",
16893
- auth: "admin"
16894
- }), method(object({
16895
- rule: NcRuleInputSchema,
16896
- lookbackMinutes: number().int().min(1).max(1440).default(60)
16897
- }), object({ results: array(NcTestResultSchema) }), {
16898
- kind: "mutation",
16899
- auth: "admin"
16900
- }), method(object({}), object({ catalog: array(NcConditionDescriptorSchema) })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
17059
+ /** The degrade engine's report what was resolved / dropped / degraded. */
17060
+ var RenderedAsSchema = object({
17061
+ level: string(),
17062
+ format: NotificationFormatSchema,
17063
+ attachmentsSent: number().int().nonnegative(),
17064
+ actionsSent: number().int().nonnegative(),
17065
+ truncated: boolean(),
17066
+ dropped: array(string())
17067
+ });
17068
+ var SendResultSchema = object({
17069
+ success: boolean(),
17070
+ error: string().optional(),
17071
+ renderedAs: RenderedAsSchema.optional()
17072
+ });
17073
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
17074
+ var TestResultSchema = SendResultSchema;
17075
+ var notificationOutputCapability = {
17076
+ name: "notification-output",
17077
+ scope: "system",
17078
+ mode: "collection",
17079
+ methods: {
17080
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
17081
+ listTargets: method(object({}), array(TargetSchema)),
17082
+ discoverTargets: method(object({
17083
+ kind: string(),
17084
+ config: record(string(), unknown()).optional()
17085
+ }), array(DiscoveredTargetSchema)),
17086
+ send: method(object({
17087
+ targetId: string(),
17088
+ notification: NotificationSchema
17089
+ }), SendResultSchema, { kind: "mutation" }),
17090
+ testTarget: method(object({
17091
+ targetId: string(),
17092
+ sample: NotificationSchema.optional()
17093
+ }), TestResultSchema, { kind: "mutation" }),
17094
+ upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
17095
+ deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
17096
+ setTargetEnabled: method(object({
17097
+ targetId: string(),
17098
+ enabled: boolean()
17099
+ }), _void(), { kind: "mutation" })
17100
+ }
17101
+ };
16901
17102
  /**
16902
17103
  * Zod schemas for persisted record types.
16903
17104
  *
@@ -21902,6 +22103,12 @@ Object.freeze({
21902
22103
  addonId: null,
21903
22104
  access: "delete"
21904
22105
  },
22106
+ "backup.deleteSchedule": {
22107
+ capName: "backup",
22108
+ capScope: "system",
22109
+ addonId: null,
22110
+ access: "delete"
22111
+ },
21905
22112
  "backup.getEntries": {
21906
22113
  capName: "backup",
21907
22114
  capScope: "system",
@@ -21932,6 +22139,12 @@ Object.freeze({
21932
22139
  addonId: null,
21933
22140
  access: "view"
21934
22141
  },
22142
+ "backup.listSchedules": {
22143
+ capName: "backup",
22144
+ capScope: "system",
22145
+ addonId: null,
22146
+ access: "view"
22147
+ },
21935
22148
  "backup.previewSchedule": {
21936
22149
  capName: "backup",
21937
22150
  capScope: "system",
@@ -21956,6 +22169,12 @@ Object.freeze({
21956
22169
  addonId: null,
21957
22170
  access: "create"
21958
22171
  },
22172
+ "backup.upsertSchedule": {
22173
+ capName: "backup",
22174
+ capScope: "system",
22175
+ addonId: null,
22176
+ access: "create"
22177
+ },
21959
22178
  "battery.wakeForStream": {
21960
22179
  capName: "battery",
21961
22180
  capScope: "device",
@@ -25790,6 +26009,36 @@ Object.freeze({
25790
26009
  addonId: null,
25791
26010
  access: "create"
25792
26011
  },
26012
+ "terminalSession.close": {
26013
+ capName: "terminal-session",
26014
+ capScope: "system",
26015
+ addonId: null,
26016
+ access: "create"
26017
+ },
26018
+ "terminalSession.listProfiles": {
26019
+ capName: "terminal-session",
26020
+ capScope: "system",
26021
+ addonId: null,
26022
+ access: "view"
26023
+ },
26024
+ "terminalSession.listSessions": {
26025
+ capName: "terminal-session",
26026
+ capScope: "system",
26027
+ addonId: null,
26028
+ access: "view"
26029
+ },
26030
+ "terminalSession.openSession": {
26031
+ capName: "terminal-session",
26032
+ capScope: "system",
26033
+ addonId: null,
26034
+ access: "create"
26035
+ },
26036
+ "terminalSession.resize": {
26037
+ capName: "terminal-session",
26038
+ capScope: "system",
26039
+ addonId: null,
26040
+ access: "create"
26041
+ },
25793
26042
  "toast.onToast": {
25794
26043
  capName: "toast",
25795
26044
  capScope: "system",