@camstack/system 1.2.179 → 1.2.181

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/addon-runner.js +2 -2
  2. package/dist/addon-runner.mjs +2 -2
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +56 -10
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +657 -62
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +658 -63
  12. package/dist/builtins/backup-orchestrator/run-queue.d.ts +103 -0
  13. package/dist/builtins/backup-orchestrator/staging-paths.d.ts +36 -0
  14. package/dist/builtins/console-logging/index.js +1 -1
  15. package/dist/builtins/console-logging/index.mjs +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  17. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  18. package/dist/builtins/device-manager/device-manager.addon.js +349 -65
  19. package/dist/builtins/device-manager/device-manager.addon.mjs +349 -65
  20. package/dist/builtins/device-manager/device-meta-actions.d.ts +8 -19
  21. package/dist/builtins/device-manager/device-provider-context.d.ts +4 -0
  22. package/dist/builtins/device-manager/migrate-device.d.ts +47 -3
  23. package/dist/builtins/device-manager/migrate-guard.d.ts +39 -0
  24. package/dist/builtins/device-manager/migrate-hardware-state.d.ts +24 -0
  25. package/dist/builtins/device-manager/migrate-reload.d.ts +8 -0
  26. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  27. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  28. package/dist/builtins/hub-forwarder/index.js +1 -1
  29. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  30. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  31. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  32. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  33. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  34. package/dist/builtins/local-network/local-network.addon.js +1 -1
  35. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  36. package/dist/builtins/loki-logging/index.js +1 -1
  37. package/dist/builtins/loki-logging/index.mjs +1 -1
  38. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  39. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  40. package/dist/builtins/platform-probe/index.js +1 -1
  41. package/dist/builtins/platform-probe/index.mjs +1 -1
  42. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  43. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  44. package/dist/builtins/snapshot/index.js +1 -1
  45. package/dist/builtins/snapshot/index.mjs +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  47. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  49. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  51. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/system-backup/system-backup.service.d.ts +55 -2
  53. package/dist/builtins/system-config/system-config.addon.js +1 -1
  54. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  55. package/dist/builtins/winston-logging/index.js +1 -1
  56. package/dist/builtins/winston-logging/index.mjs +1 -1
  57. package/dist/{dist-L_39vRmH.mjs → dist-DbnNk4Lw.mjs} +211 -2
  58. package/dist/{dist-C7qWwrK1.js → dist-whe0b4_3.js} +211 -2
  59. package/dist/index.js +530 -2
  60. package/dist/index.mjs +533 -5
  61. package/dist/kernel/addon-installer.d.ts +69 -0
  62. package/dist/kernel/deps/closure-dedupe.d.ts +105 -0
  63. package/dist/{manifest-system-deps-CMOGc5nC.mjs → manifest-system-deps-DOcMuUWK.mjs} +1 -1
  64. package/dist/{manifest-system-deps-PAIfmNun.js → manifest-system-deps-y5vZBO6q.js} +1 -1
  65. package/dist/{retired-settings-keys-3soU9pfP.js → retired-settings-keys-BdvmiaK7.js} +1 -1
  66. package/dist/{retired-settings-keys-CnbKAxmX.mjs → retired-settings-keys-CyQG8b-A.mjs} +1 -1
  67. package/package.json +1 -1
@@ -7164,6 +7164,89 @@ var LocationStatSchema = z.object({
7164
7164
  fileCount: z.number(),
7165
7165
  present: z.boolean()
7166
7166
  });
