@camstack/types 1.2.453 → 1.2.455
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/automation/action-target.d.ts +86 -0
- package/dist/automation/action.d.ts +28 -4
- package/dist/automation/index.d.ts +6 -4
- package/dist/automation/nc-action-target.d.ts +21 -0
- package/dist/automation/normalize-units.d.ts +1 -1
- package/dist/automation/recipe.d.ts +173 -18
- package/dist/automation/step-runner.d.ts +21 -2
- package/dist/automation/trace.d.ts +42 -0
- package/dist/automation/when.d.ts +33 -1
- package/dist/capabilities/action-source.cap.d.ts +14 -2
- package/dist/capabilities/actions.cap.d.ts +108 -14
- package/dist/capabilities/core-blocks.cap.d.ts +982 -136
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/notification-rules.cap.d.ts +845 -65
- package/dist/capabilities/pipeline-analytics.cap.d.ts +357 -0
- package/dist/capabilities/remote.cap.d.ts +2 -2
- package/dist/capabilities/system.cap.d.ts +4 -0
- package/dist/client.js +1629 -796
- package/dist/client.mjs +1629 -796
- package/dist/device/cap-vocabulary/action-index.d.ts +31 -0
- package/dist/device/cap-vocabulary/index.d.ts +3 -1
- package/dist/device/cap-vocabulary/types.d.ts +27 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +90952 -89858
- package/dist/index.mjs +90918 -89859
- package/dist/remote/remote-config.d.ts +14 -4
- package/dist/time/index.d.ts +2 -0
- package/dist/time/solar.d.ts +37 -0
- package/dist/time/time-spec.d.ts +117 -0
- package/package.json +1 -1
package/dist/client.mjs
CHANGED
|
@@ -79,6 +79,784 @@ function buildAlawTable() {
|
|
|
79
79
|
buildUlawTable();
|
|
80
80
|
buildAlawTable();
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region src/enums/event-category.ts
|
|
83
|
+
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
84
|
+
EventCategory["SystemBoot"] = "system.boot";
|
|
85
|
+
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
86
|
+
EventCategory["SystemRestarting"] = "system.restarting";
|
|
87
|
+
/**
|
|
88
|
+
* Fired exactly once after the hub finishes booting following a
|
|
89
|
+
* restart that was triggered by `RestartCoordinator` (today: framework
|
|
90
|
+
* live-update). Payload is the marker that was on disk at boot
|
|
91
|
+
* (`PendingRestartMarkerPayload`); admin UI listens for it to display a
|
|
92
|
+
* success toast describing what changed.
|
|
93
|
+
*
|
|
94
|
+
* Spec: docs/superpowers/specs/2026-05-14-framework-live-update-design.md
|
|
95
|
+
*/
|
|
96
|
+
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
97
|
+
/**
|
|
98
|
+
* The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
|
|
99
|
+
* Emitted only when the material on disk actually changed, so an
|
|
100
|
+
* operator who trusted the old certificate by hand is told rather than
|
|
101
|
+
* discovering it as a browser error. Payload `TlsCertChangedPayload`.
|
|
102
|
+
*
|
|
103
|
+
* Rule: docs/decisions/adr-0227-*.md
|
|
104
|
+
*/
|
|
105
|
+
EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
|
|
106
|
+
/**
|
|
107
|
+
* A newer addon or server-root package version was found by the
|
|
108
|
+
* authoritative registry check. Emitted once when any observed
|
|
109
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
110
|
+
* the payload carries the full currently-available list. Repeated
|
|
111
|
+
* polling of the same latests is silent.
|
|
112
|
+
*/
|
|
113
|
+
EventCategory["UpdateAvailable"] = "update.available";
|
|
114
|
+
/**
|
|
115
|
+
* Readiness transition for a capability provider. Every producer emits
|
|
116
|
+
* this event on `onInitialize` completion, `onDestroy`, and
|
|
117
|
+
* `$node.reconnect`; every consumer that needs to gate on a cross-process
|
|
118
|
+
* cap subscribes via the kernel's readiness module (`awaitReady` /
|
|
119
|
+
* `onReadyState`) instead of polling. Payload is
|
|
120
|
+
* `SystemReadyStatePayload` — see event-bus.ts.
|
|
121
|
+
*/
|
|
122
|
+
EventCategory["SystemReadyState"] = "system.ready-state";
|
|
123
|
+
EventCategory["AddonStarted"] = "addon.started";
|
|
124
|
+
EventCategory["AddonStopped"] = "addon.stopped";
|
|
125
|
+
EventCategory["AddonRestarted"] = "addon.restarted";
|
|
126
|
+
EventCategory["AddonUpdated"] = "addon.updated";
|
|
127
|
+
EventCategory["AddonInstalled"] = "addon.installed";
|
|
128
|
+
EventCategory["AddonUninstalled"] = "addon.uninstalled";
|
|
129
|
+
EventCategory["AddonCrashed"] = "addon.crashed";
|
|
130
|
+
/**
|
|
131
|
+
* A RUNNER the D6 crash circuit-breaker gave up on — terminal.
|
|
132
|
+
*
|
|
133
|
+
* `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
|
|
134
|
+
* respawned"); this is the one that never resolves itself. Emitted exactly
|
|
135
|
+
* once per trip, by `process-service.ts`, carrying the node, the runner, the
|
|
136
|
+
* addons it hosted and the crash count that tripped the breaker.
|
|
137
|
+
*
|
|
138
|
+
* It exists because a runner marked terminally `failed` used to be SILENT:
|
|
139
|
+
* on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
|
|
140
|
+
* errors, the breaker stopped respawning it (correctly), `/health` kept
|
|
141
|
+
* returning 200, and fleet-wide recording was gone for 3h15 before the
|
|
142
|
+
* operator's phone told him. The Notification Center's system-event intake
|
|
143
|
+
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
144
|
+
*/
|
|
145
|
+
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
146
|
+
/**
|
|
147
|
+
* A runner whose addon refused to start with a typed reason (D877) - parked
|
|
148
|
+
* in `refused`, never counted by the crash breaker, retried on operator
|
|
149
|
+
* action. One event per refusal.
|
|
150
|
+
*/
|
|
151
|
+
EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
|
|
152
|
+
/**
|
|
153
|
+
* A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
|
|
154
|
+
* `refused`. In-process telemetry for the boot check (re-evaluate while any
|
|
155
|
+
* runner is still pending) and the alert centre (close a recovered runner).
|
|
156
|
+
*/
|
|
157
|
+
EventCategory["ProcessStateChanged"] = "process.state-changed";
|
|
158
|
+
/**
|
|
159
|
+
* The node's one-shot runner boot check (D877): planned runners against the
|
|
160
|
+
* process service, `degraded` when any is failed, refused or missing. The
|
|
161
|
+
* alert centre turns it into one critical alert.
|
|
162
|
+
*/
|
|
163
|
+
EventCategory["SystemRunnerCheck"] = "system.runner-check";
|
|
164
|
+
/**
|
|
165
|
+
* An alert EPISODE opened or closed, emitted by the alert centre for alerts
|
|
166
|
+
* of severity `critical` or `error` (`AlertEpisodePayload`). The alert
|
|
167
|
+
* centre sends no notification of its own: Notification Center turns this
|
|
168
|
+
* into the `alert-critical` / `alert-error` / `alert-critical-resolved`
|
|
169
|
+
* system-event kinds, and reconciles from the alerts store because a bus
|
|
170
|
+
* event may be dropped (D8, D11).
|
|
171
|
+
*/
|
|
172
|
+
EventCategory["AlertEpisode"] = "alert.episode";
|
|
173
|
+
EventCategory["AddonError"] = "addon.error";
|
|
174
|
+
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
175
|
+
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
176
|
+
/**
|
|
177
|
+
* Addon failed to load (import or initialize). Emitted by the kernel's
|
|
178
|
+
* AddonHealthMonitor only AFTER the boot grace period ends — failures
|
|
179
|
+
* during the first 5 minutes are silently retried without alerting
|
|
180
|
+
* (slow-starting addons must have time to come up). Post-grace, this
|
|
181
|
+
* event is emitted exactly once per failure-streak; AlertCenter
|
|
182
|
+
* consumes it to create a persistent operator-visible alert.
|
|
183
|
+
*
|
|
184
|
+
* Payload: `{ packageName, addonId?, error: { message, stack }, retryCount, nextRetryAt }`.
|
|
185
|
+
*/
|
|
186
|
+
EventCategory["AddonLoadFailed"] = "addon.load-failed";
|
|
187
|
+
/**
|
|
188
|
+
* A keychain value was stored or removed (D787). Carries the ref, its
|
|
189
|
+
* owner and the new version — NEVER the value. A process holding the ref
|
|
190
|
+
* drops its cached copy and the owning `BaseAddon` re-resolves its config.
|
|
191
|
+
* Telemetry (D8): a process that misses it still revalidates by version.
|
|
192
|
+
*/
|
|
193
|
+
EventCategory["KeychainChanged"] = "keychain.changed";
|
|
194
|
+
/**
|
|
195
|
+
* Addon recovered from a previous failure. Emitted when an addon
|
|
196
|
+
* transitions from `failed` back to `healthy` (typically via the
|
|
197
|
+
* monitor's auto-retry loop, or after manual `addons.retryLoad`).
|
|
198
|
+
* AlertCenter dismisses the corresponding `AddonLoadFailed` alert
|
|
199
|
+
* on this event.
|
|
200
|
+
*/
|
|
201
|
+
EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
|
|
202
|
+
/**
|
|
203
|
+
* Monitor scheduled the next retry for a failed addon. Transient —
|
|
204
|
+
* surfaced to the UI for live-updating the "next retry in Ns"
|
|
205
|
+
* countdown on the Addons page row, NOT persisted as an alert.
|
|
206
|
+
*/
|
|
207
|
+
EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
|
|
208
|
+
/**
|
|
209
|
+
* A liveness invariant of this node is false — it has no devices, every
|
|
210
|
+
* camera is offline, or a camera that was recording has stopped producing
|
|
211
|
+
* segments. Emitted by the `liveness-monitor` builtin, once per fault (the
|
|
212
|
+
* `findingId` is stable across ticks), only after its boot grace period.
|
|
213
|
+
* AlertCenter raises a persistent operator-visible alert.
|
|
214
|
+
*
|
|
215
|
+
* It exists because on 2026-08-03 the hub ran four hours with zero devices
|
|
216
|
+
* and zero recordings while every surface stayed quiet.
|
|
217
|
+
*
|
|
218
|
+
* Payload: `{ findingId, severity, title, message, deviceId? }`.
|
|
219
|
+
*/
|
|
220
|
+
EventCategory["SystemLivenessFailed"] = "system.liveness-failed";
|
|
221
|
+
/**
|
|
222
|
+
* A previously reported liveness fault is true again. AlertCenter dismisses
|
|
223
|
+
* the matching `SystemLivenessFailed` alert.
|
|
224
|
+
*
|
|
225
|
+
* Payload: `{ findingId }`.
|
|
226
|
+
*/
|
|
227
|
+
EventCategory["SystemLivenessRecovered"] = "system.liveness-recovered";
|
|
228
|
+
/**
|
|
229
|
+
* Monitor is attempting to reload a failed addon NOW. UI uses this
|
|
230
|
+
* to show a spinner during the retry attempt. Same transient nature
|
|
231
|
+
* as AddonRetryScheduled.
|
|
232
|
+
*/
|
|
233
|
+
EventCategory["AddonRetryAttempting"] = "addon.retry-attempting";
|
|
234
|
+
EventCategory["DeviceRegistered"] = "device.registered";
|
|
235
|
+
EventCategory["DeviceUnregistered"] = "device.unregistered";
|
|
236
|
+
EventCategory["DeviceEnabled"] = "device.enabled";
|
|
237
|
+
EventCategory["DeviceDisabled"] = "device.disabled";
|
|
238
|
+
EventCategory["DeviceSettingsUpdated"] = "device.settings-updated";
|
|
239
|
+
/**
|
|
240
|
+
* Emitted when the set of native capability providers bound to a device
|
|
241
|
+
* changes — e.g. an addon registers a new native cap via
|
|
242
|
+
* `DeviceContext.registerNativeCap`, or all native bindings for a device
|
|
243
|
+
* are cleared on removal. Hub consumers re-resolve device-proxy routes
|
|
244
|
+
* when this fires.
|
|
245
|
+
*/
|
|
246
|
+
EventCategory["DeviceBindingsChanged"] = "device.bindings-changed";
|
|
247
|
+
/**
|
|
248
|
+
* Emitted when the operator-organisational meta surface changes
|
|
249
|
+
* (`name` / `location` / `disabled`). Payload: `{deviceId, field,
|
|
250
|
+
* value}`. Live consumers (UI device list, alert center) react
|
|
251
|
+
* without polling. Distinct from `DeviceSettingsUpdated` which
|
|
252
|
+
* fires on hardware-config changes (host/port/credentials/etc).
|
|
253
|
+
*/
|
|
254
|
+
EventCategory["DeviceMetaChanged"] = "device.meta-changed";
|
|
255
|
+
/**
|
|
256
|
+
* Emitted by `BaseDevice.updateSourceInfo()` after a successful patch
|
|
257
|
+
* to the device's upstream-system identity / rendering envelope. The
|
|
258
|
+
* full new SourceInfo travels in the payload so cross-process listeners
|
|
259
|
+
* (UI, export adapters) don't need to re-read the meta blob.
|
|
260
|
+
*
|
|
261
|
+
* Payload: `{deviceId, sourceInfo}`.
|
|
262
|
+
*
|
|
263
|
+
* Distinct from `DeviceMetaChanged` which covers the operator-edited
|
|
264
|
+
* surface (`name` / `location` / `disabled`). The two share the
|
|
265
|
+
* persistence layer (both ride on `device-manager.setMetadata` for
|
|
266
|
+
* SourceInfo, or `setName`/`setLocation`/`setDisabled` for the meta
|
|
267
|
+
* fields) but consumers care about different subsets.
|
|
268
|
+
*/
|
|
269
|
+
EventCategory["DeviceSourceInfoChanged"] = "device.source-info-changed";
|
|
270
|
+
/**
|
|
271
|
+
* Emitted by DeviceStreamWiringService after a successful
|
|
272
|
+
* `stream-broker.registerDeviceStreams` call. Payload includes
|
|
273
|
+
* deviceId — consumers look up the full registered device info via
|
|
274
|
+
* `brokerManager.getRegisteredDevice(deviceId)`.
|
|
275
|
+
*
|
|
276
|
+
* Replaces the legacy `StreamRouterService.onDeviceRegistered` callback.
|
|
277
|
+
*/
|
|
278
|
+
EventCategory["DeviceStreamsRegistered"] = "device.streams-registered";
|
|
279
|
+
/**
|
|
280
|
+
* Device-level "fully provisioned" signal — the EXPORT trigger. Emitted once
|
|
281
|
+
* a device's persisted export-relevant shape (its `DeviceFeature` set) is
|
|
282
|
+
* established or changes, carrying `{deviceId, fingerprint, generation}`.
|
|
283
|
+
* Export adapters (Alexa / HAP) react to THIS instead of the chatty per-cap
|
|
284
|
+
* `DeviceBindingsChanged`, turning a boot's incomplete→complete trickle into
|
|
285
|
+
* one clean delta. `fingerprint` is `canonicalDeviceFingerprint(shape)`.
|
|
286
|
+
*
|
|
287
|
+
* Spec: docs/superpowers/specs/2026-06-01-alexa-hap-export-reconciler-design.md
|
|
288
|
+
*/
|
|
289
|
+
EventCategory["DeviceProvisioned"] = "device.provisioned";
|
|
290
|
+
/**
|
|
291
|
+
* Fires once when a device's initial feature-probe completes successfully
|
|
292
|
+
* (lastProbedAt 0→>0); exported shape is now stable. Telemetry (may be
|
|
293
|
+
* dropped) — consumers MUST also gate on the device record's `probed` flag.
|
|
294
|
+
*/
|
|
295
|
+
EventCategory["DeviceReady"] = "device.ready";
|
|
296
|
+
EventCategory["IntegrationEnabled"] = "integration.enabled";
|
|
297
|
+
EventCategory["IntegrationDisabled"] = "integration.disabled";
|
|
298
|
+
EventCategory["IntegrationDeleted"] = "integration.deleted";
|
|
299
|
+
/** Emitted when a broker connection's status flips (connected /
|
|
300
|
+
* disconnected / error). Carries `{ brokerId, status, error? }`. */
|
|
301
|
+
EventCategory["BrokerStatusChanged"] = "broker.status-changed";
|
|
302
|
+
/** Emitted for every subscription-matched message routed by a
|
|
303
|
+
* broker provider. Carries `{ brokerId, subscriptionId, key, payload }`.
|
|
304
|
+
* Consumers filter by `brokerId` + `subscriptionId` in the handler. */
|
|
305
|
+
EventCategory["BrokerMessage"] = "broker.message";
|
|
306
|
+
EventCategory["ProviderStarted"] = "provider.started";
|
|
307
|
+
EventCategory["ProviderStopped"] = "provider.stopped";
|
|
308
|
+
EventCategory["ProcessCrashed"] = "process.crashed";
|
|
309
|
+
EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
|
|
310
|
+
EventCategory["ProcessRestarted"] = "process.restarted";
|
|
311
|
+
/**
|
|
312
|
+
* The SET of storage locations changed — one was created, edited, enabled,
|
|
313
|
+
* disabled or deleted through `storage.upsertLocation` / `deleteLocation`.
|
|
314
|
+
*
|
|
315
|
+
* Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
|
|
316
|
+
* it must also converge on its own periodic path, because a dropped event
|
|
317
|
+
* must not leave a node writing to yesterday's disk set forever. It exists
|
|
318
|
+
* because there was NO signal at all — an operator who added a second
|
|
319
|
+
* recordings disk in the admin UI got nothing, and the recorder kept its
|
|
320
|
+
* resolved locations until something else happened to re-resolve them
|
|
321
|
+
* (D387). Payload `StorageLocationsChangedPayload`.
|
|
322
|
+
*/
|
|
323
|
+
EventCategory["StorageLocationsChanged"] = "storage.locations-changed";
|
|
324
|
+
EventCategory["RecordingStarted"] = "recording.started";
|
|
325
|
+
EventCategory["RecordingStopped"] = "recording.stopped";
|
|
326
|
+
EventCategory["RecordingError"] = "recording.error";
|
|
327
|
+
EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
|
|
328
|
+
/** Recording enabled but no segment finalized for the NC stall window. */
|
|
329
|
+
EventCategory["RecordingHealthStalled"] = "recording.health.stalled";
|
|
330
|
+
/** Recovery from {@link RecordingHealthStalled}. */
|
|
331
|
+
EventCategory["RecordingHealthOk"] = "recording.health.ok";
|
|
332
|
+
EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
|
|
333
|
+
/** Free space on a storage location crossed the warn bar. */
|
|
334
|
+
EventCategory["StorageLocationLow"] = "storage.location.low";
|
|
335
|
+
/** Free space at/under the critical bar, or capacity probe failed (`null`). */
|
|
336
|
+
EventCategory["StorageLocationCritical"] = "storage.location.critical";
|
|
337
|
+
/** Recovery from low/critical on a storage location. */
|
|
338
|
+
EventCategory["StorageLocationOk"] = "storage.location.ok";
|
|
339
|
+
EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
|
|
340
|
+
EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
|
|
341
|
+
EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
|
|
342
|
+
/** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
|
|
343
|
+
* progress bar the client reconciles via `recordingExport.getExport`. */
|
|
344
|
+
EventCategory["RecordingExportProgress"] = "recording.export.progress";
|
|
345
|
+
EventCategory["RecordingExportCompleted"] = "recording.export.completed";
|
|
346
|
+
EventCategory["RecordingExportFailed"] = "recording.export.failed";
|
|
347
|
+
EventCategory["DetectionEvent"] = "detection.event";
|
|
348
|
+
EventCategory["SessionTrackNew"] = "session.track.new";
|
|
349
|
+
EventCategory["SessionTrackExpired"] = "session.track.expired";
|
|
350
|
+
EventCategory["BenchmarkProgress"] = "benchmark.progress";
|
|
351
|
+
EventCategory["PlatformProbePhase"] = "platform-probe.phase";
|
|
352
|
+
EventCategory["PipelineProgress"] = "pipeline.progress";
|
|
353
|
+
/** Per-frame execution trace emitted by the pipeline executor for live observability. */
|
|
354
|
+
EventCategory["PipelineTrace"] = "pipeline.trace";
|
|
355
|
+
/**
|
|
356
|
+
* Raw inference output emitted by `addon-pipeline-runner` after running the
|
|
357
|
+
* detection pipeline on a frame. Carries the `FrameResult` (with
|
|
358
|
+
* `detections[]`, `width`/`height`, timing debug) — never the frame
|
|
359
|
+
* buffer itself. Hub-side consumers (analysis pipeline, class filters,
|
|
360
|
+
* notifications) subscribe to this and re-emit `detection.result` after
|
|
361
|
+
* post-processing.
|
|
362
|
+
*/
|
|
363
|
+
EventCategory["PipelineInferenceResult"] = "pipeline.inference-result";
|
|
364
|
+
/**
|
|
365
|
+
* Camera lifecycle event emitted by `addon-pipeline-orchestrator` when it
|
|
366
|
+
* assigns or unassigns a camera to/from an agent. Carries no frame data;
|
|
367
|
+
* pure observability for UI dashboards and metrics consumers.
|
|
368
|
+
*/
|
|
369
|
+
EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
|
|
370
|
+
EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
|
|
371
|
+
/**
|
|
372
|
+
* A node the orchestrator would otherwise place cameras on has NO usable
|
|
373
|
+
* inference device: the operator enabled one or more accelerators there and
|
|
374
|
+
* the live probe reports every one of them unavailable. Emitted once per
|
|
375
|
+
* TRANSITION into that state (never per dispatch), and the node is dropped
|
|
376
|
+
* from the placement candidate set for as long as it holds.
|
|
377
|
+
*
|
|
378
|
+
* This exists because the state was previously invisible: agent-a
|
|
379
|
+
* absorbed 283k inference errors in a day while still being handed cameras,
|
|
380
|
+
* and nothing in the system said so.
|
|
381
|
+
*
|
|
382
|
+
* A node with no accelerators configured at all is NOT this — its devices
|
|
383
|
+
* are `disabled`, not `unavailable`, and the runner's default CPU pool
|
|
384
|
+
* serves it exactly as before.
|
|
385
|
+
*/
|
|
386
|
+
EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
|
|
387
|
+
/**
|
|
388
|
+
* A camera has an OPEN detection session and has produced no detection at
|
|
389
|
+
* all for longer than the blind threshold — the camera is being decoded and
|
|
390
|
+
* inferred and is returning nothing. Emitted once per transition into blind,
|
|
391
|
+
* per camera.
|
|
392
|
+
*
|
|
393
|
+
* The failure it reports: a 1h43 detection blackout on the entrance camera
|
|
394
|
+
* that nobody noticed, because "a camera that detects nothing" and "a quiet
|
|
395
|
+
* camera" produce byte-identical silence.
|
|
396
|
+
*/
|
|
397
|
+
EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
|
|
398
|
+
/**
|
|
399
|
+
* A camera's motion source has fired constantly with no subject for an hour
|
|
400
|
+
* (motion duty or session count over the bar, zero detections) — it is not
|
|
401
|
+
* blind, its motion zones or sensitivity want tuning (D907). Paired with
|
|
402
|
+
* {@link PipelineMotionCalm}.
|
|
403
|
+
*/
|
|
404
|
+
EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
|
|
405
|
+
/** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
|
|
406
|
+
EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
|
|
407
|
+
/**
|
|
408
|
+
* A camera's package LLM confirm has failed closed three times in a row
|
|
409
|
+
* (D931, 2026-10-26). One edge per camera until
|
|
410
|
+
* {@link PipelinePackageLlmAnswered}.
|
|
411
|
+
*/
|
|
412
|
+
EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
|
|
413
|
+
/** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
|
|
414
|
+
EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
|
|
415
|
+
/**
|
|
416
|
+
* Per-camera pipeline config was mutated by the orchestrator
|
|
417
|
+
* (a settings change via `setAgentInferenceDevices` /
|
|
418
|
+
* `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
|
|
419
|
+
* subscribes to its own emission to hot-reload the assigned runner
|
|
420
|
+
* via `attachCamera` so the next frame executes against the new
|
|
421
|
+
* engine/steps without waiting for a rebalance or the next
|
|
422
|
+
* `DeviceStreamsRegistered` cycle.
|
|
423
|
+
*/
|
|
424
|
+
EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
|
|
425
|
+
/**
|
|
426
|
+
* The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
|
|
427
|
+
* Emitted by addon-pipeline-orchestrator whenever it (re)derives node
|
|
428
|
+
* capabilities — at boot, on agent online/offline, and on an ingest-node
|
|
429
|
+
* flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
|
|
430
|
+
* keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
|
|
431
|
+
* cross-process `getIngestOwner` query (push the authority's decision instead
|
|
432
|
+
* of polling it on the hot path). Idempotent state — re-emitted on every
|
|
433
|
+
* topology change, so a dropped event self-heals on the next one (plus the
|
|
434
|
+
* broker's long backstop reconcile query).
|
|
435
|
+
*/
|
|
436
|
+
EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
|
|
437
|
+
/**
|
|
438
|
+
* Periodic snapshot of per-node pipeline-runner load
|
|
439
|
+
* (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
|
|
440
|
+
* subscribe instead of polling `pipelineRunner.getLocalLoad`.
|
|
441
|
+
* `nodeId` carried in the payload + on `event.source.nodeId`.
|
|
442
|
+
*/
|
|
443
|
+
EventCategory["PipelineRunnerLoadSnapshot"] = "pipeline.runner-load-snapshot";
|
|
444
|
+
/**
|
|
445
|
+
* Periodic snapshot of per-camera pipeline metrics (`CameraMetrics`
|
|
446
|
+
* + `deviceId` + `nodeId`). Emitted ~1Hz by every runner for each
|
|
447
|
+
* attached camera. UI subscribes to drive overlay phase / fps /
|
|
448
|
+
* inference time without polling `getCameraMetrics`.
|
|
449
|
+
*/
|
|
450
|
+
EventCategory["PipelineCameraMetricsSnapshot"] = "pipeline.camera-metrics-snapshot";
|
|
451
|
+
/**
|
|
452
|
+
* Periodic snapshot of stream-broker per-broker statistics (input
|
|
453
|
+
* fps, decoded fps, bitrate, codec). Emitted ~1Hz by every
|
|
454
|
+
* stream-broker process for each active broker so the UI can drive
|
|
455
|
+
* the Stream / Cluster dashboards without polling
|
|
456
|
+
* `streamBroker.listAllProfileSlots` and friends.
|
|
457
|
+
*/
|
|
458
|
+
EventCategory["StreamBrokerMetricsSnapshot"] = "stream-broker.metrics-snapshot";
|
|
459
|
+
/**
|
|
460
|
+
* Cap event fired by `stream-broker` when a profile slot enters
|
|
461
|
+
* "demanded" state — a cam stream has been assigned and at least one
|
|
462
|
+
* consumer (RTSP restream, decoded subscriber, WebRTC session, …) is
|
|
463
|
+
* present. Camera-provider addons (Reolink Baichuan push, …)
|
|
464
|
+
* subscribe to this category to start their underlying transport
|
|
465
|
+
* lazily. Payload: `{ deviceId, camStreamId, profile }`.
|
|
466
|
+
*/
|
|
467
|
+
EventCategory["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
|
|
468
|
+
/**
|
|
469
|
+
* Cap event fired by `stream-broker` when the last consumer leaves a
|
|
470
|
+
* previously-demanded cam stream. Providers tear down their
|
|
471
|
+
* underlying transport on receipt. Payload: `{ deviceId, camStreamId }`.
|
|
472
|
+
*/
|
|
473
|
+
EventCategory["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
|
|
474
|
+
/**
|
|
475
|
+
* Cap event fired by `stream-broker` when a broker fails to dial a
|
|
476
|
+
* managed-loopback source (today: `pull-rfc4571`) and the publisher
|
|
477
|
+
* needs to refresh the cached URL. The lib's TCP server idle-tears-down
|
|
478
|
+
* on its own schedule, so a re-publish with a fresh `host:port` is the
|
|
479
|
+
* only way to keep the broker dialable. Camera providers (Reolink
|
|
480
|
+
* Baichuan native, …) subscribe and respond by re-running their publish
|
|
481
|
+
* pipeline.
|
|
482
|
+
* Payload: `{ deviceId, camStreamId, brokerId }`.
|
|
483
|
+
*/
|
|
484
|
+
EventCategory["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
|
|
485
|
+
/**
|
|
486
|
+
* A camera provider changed a device's stream parameters (codec /
|
|
487
|
+
* resolution / bitrate / a stream added or removed). A LOW-LATENCY NUDGE
|
|
488
|
+
* for the stream-broker's catalog reconcile: it carries NO authoritative
|
|
489
|
+
* data — the broker re-PULLS that device's `stream-catalog` cap on receipt
|
|
490
|
+
* (the 30s reconcile poll is the backstop if this nudge is dropped). Emitted
|
|
491
|
+
* by providers from `stream-params.setProfile`. Payload: `{ deviceId }`.
|
|
492
|
+
*/
|
|
493
|
+
EventCategory["StreamParamsChanged"] = "stream-params.changed";
|
|
494
|
+
/**
|
|
495
|
+
* Generic per-device runtime-state change. Fired by `device-manager`
|
|
496
|
+
* whenever a persisted slice in any cap's `runtimeState` shape
|
|
497
|
+
* mutates. Payload: `{deviceId, capName, slice}`. Subscribers are
|
|
498
|
+
* the `deviceState` cap router (cross-process listeners) and the
|
|
499
|
+
* deviceProxy reactive bindings (`device.state.<capName>.value`).
|
|
500
|
+
* Cap-specific events (`battery.onStatusChanged`, …) still fire
|
|
501
|
+
* — they're authoritative for callers that want a typed payload
|
|
502
|
+
* without filtering on `capName`.
|
|
503
|
+
*/
|
|
504
|
+
EventCategory["DeviceStateChanged"] = "device.state-changed";
|
|
505
|
+
/**
|
|
506
|
+
* The NATIVE shadow of a cap a composition has claimed fields of moved
|
|
507
|
+
* (D663). Payload: `{ deviceId, capName, native }` — what the provider
|
|
508
|
+
* wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
|
|
509
|
+
* the composer's native self-reads, and the public event routers refuse it
|
|
510
|
+
* (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
|
|
511
|
+
* truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
|
|
512
|
+
* `replace` claim, one per native write of that cap.
|
|
513
|
+
*/
|
|
514
|
+
EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
|
|
515
|
+
/**
|
|
516
|
+
* Frame occupancy for a camera CHANGED — a tracked object was gained or
|
|
517
|
+
* lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
|
|
518
|
+
*
|
|
519
|
+
* Emitted only on a change, so a steady scene is silent. It exists so a
|
|
520
|
+
* client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
|
|
521
|
+
* one live badge with no push signal at all, and it cost a request every
|
|
522
|
+
* four seconds per visible camera.
|
|
523
|
+
*
|
|
524
|
+
* Like every event it is telemetry and may be dropped ([D8]) — a consumer
|
|
525
|
+
* keeps a slow reconcile rather than trusting it alone.
|
|
526
|
+
*/
|
|
527
|
+
EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
|
|
528
|
+
/**
|
|
529
|
+
* Cap event fired by every device that registers the `battery`
|
|
530
|
+
* capability. Mirrors the cap definition's `onStatusChanged`. Carries
|
|
531
|
+
* `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
|
|
532
|
+
* snapshot wrapper, UI) react to charge/sleep transitions without
|
|
533
|
+
* polling `batteryCapability.getStatus`.
|
|
534
|
+
*/
|
|
535
|
+
EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
|
|
536
|
+
/**
|
|
537
|
+
* Cap event fired by every device that registers the `network-link`
|
|
538
|
+
* capability. Mirrors the cap definition's `onStatusChanged`. Carries
|
|
539
|
+
* `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
|
|
540
|
+
* reading that moved.
|
|
541
|
+
*/
|
|
542
|
+
EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
|
|
543
|
+
/**
|
|
544
|
+
* Emitted by the battery cap provider WHEN `wakeForStream` enters the
|
|
545
|
+
* "wake in progress" window — between the Baichuan wake-up issue and
|
|
546
|
+
* the camera's first dialed-back RTP packet. The stream-broker
|
|
547
|
+
* manager subscribes to flip the per-broker placeholder reason to
|
|
548
|
+
* `'waking'` so viewers see a labelled "WAKING UP" tile instead of
|
|
549
|
+
* the generic `'reconnecting'` frame. Carries `{ deviceId }`. The
|
|
550
|
+
* complementary "wake complete" signal is the existing
|
|
551
|
+
* `BatteryOnStatusChanged { sleeping: false }` event.
|
|
552
|
+
*/
|
|
553
|
+
EventCategory["BatteryOnWakeStarted"] = "battery.onWakeStarted";
|
|
554
|
+
/**
|
|
555
|
+
* Cap event fired by every device that registers the `doorbell`
|
|
556
|
+
* capability. Mirrors `doorbellCapability.events.onPressed`. Carries
|
|
557
|
+
* `{ deviceId, timestamp }`. Operators consuming the UI subscribe
|
|
558
|
+
* here to render transient ring toasts and a "Recent presses" row
|
|
559
|
+
* on the device detail page.
|
|
560
|
+
*/
|
|
561
|
+
EventCategory["DoorbellOnPressed"] = "doorbell.onPressed";
|
|
562
|
+
/** Mirrors `runningStateCapability.events.onStarted`. */
|
|
563
|
+
EventCategory["RunningStarted"] = "running.started";
|
|
564
|
+
/** Mirrors `runningStateCapability.events.onFinished`. */
|
|
565
|
+
EventCategory["RunningFinished"] = "running.finished";
|
|
566
|
+
/**
|
|
567
|
+
* Cap event fired by every device that registers the `event-emitter`
|
|
568
|
+
* capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
|
|
569
|
+
* `{ deviceId, eventType, data, timestamp, seq }` — the device's EXACT
|
|
570
|
+
* declared event verbatim (NO normalization). Subscribers (UI event
|
|
571
|
+
* stream, advanced-notifier rules) react to fired events without
|
|
572
|
+
* holding a cap reference.
|
|
573
|
+
*/
|
|
574
|
+
EventCategory["EventEmitted"] = "event-emitter.event";
|
|
575
|
+
/**
|
|
576
|
+
* Periodic snapshot of the per-node detection-pipeline engine
|
|
577
|
+
* registry (loaded engines, models resident, in-use cameras, idle
|
|
578
|
+
* TTL). Emitted ~0.2Hz (every 5 s) by every detection-pipeline
|
|
579
|
+
* process. The Engines tab subscribes to drive its inventory view
|
|
580
|
+
* without polling `pipelineExecutor.listLoadedEngines`.
|
|
581
|
+
*/
|
|
582
|
+
EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
|
|
583
|
+
/**
|
|
584
|
+
* Per-node detection-engine runtime-provisioning transition. Emitted by
|
|
585
|
+
* the detection-pipeline provider on every state change of its lazy
|
|
586
|
+
* engine-provisioning machine (idle → installing → verifying → ready,
|
|
587
|
+
* or → failed with a `nextRetryAt`). Payload is the
|
|
588
|
+
* `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
|
|
589
|
+
* node. The Pipeline page subscribes to drive a live "installing
|
|
590
|
+
* OpenVINO… / ready" indicator per node without polling
|
|
591
|
+
* `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
|
|
592
|
+
* also reads the cap snapshot on mount / reconnect. Phase 2.
|
|
593
|
+
*/
|
|
594
|
+
EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
|
|
595
|
+
/**
|
|
596
|
+
* Cluster topology snapshot. Carries the same payload returned by
|
|
597
|
+
* `nodes.topology` (every reachable node + addons + processes).
|
|
598
|
+
* Emitted by the hub on any agent / addon lifecycle change
|
|
599
|
+
* (debounced) plus a periodic safety net. Replaces UI polling on
|
|
600
|
+
* `nodes.topology` — admin-ui dashboards subscribe to drive the
|
|
601
|
+
* cluster view directly from the event payload.
|
|
602
|
+
*/
|
|
603
|
+
EventCategory["ClusterTopologySnapshot"] = "cluster.topology-snapshot";
|
|
604
|
+
/**
|
|
605
|
+
* Periodic per-node system metrics snapshot (CPU / memory / GPU /
|
|
606
|
+
* disk / network). Emitted ~0.2 Hz by the metrics-provider addon
|
|
607
|
+
* for each node. Drives the dashboard SystemStatus / ProcessResources
|
|
608
|
+
* widgets without polling `metricsProvider.getCurrent`.
|
|
609
|
+
*/
|
|
610
|
+
EventCategory["MetricsNodeResourcesSnapshot"] = "metrics.node-resources-snapshot";
|
|
611
|
+
/**
|
|
612
|
+
* Periodic per-node process-tree snapshot (camstack-related pids
|
|
613
|
+
* with root / managed / system classification). Emitted ~0.2 Hz by
|
|
614
|
+
* the metrics-provider addon. Drives the Cluster → Processes tab
|
|
615
|
+
* without polling `metricsProvider.listNodeProcesses`.
|
|
616
|
+
*/
|
|
617
|
+
EventCategory["MetricsNodeProcessesSnapshot"] = "metrics.node-processes-snapshot";
|
|
618
|
+
/**
|
|
619
|
+
* Capability binding change event emitted by `addon-pipeline-orchestrator`
|
|
620
|
+
* when a user changes which addon implements a cap on a node. Subscribed
|
|
621
|
+
* by every kernel process to update its local `preferredProviderRegistry`
|
|
622
|
+
* so future capability lookups respect the new binding.
|
|
623
|
+
*/
|
|
624
|
+
/**
|
|
625
|
+
* A capability binding was changed for a node — addon X now provides
|
|
626
|
+
* capability `cap` on node `nodeId`. Lives under the generic
|
|
627
|
+
* `capability.*` namespace because capability bindings are a kernel-
|
|
628
|
+
* level concept used by many addons, not strictly pipeline-scoped.
|
|
629
|
+
*/
|
|
630
|
+
EventCategory["CapabilityBindingChanged"] = "capability.binding-changed";
|
|
631
|
+
EventCategory["ModelDownloadProgress"] = "model.download.progress";
|
|
632
|
+
EventCategory["AgentRegistered"] = "agent.registered";
|
|
633
|
+
EventCategory["AgentUnregistered"] = "agent.unregistered";
|
|
634
|
+
EventCategory["AgentOnline"] = "agent.online";
|
|
635
|
+
EventCategory["AgentOffline"] = "agent.offline";
|
|
636
|
+
/** Forked worker process (e.g. hub/pipeline) connected to the broker. */
|
|
637
|
+
EventCategory["WorkerOnline"] = "worker.online";
|
|
638
|
+
/** Forked worker process disconnected from the broker. */
|
|
639
|
+
EventCategory["WorkerOffline"] = "worker.offline";
|
|
640
|
+
EventCategory["AgentTaskDispatched"] = "agent.task.dispatched";
|
|
641
|
+
EventCategory["AgentTaskAssigned"] = "agent.task.assigned";
|
|
642
|
+
EventCategory["AgentTrpcConnected"] = "agent.trpc.connected";
|
|
643
|
+
EventCategory["AgentWsConnected"] = "agent.ws.connected";
|
|
644
|
+
EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
|
|
645
|
+
EventCategory["AgentBackupActivated"] = "agent.backup.activated";
|
|
646
|
+
EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
|
|
647
|
+
EventCategory["MotionAnalysis"] = "detection.motion-analysis";
|
|
648
|
+
/** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
|
|
649
|
+
EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
|
|
650
|
+
/**
|
|
651
|
+
* Per-camera motion phase transition (`watching ↔ active`) emitted
|
|
652
|
+
* by the runner. Mirrors the `motion.onMotionChanged` cap event
|
|
653
|
+
* surface — payload `MotionOnMotionChangedPayload` carries
|
|
654
|
+
* `{deviceId, detected, timestamp, source, regions?}`. Subscribers
|
|
655
|
+
* include addons that need to react to motion state without
|
|
656
|
+
* polling the runtime-state mirror.
|
|
657
|
+
*/
|
|
658
|
+
EventCategory["MotionOnMotionChanged"] = "motion.on-motion-changed";
|
|
659
|
+
EventCategory["DetectionResult"] = "detection.result";
|
|
660
|
+
EventCategory["DetectionRaw"] = "detection.raw";
|
|
661
|
+
EventCategory["DetectionCameraNative"] = "detection.camera-native";
|
|
662
|
+
/**
|
|
663
|
+
* Canonical per-chunk live audio pipeline output. Payload is
|
|
664
|
+
* `PipelineAudioInferenceResultPayload` carrying a full `AudioResult`
|
|
665
|
+
* (level + detections + debug). Lives on the pipeline.* namespace
|
|
666
|
+
* alongside `pipeline.inference-result` (video) for symmetry.
|
|
667
|
+
*/
|
|
668
|
+
EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
|
|
669
|
+
EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
|
|
670
|
+
/**
|
|
671
|
+
* A camera provider's RAW onboard motion push (D673): the `MotionOnMotionChanged`
|
|
672
|
+
* data shape without `triggeredBy`, built by `providerMotionEvent`. Its one
|
|
673
|
+
* consumer is the orchestrator's camera-sensors bridge, which re-emits the
|
|
674
|
+
* camera's EFFECTIVE onboard motion as `MotionOnMotionChanged` (a pass-through
|
|
675
|
+
* for a camera with no attached sensor). Providers must not emit
|
|
676
|
+
* `MotionOnMotionChanged` themselves; `scripts/check-onboard-motion-emitters.ts`
|
|
677
|
+
* keeps it so (camera-grid is a named, deferred exception).
|
|
678
|
+
*/
|
|
679
|
+
EventCategory["ProviderMotion"] = "provider.motion";
|
|
680
|
+
EventCategory["ProviderDetection"] = "provider.detection";
|
|
681
|
+
EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
|
|
682
|
+
EventCategory["EnrichmentSceneStateChanged"] = "enrichment.scene.state-changed";
|
|
683
|
+
EventCategory["EnrichmentActivitySummary"] = "enrichment.activity.summary";
|
|
684
|
+
/**
|
|
685
|
+
* Unified track-lifecycle event: ONE category carrying `phase:
|
|
686
|
+
* 'start' | 'update' | 'end'` with a rich, typed
|
|
687
|
+
* `PipelineAnalyticsTrackLifecyclePayload`. Supersedes the thin
|
|
688
|
+
* `PipelineAnalyticsTrackStarted` / `PipelineAnalyticsTrackEnded`
|
|
689
|
+
* pair — a consumer subscribes once and branches on `phase`.
|
|
690
|
+
*/
|
|
691
|
+
EventCategory["PipelineAnalyticsTrackLifecycle"] = "pipeline-analytics.track-lifecycle";
|
|
692
|
+
/**
|
|
693
|
+
* @deprecated Superseded by {@link PipelineAnalyticsTrackLifecycle}
|
|
694
|
+
* (`phase:'start'`). Kept as a soft alias — no known subscribers.
|
|
695
|
+
*/
|
|
696
|
+
EventCategory["PipelineAnalyticsTrackStarted"] = "pipeline-analytics.track-started";
|
|
697
|
+
/**
|
|
698
|
+
* @deprecated Superseded by {@link PipelineAnalyticsTrackLifecycle}
|
|
699
|
+
* (`phase:'end'`). Kept as a soft alias — no known subscribers.
|
|
700
|
+
*/
|
|
701
|
+
EventCategory["PipelineAnalyticsTrackEnded"] = "pipeline-analytics.track-ended";
|
|
702
|
+
EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
|
|
703
|
+
EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
|
|
704
|
+
/**
|
|
705
|
+
* Fired by `addon-post-analysis` when a parked (stationary) object appears
|
|
706
|
+
* (a track was promoted to a stationary-registry entry) or departs (the
|
|
707
|
+
* object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
|
|
708
|
+
* the dedicated "Stationary" section — never the live event feed. Payload:
|
|
709
|
+
* `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp, observedAt, zones }`.
|
|
710
|
+
*/
|
|
711
|
+
EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
|
|
712
|
+
/**
|
|
713
|
+
* Fired by `addon-post-analysis` when a package-drop is confirmed inside
|
|
714
|
+
* a package zone — a newly-appeared stationary object of a package class
|
|
715
|
+
* that cleared the class / zone / dwell / size gates. Payload:
|
|
716
|
+
* `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
|
|
717
|
+
* entryId, className, zoneIds, keyFrameMediaId?, bbox, timestamp }`.
|
|
718
|
+
* Telemetry (D8): the durable record is the `package-events` store row;
|
|
719
|
+
* this bus topic drives notifier rules + live UI. See
|
|
720
|
+
* docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
|
|
721
|
+
*/
|
|
722
|
+
EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
|
|
723
|
+
/**
|
|
724
|
+
* Fired by `addon-post-analysis` when a previously-delivered package
|
|
725
|
+
* leaves its zone (the stationary entry departed — moved or swept).
|
|
726
|
+
* Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
|
|
727
|
+
* `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
|
|
728
|
+
* Telemetry (D8). See package-zones-design §5.2.
|
|
729
|
+
*/
|
|
730
|
+
EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
|
|
731
|
+
/**
|
|
732
|
+
* Fired by `addon-post-analysis` whenever a gallery face row changes:
|
|
733
|
+
* `kind:'buffered'` a new detected face was persisted, `'assigned'` /
|
|
734
|
+
* `'unassigned'` its identity link changed, `'deleted'` the row was
|
|
735
|
+
* removed. Telemetry semantics (D8): a lost event only delays a refresh,
|
|
736
|
+
* never a correctness issue. Payload `PipelineAnalyticsFaceGalleryChangedPayload`.
|
|
737
|
+
*/
|
|
738
|
+
EventCategory["PipelineAnalyticsFaceGalleryChanged"] = "pipeline-analytics.face-gallery-changed";
|
|
739
|
+
/**
|
|
740
|
+
* Fired by `addon-post-analysis` whenever a gallery plate row changes:
|
|
741
|
+
* `kind:'buffered'` a new read was persisted (`PlateRecognizer.onTrackEnd`),
|
|
742
|
+
* `'assigned'` / `'unassigned'` its vehicle link changed
|
|
743
|
+
* (`PlateGalleryProvider`), `'deleted'` the row was removed. Telemetry
|
|
744
|
+
* semantics (D8): a lost event only delays a refresh, never a correctness
|
|
745
|
+
* issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
|
|
746
|
+
*/
|
|
747
|
+
EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
|
|
748
|
+
EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
|
|
749
|
+
/**
|
|
750
|
+
* Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
|
|
751
|
+
* `stream.offline` fires after STREAM_STALE_TIMEOUT_MS without an encoded
|
|
752
|
+
* packet on an active broker. `stream.online` fires on first packet
|
|
753
|
+
* after a stale gap (or on initial first packet). Payload includes
|
|
754
|
+
* the assigned camStreamId as `profileKey`.
|
|
755
|
+
*/
|
|
756
|
+
EventCategory["StreamOnline"] = "stream.online";
|
|
757
|
+
EventCategory["StreamOffline"] = "stream.offline";
|
|
758
|
+
EventCategory["NetworkTunnelStarted"] = "network.tunnel.started";
|
|
759
|
+
EventCategory["NetworkTunnelStopped"] = "network.tunnel.stopped";
|
|
760
|
+
/** Fired by the `local-network` cap when the host's interface set
|
|
761
|
+
* changes (new IP from DHCP, VPN connect, docker bridge added). */
|
|
762
|
+
EventCategory["LocalNetworkChanged"] = "network.local.changed";
|
|
763
|
+
/**
|
|
764
|
+
* Fired by a `mesh-network` provider (Tailscale, …) when its
|
|
765
|
+
* mesh-reachable host changes (join / leave / MagicDNS or 100.x IP
|
|
766
|
+
* change). Lets `local-network` fold the mesh endpoint into its
|
|
767
|
+
* connection-endpoint list without a cross-package import. Payload
|
|
768
|
+
* carries the preferred host (`host: ''` = no longer reachable),
|
|
769
|
+
* the hub port, and the scheme. Telemetry-grade (D8): consumers
|
|
770
|
+
* pull-reconcile from the provider's `getStatus` on reconnect. */
|
|
771
|
+
EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
|
|
772
|
+
EventCategory["BackupCompleted"] = "backup.completed";
|
|
773
|
+
/**
|
|
774
|
+
* A whole backup RUN finished — every destination attempted, win or lose.
|
|
775
|
+
*
|
|
776
|
+
* `backup.completed` fires once per DESTINATION, which is the right grain for
|
|
777
|
+
* a progress UI and the wrong one for a notification: an operator with three
|
|
778
|
+
* destinations would be told three times. And a run where two of three
|
|
779
|
+
* destinations succeeded is not a clean success — a single "backup
|
|
780
|
+
* completed" that hid the failed one would be a lie, so the count of each is
|
|
781
|
+
* carried here and the message says both.
|
|
782
|
+
*
|
|
783
|
+
* Emitted by the backup orchestrator only after the destination loop, so a
|
|
784
|
+
* run that dies during the BUILD phase produces no completion at all.
|
|
785
|
+
*/
|
|
786
|
+
EventCategory["BackupRunCompleted"] = "backup.run-completed";
|
|
787
|
+
EventCategory["BackupRestored"] = "backup.restored";
|
|
788
|
+
EventCategory["NotificationDispatched"] = "notification.dispatched";
|
|
789
|
+
EventCategory["NotificationFailed"] = "notification.failed";
|
|
790
|
+
/**
|
|
791
|
+
* A notification rule that cannot work (D905): its stored row does not load,
|
|
792
|
+
* or it still names a device / delivery target the prune CONFIRMED deleted.
|
|
793
|
+
* One edge per rule per distinct set of reasons; never on an unanswered read.
|
|
794
|
+
*/
|
|
795
|
+
EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
|
|
796
|
+
/** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
|
|
797
|
+
EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
|
|
798
|
+
EventCategory["DeviceUpdated"] = "device.updated";
|
|
799
|
+
/**
|
|
800
|
+
* Transport-level connectivity. Emitted by the device driver when
|
|
801
|
+
* the underlying control socket actually connects / disconnects
|
|
802
|
+
* (Baichuan TCP, ONVIF probe response, RTSP DESCRIBE, …) — NOT for
|
|
803
|
+
* power-state transitions on a battery camera. For battery wake /
|
|
804
|
+
* doze cycles see `DeviceAwake` / `DeviceSleeping`.
|
|
805
|
+
*/
|
|
806
|
+
EventCategory["DeviceOnline"] = "device.online";
|
|
807
|
+
/**
|
|
808
|
+
* Stream-broker watchdog — emitted when no encoded packet has been
|
|
809
|
+
* received for STREAM_STALE_TIMEOUT_MS on an active broker (rtsp or
|
|
810
|
+
* push). Paired with DeviceOnline which fires on first packet after
|
|
811
|
+
* a stale gap. Payload: DeviceStreamHealthPayload.
|
|
812
|
+
*/
|
|
813
|
+
EventCategory["DeviceOffline"] = "device.offline";
|
|
814
|
+
/**
|
|
815
|
+
* Battery cam woke up — physical power-state transition reported
|
|
816
|
+
* by the device firmware. Distinct from `DeviceOnline` so a UI panel
|
|
817
|
+
* watching power state doesn't flap on every UDP socket reconnect.
|
|
818
|
+
*/
|
|
819
|
+
EventCategory["DeviceAwake"] = "device.awake";
|
|
820
|
+
/**
|
|
821
|
+
* Battery cam went to sleep. See `DeviceAwake`.
|
|
822
|
+
*/
|
|
823
|
+
EventCategory["DeviceSleeping"] = "device.sleeping";
|
|
824
|
+
EventCategory["RetentionCleanup"] = "retention.cleanup";
|
|
825
|
+
/**
|
|
826
|
+
* Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
|
|
827
|
+
* emitted — F3 removed the coordinator that produced it; "Update all" now runs
|
|
828
|
+
* as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
|
|
829
|
+
* (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
|
|
830
|
+
* in F4 once live bulk progress is re-implemented over the engine events.
|
|
831
|
+
*/
|
|
832
|
+
EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
|
|
833
|
+
EventCategory["AddonsJobProgress"] = "addons.job-progress";
|
|
834
|
+
EventCategory["AddonsJobLog"] = "addons.job-log";
|
|
835
|
+
/**
|
|
836
|
+
* A container's child visibility toggled (hidden/shown). Emitted by the
|
|
837
|
+
* `accessories` cap when a child device is hidden or revealed.
|
|
838
|
+
* Payload: `{ deviceId, childDeviceId, hidden }`.
|
|
839
|
+
*/
|
|
840
|
+
EventCategory["AccessoriesChildVisibilityChanged"] = "accessories.onChildVisibilityChanged";
|
|
841
|
+
/**
|
|
842
|
+
* A container's child set changed (children added/removed/reordered).
|
|
843
|
+
* Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
|
|
844
|
+
*/
|
|
845
|
+
EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
|
|
846
|
+
/**
|
|
847
|
+
* An action run started or ended. Emitted by the `actions` surface.
|
|
848
|
+
* Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
|
|
849
|
+
*/
|
|
850
|
+
EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
|
|
851
|
+
/**
|
|
852
|
+
* Progress update from a running model conversion job.
|
|
853
|
+
* Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
|
|
854
|
+
* Emitted by `addon-model-studio` on the converting node.
|
|
855
|
+
*/
|
|
856
|
+
EventCategory["ModelConvertProgress"] = "model-convert.progress";
|
|
857
|
+
return EventCategory;
|
|
858
|
+
}({});
|
|
859
|
+
//#endregion
|
|
82
860
|
//#region src/interfaces/addon-peer-bytes.ts
|
|
83
861
|
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
84
862
|
var PeerBytesTicketSchema = z.object({
|
|
@@ -962,593 +1740,6 @@ z.object({
|
|
|
962
1740
|
app: AppLoginPathSchema.nullable()
|
|
963
1741
|
});
|
|
964
1742
|
//#endregion
|
|
965
|
-
//#region src/catalogs/sensor-active-state.ts
|
|
966
|
-
/**
|
|
967
|
-
* THE table "which boolean of this device type counts as HIGH", and the
|
|
968
|
-
* pure evaluator of its EDGE.
|
|
969
|
-
*
|
|
970
|
-
* ONE table for everyone who asks "is this device HIGH?": the
|
|
971
|
-
* recorder's `RecordingTriggers.sensorDeviceIds` trigger, the post-analysis
|
|
972
|
-
* sensor markers, the orchestrator's camera-sensors bridge and
|
|
973
|
-
* the sources of a pulse (`compilePulseOverride`, D936).
|
|
974
|
-
* A second copy would diverge at the first cap added, and the symptom — "the
|
|
975
|
-
* sensor rings the doorbell but does not record" — is exactly D62.
|
|
976
|
-
*
|
|
977
|
-
* ⚠ NOT `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), and the
|
|
978
|
-
* two must not be unified: that one answers "what is the state WORD for a
|
|
979
|
-
* rule" (and includes `presence`, `cover`, `alarm-panel`), this one "which
|
|
980
|
-
* boolean's EDGE counts". Deliberately different vocabularies.
|
|
981
|
-
*/
|
|
982
|
-
/**
|
|
983
|
-
* Known binary / switch source caps → the boolean slice field whose
|
|
984
|
-
* false→true rise counts as ACTIVE. Every entry is "fire on active".
|
|
985
|
-
* Sensors whose "active" reading is not a plain boolean (presence's string
|
|
986
|
-
* state, connectivity's connected flag) are deliberately excluded — a
|
|
987
|
-
* reconnect is not a doorbell press, and it is not a recording either.
|
|
988
|
-
*/
|
|
989
|
-
var SOURCE_CAP_ACTIVE_FIELD = {
|
|
990
|
-
contact: "entryOpen",
|
|
991
|
-
binary: "on",
|
|
992
|
-
switch: "on",
|
|
993
|
-
motion: "detected",
|
|
994
|
-
flood: "flooded",
|
|
995
|
-
gas: "detected",
|
|
996
|
-
smoke: "detected",
|
|
997
|
-
"carbon-monoxide": "detected",
|
|
998
|
-
vibration: "detected",
|
|
999
|
-
tamper: "tampered"
|
|
1000
|
-
};
|
|
1001
|
-
//#endregion
|
|
1002
|
-
//#region src/catalogs/coco-classmap.ts
|
|
1003
|
-
var MACRO_LABELS = [
|
|
1004
|
-
{
|
|
1005
|
-
id: "person",
|
|
1006
|
-
name: "Person"
|
|
1007
|
-
},
|
|
1008
|
-
{
|
|
1009
|
-
id: "vehicle",
|
|
1010
|
-
name: "Vehicle"
|
|
1011
|
-
},
|
|
1012
|
-
{
|
|
1013
|
-
id: "animal",
|
|
1014
|
-
name: "Animal"
|
|
1015
|
-
},
|
|
1016
|
-
{
|
|
1017
|
-
id: "package",
|
|
1018
|
-
name: "Package"
|
|
1019
|
-
}
|
|
1020
|
-
];
|
|
1021
|
-
new Set(MACRO_LABELS.map((l) => l.id));
|
|
1022
|
-
var COCO_TO_MACRO = {
|
|
1023
|
-
mapping: {
|
|
1024
|
-
person: "person",
|
|
1025
|
-
bicycle: "vehicle",
|
|
1026
|
-
car: "vehicle",
|
|
1027
|
-
motorcycle: "vehicle",
|
|
1028
|
-
airplane: "vehicle",
|
|
1029
|
-
bus: "vehicle",
|
|
1030
|
-
train: "vehicle",
|
|
1031
|
-
truck: "vehicle",
|
|
1032
|
-
boat: "vehicle",
|
|
1033
|
-
bird: "animal",
|
|
1034
|
-
cat: "animal",
|
|
1035
|
-
dog: "animal",
|
|
1036
|
-
horse: "animal",
|
|
1037
|
-
sheep: "animal",
|
|
1038
|
-
cow: "animal",
|
|
1039
|
-
elephant: "animal",
|
|
1040
|
-
bear: "animal",
|
|
1041
|
-
zebra: "animal",
|
|
1042
|
-
giraffe: "animal",
|
|
1043
|
-
suitcase: "package",
|
|
1044
|
-
backpack: "package",
|
|
1045
|
-
handbag: "package"
|
|
1046
|
-
},
|
|
1047
|
-
preserveOriginal: false
|
|
1048
|
-
};
|
|
1049
|
-
z.object({
|
|
1050
|
-
field: z.string().min(1),
|
|
1051
|
-
on: z.literal("advance")
|
|
1052
|
-
});
|
|
1053
|
-
/**
|
|
1054
|
-
* Output schema shared by the contribution + live methods.
|
|
1055
|
-
*
|
|
1056
|
-
* Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
|
|
1057
|
-
* tabs[]) without importing from `../interfaces/config-ui.js` — a
|
|
1058
|
-
* concrete-but-lenient Zod object keeps tRPC output inference happy
|
|
1059
|
-
* (using `z.unknown()` here collapses unrelated router branches to
|
|
1060
|
-
* `unknown` when the generator re-inlines the huge AppRouter type).
|
|
1061
|
-
*
|
|
1062
|
-
* `.passthrough()` on sections/fields accepts whatever FormBuilder
|
|
1063
|
-
* extensions the caller adds (showWhen, displayScale, …) without
|
|
1064
|
-
* rebuilding every time a new field kind is introduced.
|
|
1065
|
-
*/
|
|
1066
|
-
var ContributionSectionSchema = z.object({
|
|
1067
|
-
id: z.string(),
|
|
1068
|
-
title: z.string(),
|
|
1069
|
-
description: z.string().optional(),
|
|
1070
|
-
style: z.enum(["card", "accordion"]).optional(),
|
|
1071
|
-
defaultCollapsed: z.boolean().optional(),
|
|
1072
|
-
columns: z.union([
|
|
1073
|
-
z.literal(1),
|
|
1074
|
-
z.literal(2),
|
|
1075
|
-
z.literal(3),
|
|
1076
|
-
z.literal(4)
|
|
1077
|
-
]).optional(),
|
|
1078
|
-
tab: z.string().optional(),
|
|
1079
|
-
location: z.enum([
|
|
1080
|
-
"settings",
|
|
1081
|
-
"top-tab",
|
|
1082
|
-
"hero"
|
|
1083
|
-
]).optional(),
|
|
1084
|
-
order: z.number().optional(),
|
|
1085
|
-
fields: z.array(z.any())
|
|
1086
|
-
});
|
|
1087
|
-
var ContributionTabSchema = z.object({
|
|
1088
|
-
id: z.string(),
|
|
1089
|
-
label: z.string(),
|
|
1090
|
-
icon: z.string(),
|
|
1091
|
-
order: z.number().optional()
|
|
1092
|
-
});
|
|
1093
|
-
z.object({
|
|
1094
|
-
tabs: z.array(ContributionTabSchema).optional(),
|
|
1095
|
-
sections: z.array(ContributionSectionSchema)
|
|
1096
|
-
}).nullable();
|
|
1097
|
-
z.object({ deviceId: z.number() }), z.object({ deviceId: z.number() }), z.object({
|
|
1098
|
-
deviceId: z.number(),
|
|
1099
|
-
patch: z.record(z.string(), z.unknown())
|
|
1100
|
-
}), z.object({ success: z.literal(true) });
|
|
1101
|
-
z.object({ deviceId: z.number() }), z.unknown().nullable();
|
|
1102
|
-
function method(input, output, options) {
|
|
1103
|
-
return {
|
|
1104
|
-
input,
|
|
1105
|
-
output,
|
|
1106
|
-
kind: options?.kind ?? "query",
|
|
1107
|
-
...options?.auth !== void 0 ? { auth: options.auth } : {},
|
|
1108
|
-
...options?.access !== void 0 ? { access: options.access } : {},
|
|
1109
|
-
...options?.role !== void 0 ? { role: options.role } : {},
|
|
1110
|
-
...options?.caller !== void 0 ? { caller: options.caller } : {},
|
|
1111
|
-
timeoutMs: options?.timeoutMs
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
|
|
1116
|
-
* on `InferProvider`. The `providerOptional: true` literal is what
|
|
1117
|
-
* `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
|
|
1118
|
-
* for when this is the honest answer and when it is a soft stub.
|
|
1119
|
-
*/
|
|
1120
|
-
function optionalMethod(input, output, options) {
|
|
1121
|
-
return {
|
|
1122
|
-
...method(input, output, options),
|
|
1123
|
-
providerOptional: true
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
//#endregion
|
|
1127
|
-
//#region src/capabilities/notification-output.cap.ts
|
|
1128
|
-
/**
|
|
1129
|
-
* notification-output — canonical, capability-gated notification delivery.
|
|
1130
|
-
*
|
|
1131
|
-
* Apprise-derived model (see
|
|
1132
|
-
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
1133
|
-
* callers emit ONE canonical `Notification`; each provider declares a
|
|
1134
|
-
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
1135
|
-
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
1136
|
-
* message to what the kind supports — callers never special-case a service.
|
|
1137
|
-
*
|
|
1138
|
-
* DESIGN DECISIONS (locked):
|
|
1139
|
-
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
1140
|
-
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
1141
|
-
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
1142
|
-
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
1143
|
-
* alternative would fork the UI per addon and cannot host the
|
|
1144
|
-
* discovery→adopt flow.
|
|
1145
|
-
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
1146
|
-
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
1147
|
-
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
1148
|
-
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
1149
|
-
* `addonId` the generated collection router extracts from the call input.
|
|
1150
|
-
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
1151
|
-
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
1152
|
-
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
1153
|
-
* base64 fallback needed.
|
|
1154
|
-
*
|
|
1155
|
-
* TODO (deferred, closed-set change — separate decision): add
|
|
1156
|
-
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
1157
|
-
* admin "Integrations" page.
|
|
1158
|
-
*/
|
|
1159
|
-
/**
|
|
1160
|
-
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
1161
|
-
* adapter picks what it supports and the degrade engine filters the rest.
|
|
1162
|
-
*/
|
|
1163
|
-
var AttachmentMediaTypeSchema = z.enum([
|
|
1164
|
-
"image",
|
|
1165
|
-
"video",
|
|
1166
|
-
"gif",
|
|
1167
|
-
"audio",
|
|
1168
|
-
"icon"
|
|
1169
|
-
]);
|
|
1170
|
-
/**
|
|
1171
|
-
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
1172
|
-
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
1173
|
-
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
1174
|
-
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
1175
|
-
*/
|
|
1176
|
-
var AttachmentSchema = z.object({
|
|
1177
|
-
mediaType: AttachmentMediaTypeSchema,
|
|
1178
|
-
url: z.string().optional(),
|
|
1179
|
-
bytes: z.instanceof(Uint8Array).optional(),
|
|
1180
|
-
mime: z.string().optional(),
|
|
1181
|
-
name: z.string().optional()
|
|
1182
|
-
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
1183
|
-
var NotificationFormatSchema = z.enum([
|
|
1184
|
-
"text",
|
|
1185
|
-
"markdown",
|
|
1186
|
-
"html"
|
|
1187
|
-
]);
|
|
1188
|
-
/**
|
|
1189
|
-
* The CLOSED icon vocabulary an action button may use.
|
|
1190
|
-
*
|
|
1191
|
-
* A closed set, not a free string, and that is the whole point: an arbitrary
|
|
1192
|
-
* icon name is one that ntfy renders, zentik silently drops, and nobody
|
|
1193
|
-
* notices — the same class of gap as a zone vocabulary nothing produced
|
|
1194
|
-
* ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
|
|
1195
|
-
* declares `actionIcons: false` and the degrade engine strips the field.
|
|
1196
|
-
*
|
|
1197
|
-
* Named by INTENT, never by glyph. "check" would tie the vocabulary to one
|
|
1198
|
-
* renderer's icon set; "acknowledge" survives an adapter that draws it
|
|
1199
|
-
* differently.
|
|
1200
|
-
*
|
|
1201
|
-
* ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
|
|
1202
|
-
*
|
|
1203
|
-
* These names are for US. **No adapter may forward one verbatim.** Each maps
|
|
1204
|
-
* the whole set onto its own renderer's vocabulary through a
|
|
1205
|
-
* `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
|
|
1206
|
-
* fallback, so adding a member here fails every adapter's build until someone
|
|
1207
|
-
* decides its glyph, which is the only place that decision can be made
|
|
1208
|
-
* honestly.
|
|
1209
|
-
*
|
|
1210
|
-
* This paragraph is the bug. Zentik declared `actionIcons: true` and passed
|
|
1211
|
-
* `disarm` straight through; iOS feeds that string to
|
|
1212
|
-
* `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
|
|
1213
|
-
* and every snooze and alarm button arrived BLANK. A pass-through is not a
|
|
1214
|
-
* mapping, and "the field is documented" is not "the value renders".
|
|
1215
|
-
*
|
|
1216
|
-
* Adding a member is TRAIN-BOUND. The enum lives in the published
|
|
1217
|
-
* `@camstack/server` closure and the cap seam validates against the HUB's copy,
|
|
1218
|
-
* so an addon that emits a token the running hub does not know does not lose an
|
|
1219
|
-
* icon — its whole `send` fails Zod validation and the notification never
|
|
1220
|
-
* arrives. Never emit a new token from an addon before the train carrying it.
|
|
1221
|
-
*/
|
|
1222
|
-
var NotificationActionIconSchema = z.enum([
|
|
1223
|
-
"acknowledge",
|
|
1224
|
-
"dismiss",
|
|
1225
|
-
"silence",
|
|
1226
|
-
"snooze",
|
|
1227
|
-
"view",
|
|
1228
|
-
"play",
|
|
1229
|
-
"open",
|
|
1230
|
-
"close",
|
|
1231
|
-
"lock",
|
|
1232
|
-
"unlock",
|
|
1233
|
-
"arm",
|
|
1234
|
-
"arm-home",
|
|
1235
|
-
"arm-away",
|
|
1236
|
-
"arm-night",
|
|
1237
|
-
"disarm",
|
|
1238
|
-
"light",
|
|
1239
|
-
"alert",
|
|
1240
|
-
"camera"
|
|
1241
|
-
]);
|
|
1242
|
-
/** A single tap-through action button. */
|
|
1243
|
-
var NotificationActionSchema = z.object({
|
|
1244
|
-
id: z.string(),
|
|
1245
|
-
label: z.string(),
|
|
1246
|
-
url: z.string().optional(),
|
|
1247
|
-
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
1248
|
-
icon: NotificationActionIconSchema.optional(),
|
|
1249
|
-
/**
|
|
1250
|
-
* Renders in a warning style where the notifier supports it.
|
|
1251
|
-
*
|
|
1252
|
-
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
1253
|
-
* else — see `notification-center/action-token.ts` for what that does and
|
|
1254
|
-
* does not buy.
|
|
1255
|
-
*/
|
|
1256
|
-
destructive: z.boolean().optional(),
|
|
1257
|
-
/**
|
|
1258
|
-
* How the tap should REACH the url.
|
|
1259
|
-
*
|
|
1260
|
-
* `navigate` (absent, and every button authored before this field) opens it:
|
|
1261
|
-
* the phone leaves the notification and shows whatever the callback returns.
|
|
1262
|
-
* That is right for a button whose answer the operator wants to read.
|
|
1263
|
-
*
|
|
1264
|
-
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
1265
|
-
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
1266
|
-
* an answer to the notification, and being thrown into a browser tab to
|
|
1267
|
-
* confirm it costs more attention than the notification did. A backend that
|
|
1268
|
-
* cannot do a background call renders it as an ordinary link (the adapters
|
|
1269
|
-
* fall back rather than dropping the button), so this is a preference, never
|
|
1270
|
-
* a requirement.
|
|
1271
|
-
*/
|
|
1272
|
-
mode: z.enum(["navigate", "background"]).optional()
|
|
1273
|
-
});
|
|
1274
|
-
/** A delivery parameter value: what a notifier declared it takes. */
|
|
1275
|
-
var DeliveryParamValueSchema = z.union([
|
|
1276
|
-
z.string(),
|
|
1277
|
-
z.number(),
|
|
1278
|
-
z.boolean()
|
|
1279
|
-
]);
|
|
1280
|
-
/**
|
|
1281
|
-
* One extra delivery parameter a notifier KIND declares (D938). A rule that
|
|
1282
|
-
* addresses a target of that kind may set it; a kind that declares nothing is
|
|
1283
|
-
* never handed one. The admin UI derives its control from this descriptor —
|
|
1284
|
-
* no editor knows a service by name.
|
|
1285
|
-
*
|
|
1286
|
-
* Optional fields are `.optional()`, never `.default()` (a Zod default does not
|
|
1287
|
-
* run on the addon cap path).
|
|
1288
|
-
*/
|
|
1289
|
-
var DeliveryParamSchema = z.object({
|
|
1290
|
-
key: z.string().min(1),
|
|
1291
|
-
label: z.string(),
|
|
1292
|
-
/** i18n key of the label, resolved by the declaring addon's namespace. */
|
|
1293
|
-
labelKey: z.string().optional(),
|
|
1294
|
-
type: z.enum([
|
|
1295
|
-
"enum",
|
|
1296
|
-
"number",
|
|
1297
|
-
"boolean",
|
|
1298
|
-
"string"
|
|
1299
|
-
]),
|
|
1300
|
-
/** `enum` only: the closed set of accepted values. */
|
|
1301
|
-
options: z.array(z.object({
|
|
1302
|
-
value: z.string(),
|
|
1303
|
-
label: z.string()
|
|
1304
|
-
})).optional(),
|
|
1305
|
-
/** `number` only. */
|
|
1306
|
-
min: z.number().optional(),
|
|
1307
|
-
max: z.number().optional(),
|
|
1308
|
-
/** What the notifier does when the rule sets nothing; shown as the placeholder. */
|
|
1309
|
-
default: DeliveryParamValueSchema.optional()
|
|
1310
|
-
});
|
|
1311
|
-
/**
|
|
1312
|
-
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
1313
|
-
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
1314
|
-
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
1315
|
-
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
1316
|
-
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
1317
|
-
* `priority` for that one target.
|
|
1318
|
-
*/
|
|
1319
|
-
var NotificationSchema = z.object({
|
|
1320
|
-
body: z.string(),
|
|
1321
|
-
title: z.string().optional(),
|
|
1322
|
-
format: NotificationFormatSchema.default("text"),
|
|
1323
|
-
priority: z.number().int().min(1).max(5).default(3),
|
|
1324
|
-
level: z.string().optional(),
|
|
1325
|
-
attachments: z.array(AttachmentSchema).optional(),
|
|
1326
|
-
clickUrl: z.string().optional(),
|
|
1327
|
-
actions: z.array(NotificationActionSchema).optional(),
|
|
1328
|
-
sound: z.string().optional(),
|
|
1329
|
-
ttl: z.number().optional(),
|
|
1330
|
-
tag: z.string().optional(),
|
|
1331
|
-
deviceId: z.number().optional(),
|
|
1332
|
-
eventId: z.string().optional(),
|
|
1333
|
-
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1334
|
-
/**
|
|
1335
|
-
* Values for the parameters the target's kind DECLARES in
|
|
1336
|
-
* {@link TargetKindSchema} `deliveryParams`. Already validated against the
|
|
1337
|
-
* declaration by the sender; a notifier reads only the keys it declared.
|
|
1338
|
-
*/
|
|
1339
|
-
delivery: z.record(z.string(), DeliveryParamValueSchema).optional()
|
|
1340
|
-
});
|
|
1341
|
-
/** One declared native severity/priority level for a kind. */
|
|
1342
|
-
var TargetKindLevelSchema = z.object({
|
|
1343
|
-
id: z.string(),
|
|
1344
|
-
label: z.string(),
|
|
1345
|
-
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
1346
|
-
ordinal: z.number().int().min(1).max(5).nullable(),
|
|
1347
|
-
flags: z.object({
|
|
1348
|
-
critical: z.boolean().optional(),
|
|
1349
|
-
silent: z.boolean().optional(),
|
|
1350
|
-
noPush: z.boolean().optional()
|
|
1351
|
-
}).optional(),
|
|
1352
|
-
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
1353
|
-
requires: z.array(z.string()).optional(),
|
|
1354
|
-
description: z.string().optional()
|
|
1355
|
-
});
|
|
1356
|
-
/** Attachment capabilities for a kind (drives the degrade engine + test panel). */
|
|
1357
|
-
var TargetKindAttachmentsCapsSchema = z.object({
|
|
1358
|
-
mediaTypes: z.array(AttachmentMediaTypeSchema),
|
|
1359
|
-
mode: z.enum([
|
|
1360
|
-
"url",
|
|
1361
|
-
"bytes",
|
|
1362
|
-
"both"
|
|
1363
|
-
]),
|
|
1364
|
-
max: z.number().int().nonnegative(),
|
|
1365
|
-
maxBytes: z.number().int().positive().optional()
|
|
1366
|
-
});
|
|
1367
|
-
/** The full capability block consulted before dispatch. */
|
|
1368
|
-
var TargetKindCapsSchema = z.object({
|
|
1369
|
-
attachments: TargetKindAttachmentsCapsSchema,
|
|
1370
|
-
/** Max action buttons (0 = none). */
|
|
1371
|
-
actions: z.number().int().nonnegative(),
|
|
1372
|
-
/**
|
|
1373
|
-
* Whether this kind renders a per-action ICON.
|
|
1374
|
-
*
|
|
1375
|
-
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
1376
|
-
* run on the addon cap path — three production failures in one day taught
|
|
1377
|
-
* this repo that once. Absent is read as false by the degrade engine, which
|
|
1378
|
-
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
1379
|
-
* assumed and dropped costs the operator's trust in the field.
|
|
1380
|
-
*/
|
|
1381
|
-
actionIcons: z.boolean().optional(),
|
|
1382
|
-
levels: z.array(TargetKindLevelSchema),
|
|
1383
|
-
format: z.array(NotificationFormatSchema),
|
|
1384
|
-
clickUrl: z.boolean(),
|
|
1385
|
-
sound: z.boolean(),
|
|
1386
|
-
ttl: z.boolean(),
|
|
1387
|
-
bodyMaxLen: z.number().int().positive()
|
|
1388
|
-
});
|
|
1389
|
-
/**
|
|
1390
|
-
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
1391
|
-
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
1392
|
-
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
1393
|
-
* the union is large and not meant for runtime validation here; the exported
|
|
1394
|
-
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
1395
|
-
*/
|
|
1396
|
-
var ConfigSchemaPassthrough = z.unknown();
|
|
1397
|
-
var TargetKindSchema = z.object({
|
|
1398
|
-
kind: z.string(),
|
|
1399
|
-
label: z.string(),
|
|
1400
|
-
icon: z.string(),
|
|
1401
|
-
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
1402
|
-
addonId: z.string(),
|
|
1403
|
-
/**
|
|
1404
|
-
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
1405
|
-
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
1406
|
-
* when the addon bundles no icon for that kind — the client then falls back
|
|
1407
|
-
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
1408
|
-
*
|
|
1409
|
-
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
1410
|
-
* client, and a native client joins it onto its own hub base.
|
|
1411
|
-
*
|
|
1412
|
-
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
1413
|
-
* field that survived only because the runtime cap-router forwards provider
|
|
1414
|
-
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
1415
|
-
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
1416
|
-
* broken silently the moment output validation was tightened anywhere.
|
|
1417
|
-
*/
|
|
1418
|
-
iconUrl: z.string().optional(),
|
|
1419
|
-
/**
|
|
1420
|
-
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
1421
|
-
*
|
|
1422
|
-
* The server knows this and therefore says it, because the client cannot
|
|
1423
|
-
* safely guess: a React-Native client renders SVG and raster through two
|
|
1424
|
-
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
1425
|
-
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
1426
|
-
* placeholder glyph for every vector icon while the web build looked fine.
|
|
1427
|
-
*
|
|
1428
|
-
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
1429
|
-
* not been updated — a client that cannot determine the type should prefer
|
|
1430
|
-
* its raster path, which is the safe default for an unknown image.
|
|
1431
|
-
*/
|
|
1432
|
-
iconMediaType: z.string().optional(),
|
|
1433
|
-
configSchema: ConfigSchemaPassthrough,
|
|
1434
|
-
/**
|
|
1435
|
-
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
1436
|
-
* base URL, instance, chat, recipient). A save that changes any of them
|
|
1437
|
-
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
1438
|
-
* editor uses this to ask for the secret on exactly those edits instead of
|
|
1439
|
-
* on every edit.
|
|
1440
|
-
*
|
|
1441
|
-
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
1442
|
-
* kinds) and from a provider older than this field — an editor that finds
|
|
1443
|
-
* it absent must assume ANY edit may need the secret.
|
|
1444
|
-
*/
|
|
1445
|
-
destinationKeys: z.array(z.string()).optional(),
|
|
1446
|
-
supportsDiscovery: z.boolean(),
|
|
1447
|
-
/**
|
|
1448
|
-
* Extra delivery parameters a rule may set for a target of this kind (D938).
|
|
1449
|
-
* Absent = none; the rule editor then offers nothing.
|
|
1450
|
-
*/
|
|
1451
|
-
deliveryParams: z.array(DeliveryParamSchema).optional(),
|
|
1452
|
-
caps: TargetKindCapsSchema
|
|
1453
|
-
});
|
|
1454
|
-
/**
|
|
1455
|
-
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
1456
|
-
* (return a presence marker only) when serving `listTargets` — never
|
|
1457
|
-
* round-trip a stored secret to the UI.
|
|
1458
|
-
*/
|
|
1459
|
-
var TargetSchema = z.object({
|
|
1460
|
-
id: z.string(),
|
|
1461
|
-
name: z.string(),
|
|
1462
|
-
kind: z.string(),
|
|
1463
|
-
addonId: z.string(),
|
|
1464
|
-
enabled: z.boolean(),
|
|
1465
|
-
config: z.record(z.string(), z.unknown())
|
|
1466
|
-
});
|
|
1467
|
-
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
1468
|
-
var DiscoveredTargetSchema = z.object({
|
|
1469
|
-
kind: z.string(),
|
|
1470
|
-
suggestedName: z.string(),
|
|
1471
|
-
config: z.record(z.string(), z.unknown())
|
|
1472
|
-
});
|
|
1473
|
-
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
1474
|
-
var RenderedAsSchema = z.object({
|
|
1475
|
-
level: z.string(),
|
|
1476
|
-
format: NotificationFormatSchema,
|
|
1477
|
-
attachmentsSent: z.number().int().nonnegative(),
|
|
1478
|
-
actionsSent: z.number().int().nonnegative(),
|
|
1479
|
-
truncated: z.boolean(),
|
|
1480
|
-
dropped: z.array(z.string())
|
|
1481
|
-
});
|
|
1482
|
-
var SendResultSchema = z.object({
|
|
1483
|
-
success: z.boolean(),
|
|
1484
|
-
error: z.string().optional(),
|
|
1485
|
-
renderedAs: RenderedAsSchema.optional()
|
|
1486
|
-
});
|
|
1487
|
-
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
1488
|
-
var TestResultSchema = SendResultSchema;
|
|
1489
|
-
/**
|
|
1490
|
-
* Pre-save credential probe against a partial (unsaved) target config.
|
|
1491
|
-
* `resolvedUrl` never includes secrets (token stripped from path).
|
|
1492
|
-
*/
|
|
1493
|
-
var ProbeTargetConfigResultSchema = z.object({
|
|
1494
|
-
ok: z.boolean(),
|
|
1495
|
-
resolvedUrl: z.string(),
|
|
1496
|
-
httpStatus: z.number().int().optional(),
|
|
1497
|
-
stateInstance: z.string().optional(),
|
|
1498
|
-
error: z.string().optional()
|
|
1499
|
-
});
|
|
1500
|
-
method(z.object({}), z.array(TargetKindSchema)), method(z.object({}), z.array(TargetSchema), { auth: "admin" }), method(z.object({ targetId: z.string() }), TargetSchema, { auth: "admin" }), method(z.object({
|
|
1501
|
-
kind: z.string(),
|
|
1502
|
-
config: z.record(z.string(), z.unknown()).optional(),
|
|
1503
|
-
/**
|
|
1504
|
-
* When editing / duplicating, the blank password field never round-trips
|
|
1505
|
-
* the stored token. Pass the source target id so the provider can restore
|
|
1506
|
-
* secrets server-side before dialling (same merge as upsert).
|
|
1507
|
-
*/
|
|
1508
|
-
targetId: z.string().optional()
|
|
1509
|
-
}), z.array(DiscoveredTargetSchema), { auth: "admin" }), method(z.object({
|
|
1510
|
-
kind: z.string(),
|
|
1511
|
-
config: z.record(z.string(), z.unknown()),
|
|
1512
|
-
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
1513
|
-
targetId: z.string().optional()
|
|
1514
|
-
}), ProbeTargetConfigResultSchema, {
|
|
1515
|
-
kind: "mutation",
|
|
1516
|
-
auth: "admin"
|
|
1517
|
-
}), method(z.object({
|
|
1518
|
-
targetId: z.string(),
|
|
1519
|
-
notification: NotificationSchema
|
|
1520
|
-
}), SendResultSchema, {
|
|
1521
|
-
kind: "mutation",
|
|
1522
|
-
auth: "admin"
|
|
1523
|
-
}), method(z.object({
|
|
1524
|
-
targetId: z.string(),
|
|
1525
|
-
sample: NotificationSchema.optional()
|
|
1526
|
-
}), TestResultSchema, {
|
|
1527
|
-
kind: "mutation",
|
|
1528
|
-
auth: "admin"
|
|
1529
|
-
}), method(z.object({
|
|
1530
|
-
target: TargetSchema,
|
|
1531
|
-
/**
|
|
1532
|
-
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
1533
|
-
* existing row. The UI never holds the stored token; blank / redacted
|
|
1534
|
-
* password fields are restored from this source (D634 still applies if
|
|
1535
|
-
* destinations diverge without a re-typed secret).
|
|
1536
|
-
*/
|
|
1537
|
-
cloneSecretsFrom: z.string().optional()
|
|
1538
|
-
}), TargetSchema, {
|
|
1539
|
-
kind: "mutation",
|
|
1540
|
-
auth: "admin"
|
|
1541
|
-
}), method(z.object({ targetId: z.string() }), z.void(), {
|
|
1542
|
-
kind: "mutation",
|
|
1543
|
-
auth: "admin"
|
|
1544
|
-
}), method(z.object({
|
|
1545
|
-
targetId: z.string(),
|
|
1546
|
-
enabled: z.boolean()
|
|
1547
|
-
}), z.void(), {
|
|
1548
|
-
kind: "mutation",
|
|
1549
|
-
auth: "admin"
|
|
1550
|
-
});
|
|
1551
|
-
//#endregion
|
|
1552
1743
|
//#region src/device/icon-catalog.generated.ts
|
|
1553
1744
|
/** Lucide category ids, alphabetical. `ICON_META` indexes into this. */
|
|
1554
1745
|
var ICON_CATEGORY_IDS = [
|
|
@@ -21370,242 +21561,869 @@ var ICON_SYNONYMS = {
|
|
|
21370
21561
|
]
|
|
21371
21562
|
};
|
|
21372
21563
|
/**
|
|
21373
|
-
* Spellings an operator may already have PERSISTED that no lucide package
|
|
21374
|
-
* accepts any more, and the catalog key they draw. They resolve
|
|
21375
|
-
* (`canonicalIconKey`) but are never offered. `gauge2` was this repo's own
|
|
21376
|
-
* pre-catalog spelling of `gauge`.
|
|
21564
|
+
* Spellings an operator may already have PERSISTED that no lucide package
|
|
21565
|
+
* accepts any more, and the catalog key they draw. They resolve
|
|
21566
|
+
* (`canonicalIconKey`) but are never offered. `gauge2` was this repo's own
|
|
21567
|
+
* pre-catalog spelling of `gauge`.
|
|
21568
|
+
*/
|
|
21569
|
+
var LEGACY_ICON_ALIASES = { gauge2: "gauge" };
|
|
21570
|
+
//#endregion
|
|
21571
|
+
//#region src/device/icon-catalog.ts
|
|
21572
|
+
/**
|
|
21573
|
+
* icon-catalog — THE icon vocabulary (D854).
|
|
21574
|
+
*
|
|
21575
|
+
* Every place that picks or stores an icon — a device's display override, a
|
|
21576
|
+
* location, a site zone, a viewer accessory, an `icon` settings field — names
|
|
21577
|
+
* it by a key of this catalog and nothing else. There is no curated subset and
|
|
21578
|
+
* no second list: the catalog is every lucide icon that both surfaces can draw
|
|
21579
|
+
* (the admin UI's `lucide-react` and the viewer's `lucide-react-native`), the
|
|
21580
|
+
* intersection computed by `scripts/generate-icon-catalog.ts` into the
|
|
21581
|
+
* committed `icon-catalog.generated.ts`.
|
|
21582
|
+
*
|
|
21583
|
+
* Like the vocabularies it replaces, this module holds STRINGS only — no React,
|
|
21584
|
+
* no lucide import, no glyph. Each surface owns its glyph table (the web one
|
|
21585
|
+
* lives in `ui-library`, loaded lazily), and a test on each side pins that it
|
|
21586
|
+
* covers {@link ICON_KEYS}.
|
|
21587
|
+
*
|
|
21588
|
+
* ## What is persisted
|
|
21589
|
+
*
|
|
21590
|
+
* The canonical kebab-case lucide id ({@link IconKey}). Spellings that a lucide
|
|
21591
|
+
* package still accepts (`home` for `house`) and this repo's own legacy ones
|
|
21592
|
+
* (`gauge2`) RESOLVE ({@link canonicalIconKey}) so a stored value never goes
|
|
21593
|
+
* stale, but they are never OFFERED by search.
|
|
21594
|
+
*
|
|
21595
|
+
* ## Search
|
|
21596
|
+
*
|
|
21597
|
+
* Over the key, the Italian/English overlay in `icon-synonyms.ts`, any words
|
|
21598
|
+
* the caller adds, and lucide's English tags — in that order of weight.
|
|
21599
|
+
*/
|
|
21600
|
+
var KEY_SET$1 = new Set(ICON_KEYS);
|
|
21601
|
+
var ALIAS_TO_KEY = new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
|
|
21602
|
+
/** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
|
|
21603
|
+
function isIconKey(value) {
|
|
21604
|
+
return typeof value === "string" && KEY_SET$1.has(value);
|
|
21605
|
+
}
|
|
21606
|
+
/**
|
|
21607
|
+
* A stored value as a catalog key: the key itself, the key a still-accepted or
|
|
21608
|
+
* legacy spelling stands for, or `undefined` — never a guess.
|
|
21609
|
+
*/
|
|
21610
|
+
function canonicalIconKey(value) {
|
|
21611
|
+
if (typeof value !== "string") return void 0;
|
|
21612
|
+
if (isIconKey(value)) return value;
|
|
21613
|
+
return ALIAS_TO_KEY.get(value);
|
|
21614
|
+
}
|
|
21615
|
+
/**
|
|
21616
|
+
* A stored icon value as a catalog key: its canonical key when it is (or
|
|
21617
|
+
* aliases) one, `fallback` otherwise. For an `icon` settings field whose stored
|
|
21618
|
+
* value may predate the catalog (a free-text lucide name) — the field's default
|
|
21619
|
+
* is drawn rather than nothing.
|
|
21620
|
+
*/
|
|
21621
|
+
function resolveIconKey(value, fallback) {
|
|
21622
|
+
return canonicalIconKey(value) ?? fallback;
|
|
21623
|
+
}
|
|
21624
|
+
/**
|
|
21625
|
+
* Wire/storage schema for one icon key.
|
|
21626
|
+
*
|
|
21627
|
+
* Deliberately NOT a `z.enum` of the catalog: ~1,700 literals would be inlined
|
|
21628
|
+
* into every cap router, type and API-reference row that carries an icon. The
|
|
21629
|
+
* check is membership in the catalog and the type is {@link IconKey}.
|
|
21630
|
+
*/
|
|
21631
|
+
var IconKeySchema = z.custom(isIconKey, "Not an icon of the catalog");
|
|
21632
|
+
var ENTRIES = ICON_KEYS.map((key, index) => {
|
|
21633
|
+
const [categoryIndexes, tags] = ICON_META[index] ?? [[], []];
|
|
21634
|
+
const categories = categoryIndexes.flatMap((i) => {
|
|
21635
|
+
const id = ICON_CATEGORY_IDS[i];
|
|
21636
|
+
return id === void 0 ? [] : [id];
|
|
21637
|
+
});
|
|
21638
|
+
return {
|
|
21639
|
+
key,
|
|
21640
|
+
categories,
|
|
21641
|
+
primaryCategory: categories[0] ?? ICON_CATEGORY_IDS[0],
|
|
21642
|
+
tags,
|
|
21643
|
+
synonyms: ICON_SYNONYMS[key] ?? []
|
|
21644
|
+
};
|
|
21645
|
+
});
|
|
21646
|
+
var ENTRY_BY_KEY = new Map(ENTRIES.map((e) => [e.key, e]));
|
|
21647
|
+
/** The entry for a canonical key, or `undefined`. */
|
|
21648
|
+
function iconEntry(key) {
|
|
21649
|
+
return ENTRY_BY_KEY.get(key);
|
|
21650
|
+
}
|
|
21651
|
+
/** Every entry, in key order. */
|
|
21652
|
+
var ICON_ENTRIES = ENTRIES;
|
|
21653
|
+
/** Categories a picker offers first: the ones this product's icons mostly live in. */
|
|
21654
|
+
var LEADING_CATEGORIES = [
|
|
21655
|
+
"devices",
|
|
21656
|
+
"home",
|
|
21657
|
+
"security",
|
|
21658
|
+
"buildings",
|
|
21659
|
+
"connectivity",
|
|
21660
|
+
"multimedia",
|
|
21661
|
+
"nature",
|
|
21662
|
+
"weather",
|
|
21663
|
+
"transportation",
|
|
21664
|
+
"animals"
|
|
21665
|
+
];
|
|
21666
|
+
var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
|
|
21667
|
+
/**
|
|
21668
|
+
* The categories a picker files icons under, in picker order: the leading ones,
|
|
21669
|
+
* then the rest alphabetically. Only categories that are some icon's PRIMARY
|
|
21670
|
+
* one — a category lucide lists only as a secondary has no section of its own.
|
|
21671
|
+
*/
|
|
21672
|
+
var ICON_CATEGORIES = [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
|
|
21673
|
+
/** The icons filed under a category as their PRIMARY one, in key order. */
|
|
21674
|
+
function iconsInCategory(category) {
|
|
21675
|
+
return ENTRIES.filter((e) => e.primaryCategory === category);
|
|
21676
|
+
}
|
|
21677
|
+
function normalise(text) {
|
|
21678
|
+
return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().replace(/[_-]+/g, " ").trim();
|
|
21679
|
+
}
|
|
21680
|
+
/** Does `needle` appear in `haystack` in order, though not adjacently? */
|
|
21681
|
+
function isSubsequence(needle, haystack) {
|
|
21682
|
+
let i = 0;
|
|
21683
|
+
for (const ch of haystack) {
|
|
21684
|
+
if (ch === needle[i]) i += 1;
|
|
21685
|
+
if (i === needle.length) return true;
|
|
21686
|
+
}
|
|
21687
|
+
return needle.length === 0;
|
|
21688
|
+
}
|
|
21689
|
+
/**
|
|
21690
|
+
* How well one word answers the query. 0 = not at all. An exact word beats a
|
|
21691
|
+
* prefix, a prefix beats a word STARTING inside the phrase, and a loose
|
|
21692
|
+
* subsequence comes last because it matches almost anything.
|
|
21693
|
+
*/
|
|
21694
|
+
function scoreWord(query, word, loose) {
|
|
21695
|
+
if (word === query) return 100;
|
|
21696
|
+
if (word.startsWith(query)) return 80;
|
|
21697
|
+
if (word.split(" ").some((w) => w.startsWith(query))) return 60;
|
|
21698
|
+
if (word.includes(query)) return 40;
|
|
21699
|
+
if (loose && isSubsequence(query, word)) return 15;
|
|
21700
|
+
return 0;
|
|
21701
|
+
}
|
|
21702
|
+
var WORDS_CACHE = /* @__PURE__ */ new Map();
|
|
21703
|
+
/** Normalised once per entry — search runs on every keystroke over ~1,700 entries. */
|
|
21704
|
+
function wordsOf(entry) {
|
|
21705
|
+
const cached = WORDS_CACHE.get(entry.key);
|
|
21706
|
+
if (cached !== void 0) return cached;
|
|
21707
|
+
const built = {
|
|
21708
|
+
key: normalise(entry.key),
|
|
21709
|
+
synonyms: entry.synonyms.map(normalise),
|
|
21710
|
+
tags: entry.tags.map(normalise)
|
|
21711
|
+
};
|
|
21712
|
+
WORDS_CACHE.set(entry.key, built);
|
|
21713
|
+
return built;
|
|
21714
|
+
}
|
|
21715
|
+
/**
|
|
21716
|
+
* Rank the catalog against a free-text query.
|
|
21717
|
+
*
|
|
21718
|
+
* An empty query returns the WHOLE catalog in key order, because a picker's
|
|
21719
|
+
* resting state is the full list. `extraWords` lets a caller add the words IT
|
|
21720
|
+
* can translate without this module knowing what a locale is; they score like a
|
|
21721
|
+
* synonym. The key outranks a synonym, a synonym outranks a lucide tag, and
|
|
21722
|
+
* ties keep key order so the list does not reshuffle between keystrokes.
|
|
21723
|
+
*/
|
|
21724
|
+
function searchIcons(query, extraWords) {
|
|
21725
|
+
const q = normalise(query);
|
|
21726
|
+
if (q.length === 0) return ENTRIES.map((entry) => ({
|
|
21727
|
+
entry,
|
|
21728
|
+
score: 0
|
|
21729
|
+
}));
|
|
21730
|
+
const hits = [];
|
|
21731
|
+
for (const entry of ENTRIES) {
|
|
21732
|
+
const words = wordsOf(entry);
|
|
21733
|
+
const keyScore = scoreWord(q, words.key, true);
|
|
21734
|
+
let best = keyScore > 0 ? keyScore + 2 : 0;
|
|
21735
|
+
for (const word of [...words.synonyms, ...(extraWords?.get(entry.key) ?? []).map(normalise)]) {
|
|
21736
|
+
const score = scoreWord(q, word, true);
|
|
21737
|
+
if (score > 0 && score + 1 > best) best = score + 1;
|
|
21738
|
+
}
|
|
21739
|
+
for (const word of words.tags) {
|
|
21740
|
+
const score = scoreWord(q, word, false);
|
|
21741
|
+
if (score > best) best = score;
|
|
21742
|
+
}
|
|
21743
|
+
if (best > 0) hits.push({
|
|
21744
|
+
entry,
|
|
21745
|
+
score: best
|
|
21746
|
+
});
|
|
21747
|
+
}
|
|
21748
|
+
return hits.toSorted((a, b) => b.score - a.score);
|
|
21749
|
+
}
|
|
21750
|
+
/** A readable label derived from a lucide category id (`food-beverage` → `Food beverage`). */
|
|
21751
|
+
function iconCategoryLabel(category) {
|
|
21752
|
+
const spaced = category.replace(/-/g, " ");
|
|
21753
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
21754
|
+
}
|
|
21755
|
+
//#endregion
|
|
21756
|
+
//#region src/device/location-icon.ts
|
|
21757
|
+
/**
|
|
21758
|
+
* What a location's icon is, and the ONE key its label joins on.
|
|
21759
|
+
*
|
|
21760
|
+
* A location is not an entity — it is a free-form label (`PersistedDeviceMeta.
|
|
21761
|
+
* location`) plus an operator-curated registry of labels in the device-manager
|
|
21762
|
+
* addon store. The icon is a second map beside that registry, keyed the same
|
|
21763
|
+
* way the registry matches everywhere else: {@link locationIconKey}, i.e.
|
|
21764
|
+
* trimmed + lower-cased.
|
|
21765
|
+
*
|
|
21766
|
+
* The icon itself is a key of the ONE icon catalog (`icon-catalog.ts`, D854):
|
|
21767
|
+
* a location may wear any icon both surfaces can draw. There used to be a
|
|
21768
|
+
* closed 20-icon subset here (D511); it was a second vocabulary on the same two
|
|
21769
|
+
* surfaces and is gone. The names below keep their meaning:
|
|
21770
|
+
*
|
|
21771
|
+
* DEGRADATION IS A FALLBACK, NEVER A HOLE. A surface that cannot draw a stored
|
|
21772
|
+
* value — a persisted value from a newer hub, a hand-edited settings row —
|
|
21773
|
+
* draws {@link LOCATION_ICON_FALLBACK} (`map-pin`, the one every surface has
|
|
21774
|
+
* always had). Read a stored/wire value through
|
|
21775
|
+
* `resolveIconKey(value, LOCATION_ICON_FALLBACK)` and that is what you get;
|
|
21776
|
+
* nothing anywhere renders an empty box.
|
|
21777
|
+
*/
|
|
21778
|
+
/**
|
|
21779
|
+
* The id every surface is guaranteed to draw, and what an unrecognised value
|
|
21780
|
+
* degrades to. It has been in both icon sets since before the catalog existed
|
|
21781
|
+
* (the Locations page's own page icon), so "the fallback is missing" is not a
|
|
21782
|
+
* state a build can reach.
|
|
21783
|
+
*/
|
|
21784
|
+
var LOCATION_ICON_FALLBACK = "map-pin";
|
|
21785
|
+
z.object({
|
|
21786
|
+
locationKey: z.string(),
|
|
21787
|
+
icon: IconKeySchema
|
|
21788
|
+
});
|
|
21789
|
+
/**
|
|
21790
|
+
* The join key for a location label: trimmed, lower-cased. The ONE place this
|
|
21791
|
+
* derivation lives — a caller that re-spells it is a rename away from an
|
|
21792
|
+
* orphaned icon.
|
|
21793
|
+
*/
|
|
21794
|
+
function locationIconKey(label) {
|
|
21795
|
+
return label.trim().toLowerCase();
|
|
21796
|
+
}
|
|
21797
|
+
z.object({
|
|
21798
|
+
locationKey: z.string(),
|
|
21799
|
+
description: z.string()
|
|
21800
|
+
});
|
|
21801
|
+
//#endregion
|
|
21802
|
+
//#region src/automation/action-target.ts
|
|
21803
|
+
/**
|
|
21804
|
+
* WHO an action step acts on (D983) — one target shape for every place that
|
|
21805
|
+
* performs an action (automation steps, Action entries, notification
|
|
21806
|
+
* sequences, scheduler points):
|
|
21807
|
+
*
|
|
21808
|
+
* - `devices` — device ids the operator picked, frozen at save time;
|
|
21809
|
+
* - `domain` — every device that offers the step's VOCABULARY action,
|
|
21810
|
+
* optionally narrowed to one device type and one room, resolved WHEN THE
|
|
21811
|
+
* STEP RUNS (a device added later is included). The action always comes
|
|
21812
|
+
* from the vocabulary (`cover.open`), never from a raw cap name, so a
|
|
21813
|
+
* `domain` target needs an `actionId` (enforced on the step schema);
|
|
21814
|
+
* - `var` — a `$var` resolved from the run's scope.
|
|
21815
|
+
*
|
|
21816
|
+
* {@link ActionTargetResolver} turns a target into device ids. It is pure
|
|
21817
|
+
* (a {@link DeviceDirectory} is handed in) so the hub's core-blocks service and
|
|
21818
|
+
* a generated automation block share it.
|
|
21819
|
+
*
|
|
21820
|
+
* A room is the device's `location` label (`DeviceInfo.location`); two labels
|
|
21821
|
+
* name the same room when `locationIconKey` agrees (trimmed, lower-cased).
|
|
21822
|
+
*/
|
|
21823
|
+
var ActionTargetSchema = z.discriminatedUnion("kind", [
|
|
21824
|
+
z.object({
|
|
21825
|
+
kind: z.literal("devices"),
|
|
21826
|
+
ids: z.array(z.number().int()).min(1)
|
|
21827
|
+
}),
|
|
21828
|
+
z.object({
|
|
21829
|
+
kind: z.literal("domain"),
|
|
21830
|
+
/** Narrow to one kind of device. */
|
|
21831
|
+
deviceType: z.enum(DeviceType).optional(),
|
|
21832
|
+
/** Narrow to one room: the `location` label devices carry. */
|
|
21833
|
+
roomId: z.string().min(1).optional()
|
|
21834
|
+
}),
|
|
21835
|
+
z.object({
|
|
21836
|
+
kind: z.literal("var"),
|
|
21837
|
+
name: z.string().min(1)
|
|
21838
|
+
})
|
|
21839
|
+
]);
|
|
21840
|
+
EventCategory.DeviceRegistered, EventCategory.DeviceUnregistered, EventCategory.DeviceUpdated, EventCategory.DeviceMetaChanged, EventCategory.DeviceBindingsChanged;
|
|
21841
|
+
//#endregion
|
|
21842
|
+
//#region src/catalogs/sensor-active-state.ts
|
|
21843
|
+
/**
|
|
21844
|
+
* THE table "which boolean of this device type counts as HIGH", and the
|
|
21845
|
+
* pure evaluator of its EDGE.
|
|
21846
|
+
*
|
|
21847
|
+
* ONE table for everyone who asks "is this device HIGH?": the
|
|
21848
|
+
* recorder's `RecordingTriggers.sensorDeviceIds` trigger, the post-analysis
|
|
21849
|
+
* sensor markers, the orchestrator's camera-sensors bridge and
|
|
21850
|
+
* the sources of a pulse (`compilePulseOverride`, D936).
|
|
21851
|
+
* A second copy would diverge at the first cap added, and the symptom — "the
|
|
21852
|
+
* sensor rings the doorbell but does not record" — is exactly D62.
|
|
21853
|
+
*
|
|
21854
|
+
* ⚠ NOT `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), and the
|
|
21855
|
+
* two must not be unified: that one answers "what is the state WORD for a
|
|
21856
|
+
* rule" (and includes `presence`, `cover`, `alarm-panel`), this one "which
|
|
21857
|
+
* boolean's EDGE counts". Deliberately different vocabularies.
|
|
21377
21858
|
*/
|
|
21378
|
-
|
|
21859
|
+
/**
|
|
21860
|
+
* Known binary / switch source caps → the boolean slice field whose
|
|
21861
|
+
* false→true rise counts as ACTIVE. Every entry is "fire on active".
|
|
21862
|
+
* Sensors whose "active" reading is not a plain boolean (presence's string
|
|
21863
|
+
* state, connectivity's connected flag) are deliberately excluded — a
|
|
21864
|
+
* reconnect is not a doorbell press, and it is not a recording either.
|
|
21865
|
+
*/
|
|
21866
|
+
var SOURCE_CAP_ACTIVE_FIELD = {
|
|
21867
|
+
contact: "entryOpen",
|
|
21868
|
+
binary: "on",
|
|
21869
|
+
switch: "on",
|
|
21870
|
+
motion: "detected",
|
|
21871
|
+
flood: "flooded",
|
|
21872
|
+
gas: "detected",
|
|
21873
|
+
smoke: "detected",
|
|
21874
|
+
"carbon-monoxide": "detected",
|
|
21875
|
+
vibration: "detected",
|
|
21876
|
+
tamper: "tampered"
|
|
21877
|
+
};
|
|
21379
21878
|
//#endregion
|
|
21380
|
-
//#region src/
|
|
21879
|
+
//#region src/catalogs/coco-classmap.ts
|
|
21880
|
+
var MACRO_LABELS = [
|
|
21881
|
+
{
|
|
21882
|
+
id: "person",
|
|
21883
|
+
name: "Person"
|
|
21884
|
+
},
|
|
21885
|
+
{
|
|
21886
|
+
id: "vehicle",
|
|
21887
|
+
name: "Vehicle"
|
|
21888
|
+
},
|
|
21889
|
+
{
|
|
21890
|
+
id: "animal",
|
|
21891
|
+
name: "Animal"
|
|
21892
|
+
},
|
|
21893
|
+
{
|
|
21894
|
+
id: "package",
|
|
21895
|
+
name: "Package"
|
|
21896
|
+
}
|
|
21897
|
+
];
|
|
21898
|
+
new Set(MACRO_LABELS.map((l) => l.id));
|
|
21899
|
+
var COCO_TO_MACRO = {
|
|
21900
|
+
mapping: {
|
|
21901
|
+
person: "person",
|
|
21902
|
+
bicycle: "vehicle",
|
|
21903
|
+
car: "vehicle",
|
|
21904
|
+
motorcycle: "vehicle",
|
|
21905
|
+
airplane: "vehicle",
|
|
21906
|
+
bus: "vehicle",
|
|
21907
|
+
train: "vehicle",
|
|
21908
|
+
truck: "vehicle",
|
|
21909
|
+
boat: "vehicle",
|
|
21910
|
+
bird: "animal",
|
|
21911
|
+
cat: "animal",
|
|
21912
|
+
dog: "animal",
|
|
21913
|
+
horse: "animal",
|
|
21914
|
+
sheep: "animal",
|
|
21915
|
+
cow: "animal",
|
|
21916
|
+
elephant: "animal",
|
|
21917
|
+
bear: "animal",
|
|
21918
|
+
zebra: "animal",
|
|
21919
|
+
giraffe: "animal",
|
|
21920
|
+
suitcase: "package",
|
|
21921
|
+
backpack: "package",
|
|
21922
|
+
handbag: "package"
|
|
21923
|
+
},
|
|
21924
|
+
preserveOriginal: false
|
|
21925
|
+
};
|
|
21926
|
+
z.object({
|
|
21927
|
+
field: z.string().min(1),
|
|
21928
|
+
on: z.literal("advance")
|
|
21929
|
+
});
|
|
21381
21930
|
/**
|
|
21382
|
-
*
|
|
21383
|
-
*
|
|
21384
|
-
* Every place that picks or stores an icon — a device's display override, a
|
|
21385
|
-
* location, a site zone, a viewer accessory, an `icon` settings field — names
|
|
21386
|
-
* it by a key of this catalog and nothing else. There is no curated subset and
|
|
21387
|
-
* no second list: the catalog is every lucide icon that both surfaces can draw
|
|
21388
|
-
* (the admin UI's `lucide-react` and the viewer's `lucide-react-native`), the
|
|
21389
|
-
* intersection computed by `scripts/generate-icon-catalog.ts` into the
|
|
21390
|
-
* committed `icon-catalog.generated.ts`.
|
|
21931
|
+
* Output schema shared by the contribution + live methods.
|
|
21391
21932
|
*
|
|
21392
|
-
*
|
|
21393
|
-
*
|
|
21394
|
-
*
|
|
21395
|
-
*
|
|
21933
|
+
* Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
|
|
21934
|
+
* tabs[]) without importing from `../interfaces/config-ui.js` — a
|
|
21935
|
+
* concrete-but-lenient Zod object keeps tRPC output inference happy
|
|
21936
|
+
* (using `z.unknown()` here collapses unrelated router branches to
|
|
21937
|
+
* `unknown` when the generator re-inlines the huge AppRouter type).
|
|
21396
21938
|
*
|
|
21397
|
-
*
|
|
21939
|
+
* `.passthrough()` on sections/fields accepts whatever FormBuilder
|
|
21940
|
+
* extensions the caller adds (showWhen, displayScale, …) without
|
|
21941
|
+
* rebuilding every time a new field kind is introduced.
|
|
21942
|
+
*/
|
|
21943
|
+
var ContributionSectionSchema = z.object({
|
|
21944
|
+
id: z.string(),
|
|
21945
|
+
title: z.string(),
|
|
21946
|
+
description: z.string().optional(),
|
|
21947
|
+
style: z.enum(["card", "accordion"]).optional(),
|
|
21948
|
+
defaultCollapsed: z.boolean().optional(),
|
|
21949
|
+
columns: z.union([
|
|
21950
|
+
z.literal(1),
|
|
21951
|
+
z.literal(2),
|
|
21952
|
+
z.literal(3),
|
|
21953
|
+
z.literal(4)
|
|
21954
|
+
]).optional(),
|
|
21955
|
+
tab: z.string().optional(),
|
|
21956
|
+
location: z.enum([
|
|
21957
|
+
"settings",
|
|
21958
|
+
"top-tab",
|
|
21959
|
+
"hero"
|
|
21960
|
+
]).optional(),
|
|
21961
|
+
order: z.number().optional(),
|
|
21962
|
+
fields: z.array(z.any())
|
|
21963
|
+
});
|
|
21964
|
+
var ContributionTabSchema = z.object({
|
|
21965
|
+
id: z.string(),
|
|
21966
|
+
label: z.string(),
|
|
21967
|
+
icon: z.string(),
|
|
21968
|
+
order: z.number().optional()
|
|
21969
|
+
});
|
|
21970
|
+
z.object({
|
|
21971
|
+
tabs: z.array(ContributionTabSchema).optional(),
|
|
21972
|
+
sections: z.array(ContributionSectionSchema)
|
|
21973
|
+
}).nullable();
|
|
21974
|
+
z.object({ deviceId: z.number() }), z.object({ deviceId: z.number() }), z.object({
|
|
21975
|
+
deviceId: z.number(),
|
|
21976
|
+
patch: z.record(z.string(), z.unknown())
|
|
21977
|
+
}), z.object({ success: z.literal(true) });
|
|
21978
|
+
z.object({ deviceId: z.number() }), z.unknown().nullable();
|
|
21979
|
+
function method(input, output, options) {
|
|
21980
|
+
return {
|
|
21981
|
+
input,
|
|
21982
|
+
output,
|
|
21983
|
+
kind: options?.kind ?? "query",
|
|
21984
|
+
...options?.auth !== void 0 ? { auth: options.auth } : {},
|
|
21985
|
+
...options?.access !== void 0 ? { access: options.access } : {},
|
|
21986
|
+
...options?.role !== void 0 ? { role: options.role } : {},
|
|
21987
|
+
...options?.caller !== void 0 ? { caller: options.caller } : {},
|
|
21988
|
+
timeoutMs: options?.timeoutMs
|
|
21989
|
+
};
|
|
21990
|
+
}
|
|
21991
|
+
/**
|
|
21992
|
+
* A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
|
|
21993
|
+
* on `InferProvider`. The `providerOptional: true` literal is what
|
|
21994
|
+
* `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
|
|
21995
|
+
* for when this is the honest answer and when it is a soft stub.
|
|
21996
|
+
*/
|
|
21997
|
+
function optionalMethod(input, output, options) {
|
|
21998
|
+
return {
|
|
21999
|
+
...method(input, output, options),
|
|
22000
|
+
providerOptional: true
|
|
22001
|
+
};
|
|
22002
|
+
}
|
|
22003
|
+
//#endregion
|
|
22004
|
+
//#region src/capabilities/notification-output.cap.ts
|
|
22005
|
+
/**
|
|
22006
|
+
* notification-output — canonical, capability-gated notification delivery.
|
|
21398
22007
|
*
|
|
21399
|
-
*
|
|
21400
|
-
*
|
|
21401
|
-
*
|
|
21402
|
-
*
|
|
22008
|
+
* Apprise-derived model (see
|
|
22009
|
+
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
22010
|
+
* callers emit ONE canonical `Notification`; each provider declares a
|
|
22011
|
+
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
22012
|
+
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
22013
|
+
* message to what the kind supports — callers never special-case a service.
|
|
21403
22014
|
*
|
|
21404
|
-
*
|
|
22015
|
+
* DESIGN DECISIONS (locked):
|
|
22016
|
+
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
22017
|
+
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
22018
|
+
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
22019
|
+
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
22020
|
+
* alternative would fork the UI per addon and cannot host the
|
|
22021
|
+
* discovery→adopt flow.
|
|
22022
|
+
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
22023
|
+
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
22024
|
+
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
22025
|
+
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
22026
|
+
* `addonId` the generated collection router extracts from the call input.
|
|
22027
|
+
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
22028
|
+
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
22029
|
+
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
22030
|
+
* base64 fallback needed.
|
|
21405
22031
|
*
|
|
21406
|
-
*
|
|
21407
|
-
*
|
|
22032
|
+
* TODO (deferred, closed-set change — separate decision): add
|
|
22033
|
+
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
22034
|
+
* admin "Integrations" page.
|
|
21408
22035
|
*/
|
|
21409
|
-
var KEY_SET$1 = new Set(ICON_KEYS);
|
|
21410
|
-
var ALIAS_TO_KEY = new Map([...Object.entries(ICON_ALIASES), ...Object.entries(LEGACY_ICON_ALIASES)]);
|
|
21411
|
-
/** Is this a canonical key of the catalog? (An alias is not — see {@link canonicalIconKey}.) */
|
|
21412
|
-
function isIconKey(value) {
|
|
21413
|
-
return typeof value === "string" && KEY_SET$1.has(value);
|
|
21414
|
-
}
|
|
21415
22036
|
/**
|
|
21416
|
-
*
|
|
21417
|
-
*
|
|
22037
|
+
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
22038
|
+
* adapter picks what it supports and the degrade engine filters the rest.
|
|
21418
22039
|
*/
|
|
21419
|
-
|
|
21420
|
-
|
|
21421
|
-
|
|
21422
|
-
|
|
21423
|
-
|
|
22040
|
+
var AttachmentMediaTypeSchema = z.enum([
|
|
22041
|
+
"image",
|
|
22042
|
+
"video",
|
|
22043
|
+
"gif",
|
|
22044
|
+
"audio",
|
|
22045
|
+
"icon"
|
|
22046
|
+
]);
|
|
21424
22047
|
/**
|
|
21425
|
-
* A
|
|
21426
|
-
*
|
|
21427
|
-
*
|
|
21428
|
-
*
|
|
22048
|
+
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
22049
|
+
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
22050
|
+
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
22051
|
+
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
21429
22052
|
*/
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
22053
|
+
var AttachmentSchema = z.object({
|
|
22054
|
+
mediaType: AttachmentMediaTypeSchema,
|
|
22055
|
+
url: z.string().optional(),
|
|
22056
|
+
bytes: z.instanceof(Uint8Array).optional(),
|
|
22057
|
+
mime: z.string().optional(),
|
|
22058
|
+
name: z.string().optional()
|
|
22059
|
+
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
22060
|
+
var NotificationFormatSchema = z.enum([
|
|
22061
|
+
"text",
|
|
22062
|
+
"markdown",
|
|
22063
|
+
"html"
|
|
22064
|
+
]);
|
|
21433
22065
|
/**
|
|
21434
|
-
*
|
|
22066
|
+
* The CLOSED icon vocabulary an action button may use.
|
|
21435
22067
|
*
|
|
21436
|
-
*
|
|
21437
|
-
*
|
|
21438
|
-
*
|
|
22068
|
+
* A closed set, not a free string, and that is the whole point: an arbitrary
|
|
22069
|
+
* icon name is one that ntfy renders, zentik silently drops, and nobody
|
|
22070
|
+
* notices — the same class of gap as a zone vocabulary nothing produced
|
|
22071
|
+
* ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
|
|
22072
|
+
* declares `actionIcons: false` and the degrade engine strips the field.
|
|
22073
|
+
*
|
|
22074
|
+
* Named by INTENT, never by glyph. "check" would tie the vocabulary to one
|
|
22075
|
+
* renderer's icon set; "acknowledge" survives an adapter that draws it
|
|
22076
|
+
* differently.
|
|
22077
|
+
*
|
|
22078
|
+
* ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
|
|
22079
|
+
*
|
|
22080
|
+
* These names are for US. **No adapter may forward one verbatim.** Each maps
|
|
22081
|
+
* the whole set onto its own renderer's vocabulary through a
|
|
22082
|
+
* `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
|
|
22083
|
+
* fallback, so adding a member here fails every adapter's build until someone
|
|
22084
|
+
* decides its glyph, which is the only place that decision can be made
|
|
22085
|
+
* honestly.
|
|
22086
|
+
*
|
|
22087
|
+
* This paragraph is the bug. Zentik declared `actionIcons: true` and passed
|
|
22088
|
+
* `disarm` straight through; iOS feeds that string to
|
|
22089
|
+
* `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
|
|
22090
|
+
* and every snooze and alarm button arrived BLANK. A pass-through is not a
|
|
22091
|
+
* mapping, and "the field is documented" is not "the value renders".
|
|
22092
|
+
*
|
|
22093
|
+
* Adding a member is TRAIN-BOUND. The enum lives in the published
|
|
22094
|
+
* `@camstack/server` closure and the cap seam validates against the HUB's copy,
|
|
22095
|
+
* so an addon that emits a token the running hub does not know does not lose an
|
|
22096
|
+
* icon — its whole `send` fails Zod validation and the notification never
|
|
22097
|
+
* arrives. Never emit a new token from an addon before the train carrying it.
|
|
21439
22098
|
*/
|
|
21440
|
-
var
|
|
21441
|
-
|
|
21442
|
-
|
|
21443
|
-
|
|
21444
|
-
|
|
21445
|
-
|
|
21446
|
-
|
|
21447
|
-
|
|
21448
|
-
|
|
21449
|
-
|
|
21450
|
-
|
|
21451
|
-
|
|
21452
|
-
|
|
21453
|
-
|
|
22099
|
+
var NotificationActionIconSchema = z.enum([
|
|
22100
|
+
"acknowledge",
|
|
22101
|
+
"dismiss",
|
|
22102
|
+
"silence",
|
|
22103
|
+
"snooze",
|
|
22104
|
+
"view",
|
|
22105
|
+
"play",
|
|
22106
|
+
"open",
|
|
22107
|
+
"close",
|
|
22108
|
+
"lock",
|
|
22109
|
+
"unlock",
|
|
22110
|
+
"arm",
|
|
22111
|
+
"arm-home",
|
|
22112
|
+
"arm-away",
|
|
22113
|
+
"arm-night",
|
|
22114
|
+
"disarm",
|
|
22115
|
+
"light",
|
|
22116
|
+
"alert",
|
|
22117
|
+
"camera"
|
|
22118
|
+
]);
|
|
22119
|
+
/** A single tap-through action button. */
|
|
22120
|
+
var NotificationActionSchema = z.object({
|
|
22121
|
+
id: z.string(),
|
|
22122
|
+
label: z.string(),
|
|
22123
|
+
url: z.string().optional(),
|
|
22124
|
+
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
22125
|
+
icon: NotificationActionIconSchema.optional(),
|
|
22126
|
+
/**
|
|
22127
|
+
* Renders in a warning style where the notifier supports it.
|
|
22128
|
+
*
|
|
22129
|
+
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
22130
|
+
* else — see `notification-center/action-token.ts` for what that does and
|
|
22131
|
+
* does not buy.
|
|
22132
|
+
*/
|
|
22133
|
+
destructive: z.boolean().optional(),
|
|
22134
|
+
/**
|
|
22135
|
+
* How the tap should REACH the url.
|
|
22136
|
+
*
|
|
22137
|
+
* `navigate` (absent, and every button authored before this field) opens it:
|
|
22138
|
+
* the phone leaves the notification and shows whatever the callback returns.
|
|
22139
|
+
* That is right for a button whose answer the operator wants to read.
|
|
22140
|
+
*
|
|
22141
|
+
* `background` fires it as a POST and stays put. It exists for the buttons
|
|
22142
|
+
* whose whole point is not to interrupt — "silence this for 30 minutes" is
|
|
22143
|
+
* an answer to the notification, and being thrown into a browser tab to
|
|
22144
|
+
* confirm it costs more attention than the notification did. A backend that
|
|
22145
|
+
* cannot do a background call renders it as an ordinary link (the adapters
|
|
22146
|
+
* fall back rather than dropping the button), so this is a preference, never
|
|
22147
|
+
* a requirement.
|
|
22148
|
+
*/
|
|
22149
|
+
mode: z.enum(["navigate", "background"]).optional()
|
|
21454
22150
|
});
|
|
21455
|
-
|
|
21456
|
-
|
|
21457
|
-
|
|
21458
|
-
|
|
21459
|
-
|
|
21460
|
-
|
|
21461
|
-
var ICON_ENTRIES = ENTRIES;
|
|
21462
|
-
/** Categories a picker offers first: the ones this product's icons mostly live in. */
|
|
21463
|
-
var LEADING_CATEGORIES = [
|
|
21464
|
-
"devices",
|
|
21465
|
-
"home",
|
|
21466
|
-
"security",
|
|
21467
|
-
"buildings",
|
|
21468
|
-
"connectivity",
|
|
21469
|
-
"multimedia",
|
|
21470
|
-
"nature",
|
|
21471
|
-
"weather",
|
|
21472
|
-
"transportation",
|
|
21473
|
-
"animals"
|
|
21474
|
-
];
|
|
21475
|
-
var PRIMARY_CATEGORIES = new Set(ENTRIES.map((e) => e.primaryCategory));
|
|
21476
|
-
/**
|
|
21477
|
-
* The categories a picker files icons under, in picker order: the leading ones,
|
|
21478
|
-
* then the rest alphabetically. Only categories that are some icon's PRIMARY
|
|
21479
|
-
* one — a category lucide lists only as a secondary has no section of its own.
|
|
21480
|
-
*/
|
|
21481
|
-
var ICON_CATEGORIES = [...LEADING_CATEGORIES, ...ICON_CATEGORY_IDS.filter((c) => !LEADING_CATEGORIES.includes(c))].filter((c) => PRIMARY_CATEGORIES.has(c));
|
|
21482
|
-
/** The icons filed under a category as their PRIMARY one, in key order. */
|
|
21483
|
-
function iconsInCategory(category) {
|
|
21484
|
-
return ENTRIES.filter((e) => e.primaryCategory === category);
|
|
21485
|
-
}
|
|
21486
|
-
function normalise(text) {
|
|
21487
|
-
return text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().replace(/[_-]+/g, " ").trim();
|
|
21488
|
-
}
|
|
21489
|
-
/** Does `needle` appear in `haystack` in order, though not adjacently? */
|
|
21490
|
-
function isSubsequence(needle, haystack) {
|
|
21491
|
-
let i = 0;
|
|
21492
|
-
for (const ch of haystack) {
|
|
21493
|
-
if (ch === needle[i]) i += 1;
|
|
21494
|
-
if (i === needle.length) return true;
|
|
21495
|
-
}
|
|
21496
|
-
return needle.length === 0;
|
|
21497
|
-
}
|
|
22151
|
+
/** A delivery parameter value: what a notifier declared it takes. */
|
|
22152
|
+
var DeliveryParamValueSchema = z.union([
|
|
22153
|
+
z.string(),
|
|
22154
|
+
z.number(),
|
|
22155
|
+
z.boolean()
|
|
22156
|
+
]);
|
|
21498
22157
|
/**
|
|
21499
|
-
*
|
|
21500
|
-
*
|
|
21501
|
-
*
|
|
22158
|
+
* One extra delivery parameter a notifier KIND declares (D938). A rule that
|
|
22159
|
+
* addresses a target of that kind may set it; a kind that declares nothing is
|
|
22160
|
+
* never handed one. The admin UI derives its control from this descriptor —
|
|
22161
|
+
* no editor knows a service by name.
|
|
22162
|
+
*
|
|
22163
|
+
* Optional fields are `.optional()`, never `.default()` (a Zod default does not
|
|
22164
|
+
* run on the addon cap path).
|
|
21502
22165
|
*/
|
|
21503
|
-
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
|
|
21507
|
-
|
|
21508
|
-
|
|
21509
|
-
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
22166
|
+
var DeliveryParamSchema = z.object({
|
|
22167
|
+
key: z.string().min(1),
|
|
22168
|
+
label: z.string(),
|
|
22169
|
+
/** i18n key of the label, resolved by the declaring addon's namespace. */
|
|
22170
|
+
labelKey: z.string().optional(),
|
|
22171
|
+
type: z.enum([
|
|
22172
|
+
"enum",
|
|
22173
|
+
"number",
|
|
22174
|
+
"boolean",
|
|
22175
|
+
"string"
|
|
22176
|
+
]),
|
|
22177
|
+
/** `enum` only: the closed set of accepted values. */
|
|
22178
|
+
options: z.array(z.object({
|
|
22179
|
+
value: z.string(),
|
|
22180
|
+
label: z.string()
|
|
22181
|
+
})).optional(),
|
|
22182
|
+
/** `number` only. */
|
|
22183
|
+
min: z.number().optional(),
|
|
22184
|
+
max: z.number().optional(),
|
|
22185
|
+
/** What the notifier does when the rule sets nothing; shown as the placeholder. */
|
|
22186
|
+
default: DeliveryParamValueSchema.optional()
|
|
22187
|
+
});
|
|
21524
22188
|
/**
|
|
21525
|
-
*
|
|
21526
|
-
*
|
|
21527
|
-
*
|
|
21528
|
-
*
|
|
21529
|
-
*
|
|
21530
|
-
*
|
|
21531
|
-
* ties keep key order so the list does not reshuffle between keystrokes.
|
|
22189
|
+
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
22190
|
+
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
22191
|
+
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
22192
|
+
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
22193
|
+
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
22194
|
+
* `priority` for that one target.
|
|
21532
22195
|
*/
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
|
|
21538
|
-
|
|
21539
|
-
|
|
21540
|
-
|
|
21541
|
-
|
|
21542
|
-
|
|
21543
|
-
|
|
21544
|
-
|
|
21545
|
-
|
|
21546
|
-
|
|
21547
|
-
|
|
21548
|
-
|
|
21549
|
-
|
|
21550
|
-
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
21557
|
-
|
|
21558
|
-
|
|
21559
|
-
/**
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
22196
|
+
var NotificationSchema = z.object({
|
|
22197
|
+
body: z.string(),
|
|
22198
|
+
title: z.string().optional(),
|
|
22199
|
+
format: NotificationFormatSchema.default("text"),
|
|
22200
|
+
priority: z.number().int().min(1).max(5).default(3),
|
|
22201
|
+
level: z.string().optional(),
|
|
22202
|
+
attachments: z.array(AttachmentSchema).optional(),
|
|
22203
|
+
clickUrl: z.string().optional(),
|
|
22204
|
+
actions: z.array(NotificationActionSchema).optional(),
|
|
22205
|
+
sound: z.string().optional(),
|
|
22206
|
+
ttl: z.number().optional(),
|
|
22207
|
+
tag: z.string().optional(),
|
|
22208
|
+
deviceId: z.number().optional(),
|
|
22209
|
+
eventId: z.string().optional(),
|
|
22210
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
22211
|
+
/**
|
|
22212
|
+
* Values for the parameters the target's kind DECLARES in
|
|
22213
|
+
* {@link TargetKindSchema} `deliveryParams`. Already validated against the
|
|
22214
|
+
* declaration by the sender; a notifier reads only the keys it declared.
|
|
22215
|
+
*/
|
|
22216
|
+
delivery: z.record(z.string(), DeliveryParamValueSchema).optional()
|
|
22217
|
+
});
|
|
22218
|
+
/** One declared native severity/priority level for a kind. */
|
|
22219
|
+
var TargetKindLevelSchema = z.object({
|
|
22220
|
+
id: z.string(),
|
|
22221
|
+
label: z.string(),
|
|
22222
|
+
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
22223
|
+
ordinal: z.number().int().min(1).max(5).nullable(),
|
|
22224
|
+
flags: z.object({
|
|
22225
|
+
critical: z.boolean().optional(),
|
|
22226
|
+
silent: z.boolean().optional(),
|
|
22227
|
+
noPush: z.boolean().optional()
|
|
22228
|
+
}).optional(),
|
|
22229
|
+
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
22230
|
+
requires: z.array(z.string()).optional(),
|
|
22231
|
+
description: z.string().optional()
|
|
22232
|
+
});
|
|
22233
|
+
/** Attachment capabilities for a kind (drives the degrade engine + test panel). */
|
|
22234
|
+
var TargetKindAttachmentsCapsSchema = z.object({
|
|
22235
|
+
mediaTypes: z.array(AttachmentMediaTypeSchema),
|
|
22236
|
+
mode: z.enum([
|
|
22237
|
+
"url",
|
|
22238
|
+
"bytes",
|
|
22239
|
+
"both"
|
|
22240
|
+
]),
|
|
22241
|
+
max: z.number().int().nonnegative(),
|
|
22242
|
+
maxBytes: z.number().int().positive().optional()
|
|
22243
|
+
});
|
|
22244
|
+
/** The full capability block consulted before dispatch. */
|
|
22245
|
+
var TargetKindCapsSchema = z.object({
|
|
22246
|
+
attachments: TargetKindAttachmentsCapsSchema,
|
|
22247
|
+
/** Max action buttons (0 = none). */
|
|
22248
|
+
actions: z.number().int().nonnegative(),
|
|
22249
|
+
/**
|
|
22250
|
+
* Whether this kind renders a per-action ICON.
|
|
22251
|
+
*
|
|
22252
|
+
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
22253
|
+
* run on the addon cap path — three production failures in one day taught
|
|
22254
|
+
* this repo that once. Absent is read as false by the degrade engine, which
|
|
22255
|
+
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
22256
|
+
* assumed and dropped costs the operator's trust in the field.
|
|
22257
|
+
*/
|
|
22258
|
+
actionIcons: z.boolean().optional(),
|
|
22259
|
+
levels: z.array(TargetKindLevelSchema),
|
|
22260
|
+
format: z.array(NotificationFormatSchema),
|
|
22261
|
+
clickUrl: z.boolean(),
|
|
22262
|
+
sound: z.boolean(),
|
|
22263
|
+
ttl: z.boolean(),
|
|
22264
|
+
bodyMaxLen: z.number().int().positive()
|
|
22265
|
+
});
|
|
21566
22266
|
/**
|
|
21567
|
-
*
|
|
21568
|
-
*
|
|
21569
|
-
*
|
|
21570
|
-
*
|
|
21571
|
-
*
|
|
21572
|
-
* way the registry matches everywhere else: {@link locationIconKey}, i.e.
|
|
21573
|
-
* trimmed + lower-cased.
|
|
21574
|
-
*
|
|
21575
|
-
* The icon itself is a key of the ONE icon catalog (`icon-catalog.ts`, D854):
|
|
21576
|
-
* a location may wear any icon both surfaces can draw. There used to be a
|
|
21577
|
-
* closed 20-icon subset here (D511); it was a second vocabulary on the same two
|
|
21578
|
-
* surfaces and is gone. The names below keep their meaning:
|
|
21579
|
-
*
|
|
21580
|
-
* DEGRADATION IS A FALLBACK, NEVER A HOLE. A surface that cannot draw a stored
|
|
21581
|
-
* value — a persisted value from a newer hub, a hand-edited settings row —
|
|
21582
|
-
* draws {@link LOCATION_ICON_FALLBACK} (`map-pin`, the one every surface has
|
|
21583
|
-
* always had). Read a stored/wire value through
|
|
21584
|
-
* `resolveIconKey(value, LOCATION_ICON_FALLBACK)` and that is what you get;
|
|
21585
|
-
* nothing anywhere renders an empty box.
|
|
22267
|
+
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
22268
|
+
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
22269
|
+
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
22270
|
+
* the union is large and not meant for runtime validation here; the exported
|
|
22271
|
+
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
21586
22272
|
*/
|
|
22273
|
+
var ConfigSchemaPassthrough = z.unknown();
|
|
22274
|
+
var TargetKindSchema = z.object({
|
|
22275
|
+
kind: z.string(),
|
|
22276
|
+
label: z.string(),
|
|
22277
|
+
icon: z.string(),
|
|
22278
|
+
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
22279
|
+
addonId: z.string(),
|
|
22280
|
+
/**
|
|
22281
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
22282
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
22283
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
22284
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
22285
|
+
*
|
|
22286
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
22287
|
+
* client, and a native client joins it onto its own hub base.
|
|
22288
|
+
*
|
|
22289
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
22290
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
22291
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
22292
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
22293
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
22294
|
+
*/
|
|
22295
|
+
iconUrl: z.string().optional(),
|
|
22296
|
+
/**
|
|
22297
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
22298
|
+
*
|
|
22299
|
+
* The server knows this and therefore says it, because the client cannot
|
|
22300
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
22301
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
22302
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
22303
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
22304
|
+
*
|
|
22305
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
22306
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
22307
|
+
* its raster path, which is the safe default for an unknown image.
|
|
22308
|
+
*/
|
|
22309
|
+
iconMediaType: z.string().optional(),
|
|
22310
|
+
configSchema: ConfigSchemaPassthrough,
|
|
22311
|
+
/**
|
|
22312
|
+
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
22313
|
+
* base URL, instance, chat, recipient). A save that changes any of them
|
|
22314
|
+
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
22315
|
+
* editor uses this to ask for the secret on exactly those edits instead of
|
|
22316
|
+
* on every edit.
|
|
22317
|
+
*
|
|
22318
|
+
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
22319
|
+
* kinds) and from a provider older than this field — an editor that finds
|
|
22320
|
+
* it absent must assume ANY edit may need the secret.
|
|
22321
|
+
*/
|
|
22322
|
+
destinationKeys: z.array(z.string()).optional(),
|
|
22323
|
+
supportsDiscovery: z.boolean(),
|
|
22324
|
+
/**
|
|
22325
|
+
* Extra delivery parameters a rule may set for a target of this kind (D938).
|
|
22326
|
+
* Absent = none; the rule editor then offers nothing.
|
|
22327
|
+
*/
|
|
22328
|
+
deliveryParams: z.array(DeliveryParamSchema).optional(),
|
|
22329
|
+
caps: TargetKindCapsSchema
|
|
22330
|
+
});
|
|
21587
22331
|
/**
|
|
21588
|
-
*
|
|
21589
|
-
*
|
|
21590
|
-
*
|
|
21591
|
-
* state a build can reach.
|
|
22332
|
+
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
22333
|
+
* (return a presence marker only) when serving `listTargets` — never
|
|
22334
|
+
* round-trip a stored secret to the UI.
|
|
21592
22335
|
*/
|
|
21593
|
-
var
|
|
21594
|
-
z.
|
|
21595
|
-
|
|
21596
|
-
|
|
22336
|
+
var TargetSchema = z.object({
|
|
22337
|
+
id: z.string(),
|
|
22338
|
+
name: z.string(),
|
|
22339
|
+
kind: z.string(),
|
|
22340
|
+
addonId: z.string(),
|
|
22341
|
+
enabled: z.boolean(),
|
|
22342
|
+
config: z.record(z.string(), z.unknown())
|
|
22343
|
+
});
|
|
22344
|
+
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
22345
|
+
var DiscoveredTargetSchema = z.object({
|
|
22346
|
+
kind: z.string(),
|
|
22347
|
+
suggestedName: z.string(),
|
|
22348
|
+
config: z.record(z.string(), z.unknown())
|
|
22349
|
+
});
|
|
22350
|
+
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
22351
|
+
var RenderedAsSchema = z.object({
|
|
22352
|
+
level: z.string(),
|
|
22353
|
+
format: NotificationFormatSchema,
|
|
22354
|
+
attachmentsSent: z.number().int().nonnegative(),
|
|
22355
|
+
actionsSent: z.number().int().nonnegative(),
|
|
22356
|
+
truncated: z.boolean(),
|
|
22357
|
+
dropped: z.array(z.string())
|
|
22358
|
+
});
|
|
22359
|
+
var SendResultSchema = z.object({
|
|
22360
|
+
success: z.boolean(),
|
|
22361
|
+
error: z.string().optional(),
|
|
22362
|
+
renderedAs: RenderedAsSchema.optional()
|
|
21597
22363
|
});
|
|
22364
|
+
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
22365
|
+
var TestResultSchema = SendResultSchema;
|
|
21598
22366
|
/**
|
|
21599
|
-
*
|
|
21600
|
-
*
|
|
21601
|
-
* orphaned icon.
|
|
22367
|
+
* Pre-save credential probe against a partial (unsaved) target config.
|
|
22368
|
+
* `resolvedUrl` never includes secrets (token stripped from path).
|
|
21602
22369
|
*/
|
|
21603
|
-
|
|
21604
|
-
|
|
21605
|
-
|
|
21606
|
-
z.
|
|
21607
|
-
|
|
21608
|
-
|
|
22370
|
+
var ProbeTargetConfigResultSchema = z.object({
|
|
22371
|
+
ok: z.boolean(),
|
|
22372
|
+
resolvedUrl: z.string(),
|
|
22373
|
+
httpStatus: z.number().int().optional(),
|
|
22374
|
+
stateInstance: z.string().optional(),
|
|
22375
|
+
error: z.string().optional()
|
|
22376
|
+
});
|
|
22377
|
+
method(z.object({}), z.array(TargetKindSchema)), method(z.object({}), z.array(TargetSchema), { auth: "admin" }), method(z.object({ targetId: z.string() }), TargetSchema, { auth: "admin" }), method(z.object({
|
|
22378
|
+
kind: z.string(),
|
|
22379
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
22380
|
+
/**
|
|
22381
|
+
* When editing / duplicating, the blank password field never round-trips
|
|
22382
|
+
* the stored token. Pass the source target id so the provider can restore
|
|
22383
|
+
* secrets server-side before dialling (same merge as upsert).
|
|
22384
|
+
*/
|
|
22385
|
+
targetId: z.string().optional()
|
|
22386
|
+
}), z.array(DiscoveredTargetSchema), { auth: "admin" }), method(z.object({
|
|
22387
|
+
kind: z.string(),
|
|
22388
|
+
config: z.record(z.string(), z.unknown()),
|
|
22389
|
+
/** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
|
|
22390
|
+
targetId: z.string().optional()
|
|
22391
|
+
}), ProbeTargetConfigResultSchema, {
|
|
22392
|
+
kind: "mutation",
|
|
22393
|
+
auth: "admin"
|
|
22394
|
+
}), method(z.object({
|
|
22395
|
+
targetId: z.string(),
|
|
22396
|
+
notification: NotificationSchema
|
|
22397
|
+
}), SendResultSchema, {
|
|
22398
|
+
kind: "mutation",
|
|
22399
|
+
auth: "admin"
|
|
22400
|
+
}), method(z.object({
|
|
22401
|
+
targetId: z.string(),
|
|
22402
|
+
sample: NotificationSchema.optional()
|
|
22403
|
+
}), TestResultSchema, {
|
|
22404
|
+
kind: "mutation",
|
|
22405
|
+
auth: "admin"
|
|
22406
|
+
}), method(z.object({
|
|
22407
|
+
target: TargetSchema,
|
|
22408
|
+
/**
|
|
22409
|
+
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
22410
|
+
* existing row. The UI never holds the stored token; blank / redacted
|
|
22411
|
+
* password fields are restored from this source (D634 still applies if
|
|
22412
|
+
* destinations diverge without a re-typed secret).
|
|
22413
|
+
*/
|
|
22414
|
+
cloneSecretsFrom: z.string().optional()
|
|
22415
|
+
}), TargetSchema, {
|
|
22416
|
+
kind: "mutation",
|
|
22417
|
+
auth: "admin"
|
|
22418
|
+
}), method(z.object({ targetId: z.string() }), z.void(), {
|
|
22419
|
+
kind: "mutation",
|
|
22420
|
+
auth: "admin"
|
|
22421
|
+
}), method(z.object({
|
|
22422
|
+
targetId: z.string(),
|
|
22423
|
+
enabled: z.boolean()
|
|
22424
|
+
}), z.void(), {
|
|
22425
|
+
kind: "mutation",
|
|
22426
|
+
auth: "admin"
|
|
21609
22427
|
});
|
|
21610
22428
|
//#endregion
|
|
21611
22429
|
//#region src/catalogs/audio-classmap.ts
|
|
@@ -23122,13 +23940,28 @@ var NcRuleActionSchema = z.discriminatedUnion("kind", [z.object({
|
|
|
23122
23940
|
seconds: z.number().min(0).max(300)
|
|
23123
23941
|
}), z.object({
|
|
23124
23942
|
kind: z.literal("cap"),
|
|
23125
|
-
|
|
23943
|
+
/**
|
|
23944
|
+
* WHO the step acts on (D983) — the only shape that is written. A sequence
|
|
23945
|
+
* stored before D983 carries `deviceId`; the sequence store reads it as a
|
|
23946
|
+
* `devices` target.
|
|
23947
|
+
*/
|
|
23948
|
+
target: ActionTargetSchema.optional(),
|
|
23949
|
+
/** The vocabulary action the step performs; required for a `domain` target. */
|
|
23950
|
+
actionId: z.string().min(1).optional(),
|
|
23951
|
+
/** @deprecated The pre-D983 single device. Read, never written. */
|
|
23952
|
+
deviceId: z.number().int().optional(),
|
|
23126
23953
|
/** Capability name, e.g. `alarm-panel`. */
|
|
23127
23954
|
cap: z.string().min(1),
|
|
23128
23955
|
/** Method on it. The executor refuses a non-device-scoped cap. */
|
|
23129
23956
|
method: z.string().min(1),
|
|
23130
23957
|
/** Method arguments, minus `deviceId` (the executor injects it). */
|
|
23131
23958
|
args: z.record(z.string(), z.unknown()).optional()
|
|
23959
|
+
}).refine((step) => step.target !== void 0 || step.deviceId !== void 0, {
|
|
23960
|
+
message: "a cap step needs a target",
|
|
23961
|
+
path: ["target"]
|
|
23962
|
+
}).refine((step) => step.target?.kind !== "domain" || step.actionId !== void 0, {
|
|
23963
|
+
message: "a domain target needs the vocabulary action it applies to (actionId)",
|
|
23964
|
+
path: ["actionId"]
|
|
23132
23965
|
})]);
|
|
23133
23966
|
/**
|
|
23134
23967
|
* A named, ordered run of steps shared across rules, with its own throttle.
|