7167
+ /** Lifecycle of a backup run. Terminal states: succeeded / failed / cancelled. */
7168
+ var BackupRunStateSchema = z.enum([
7169
+ "queued",
7170
+ "running",
7171
+ "succeeded",
7172
+ "failed",
7173
+ "cancelled"
7174
+ ]);
7175
+ /**
7176
+ * Where a running backup currently is. `queued` before it starts,
7177
+ * `building` while the tar.gz is being staged, `uploading` during the
7178
+ * per-destination fan-out, `done` once terminal.
7179
+ */
7180
+ var BackupRunPhaseSchema = z.enum([
7181
+ "queued",
7182
+ "building",
7183
+ "uploading",
7184
+ "done"
7185
+ ]);
7186
+ /**
7187
+ * Observable state of one backup run — readable WHILE it runs via
7188
+ * `backup.listRuns`. This is what makes the execution queue and
7189
+ * `backup.cancel` usable: the 2026-09-04 incident (two concurrent
7190
+ * multi-GB builds, staging 5.1 GB → 18 GB, load 62) was only
7191
+ * diagnosable with `du` because nothing reported that runs existed or
7192
+ * how large the staged archive had grown.
7193
+ */
7194
+ var BackupRunSchema = z.object({
7195
+ /** Stable run id — the handle `backup.cancel` takes. */
7196
+ id: z.string(),
7197
+ state: BackupRunStateSchema,
7198
+ phase: BackupRunPhaseSchema,
7199
+ /**
7200
+ * Resolved destination location ids. Empty while queued (targets are
7201
+ * resolved when the run starts, against the then-current policies).
7202
+ */
7203
+ destinationIds: z.array(z.string()).readonly(),
7204
+ label: z.string().optional(),
7205
+ /** ms-epoch when the run was submitted (trigger call / schedule fire). */
7206
+ requestedAt: z.number(),
7207
+ /** ms-epoch when the run left the queue and started building. */
7208
+ startedAt: z.number().optional(),
7209
+ /** ms-epoch when the run reached a terminal state. */
7210
+ finishedAt: z.number().optional(),
7211
+ /** Compressed bytes of the staging archive written so far. */
7212
+ stagedBytes: z.number(),
7213
+ /** Final staged archive size, once the build phase completes. */
7214
+ archiveSizeBytes: z.number().optional(),
7215
+ /** Bytes pushed to the destination currently uploading. */
7216
+ uploadedBytes: z.number(),
7217
+ /** Destinations where the archive fully landed (uploaded + indexed). */
7218
+ completedDestinationIds: z.array(z.string()).readonly(),
7219
+ /** Destinations that failed during the fan-out. */
7220
+ failedDestinationIds: z.array(z.string()).readonly(),
7221
+ /** Failure message when `state === 'failed'`. */
7222
+ error: z.string().optional(),
7223
+ /**
7224
+ * 1-based place in the execution queue — 1 = runs next. Present only
7225
+ * while `state === 'queued'`. Stamped by the orchestrator from the
7226
+ * queue's OWN pending order, never derived from timestamps, so the
7227
+ * UI cannot show an order the executor will not honour.
7228
+ */
7229
+ queuePosition: z.number().int().min(1).optional()
7230
+ });
7231
+ /**
7232
+ * Result of `backup.trigger`. The call still resolves when the run
7233
+ * terminates (compat with schedule-driven runs and the admin UI), but
7234
+ * it now names the run and says whether it had to WAIT: a trigger that
7235
+ * arrives while another run is in flight is enqueued (or joined onto
7236
+ * an identical already-queued run), never started concurrently.
7237
+ */
7238
+ var BackupTriggerResultSchema = z.object({
7239
+ /** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
7240
+ runId: z.string(),
7241
+ /** True when the run waited behind an in-flight run instead of starting immediately. */
7242
+ queued: z.boolean(),
7243
+ /** True when this trigger was coalesced onto an identical already-queued run. */
7244
+ joined: z.boolean(),
7245
+ /** True when the run was cancelled before completing every destination. */
7246
+ cancelled: z.boolean(),
7247
+ /** One entry per destination the archive landed at (partial on cancel). */
7248
+ entries: z.array(BackupEntrySchema).readonly()
7249
+ });
7167
7250
  /**
7168
7251
  * A backup schedule — the N:M "entry" that binds one cron cadence to a
7169
7252
  * SET of destination locations. Supersedes the per-location cron on
@@ -7221,6 +7304,11 @@ var backupCapability = {
7221
7304
  * Trigger a backup. Without `destinations` the orchestrator fans
7222
7305
  * out to every destination flagged as enabled in the routing
7223
7306
  * config; with it, only the listed addons receive the archive.
7307
+ *
7308
+ * At most ONE backup run executes at a time — the source tree and
7309
+ * the staging disk are shared by every run, so a second trigger
7310
+ * while one is in flight is enqueued (or joined onto an identical
7311
+ * queued run) and the result says so. See D342.
7224
7312
  */
7225
7313
  trigger: method(z.object({
7226
7314
  /** Subset of registered `backup-destination` addon ids to write to. */
@@ -7234,7 +7322,30 @@ var backupCapability = {
7234
7322
  * retention (manual runs).
7235
7323
  */
7236
7324
  retentionCount: z.number().int().min(1).max(1e3).optional()
7237
- }).optional(), z.array(BackupEntrySchema).readonly(), {
7325
+ }).optional(), BackupTriggerResultSchema, {
7326
+ kind: "mutation",
7327
+ auth: "admin"
7328
+ }),
7329
+ /**
7330
+ * Every run the orchestrator knows about, in EXECUTION order: the
7331
+ * running run first, then queued runs in the exact order they will
7332
+ * execute (each with `queuePosition`, 1 = next), then the bounded
7333
+ * finished history newest-first. Only one run executes at a time
7334
+ * (D342) — the queued section IS the line. Each run carries live
7335
+ * phase + byte counters so a runaway build is visible in seconds,
7336
+ * not via `du`.
7337
+ */
7338
+ listRuns: method(z.void(), z.array(BackupRunSchema).readonly(), { auth: "admin" }),
7339
+ /**
7340
+ * Stop a backup run. Mirrors `storage-migration.cancel` semantics:
7341
+ * id in, `{ cancelled }` out — `false` when the run is unknown or
7342
+ * already terminal. A QUEUED run is removed before it ever starts;
7343
+ * the RUNNING run has its tar/upload stream actually aborted, the
7344
+ * half-written staging archive is deleted, and the in-flight
7345
+ * destination upload is aborted server-side (partial discarded).
7346
+ * Destinations that already completed keep their archive.
7347
+ */
7348
+ cancel: method(z.object({ runId: z.string() }), z.object({ cancelled: z.boolean() }), {
7238
7349
  kind: "mutation",
7239
7350
  auth: "admin"
7240
7351
  }),
@@ -8112,6 +8223,38 @@ var streamBrokerCapability = {
8112
8223
  auth: "admin"
8113
8224
  }),
8114
8225
  /**
8226
+ * The HARDWARE behind a device number was replaced
8227
+ * (`deviceManager.migrateDevice`). Forget every piece of broker state that
8228
+ * described the old box, so the next catalog pull derives everything from
8229
+ * the camera that is actually there:
8230
+ *
8231
+ * - every `derived:*` stream definition — a derived is authored against a
8232
+ * specific profile layout, and against the wrong hardware its feeder
8233
+ * respawns forever (observed at attempt 2732 on the live hub,
8234
+ * 2026-09-01);
8235
+ * - the profile-slot assignment entry, PURGED (not unassigned — unassign
8236
+ * marks the slot manual, which would pin the stale choice instead of
8237
+ * letting `computeInitialAssignment` re-derive it);
8238
+ * - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
8239
+ * old hardware);
8240
+ * - the persisted RTSP token rows for the device's brokers (keyed
8241
+ * `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
8242
+ * so the rows are dead URLs).
8243
+ *
8244
+ * An RPC, deliberately — an event is telemetry and may be dropped (D8),
8245
+ * and a dropped forget leaves a feeder respawning against a stream that
8246
+ * does not exist.
8247
+ */
8248
+ forgetDeviceHardware: method(z.object({ deviceId: z.number().int().nonnegative() }), z.object({
8249
+ derivedStreamsDeleted: z.array(z.string()).readonly(),
8250
+ assignmentsPurged: z.boolean(),
8251
+ probeSnapshotsDropped: z.number().int().nonnegative(),
8252
+ rtspTokenRowsDeleted: z.number().int().nonnegative()
8253
+ }), {
8254
+ kind: "mutation",
8255
+ auth: "admin"
8256
+ }),
8257
+ /**
8115
8258
  * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
8116
8259
  *
8117
8260
  * The pre-buffer is the only source that already holds the seconds BEFORE
@@ -10336,6 +10479,35 @@ var deviceProviderCapability = {
10336
10479
  name: z.string(),
10337
10480
  type: z.string()
10338
10481
  }))),
10482
+ /**
10483
+ * Tear down and reconstruct ONE device in place from its persisted rows —
10484
+ * touching no other device this provider owns.
10485
+ *
10486
+ * The primitive `deviceManager.migrateDevice` uses to flush the two
10487
+ * migrated numbers: after `swapIds` the runner's live instance still
10488
+ * carries the PRE-swap numeric id (baked into the object, its native-cap
10489
+ * registrations and its log tags), and a live object cannot be renumbered.
10490
+ * Before this method the only flush was restarting the whole owning addon
10491
+ * — which took every camera the provider owns down with it (28 devices
10492
+ * for one migrated camera, measured 2026-09-04, and the morning of the
10493
+ * same day ~27 devices' native caps did not come back on their own).
10494
+ *
10495
+ * Keyed by `stableId`, deliberately: the numeric id is exactly the thing
10496
+ * that changes. The reply carries the id the device answers on NOW.
10497
+ * Implemented once in `BaseDeviceProvider` — decommission the live
10498
+ * instance (if any), then re-create from the persisted row: the same
10499
+ * teardown/rehydrate pair every graceful shutdown + boot already uses.
10500
+ * An RPC, never an event: a dropped event would leave the runner writing
10501
+ * against the wrong camera (D8).
10502
+ *
10503
+ * Construction can dial hardware, and the migrated source is
10504
+ * characteristically dead — the timeout covers a full activate window
10505
+ * rather than the 60 s default.
10506
+ */
10507
+ reloadDevice: method(z.object({ stableId: z.string() }), z.object({ deviceId: z.number() }), {
10508
+ kind: "mutation",
10509
+ timeoutMs: 3 * 6e4
10510
+ }),
10339
10511
  supportsDiscovery: method(z.object({}), z.boolean()),
10340
10512
  /**
10341
10513
  * Run a network scan. `params` carries optional provider-specific scan
@@ -10688,13 +10860,21 @@ var deviceManagerCapability = {
10688
10860
  * `sourceStillLive` names what stayed live; empty is the only value that
10689
10861
  * means the old hardware is quiet. The migration proceeds either way —
10690
10862
  * refusing would refuse the case this exists for.
10863
+ *
10864
+ * **Safe to retry.** A duplicate call while a migration involving either
10865
+ * device is in flight is REFUSED, never queued; a repeat of a swap that
10866
+ * already completed (matched against the `addonId`/`stableId` fingerprint
10867
+ * the swap left on the rows, within 15 min) is a no-op returning the
10868
+ * original report. A blind re-run can no longer silently undo the
10869
+ * migration it retried.
10691
10870
  */
10692
10871
  migrateDevice: method(z.object({
10693
10872
  sourceId: z.number(),
10694
10873
  targetId: z.number()
10695
10874
  }), MigrateDeviceResultSchema, {
10696
10875
  kind: "mutation",
10697
- auth: "admin"
10876
+ auth: "admin",
10877
+ timeoutMs: 12 * 6e4
10698
10878
  }),
10699
10879
  /** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
10700
10880
  registerDevice: method(DeviceRegisterPayloadSchema, z.void(), { kind: "mutation" }),
@@ -34933,6 +35113,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34933
35113
  addonId: null,
34934
35114
  access: "create"
34935
35115
  },
35116
+ "backup.cancel": {
35117
+ capName: "backup",
35118
+ capScope: "system",
35119
+ addonId: null,
35120
+ access: "create"
35121
+ },
34936
35122
  "backup.delete": {
34937
35123
  capName: "backup",
34938
35124
  capScope: "system",
@@ -34975,6 +35161,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34975
35161
  addonId: null,
34976
35162
  access: "view"
34977
35163
  },
35164
+ "backup.listRuns": {
35165
+ capName: "backup",
35166
+ capScope: "system",
35167
+ addonId: null,
35168
+ access: "view"
35169
+ },
34978
35170
  "backup.listSchedules": {
34979
35171
  capName: "backup",
34980
35172
  capScope: "system",
@@ -36175,6 +36367,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
36175
36367
  addonId: null,
36176
36368
  access: "view"
36177
36369
  },
36370
+ "deviceProvider.reloadDevice": {
36371
+ capName: "device-provider",
36372
+ capScope: "system",
36373
+ addonId: null,
36374
+ access: "create"
36375
+ },
36178
36376
  "deviceProvider.start": {
36179
36377
  capName: "device-provider",
36180
36378
  capScope: "system",
@@ -39541,6 +39739,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
39541
39739
  addonId: null,
39542
39740
  access: "create"
39543
39741
  },
39742
+ "streamBroker.forgetDeviceHardware": {
39743
+ capName: "stream-broker",
39744
+ capScope: "system",
39745
+ addonId: null,
39746
+ access: "delete"
39747
+ },
39544
39748
  "streamBroker.getAllRtspEntries": {
39545
39749
  capName: "stream-broker",
39546
39750
  capScope: "system",
@@ -41999,6 +42203,11 @@ Object.freeze({
41999
42203
  form: "single",
42000
42204
  optional: false
42001
42205
  }],
42206
+ "streamBroker.forgetDeviceHardware": [{
42207
+ name: "deviceId",
42208
+ form: "single",
42209
+ optional: false
42210
+ }],
42002
42211
  "streamBroker.getDeviceAudioMute": [{
42003
42212
  name: "deviceId",
42004
42213
  form: "single",
@@ -7164,6 +7164,89 @@ var LocationStatSchema = zod.z.object({
7164
7164
  fileCount: zod.z.number(),
7165
7165
  present: zod.z.boolean()
7166
7166
  });
7167
+ /** Lifecycle of a backup run. Terminal states: succeeded / failed / cancelled. */
7168
+ var BackupRunStateSchema = zod.z.enum([
7169
+ "queued",
7170
+ "running",
7171
+ "succeeded",
7172
+ "failed",
7173
+ "cancelled"
7174
+ ]);
7175
+ /**
7176
+ * Where a running backup currently is. `queued` before it starts,
7177
+ * `building` while the tar.gz is being staged, `uploading` during the
7178
+ * per-destination fan-out, `done` once terminal.
7179
+ */
7180
+ var BackupRunPhaseSchema = zod.z.enum([
7181
+ "queued",
7182
+ "building",
7183
+ "uploading",
7184
+ "done"
7185
+ ]);
7186
+ /**
7187
+ * Observable state of one backup run — readable WHILE it runs via
7188
+ * `backup.listRuns`. This is what makes the execution queue and
7189
+ * `backup.cancel` usable: the 2026-09-04 incident (two concurrent
7190
+ * multi-GB builds, staging 5.1 GB → 18 GB, load 62) was only
7191
+ * diagnosable with `du` because nothing reported that runs existed or
7192
+ * how large the staged archive had grown.
7193
+ */
7194
+ var BackupRunSchema = zod.z.object({
7195
+ /** Stable run id — the handle `backup.cancel` takes. */
7196
+ id: zod.z.string(),
7197
+ state: BackupRunStateSchema,
7198
+ phase: BackupRunPhaseSchema,
7199
+ /**
7200
+ * Resolved destination location ids. Empty while queued (targets are
7201
+ * resolved when the run starts, against the then-current policies).
7202
+ */
7203
+ destinationIds: zod.z.array(zod.z.string()).readonly(),
7204
+ label: zod.z.string().optional(),
7205
+ /** ms-epoch when the run was submitted (trigger call / schedule fire). */
7206
+ requestedAt: zod.z.number(),
7207
+ /** ms-epoch when the run left the queue and started building. */
7208
+ startedAt: zod.z.number().optional(),
7209
+ /** ms-epoch when the run reached a terminal state. */
7210
+ finishedAt: zod.z.number().optional(),
7211
+ /** Compressed bytes of the staging archive written so far. */
7212
+ stagedBytes: zod.z.number(),
7213
+ /** Final staged archive size, once the build phase completes. */
7214
+ archiveSizeBytes: zod.z.number().optional(),
7215
+ /** Bytes pushed to the destination currently uploading. */
7216
+ uploadedBytes: zod.z.number(),
7217
+ /** Destinations where the archive fully landed (uploaded + indexed). */
7218
+ completedDestinationIds: zod.z.array(zod.z.string()).readonly(),
7219
+ /** Destinations that failed during the fan-out. */
7220
+ failedDestinationIds: zod.z.array(zod.z.string()).readonly(),
7221
+ /** Failure message when `state === 'failed'`. */
7222
+ error: zod.z.string().optional(),
7223
+ /**
7224
+ * 1-based place in the execution queue — 1 = runs next. Present only
7225
+ * while `state === 'queued'`. Stamped by the orchestrator from the
7226
+ * queue's OWN pending order, never derived from timestamps, so the
7227
+ * UI cannot show an order the executor will not honour.
7228
+ */
7229
+ queuePosition: zod.z.number().int().min(1).optional()
7230
+ });
7231
+ /**
7232
+ * Result of `backup.trigger`. The call still resolves when the run
7233
+ * terminates (compat with schedule-driven runs and the admin UI), but
7234
+ * it now names the run and says whether it had to WAIT: a trigger that
7235
+ * arrives while another run is in flight is enqueued (or joined onto
7236
+ * an identical already-queued run), never started concurrently.
7237
+ */
7238
+ var BackupTriggerResultSchema = zod.z.object({
7239
+ /** The run this trigger mapped to — poll it via `listRuns`, stop it via `cancel`. */
7240
+ runId: zod.z.string(),
7241
+ /** True when the run waited behind an in-flight run instead of starting immediately. */
7242
+ queued: zod.z.boolean(),
7243
+ /** True when this trigger was coalesced onto an identical already-queued run. */
7244
+ joined: zod.z.boolean(),
7245
+ /** True when the run was cancelled before completing every destination. */
7246
+ cancelled: zod.z.boolean(),
7247
+ /** One entry per destination the archive landed at (partial on cancel). */
7248
+ entries: zod.z.array(BackupEntrySchema).readonly()
7249
+ });
7167
7250
  /**
7168
7251
  * A backup schedule — the N:M "entry" that binds one cron cadence to a
7169
7252
  * SET of destination locations. Supersedes the per-location cron on
@@ -7221,6 +7304,11 @@ var backupCapability = {
7221
7304
  * Trigger a backup. Without `destinations` the orchestrator fans
7222
7305
  * out to every destination flagged as enabled in the routing
7223
7306
  * config; with it, only the listed addons receive the archive.
7307
+ *
7308
+ * At most ONE backup run executes at a time — the source tree and
7309
+ * the staging disk are shared by every run, so a second trigger
7310
+ * while one is in flight is enqueued (or joined onto an identical
7311
+ * queued run) and the result says so. See D342.
7224
7312
  */
7225
7313
  trigger: method(zod.z.object({
7226
7314
  /** Subset of registered `backup-destination` addon ids to write to. */
@@ -7234,7 +7322,30 @@ var backupCapability = {
7234
7322
  * retention (manual runs).
7235
7323
  */
7236
7324
  retentionCount: zod.z.number().int().min(1).max(1e3).optional()
7237
- }).optional(), zod.z.array(BackupEntrySchema).readonly(), {
7325
+ }).optional(), BackupTriggerResultSchema, {
7326
+ kind: "mutation",
7327
+ auth: "admin"
7328
+ }),
7329
+ /**
7330
+ * Every run the orchestrator knows about, in EXECUTION order: the
7331
+ * running run first, then queued runs in the exact order they will
7332
+ * execute (each with `queuePosition`, 1 = next), then the bounded
7333
+ * finished history newest-first. Only one run executes at a time
7334
+ * (D342) — the queued section IS the line. Each run carries live
7335
+ * phase + byte counters so a runaway build is visible in seconds,
7336
+ * not via `du`.
7337
+ */
7338
+ listRuns: method(zod.z.void(), zod.z.array(BackupRunSchema).readonly(), { auth: "admin" }),
7339
+ /**
7340
+ * Stop a backup run. Mirrors `storage-migration.cancel` semantics:
7341
+ * id in, `{ cancelled }` out — `false` when the run is unknown or
7342
+ * already terminal. A QUEUED run is removed before it ever starts;
7343
+ * the RUNNING run has its tar/upload stream actually aborted, the
7344
+ * half-written staging archive is deleted, and the in-flight
7345
+ * destination upload is aborted server-side (partial discarded).
7346
+ * Destinations that already completed keep their archive.
7347
+ */
7348
+ cancel: method(zod.z.object({ runId: zod.z.string() }), zod.z.object({ cancelled: zod.z.boolean() }), {
7238
7349
  kind: "mutation",
7239
7350
  auth: "admin"
7240
7351
  }),
@@ -8112,6 +8223,38 @@ var streamBrokerCapability = {
8112
8223
  auth: "admin"
8113
8224
  }),
8114
8225
  /**
8226
+ * The HARDWARE behind a device number was replaced
8227
+ * (`deviceManager.migrateDevice`). Forget every piece of broker state that
8228
+ * described the old box, so the next catalog pull derives everything from
8229
+ * the camera that is actually there:
8230
+ *
8231
+ * - every `derived:*` stream definition — a derived is authored against a
8232
+ * specific profile layout, and against the wrong hardware its feeder
8233
+ * respawns forever (observed at attempt 2732 on the live hub,
8234
+ * 2026-09-01);
8235
+ * - the profile-slot assignment entry, PURGED (not unassigned — unassign
8236
+ * marks the slot manual, which would pin the stale choice instead of
8237
+ * letting `computeInitialAssignment` re-derive it);
8238
+ * - the probe snapshots (`<deviceId>/…` — probed codec/resolution of the
8239
+ * old hardware);
8240
+ * - the persisted RTSP token rows for the device's brokers (keyed
8241
+ * `<deviceId>/<camStreamId>`; the stream ids change with the hardware,
8242
+ * so the rows are dead URLs).
8243
+ *
8244
+ * An RPC, deliberately — an event is telemetry and may be dropped (D8),
8245
+ * and a dropped forget leaves a feeder respawning against a stream that
8246
+ * does not exist.
8247
+ */
8248
+ forgetDeviceHardware: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.object({
8249
+ derivedStreamsDeleted: zod.z.array(zod.z.string()).readonly(),
8250
+ assignmentsPurged: zod.z.boolean(),
8251
+ probeSnapshotsDropped: zod.z.number().int().nonnegative(),
8252
+ rtspTokenRowsDeleted: zod.z.number().int().nonnegative()
8253
+ }), {
8254
+ kind: "mutation",
8255
+ auth: "admin"
8256
+ }),
8257
+ /**
8115
8258
  * Render a short GIF or MP4 from the broker's PRE-BUFFER around an instant.
8116
8259
  *
8117
8260
  * The pre-buffer is the only source that already holds the seconds BEFORE
@@ -10336,6 +10479,35 @@ var deviceProviderCapability = {
10336
10479
  name: zod.z.string(),
10337
10480
  type: zod.z.string()
10338
10481
  }))),
10482
+ /**
10483
+ * Tear down and reconstruct ONE device in place from its persisted rows —
10484
+ * touching no other device this provider owns.
10485
+ *
10486
+ * The primitive `deviceManager.migrateDevice` uses to flush the two
10487
+ * migrated numbers: after `swapIds` the runner's live instance still
10488
+ * carries the PRE-swap numeric id (baked into the object, its native-cap
10489
+ * registrations and its log tags), and a live object cannot be renumbered.
10490
+ * Before this method the only flush was restarting the whole owning addon
10491
+ * — which took every camera the provider owns down with it (28 devices
10492
+ * for one migrated camera, measured 2026-09-04, and the morning of the
10493
+ * same day ~27 devices' native caps did not come back on their own).
10494
+ *
10495
+ * Keyed by `stableId`, deliberately: the numeric id is exactly the thing
10496
+ * that changes. The reply carries the id the device answers on NOW.
10497
+ * Implemented once in `BaseDeviceProvider` — decommission the live
10498
+ * instance (if any), then re-create from the persisted row: the same
10499
+ * teardown/rehydrate pair every graceful shutdown + boot already uses.
10500
+ * An RPC, never an event: a dropped event would leave the runner writing
10501
+ * against the wrong camera (D8).
10502
+ *
10503
+ * Construction can dial hardware, and the migrated source is
10504
+ * characteristically dead — the timeout covers a full activate window
10505
+ * rather than the 60 s default.
10506
+ */
10507
+ reloadDevice: method(zod.z.object({ stableId: zod.z.string() }), zod.z.object({ deviceId: zod.z.number() }), {
10508
+ kind: "mutation",
10509
+ timeoutMs: 3 * 6e4
10510
+ }),
10339
10511
  supportsDiscovery: method(zod.z.object({}), zod.z.boolean()),
10340
10512
  /**
10341
10513
  * Run a network scan. `params` carries optional provider-specific scan
@@ -10688,13 +10860,21 @@ var deviceManagerCapability = {
10688
10860
  * `sourceStillLive` names what stayed live; empty is the only value that
10689
10861
  * means the old hardware is quiet. The migration proceeds either way —
10690
10862
  * refusing would refuse the case this exists for.
10863
+ *
10864
+ * **Safe to retry.** A duplicate call while a migration involving either
10865
+ * device is in flight is REFUSED, never queued; a repeat of a swap that
10866
+ * already completed (matched against the `addonId`/`stableId` fingerprint
10867
+ * the swap left on the rows, within 15 min) is a no-op returning the
10868
+ * original report. A blind re-run can no longer silently undo the
10869
+ * migration it retried.
10691
10870
  */
10692
10871
  migrateDevice: method(zod.z.object({
10693
10872
  sourceId: zod.z.number(),
10694
10873
  targetId: zod.z.number()
10695
10874
  }), MigrateDeviceResultSchema, {
10696
10875
  kind: "mutation",
10697
- auth: "admin"
10876
+ auth: "admin",
10877
+ timeoutMs: 12 * 6e4
10698
10878
  }),
10699
10879
  /** Register a device in the DB + in-memory registry. Called by DeviceManagerApi.register(). */
10700
10880
  registerDevice: method(DeviceRegisterPayloadSchema, zod.z.void(), { kind: "mutation" }),
@@ -34933,6 +35113,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34933
35113
  addonId: null,
34934
35114
  access: "create"
34935
35115
  },
35116
+ "backup.cancel": {
35117
+ capName: "backup",
35118
+ capScope: "system",
35119
+ addonId: null,
35120
+ access: "create"
35121
+ },
34936
35122
  "backup.delete": {
34937
35123
  capName: "backup",
34938
35124
  capScope: "system",
@@ -34975,6 +35161,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
34975
35161
  addonId: null,
34976
35162
  access: "view"
34977
35163
  },
35164
+ "backup.listRuns": {
35165
+ capName: "backup",
35166
+ capScope: "system",
35167
+ addonId: null,
35168
+ access: "view"
35169
+ },
34978
35170
  "backup.listSchedules": {
34979
35171
  capName: "backup",
34980
35172
  capScope: "system",
@@ -36175,6 +36367,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
36175
36367
  addonId: null,
36176
36368
  access: "view"
36177
36369
  },
36370
+ "deviceProvider.reloadDevice": {
36371
+ capName: "device-provider",
36372
+ capScope: "system",
36373
+ addonId: null,
36374
+ access: "create"
36375
+ },
36178
36376
  "deviceProvider.start": {
36179
36377
  capName: "device-provider",
36180
36378
  capScope: "system",
@@ -39541,6 +39739,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
39541
39739
  addonId: null,
39542
39740
  access: "create"
39543
39741
  },
39742
+ "streamBroker.forgetDeviceHardware": {
39743
+ capName: "stream-broker",
39744
+ capScope: "system",
39745
+ addonId: null,
39746
+ access: "delete"
39747
+ },
39544
39748
  "streamBroker.getAllRtspEntries": {
39545
39749
  capName: "stream-broker",
39546
39750
  capScope: "system",
@@ -41999,6 +42203,11 @@ Object.freeze({
41999
42203
  form: "single",
42000
42204
  optional: false
42001
42205
  }],
42206
+ "streamBroker.forgetDeviceHardware": [{
42207
+ name: "deviceId",
42208
+ form: "single",
42209
+ optional: false
42210
+ }],
42002
42211
  "streamBroker.getDeviceAudioMute": [{
42003
42212
  name: "deviceId",
42004
42213
  form: "single",