camstack 1.2.189 → 1.2.190

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.
@@ -24024,9 +24024,9 @@ var require_zod = __commonJS({
24024
24024
  }
24025
24025
  });
24026
24026
 
24027
- // ../system/dist/dist-DyjNaeIj.js
24028
- var require_dist_DyjNaeIj = __commonJS({
24029
- "../system/dist/dist-DyjNaeIj.js"(exports) {
24027
+ // ../system/dist/dist-Bi4Ioxop.js
24028
+ var require_dist_Bi4Ioxop = __commonJS({
24029
+ "../system/dist/dist-Bi4Ioxop.js"(exports) {
24030
24030
  "use strict";
24031
24031
  var zod = require_zod();
24032
24032
  __require("crypto");
@@ -24049,6 +24049,7 @@ var require_dist_DyjNaeIj = __commonJS({
24049
24049
  EventCategory2["AddonRunnerRefused"] = "addon.runner-refused";
24050
24050
  EventCategory2["ProcessStateChanged"] = "process.state-changed";
24051
24051
  EventCategory2["SystemRunnerCheck"] = "system.runner-check";
24052
+ EventCategory2["AlertEpisode"] = "alert.episode";
24052
24053
  EventCategory2["AddonError"] = "addon.error";
24053
24054
  EventCategory2["AddonPageReady"] = "addon.page-ready";
24054
24055
  EventCategory2["AddonWidgetReady"] = "addon.widget-ready";
@@ -32045,7 +32046,14 @@ var require_dist_DyjNaeIj = __commonJS({
32045
32046
  ]);
32046
32047
  var AlertActionSchema = zod.z.object({
32047
32048
  label: zod.z.string(),
32048
- href: zod.z.string().optional()
32049
+ href: zod.z.string().optional(),
32050
+ /**
32051
+ * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
32052
+ * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
32053
+ * start) — the browser must leave the SPA with a full navigation, because
32054
+ * the SPA has no such route and would bounce to the home page.
32055
+ */
32056
+ navigation: zod.z.enum(["app", "server"]).optional()
32049
32057
  });
32050
32058
  var AlertStatusSchema = zod.z.enum([
32051
32059
  "active",
@@ -62794,6 +62802,9 @@ var require_dist_DyjNaeIj = __commonJS({
62794
62802
  "server-updated",
62795
62803
  "export-completed",
62796
62804
  "backup-completed",
62805
+ "alert-critical",
62806
+ "alert-error",
62807
+ "alert-critical-resolved",
62797
62808
  "camera-online",
62798
62809
  "camera-offline",
62799
62810
  "camera-disabled",
@@ -71992,6 +72003,18 @@ var require_dist_DyjNaeIj = __commonJS({
71992
72003
  */
71993
72004
  movers: method(zod.z.object({}), zod.z.array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }),
71994
72005
  /**
72006
+ * May a mover that touches these locations run NOW? One claim per physical
72007
+ * root is owned by the storage orchestrator: a single-location repoint holds
72008
+ * it while it carries its subtree, and a mover (`relocateMedia`,
72009
+ * `relocateFootage`, a migration move) asks here AFTER registering its job and
72010
+ * cancels itself on `clear: false`. The repoint, in turn, refuses while a
72011
+ * registered mover touches its root. Asked, never polled (D3).
72012
+ */
72013
+ moverClearance: method(zod.z.object({ locationIds: zod.z.array(zod.z.string()).readonly() }), zod.z.object({
72014
+ clear: zod.z.boolean(),
72015
+ repointLocationId: zod.z.string().optional()
72016
+ }), { auth: "admin" }),
72017
+ /**
71995
72018
  * What each class's SOURCE still holds, from the archive — never from the
71996
72019
  * resident index (D295). Only classes with something left (or something
71997
72020
  * unknown) are listed, so an empty list means there is nothing to drain and
@@ -76209,43 +76232,139 @@ var require_dist_DyjNaeIj = __commonJS({
76209
76232
  "error",
76210
76233
  "unavailable"
76211
76234
  ]);
76235
+ var DishwasherOperationStateSchema = zod.z.enum([
76236
+ "Inactive",
76237
+ "Ready",
76238
+ "DelayedStart",
76239
+ "Run",
76240
+ "Pause",
76241
+ "ActionRequired",
76242
+ "Finished",
76243
+ "Error",
76244
+ "Aborting"
76245
+ ]);
76246
+ var DishwasherAvailabilitySchema = zod.z.enum([
76247
+ "available",
76248
+ "offline",
76249
+ "description-missing"
76250
+ ]);
76212
76251
  var DishwasherConsumableLevelSchema = zod.z.enum([
76213
76252
  "ok",
76214
76253
  "low",
76215
76254
  "empty"
76216
76255
  ]);
76217
76256
  var DishwasherStatusSchema = zod.z.object({
76257
+ availability: DishwasherAvailabilitySchema,
76258
+ /** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
76218
76259
  state: DishwasherStateSchema,
76219
- /** Active program id/name, or null when none selected / unknown. */
76260
+ /** The appliance's operation state as reported; null when not known. */
76261
+ operationState: DishwasherOperationStateSchema.nullable(),
76262
+ /** Feature key of the ACTIVE (running) program, or null when none runs / not known. */
76220
76263
  program: zod.z.string().nullable(),
76221
- /** Program tokens the hardware accepts — drives the UI selector. */
76264
+ /** Feature key of the SELECTED program (what Start would run), or null when none / not known. */
76265
+ selectedProgram: zod.z.string().nullable(),
76266
+ /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
76222
76267
  availablePrograms: zod.z.array(zod.z.string()),
76223
- /** Minutes remaining on the active cycle, or null when not running. */
76224
- remainingMinutes: zod.z.number().int().nonnegative().nullable(),
76268
+ /** Seconds remaining on the active cycle, or null when not running / not known. */
76269
+ remainingSeconds: zod.z.number().int().nonnegative().nullable(),
76270
+ /** Seconds the delayed start is set to (time until the cycle starts), or null. */
76271
+ delayedStartSeconds: zod.z.number().int().nonnegative().nullable(),
76225
76272
  /** Cycle completion 0..100, or null when not applicable. */
76226
76273
  progressPercent: zod.z.number().min(0).max(100).nullable(),
76227
- /** True when the door is reported open. */
76228
- doorOpen: zod.z.boolean(),
76229
- /** True when remote start is allowed (door closed, power on, etc.). */
76230
- remoteStartAllowed: zod.z.boolean(),
76231
- /** Main power / switched-on state. */
76232
- powerOn: zod.z.boolean(),
76274
+ /** True when the door is reported open; null when the door state is not known. */
76275
+ doorOpen: zod.z.boolean().nullable(),
76276
+ /** True when the door is latched (locked) during a cycle; null when not reported. */
76277
+ doorLocked: zod.z.boolean().nullable(),
76278
+ /** True when remote start is allowed (door closed, power on, etc.); null when not known. */
76279
+ remoteStartAllowed: zod.z.boolean().nullable(),
76280
+ /** True when the appliance accepts remote control now; null when not known. */
76281
+ remoteControlActive: zod.z.boolean().nullable(),
76282
+ /** True while somebody operates the appliance at its panel; null when not reported. */
76283
+ localControlActive: zod.z.boolean().nullable(),
76284
+ /** Main power / switched-on state; null when not known. */
76285
+ powerOn: zod.z.boolean().nullable(),
76286
+ /** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
76287
+ powerState: zod.z.string().nullable(),
76233
76288
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
76234
76289
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
76235
- halfLoad: zod.z.boolean(),
76236
- hygienePlus: zod.z.boolean(),
76290
+ /** Option states: null when the appliance does not expose the option or has not said. */
76291
+ halfLoad: zod.z.boolean().nullable(),
76292
+ hygienePlus: zod.z.boolean().nullable(),
76237
76293
  /** Extra dry option when the appliance exposes it. */
76238
- extraDry: zod.z.boolean(),
76294
+ extraDry: zod.z.boolean().nullable(),
76239
76295
  /** Intensive zone (lower basket) when the appliance exposes it. */
76240
- intensiveZone: zod.z.boolean(),
76296
+ intensiveZone: zod.z.boolean().nullable(),
76241
76297
  /** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
76242
- varioSpeedPlus: zod.z.boolean(),
76243
- /** Child lock (controls disabled on the appliance). */
76244
- childLock: zod.z.boolean(),
76298
+ varioSpeedPlus: zod.z.boolean().nullable(),
76299
+ /** Child lock (controls disabled on the appliance); null when not known. */
76300
+ childLock: zod.z.boolean().nullable(),
76301
+ /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
76302
+ activeEvents: zod.z.array(zod.z.string()),
76303
+ /** Numeric error code when the appliance reports one; null otherwise. */
76245
76304
  errorCode: zod.z.number().nullable(),
76305
+ /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
76246
76306
  errorLabel: zod.z.string().nullable(),
76307
+ /** Ms epoch of the last value received from the appliance; 0 = never. */
76247
76308
  lastFetchedAt: zod.z.number()
76248
76309
  });
76310
+ var DeviceIdSchema = zod.z.number().int().nonnegative();
76311
+ var DishwasherValueSchema = zod.z.union([
76312
+ zod.z.boolean(),
76313
+ zod.z.string(),
76314
+ zod.z.number()
76315
+ ]);
76316
+ var DishwasherAccessSchema = zod.z.enum([
76317
+ "none",
76318
+ "read",
76319
+ "readWrite",
76320
+ "writeOnly",
76321
+ "readStatic"
76322
+ ]);
76323
+ var DishwasherOptionDescriptorSchema = zod.z.object({
76324
+ /** Feature key (`Dishcare.Dishwasher.Option.ExtraDry`). */
76325
+ key: zod.z.string(),
76326
+ /** Last segment of the key, for display. */
76327
+ name: zod.z.string(),
76328
+ type: zod.z.enum([
76329
+ "boolean",
76330
+ "number",
76331
+ "enum",
76332
+ "string"
76333
+ ]),
76334
+ access: DishwasherAccessSchema,
76335
+ /** The appliance reports this entity as currently available. */
76336
+ available: zod.z.boolean(),
76337
+ /** Unit the description declares for a number; null when it declares none. */
76338
+ unit: zod.z.string().nullable(),
76339
+ min: zod.z.number().nullable(),
76340
+ max: zod.z.number().nullable(),
76341
+ step: zod.z.number().nullable(),
76342
+ /** Allowed members of an enum; null for other types. */
76343
+ enumValues: zod.z.array(zod.z.string()).nullable(),
76344
+ defaultValue: DishwasherValueSchema.nullable()
76345
+ });
76346
+ var DishwasherProgramSchema = zod.z.object({
76347
+ /** Feature key (`Dishcare.Dishwasher.Program.Eco50`) — what `setProgram` / `start` take. */
76348
+ key: zod.z.string(),
76349
+ name: zod.z.string(),
76350
+ /** The appliance currently offers this program. */
76351
+ available: zod.z.boolean(),
76352
+ /** The options this program lists, with the program-level access / availability / default. */
76353
+ options: zod.z.array(DishwasherOptionDescriptorSchema)
76354
+ });
76355
+ var DishwasherOptionsSchema = zod.z.object({
76356
+ availability: DishwasherAvailabilitySchema,
76357
+ /** What the appliance can be told to do, from the commands its description declares. */
76358
+ supportsStart: zod.z.boolean(),
76359
+ supportsPause: zod.z.boolean(),
76360
+ supportsResume: zod.z.boolean(),
76361
+ supportsStop: zod.z.boolean(),
76362
+ supportsPower: zod.z.boolean(),
76363
+ /** Every program the description lists, with its options. */
76364
+ programs: zod.z.array(DishwasherProgramSchema),
76365
+ /** Every option and setting entity the description lists (programs reference these by key). */
76366
+ options: zod.z.array(DishwasherOptionDescriptorSchema)
76367
+ });
76249
76368
  var dishwasherCapability = {
76250
76369
  name: "dishwasher",
76251
76370
  scope: "device",
@@ -76253,86 +76372,102 @@ var require_dist_DyjNaeIj = __commonJS({
76253
76372
  mode: "singleton",
76254
76373
  deviceTypes: [DeviceType.Dishwasher],
76255
76374
  methods: {
76256
- start: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
76375
+ /**
76376
+ * Start a program. Without `program` the selected one starts. `options` are
76377
+ * option feature keys; each is checked against the program's constraints.
76378
+ */
76379
+ start: method(zod.z.object({
76380
+ deviceId: DeviceIdSchema,
76381
+ program: zod.z.string().min(1).optional(),
76382
+ options: zod.z.record(zod.z.string(), DishwasherValueSchema).optional()
76383
+ }), zod.z.void(), {
76257
76384
  kind: "mutation",
76258
76385
  auth: "protected"
76259
76386
  }),
76260
- pause: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
76387
+ pause: method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
76261
76388
  kind: "mutation",
76262
76389
  auth: "protected"
76263
76390
  }),
76264
- stop: method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
76391
+ resume: method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
76265
76392
  kind: "mutation",
76266
76393
  auth: "protected"
76267
76394
  }),
76395
+ stop: method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
76396
+ kind: "mutation",
76397
+ auth: "protected"
76398
+ }),
76399
+ /** Select (not start) a program; refused when the appliance does not offer it. */
76268
76400
  setProgram: method(zod.z.object({
76269
- deviceId: zod.z.number().int().nonnegative(),
76401
+ deviceId: DeviceIdSchema,
76270
76402
  program: zod.z.string().min(1)
76271
76403
  }), zod.z.void(), {
76272
76404
  kind: "mutation",
76273
76405
  auth: "protected"
76274
76406
  }),
76275
76407
  setPower: method(zod.z.object({
76276
- deviceId: zod.z.number().int().nonnegative(),
76408
+ deviceId: DeviceIdSchema,
76277
76409
  on: zod.z.boolean()
76278
76410
  }), zod.z.void(), {
76279
76411
  kind: "mutation",
76280
76412
  auth: "protected"
76281
76413
  }),
76282
76414
  setHalfLoad: method(zod.z.object({
76283
- deviceId: zod.z.number().int().nonnegative(),
76415
+ deviceId: DeviceIdSchema,
76284
76416
  on: zod.z.boolean()
76285
76417
  }), zod.z.void(), {
76286
76418
  kind: "mutation",
76287
76419
  auth: "protected"
76288
76420
  }),
76289
76421
  setHygienePlus: method(zod.z.object({
76290
- deviceId: zod.z.number().int().nonnegative(),
76422
+ deviceId: DeviceIdSchema,
76291
76423
  on: zod.z.boolean()
76292
76424
  }), zod.z.void(), {
76293
76425
  kind: "mutation",
76294
76426
  auth: "protected"
76295
76427
  }),
76296
76428
  setExtraDry: method(zod.z.object({
76297
- deviceId: zod.z.number().int().nonnegative(),
76429
+ deviceId: DeviceIdSchema,
76298
76430
  on: zod.z.boolean()
76299
76431
  }), zod.z.void(), {
76300
76432
  kind: "mutation",
76301
76433
  auth: "protected"
76302
76434
  }),
76303
76435
  setIntensiveZone: method(zod.z.object({
76304
- deviceId: zod.z.number().int().nonnegative(),
76436
+ deviceId: DeviceIdSchema,
76305
76437
  on: zod.z.boolean()
76306
76438
  }), zod.z.void(), {
76307
76439
  kind: "mutation",
76308
76440
  auth: "protected"
76309
76441
  }),
76310
76442
  setVarioSpeedPlus: method(zod.z.object({
76311
- deviceId: zod.z.number().int().nonnegative(),
76443
+ deviceId: DeviceIdSchema,
76312
76444
  on: zod.z.boolean()
76313
76445
  }), zod.z.void(), {
76314
76446
  kind: "mutation",
76315
76447
  auth: "protected"
76316
76448
  }),
76317
76449
  setChildLock: method(zod.z.object({
76318
- deviceId: zod.z.number().int().nonnegative(),
76450
+ deviceId: DeviceIdSchema,
76319
76451
  on: zod.z.boolean()
76320
76452
  }), zod.z.void(), {
76321
76453
  kind: "mutation",
76322
76454
  auth: "protected"
76323
76455
  }),
76456
+ /**
76457
+ * Write any option or setting by feature key. Refused when the description does
76458
+ * not list it, it is not writable or not available now, or the value is outside
76459
+ * its declared range / members.
76460
+ */
76324
76461
  setOption: method(zod.z.object({
76325
- deviceId: zod.z.number().int().nonnegative(),
76462
+ deviceId: DeviceIdSchema,
76326
76463
  key: zod.z.string().min(1),
76327
- value: zod.z.union([
76328
- zod.z.boolean(),
76329
- zod.z.string(),
76330
- zod.z.number()
76331
- ])
76464
+ value: DishwasherValueSchema
76332
76465
  }), zod.z.void(), {
76333
76466
  kind: "mutation",
76334
76467
  auth: "protected"
76335
- })
76468
+ }),
76469
+ /** What this appliance can do, from its own description — the UI is derived from it. */
76470
+ getOptions: method(zod.z.object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
76336
76471
  },
76337
76472
  status: {
76338
76473
  schema: DishwasherStatusSchema,
@@ -89662,6 +89797,12 @@ var require_dist_DyjNaeIj = __commonJS({
89662
89797
  addonId: null,
89663
89798
  access: "view"
89664
89799
  },
89800
+ "dishwasher.getOptions": {
89801
+ capName: "dishwasher",
89802
+ capScope: "device",
89803
+ addonId: null,
89804
+ access: "view"
89805
+ },
89665
89806
  "dishwasher.getStatus": {
89666
89807
  capName: "dishwasher",
89667
89808
  capScope: "device",
@@ -89674,6 +89815,12 @@ var require_dist_DyjNaeIj = __commonJS({
89674
89815
  addonId: null,
89675
89816
  access: "create"
89676
89817
  },
89818
+ "dishwasher.resume": {
89819
+ capName: "dishwasher",
89820
+ capScope: "device",
89821
+ addonId: null,
89822
+ access: "create"
89823
+ },
89677
89824
  "dishwasher.setChildLock": {
89678
89825
  capName: "dishwasher",
89679
89826
  capScope: "device",
@@ -94174,6 +94321,12 @@ var require_dist_DyjNaeIj = __commonJS({
94174
94321
  addonId: null,
94175
94322
  access: "view"
94176
94323
  },
94324
+ "storageMigration.moverClearance": {
94325
+ capName: "storage-migration",
94326
+ capScope: "system",
94327
+ addonId: null,
94328
+ access: "view"
94329
+ },
94177
94330
  "storageMigration.movers": {
94178
94331
  capName: "storage-migration",
94179
94332
  capScope: "system",
@@ -96624,6 +96777,11 @@ var require_dist_DyjNaeIj = __commonJS({
96624
96777
  form: "single",
96625
96778
  optional: false
96626
96779
  }],
96780
+ "dishwasher.getOptions": [{
96781
+ name: "deviceId",
96782
+ form: "single",
96783
+ optional: false
96784
+ }],
96627
96785
  "dishwasher.getStatus": [{
96628
96786
  name: "deviceId",
96629
96787
  form: "single",
@@ -96634,6 +96792,11 @@ var require_dist_DyjNaeIj = __commonJS({
96634
96792
  form: "single",
96635
96793
  optional: false
96636
96794
  }],
96795
+ "dishwasher.resume": [{
96796
+ name: "deviceId",
96797
+ form: "single",
96798
+ optional: false
96799
+ }],
96637
96800
  "dishwasher.setChildLock": [{
96638
96801
  name: "deviceId",
96639
96802
  form: "single",
@@ -99207,6 +99370,9 @@ var require_dist_DyjNaeIj = __commonJS({
99207
99370
  "wrapper-update-available": "system",
99208
99371
  "backup-completed": "system",
99209
99372
  "export-completed": "system",
99373
+ "alert-critical": "system",
99374
+ "alert-error": "system",
99375
+ "alert-critical-resolved": "system",
99210
99376
  "storage-location-low": "system",
99211
99377
  "storage-location-critical": "system",
99212
99378
  "storage-location-ok": "system",
@@ -99331,6 +99497,16 @@ var require_dist_DyjNaeIj = __commonJS({
99331
99497
  "storage-location-ok"
99332
99498
  ]
99333
99499
  },
99500
+ {
99501
+ id: "alerts",
99502
+ family: "system",
99503
+ label: "Alerts",
99504
+ kinds: [
99505
+ "alert-critical",
99506
+ "alert-error",
99507
+ "alert-critical-resolved"
99508
+ ]
99509
+ },
99334
99510
  {
99335
99511
  id: "alarm",
99336
99512
  family: "alarm",
@@ -99348,6 +99524,60 @@ var require_dist_DyjNaeIj = __commonJS({
99348
99524
  return [[descriptor.kind, field]];
99349
99525
  })));
99350
99526
  Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
99527
+ zod.z.object({
99528
+ phase: zod.z.enum(["raised", "resolved"]),
99529
+ episodeId: zod.z.string().min(1),
99530
+ alertId: zod.z.string().min(1),
99531
+ severity: AlertSeveritySchema,
99532
+ category: zod.z.string(),
99533
+ title: zod.z.string(),
99534
+ message: zod.z.string(),
99535
+ sourceType: zod.z.string().optional(),
99536
+ sourceId: zod.z.string().optional(),
99537
+ nodeId: zod.z.string().optional(),
99538
+ /** The alert's own action link (where the operator acts on it), when it has one. */
99539
+ actionUrl: zod.z.string().optional(),
99540
+ at: zod.z.number()
99541
+ });
99542
+ function isAlertEpisodeOpen(status) {
99543
+ return status !== "completed" && status !== "dismissed";
99544
+ }
99545
+ function isAlertEpisodeSeverity(severity) {
99546
+ return severity === "critical" || severity === "error";
99547
+ }
99548
+ function alertEpisodeId(alert) {
99549
+ const minted = alert.metadata?.["episodeId"];
99550
+ return typeof minted === "string" && minted.length > 0 ? minted : `${alert.id}@${String(alert.createdAt)}`;
99551
+ }
99552
+ function alertNodeId(alert) {
99553
+ const direct = alert.metadata?.["nodeId"];
99554
+ if (typeof direct === "string") return direct;
99555
+ const runner = alert.metadata?.["runner"];
99556
+ if (typeof runner === "object" && runner !== null && "nodeId" in runner) {
99557
+ const id = runner.nodeId;
99558
+ if (typeof id === "string") return id;
99559
+ }
99560
+ return alert.source?.type === "node" ? alert.source.id : void 0;
99561
+ }
99562
+ function alertEpisodePayload(alert, phase, at) {
99563
+ const nodeId = alertNodeId(alert);
99564
+ return {
99565
+ phase,
99566
+ episodeId: alertEpisodeId(alert),
99567
+ alertId: alert.id,
99568
+ severity: alert.severity,
99569
+ category: alert.category,
99570
+ title: alert.title,
99571
+ message: alert.message,
99572
+ ...alert.source !== void 0 ? {
99573
+ sourceType: alert.source.type,
99574
+ sourceId: alert.source.id
99575
+ } : {},
99576
+ ...nodeId !== void 0 ? { nodeId } : {},
99577
+ ...alert.action?.href !== void 0 ? { actionUrl: alert.action.href } : {},
99578
+ at
99579
+ };
99580
+ }
99351
99581
  var RULE_BODY_ONLY = ["body"];
99352
99582
  var RULE_TEXT = ["title", "body"];
99353
99583
  var BATTERY = ["device-battery-low", "device-battery-normal"];
@@ -99357,6 +99587,11 @@ var require_dist_DyjNaeIj = __commonJS({
99357
99587
  "device-tank-full",
99358
99588
  "device-tank-normal"
99359
99589
  ];
99590
+ var ALERT_EPISODE = [
99591
+ "alert-critical",
99592
+ "alert-error",
99593
+ "alert-critical-resolved"
99594
+ ];
99360
99595
  var UPDATES = [
99361
99596
  "addon-update-available",
99362
99597
  "server-update-available",
@@ -99546,14 +99781,14 @@ var require_dist_DyjNaeIj = __commonJS({
99546
99781
  ([...RULE_TEXT], [...CONSUMABLE]),
99547
99782
  ([...RULE_TEXT], [...TANK]),
99548
99783
  [...RULE_TEXT],
99549
- ([...RULE_TEXT], [...UPDATES]),
99550
- ([...RULE_TEXT], [...UPDATES]),
99551
- [...RULE_TEXT],
99784
+ ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
99785
+ ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
99552
99786
  [...RULE_TEXT],
99553
99787
  [...RULE_TEXT],
99554
99788
  [...RULE_TEXT],
99555
99789
  [...RULE_TEXT],
99556
99790
  [...RULE_TEXT],
99791
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
99557
99792
  [...RULE_TEXT],
99558
99793
  [...RULE_TEXT],
99559
99794
  [...RULE_TEXT],
@@ -99571,7 +99806,11 @@ var require_dist_DyjNaeIj = __commonJS({
99571
99806
  [...RULE_TEXT],
99572
99807
  [...RULE_TEXT],
99573
99808
  [...RULE_TEXT],
99574
- [...RULE_TEXT],
99809
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
99810
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
99811
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
99812
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
99813
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
99575
99814
  [...RULE_TEXT],
99576
99815
  [...RULE_TEXT],
99577
99816
  [...RULE_TEXT],
@@ -100759,6 +100998,18 @@ var require_dist_DyjNaeIj = __commonJS({
100759
100998
  return addonWidgetsCapability;
100760
100999
  }
100761
101000
  });
101001
+ Object.defineProperty(exports, "alertEpisodeId", {
101002
+ enumerable: true,
101003
+ get: function() {
101004
+ return alertEpisodeId;
101005
+ }
101006
+ });
101007
+ Object.defineProperty(exports, "alertEpisodePayload", {
101008
+ enumerable: true,
101009
+ get: function() {
101010
+ return alertEpisodePayload;
101011
+ }
101012
+ });
100762
101013
  Object.defineProperty(exports, "alertsCapability", {
100763
101014
  enumerable: true,
100764
101015
  get: function() {
@@ -101245,6 +101496,18 @@ var require_dist_DyjNaeIj = __commonJS({
101245
101496
  return interpolate;
101246
101497
  }
101247
101498
  });
101499
+ Object.defineProperty(exports, "isAlertEpisodeOpen", {
101500
+ enumerable: true,
101501
+ get: function() {
101502
+ return isAlertEpisodeOpen;
101503
+ }
101504
+ });
101505
+ Object.defineProperty(exports, "isAlertEpisodeSeverity", {
101506
+ enumerable: true,
101507
+ get: function() {
101508
+ return isAlertEpisodeSeverity;
101509
+ }
101510
+ });
101248
101511
  Object.defineProperty(exports, "isAppLinkBase", {
101249
101512
  enumerable: true,
101250
101513
  get: function() {
@@ -113359,11 +113622,11 @@ ${values.join("\n")}` : `${blockName} :`;
113359
113622
  }
113360
113623
  });
113361
113624
 
113362
- // ../system/dist/device-ca-njpTMpJC.js
113363
- var require_device_ca_njpTMpJC = __commonJS({
113364
- "../system/dist/device-ca-njpTMpJC.js"(exports) {
113625
+ // ../system/dist/device-ca-B3hUVfYk.js
113626
+ var require_device_ca_B3hUVfYk = __commonJS({
113627
+ "../system/dist/device-ca-B3hUVfYk.js"(exports) {
113365
113628
  "use strict";
113366
- var require_dist10 = require_dist_DyjNaeIj();
113629
+ var require_dist10 = require_dist_Bi4Ioxop();
113367
113630
  var require_x509_es = require_x509_es_C4cOo48M();
113368
113631
  var node_crypto = __require("crypto");
113369
113632
  var SYSTEM_SECRET_OWNER = "system";
@@ -113778,11 +114041,11 @@ var require_device_ca_njpTMpJC = __commonJS({
113778
114041
  }
113779
114042
  });
113780
114043
 
113781
- // ../system/dist/phase6-declare-gate-BlTDMI3t.js
113782
- var require_phase6_declare_gate_BlTDMI3t = __commonJS({
113783
- "../system/dist/phase6-declare-gate-BlTDMI3t.js"(exports) {
114044
+ // ../system/dist/phase6-declare-gate-DwYAep7y.js
114045
+ var require_phase6_declare_gate_DwYAep7y = __commonJS({
114046
+ "../system/dist/phase6-declare-gate-DwYAep7y.js"(exports) {
113784
114047
  "use strict";
113785
- var require_dist10 = require_dist_DyjNaeIj();
114048
+ var require_dist10 = require_dist_Bi4Ioxop();
113786
114049
  function legacyTableRowCountFromList(tables, collection) {
113787
114050
  return tables.find((t) => t.name === collection)?.rows ?? 0;
113788
114051
  }
@@ -113876,124 +114139,33 @@ var require_alerts_addon = __commonJS({
113876
114139
  [Symbol.toStringTag]: { value: "Module" }
113877
114140
  });
113878
114141
  require_chunk_Cek0wNdY();
113879
- var require_dist10 = require_dist_DyjNaeIj();
114142
+ var require_dist10 = require_dist_Bi4Ioxop();
113880
114143
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
113881
- var require_phase6_declare_gate = require_phase6_declare_gate_BlTDMI3t();
113882
- var CRITICAL_CATEGORIES = /* @__PURE__ */ new Set([
113883
- "addon.runner-failed",
113884
- "addon.runner-refused",
113885
- "system.runner-check"
113886
- ]);
113887
- function isCriticalPush(alert) {
113888
- return alert.severity === "critical" || CRITICAL_CATEGORIES.has(alert.category);
113889
- }
113890
- function isPushState(value) {
113891
- if (typeof value !== "object" || value === null) return false;
113892
- const rec = value;
113893
- return (rec["state"] === "pending" || rec["state"] === "delivered" || rec["state"] === "disabled") && typeof rec["key"] === "string" && typeof rec["attempts"] === "number";
113894
- }
113895
- function selectPushTargets(targets, policy) {
113896
- const enabled = targets.filter((t) => t.enabled);
113897
- if (policy.targetIds.length > 0) return enabled.filter((t) => policy.targetIds.includes(t.id));
113898
- return enabled.filter((t) => t.ownerUserId === void 0);
113899
- }
113900
- var MAX_DIGEST_LINES = 20;
113901
- function composeDigest(items) {
113902
- const first = items[0];
113903
- if (items.length === 1 && first !== void 0) return {
113904
- title: first.title,
113905
- body: first.line
113906
- };
113907
- const shown = items.slice(0, MAX_DIGEST_LINES).map((i) => i.line);
113908
- const more = items.length - shown.length;
113909
- return {
113910
- title: `${String(items.length)} addons need attention`,
113911
- body: [...shown, ...more > 0 ? [`... and ${String(more)} more`] : []].join("\n")
113912
- };
113913
- }
113914
- async function deliverDigest(input) {
113915
- const out = /* @__PURE__ */ new Map();
113916
- const work = input.items.filter((i) => !(i.previous !== null && i.previous.key === i.key && i.previous.state !== "pending"));
113917
- if (work.length === 0) return out;
113918
- const sameKey = (i) => i.previous !== null && i.previous.key === i.key ? i.previous : null;
113919
- const attemptsOf = (i) => (sameKey(i)?.attempts ?? 0) + 1;
113920
- const sentOf = (i) => sameKey(i)?.sentTo ?? [];
113921
- if (!input.policy.enabled) {
113922
- for (const i of work) out.set(i.alertId, {
113923
- state: "disabled",
113924
- key: i.key,
113925
- attempts: attemptsOf(i)
113926
- });
113927
- return out;
113928
- }
113929
- const fail = (error) => {
113930
- for (const i of work) out.set(i.alertId, {
113931
- state: "pending",
113932
- key: i.key,
113933
- attempts: attemptsOf(i),
113934
- lastError: error,
113935
- ...sentOf(i).length > 0 ? { sentTo: sentOf(i) } : {}
113936
- });
113937
- return out;
113938
- };
113939
- let targets;
113940
- try {
113941
- targets = selectPushTargets(await input.ports.listTargets(), input.policy);
113942
- } catch (err) {
113943
- return fail(`notifiers unavailable: ${err instanceof Error ? err.message : String(err)}`);
113944
- }
113945
- if (targets.length === 0) return fail("no eligible notifier target");
113946
- const sent = new Map(work.map((i) => [i.alertId, [...sentOf(i)]]));
113947
- const errors = new Map(work.map((i) => [i.alertId, []]));
113948
- for (const target of targets) {
113949
- const todo = work.filter((i) => !(sent.get(i.alertId) ?? []).includes(target.id));
113950
- if (todo.length === 0) continue;
113951
- let failure = null;
113952
- try {
113953
- const result = await input.ports.send(target, composeDigest(todo));
113954
- if (!result.success) failure = `${target.id}: ${result.error ?? "send failed"}`;
113955
- } catch (err) {
113956
- failure = `${target.id}: ${err instanceof Error ? err.message : String(err)}`;
113957
- }
113958
- for (const i of todo) if (failure === null) sent.get(i.alertId)?.push(target.id);
113959
- else errors.get(i.alertId)?.push(failure);
113960
- }
113961
- for (const i of work) {
113962
- const sentTo = sent.get(i.alertId) ?? [];
113963
- const complete = targets.every((t) => sentTo.includes(t.id));
113964
- out.set(i.alertId, complete ? {
113965
- state: "delivered",
113966
- key: i.key,
113967
- at: input.now,
113968
- attempts: attemptsOf(i),
113969
- sentTo
113970
- } : {
113971
- state: "pending",
113972
- key: i.key,
113973
- attempts: attemptsOf(i),
113974
- lastError: (errors.get(i.alertId) ?? []).join("; "),
113975
- ...sentTo.length > 0 ? { sentTo } : {}
113976
- });
113977
- }
113978
- return out;
114144
+ var require_phase6_declare_gate = require_phase6_declare_gate_DwYAep7y();
114145
+ function nextEpisodeId(input) {
114146
+ const { previous } = input;
114147
+ if (previous !== null && require_dist10.isAlertEpisodeOpen(previous.status)) {
114148
+ if (input.incomingKey === void 0 || input.incomingKey === previous.episodeKey) return previous.episodeId;
114149
+ }
114150
+ const minted = `${input.alertId}@${String(input.now)}`;
114151
+ return previous !== null && previous.episodeId === minted ? `${minted}+` : minted;
114152
+ }
114153
+ function episodeEdge(previous, next) {
114154
+ const nextOpen = require_dist10.isAlertEpisodeOpen(next.status);
114155
+ const continues = previous !== null && require_dist10.isAlertEpisodeOpen(previous.status) && previous.episodeId === next.episodeId;
114156
+ if (nextOpen) {
114157
+ if (!require_dist10.isAlertEpisodeSeverity(next.severity)) return null;
114158
+ if (!continues || previous === null) return "raised";
114159
+ if (!require_dist10.isAlertEpisodeSeverity(previous.severity)) return "raised";
114160
+ return previous.severity === "error" && next.severity === "critical" ? "raised" : null;
114161
+ }
114162
+ if (previous !== null && require_dist10.isAlertEpisodeOpen(previous.status) && require_dist10.isAlertEpisodeSeverity(previous.severity)) return "resolved";
114163
+ return null;
113979
114164
  }
113980
114165
  function episodeKeyFromMetadata(metadata) {
113981
114166
  const key = metadata?.["episodeKey"];
113982
114167
  return typeof key === "string" ? key : void 0;
113983
114168
  }
113984
- function shouldDropPriorPush(existingStatus) {
113985
- return existingStatus === "completed";
113986
- }
113987
- function alertWithRetainedPush(toStore, existingPush, existingStatus) {
113988
- if (existingPush === void 0 || shouldDropPriorPush(existingStatus)) return toStore;
113989
- return {
113990
- ...toStore,
113991
- metadata: {
113992
- ...toStore.metadata,
113993
- push: existingPush
113994
- }
113995
- };
113996
- }
113997
114169
  function mergeEmitAlert(input) {
113998
114170
  const incomingKey = episodeKeyFromMetadata(input.incoming.metadata);
113999
114171
  const existingKey = input.existing?.episodeKey;
@@ -114001,26 +114173,11 @@ var require_alerts_addon = __commonJS({
114001
114173
  const sameEpisode = incomingKey === void 0 || incomingKey === existingKey;
114002
114174
  const priorCompleted = input.existing.status === "completed";
114003
114175
  if (sameEpisode && !priorCompleted) return {
114004
- alert: {
114005
- ...input.incoming,
114006
- read: true
114007
- },
114008
- queuePush: false
114176
+ ...input.incoming,
114177
+ read: true
114009
114178
  };
114010
114179
  }
114011
- const queuePush = isCriticalPush(input.incoming) && input.incoming.status === "active" && input.incoming.read === false;
114012
- return {
114013
- alert: input.incoming,
114014
- queuePush
114015
- };
114016
- }
114017
- function criticalPushPayload(alert) {
114018
- const key = episodeKeyFromMetadata(alert.metadata) ?? alert.id;
114019
- const pushLine = alert.metadata?.["pushLine"];
114020
- return {
114021
- key,
114022
- line: typeof pushLine === "string" ? pushLine : alert.title
114023
- };
114180
+ return input.incoming;
114024
114181
  }
114025
114182
  function deviceLabelFromEventData(data) {
114026
114183
  const name = stringNonEmpty(data["name"]) ?? stringNonEmpty(data["deviceName"]);
@@ -114130,13 +114287,13 @@ var require_alerts_addon = __commonJS({
114130
114287
  switch (record.state) {
114131
114288
  case "refused":
114132
114289
  return {
114133
- severity: "error",
114290
+ severity: "critical",
114134
114291
  title: "Addon refused to start",
114135
114292
  message: `${where}: refused - ${record.reason ?? "no reason reported"}${record.remedy !== void 0 ? ` ${record.remedy}` : ""}`
114136
114293
  };
114137
114294
  case "failed":
114138
114295
  return {
114139
- severity: "error",
114296
+ severity: "critical",
114140
114297
  title: "Addon crash loop",
114141
114298
  message: `Runner "${where}" gave up${record.crashesInWindow !== void 0 ? ` after ${String(record.crashesInWindow)} crashes` : ""}${record.addonIds.length > 0 ? ` (addons: ${record.addonIds.join(", ")})` : ""} - ${String(record.count)} events`
114142
114299
  };
@@ -114148,22 +114305,6 @@ var require_alerts_addon = __commonJS({
114148
114305
  };
114149
114306
  }
114150
114307
  }
114151
- function isTerminalRunnerState(state) {
114152
- return state === "failed" || state === "refused";
114153
- }
114154
- function hash(text) {
114155
- let h = 5381;
114156
- for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
114157
- return (h >>> 0).toString(36);
114158
- }
114159
- function runnerPushKey(record) {
114160
- const cause = record.state === "refused" ? hash(`${record.reason ?? ""}|${record.remedy ?? ""}`) : "breaker";
114161
- return `runner:${record.state}:${String(record.since)}:${cause}`;
114162
- }
114163
- function runnerPushLine(record) {
114164
- const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
114165
- return record.state === "refused" ? `${where}: refused to start. ${record.remedy ?? "See the Addons page."}` : `${where}: stopped after repeated crashes. Restart it from the Addons page.`;
114166
- }
114167
114308
  var import_read_all_rows = require_read_all_rows$1.require_read_all_rows();
114168
114309
  var ALERTS_COLLECTION = "alerts";
114169
114310
  var AlertBodySchema = require_dist10.AlertSchema.omit({ id: true });
@@ -114455,7 +114596,6 @@ var require_alerts_addon = __commonJS({
114455
114596
  }
114456
114597
  };
114457
114598
  var SILENT_TRACKED_CATEGORIES = [
114458
- "addon.started",
114459
114599
  "process.state-changed",
114460
114600
  "addon.load-recovered",
114461
114601
  "system.liveness-recovered",
@@ -114506,21 +114646,25 @@ var require_alerts_addon = __commonJS({
114506
114646
  }
114507
114647
  var DEFAULT_CONFIG = {
114508
114648
  maxAlerts: 500,
114509
- retentionDays: 30,
114510
- criticalPushEnabled: true,
114511
- criticalPushTargetIds: []
114649
+ retentionDays: 30
114512
114650
  };
114513
- function normalizeCriticalPushTargetIds(raw) {
114514
- if (Array.isArray(raw)) return raw.filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim());
114515
- if (typeof raw === "string" && raw.trim().length > 0) return raw.split(",").map((id) => id.trim()).filter((id) => id.length > 0);
114516
- return [];
114517
- }
114518
- var PUSH_COALESCE_MS = 2e3;
114519
- var PUSH_RECONCILE_ADDONS = /* @__PURE__ */ new Set(["notifiers", "notification-center"]);
114520
114651
  var EVENT_SOURCE = {
114521
114652
  type: "addon",
114522
114653
  id: "alert-center"
114523
114654
  };
114655
+ var ADDONS_PAGE_ACTION = {
114656
+ label: "Open Addons",
114657
+ href: "/system/addons"
114658
+ };
114659
+ function episodeViewOf(alert) {
114660
+ const key = episodeKeyFromMetadata(alert.metadata);
114661
+ return {
114662
+ status: alert.status,
114663
+ severity: alert.severity,
114664
+ episodeId: require_dist10.alertEpisodeId(alert),
114665
+ ...key !== void 0 ? { episodeKey: key } : {}
114666
+ };
114667
+ }
114524
114668
  function toPrunable(record) {
114525
114669
  return {
114526
114670
  id: record.id,
@@ -114531,11 +114675,8 @@ var require_alerts_addon = __commonJS({
114531
114675
  var AlertCenterAddon = class extends require_dist10.BaseAddon {
114532
114676
  alertStore = null;
114533
114677
  unsubscribers = [];
114534
- /** How long a burst of critical findings is gathered into one digest push. */
114535
- pushCoalesceMs;
114536
- constructor(options) {
114678
+ constructor() {
114537
114679
  super({ ...DEFAULT_CONFIG });
114538
- this.pushCoalesceMs = options?.pushCoalesceMs ?? PUSH_COALESCE_MS;
114539
114680
  }
114540
114681
  /**
114541
114682
  * Per `(category, source.id)` counter used to rate-limit chatty
@@ -114566,9 +114707,7 @@ var require_alerts_addon = __commonJS({
114566
114707
  unsubs.push(unsub);
114567
114708
  }
114568
114709
  this.unsubscribers = unsubs;
114569
- this.reconcilePendingPushes();
114570
114710
  this.dismissLegacyStreamCards(void 0);
114571
- this.migrateCriticalPushTargetIds();
114572
114711
  this.ctx.logger.info("Initialized", { meta: { trackedCategoriesCount: TRACKED_CATEGORIES.length } });
114573
114712
  return [{
114574
114713
  capability: require_dist10.alertsCapability,
@@ -114587,46 +114726,29 @@ var require_alerts_addon = __commonJS({
114587
114726
  description: "Configure alert storage and retention.",
114588
114727
  descriptionKey: "server:globalSettings.s_alerts.description",
114589
114728
  columns: 1,
114590
- fields: [
114591
- this.field({
114592
- type: "number",
114593
- key: "maxAlerts",
114594
- label: "Maximum stored alerts",
114595
- labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
114596
- description: "Oldest read alerts are deleted when this limit is exceeded.",
114597
- descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
114598
- min: 50,
114599
- max: 5e3,
114600
- step: 50,
114601
- default: 500
114602
- }),
114603
- this.field({
114604
- type: "number",
114605
- key: "retentionDays",
114606
- label: "Alert retention (days)",
114607
- labelKey: "server:globalSettings.s_alerts.retentionDays.label",
114608
- description: "Alerts older than this are automatically removed.",
114609
- descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
114610
- min: 1,
114611
- max: 365,
114612
- step: 1,
114613
- default: 30
114614
- }),
114615
- this.field({
114616
- type: "boolean",
114617
- key: "criticalPushEnabled",
114618
- label: "Push critical system alerts",
114619
- description: "A runner that failed or refused to start, or a node whose runners are degraded, is pushed straight to the notifier targets \u2014 even when Notification Center is down. This path does not use notification rules.",
114620
- default: true
114621
- }),
114622
- this.field({
114623
- type: "notification-target-multiselect",
114624
- key: "criticalPushTargetIds",
114625
- label: "Critical push targets",
114626
- description: "Notifier targets that receive critical system alerts. Empty = every enabled shared (not personal) target.",
114627
- default: []
114628
- })
114629
- ]
114729
+ fields: [this.field({
114730
+ type: "number",
114731
+ key: "maxAlerts",
114732
+ label: "Maximum stored alerts",
114733
+ labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
114734
+ description: "Oldest read alerts are deleted when this limit is exceeded.",
114735
+ descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
114736
+ min: 50,
114737
+ max: 5e3,
114738
+ step: 50,
114739
+ default: 500
114740
+ }), this.field({
114741
+ type: "number",
114742
+ key: "retentionDays",
114743
+ label: "Alert retention (days)",
114744
+ labelKey: "server:globalSettings.s_alerts.retentionDays.label",
114745
+ description: "Alerts older than this are automatically removed.",
114746
+ descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
114747
+ min: 1,
114748
+ max: 365,
114749
+ step: 1,
114750
+ default: 30
114751
+ })]
114630
114752
  }] });
114631
114753
  }
114632
114754
  async listAlerts(filter) {
@@ -114665,7 +114787,17 @@ var require_alerts_addon = __commonJS({
114665
114787
  updatedAt: Date.now()
114666
114788
  });
114667
114789
  }
114668
- async handleEvent(category, event) {
114790
+ /**
114791
+ * Events are handled one at a time: every handler is a read-modify-write of
114792
+ * an alert, and two concurrent events about one runner would both read "no
114793
+ * alert yet" and open two episodes for it.
114794
+ */
114795
+ eventChain = Promise.resolve();
114796
+ handleEvent(category, event) {
114797
+ this.eventChain = this.eventChain.then(() => this.processEvent(category, event));
114798
+ return this.eventChain;
114799
+ }
114800
+ async processEvent(category, event) {
114669
114801
  try {
114670
114802
  if (category === "model.download.progress") {
114671
114803
  await this.handleModelDownload(event);
@@ -114683,11 +114815,6 @@ var require_alerts_addon = __commonJS({
114683
114815
  await this.handleRunnerCheck(event);
114684
114816
  return;
114685
114817
  }
114686
- if (category === require_dist10.EventCategory.AddonStarted) {
114687
- const addonId = event.data["addonId"];
114688
- if (typeof addonId === "string" && PUSH_RECONCILE_ADDONS.has(addonId)) await this.reconcilePendingPushes();
114689
- return;
114690
- }
114691
114818
  if (category === require_dist10.EventCategory.AddonLoadFailed) {
114692
114819
  await this.handleAddonLoadFailed(event);
114693
114820
  return;
@@ -114788,11 +114915,8 @@ var require_alerts_addon = __commonJS({
114788
114915
  type: "runner",
114789
114916
  id: record.runnerId
114790
114917
  },
114791
- metadata: {
114792
- runner: record,
114793
- pushLine: runnerPushLine(record),
114794
- ...existing?.push !== void 0 ? { push: existing.push } : {}
114795
- }
114918
+ action: ADDONS_PAGE_ACTION,
114919
+ metadata: { runner: record }
114796
114920
  };
114797
114921
  await this.persistAlert(alert);
114798
114922
  if (previous === null) this.emitAlertCreatedEvent(alert);
@@ -114801,17 +114925,11 @@ var require_alerts_addon = __commonJS({
114801
114925
  title: alert.title
114802
114926
  });
114803
114927
  await this.enforceMaxAlerts();
114804
- if (isTerminalRunnerState(record.state)) this.queuePush({
114805
- alertId,
114806
- key: runnerPushKey(record),
114807
- title: rendered.title,
114808
- line: runnerPushLine(record)
114809
- });
114810
114928
  }
114811
114929
  /**
114812
114930
  * A runner reached `running`: its episode is over. Close the runner alert so a
114813
- * later failure opens a NEW episode (new push key), instead of folding into a
114814
- * stale `refused` record that would never push again.
114931
+ * later failure opens a NEW episode, instead of folding into a stale
114932
+ * `refused` record whose notification was already sent.
114815
114933
  */
114816
114934
  async handleRunnerStateChanged(event) {
114817
114935
  if (event.data["state"] !== "running") return;
@@ -114837,24 +114955,20 @@ var require_alerts_addon = __commonJS({
114837
114955
  }) : [];
114838
114956
  const failedIds = ids(event.data["failed"]);
114839
114957
  const missingIds = ids(event.data["missing"]);
114840
- const pushLine = `${nodeId}: runners degraded - ${[
114958
+ const parts = [
114841
114959
  ...failedIds.length > 0 ? [`failed: ${failedIds.join(", ")}`] : [],
114842
114960
  ...refusedIds.length > 0 ? [`refused: ${refusedIds.join(", ")}`] : [],
114843
114961
  ...missingIds.length > 0 ? [`missing: ${missingIds.join(", ")}`] : []
114844
- ].join("; ")}. See the Addons page.`;
114845
- const pushKey = `check:${[
114846
- ...failedIds,
114847
- ...refusedIds,
114848
- ...missingIds
114849
- ].sort().join(",")}`;
114962
+ ];
114850
114963
  const now = Date.now();
114851
114964
  const existing = await this.readAlertRaw(alertId);
114852
114965
  const alert = {
114853
114966
  id: alertId,
114854
114967
  category: "system.runner-check",
114855
- severity: "error",
114968
+ severity: "critical",
114856
114969
  title: "Runners degraded",
114857
- message: `${nodeId}: ${summary}`,
114970
+ message: `${nodeId}: ${summary}${parts.length > 0 ? ` (${parts.join("; ")})` : ""}`,
114971
+ action: ADDONS_PAGE_ACTION,
114858
114972
  status: "active",
114859
114973
  read: false,
114860
114974
  createdAt: existing?.createdAt ?? now,
@@ -114867,19 +114981,11 @@ var require_alerts_addon = __commonJS({
114867
114981
  nodeId,
114868
114982
  failed: event.data["failed"],
114869
114983
  refused: event.data["refused"],
114870
- missing: event.data["missing"],
114871
- pushLine,
114872
- ...existing?.push !== void 0 ? { push: existing.push } : {}
114984
+ missing: event.data["missing"]
114873
114985
  }
114874
114986
  };
114875
114987
  await this.persistAlert(alert);
114876
114988
  this.emitAlertCreatedEvent(alert);
114877
- this.queuePush({
114878
- alertId,
114879
- key: pushKey,
114880
- title: alert.title,
114881
- line: pushLine
114882
- });
114883
114989
  }
114884
114990
  async readAlertRaw(alertId) {
114885
114991
  const store = this.alertStore;
@@ -114888,155 +114994,19 @@ var require_alerts_addon = __commonJS({
114888
114994
  if (raw === null) return null;
114889
114995
  const rec = Object.fromEntries(Object.entries(raw));
114890
114996
  const meta = rec["metadata"];
114891
- const push = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta["push"] : void 0;
114892
114997
  const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : void 0;
114998
+ const parsed = require_dist10.AlertSchema.safeParse({
114999
+ id: alertId,
115000
+ ...rec
115001
+ });
114893
115002
  return {
114894
115003
  ...typeof rec["createdAt"] === "number" ? { createdAt: rec["createdAt"] } : {},
114895
115004
  ...typeof rec["read"] === "boolean" ? { read: rec["read"] } : {},
114896
115005
  ...typeof rec["status"] === "string" ? { status: rec["status"] } : {},
114897
- ...isPushState(push) ? { push } : {},
114898
- ...episodeKeyFromMetadata(metadata) !== void 0 ? { episodeKey: episodeKeyFromMetadata(metadata) } : {}
114899
- };
114900
- }
114901
- /** Pushes run one at a time so a burst can never send the same state twice. */
114902
- pushChain = Promise.resolve();
114903
- /** Findings waiting for the next digest, by alert id. */
114904
- pushQueue = /* @__PURE__ */ new Map();
114905
- pushTimer = null;
114906
- pushPolicy() {
114907
- return {
114908
- enabled: this.config.criticalPushEnabled,
114909
- targetIds: normalizeCriticalPushTargetIds(this.config.criticalPushTargetIds)
114910
- };
114911
- }
114912
- /** Rewrite a legacy CSV `criticalPushTargetIds` string into `string[]`. */
114913
- async migrateCriticalPushTargetIds() {
114914
- const raw = this.config.criticalPushTargetIds;
114915
- if (typeof raw !== "string") return;
114916
- try {
114917
- await this.updateGlobalSettings({ criticalPushTargetIds: normalizeCriticalPushTargetIds(raw) });
114918
- } catch (err) {
114919
- this.ctx.logger.warn("criticalPushTargetIds CSV migration failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
114920
- }
114921
- }
114922
- pushPorts() {
114923
- const api = this.ctx.api;
114924
- return {
114925
- listTargets: async () => {
114926
- return (await api.notificationOutput.listTargets.query({})).map((t) => {
114927
- const owner = t.config["ownerUserId"];
114928
- return {
114929
- id: t.id,
114930
- addonId: t.addonId,
114931
- enabled: t.enabled,
114932
- ...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {}
114933
- };
114934
- });
114935
- },
114936
- send: async (target, message) => api.notificationOutput.send.mutate({
114937
- addonId: target.addonId,
114938
- targetId: target.id,
114939
- notification: {
114940
- title: message.title,
114941
- body: message.body,
114942
- format: "text",
114943
- priority: 5
114944
- }
114945
- })
115006
+ ...episodeKeyFromMetadata(metadata) !== void 0 ? { episodeKey: episodeKeyFromMetadata(metadata) } : {},
115007
+ ...parsed.success ? { episode: episodeViewOf(parsed.data) } : {}
114946
115008
  };
114947
115009
  }
114948
- /**
114949
- * Queue a critical finding for the next digest. A burst (every runner of a boot)
114950
- * arrives within the coalesce window and leaves as ONE message per target.
114951
- */
114952
- queuePush(item) {
114953
- this.pushQueue.set(item.alertId, item);
114954
- if (this.pushTimer !== null) return;
114955
- this.pushTimer = setTimeout(() => {
114956
- this.pushTimer = null;
114957
- this.flushPushes();
114958
- }, this.pushCoalesceMs);
114959
- this.pushTimer.unref?.();
114960
- }
114961
- /** Send the queued findings now (also the reconcile path). */
114962
- flushPushes() {
114963
- this.pushChain = this.pushChain.then(async () => {
114964
- const queued = [...this.pushQueue.values()];
114965
- this.pushQueue.clear();
114966
- if (queued.length === 0) return;
114967
- try {
114968
- const items = [];
114969
- for (const q of queued) {
114970
- const existing = await this.readAlertRaw(q.alertId);
114971
- items.push({
114972
- ...q,
114973
- previous: existing?.push ?? null
114974
- });
114975
- }
114976
- const states = await deliverDigest({
114977
- items,
114978
- policy: this.pushPolicy(),
114979
- ports: this.pushPorts(),
114980
- now: Date.now()
114981
- });
114982
- for (const [alertId, state] of states) {
114983
- await this.storePushState(alertId, state);
114984
- this.ctx.logger?.info("critical alert push", { meta: {
114985
- alertId,
114986
- state: state.state,
114987
- attempts: state.attempts,
114988
- error: state.lastError
114989
- } });
114990
- }
114991
- } catch (err) {
114992
- this.ctx.logger?.warn("critical alert push failed unexpectedly", { meta: { error: require_dist10.errMsg(err) } });
114993
- }
114994
- });
114995
- return this.pushChain;
114996
- }
114997
- async storePushState(alertId, push) {
114998
- const store = this.alertStore;
114999
- if (store === null) return;
115000
- const raw = await store.getData(alertId);
115001
- if (raw === null) return;
115002
- const meta = raw["metadata"];
115003
- const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
115004
- await store.mergeUpdate(alertId, { metadata: {
115005
- ...metadata,
115006
- push
115007
- } });
115008
- }
115009
- /**
115010
- * Retry every critical alert whose push is still pending - at boot and when
115011
- * the notifiers or notification-center start. An alert whose state was already
115012
- * delivered is skipped by its key, so reconciling never sends twice.
115013
- */
115014
- async reconcilePendingPushes() {
115015
- const store = this.alertStore;
115016
- if (store === null) return;
115017
- try {
115018
- const rows = await store.listRecent(500);
115019
- for (const row of rows) {
115020
- const parsed = require_dist10.AlertSchema.safeParse({
115021
- id: row.id,
115022
- ...row.data
115023
- });
115024
- if (!parsed.success || parsed.data.status !== "active") continue;
115025
- if (!isCriticalPush(parsed.data)) continue;
115026
- const push = parsed.data.metadata?.["push"];
115027
- if (!isPushState(push) || push.state !== "pending") continue;
115028
- this.pushQueue.set(parsed.data.id, {
115029
- alertId: parsed.data.id,
115030
- key: push.key,
115031
- title: parsed.data.title,
115032
- line: String(parsed.data.metadata?.["pushLine"] ?? parsed.data.title)
115033
- });
115034
- }
115035
- await this.flushPushes();
115036
- } catch (err) {
115037
- this.ctx.logger?.warn("pending critical push reconcile failed", { meta: { error: require_dist10.errMsg(err) } });
115038
- }
115039
- }
115040
115010
  async handleModelDownload(event) {
115041
115011
  const data = event.data;
115042
115012
  const modelId = data.modelId ?? "unknown";
@@ -115383,7 +115353,7 @@ var require_alerts_addon = __commonJS({
115383
115353
  }
115384
115354
  async emitAlert(alert) {
115385
115355
  const existingRaw = await this.readAlertRaw(alert.id);
115386
- const { alert: toStore, queuePush } = mergeEmitAlert({
115356
+ const toStore = mergeEmitAlert({
115387
115357
  existing: existingRaw === null ? null : {
115388
115358
  read: existingRaw.read,
115389
115359
  episodeKey: existingRaw.episodeKey,
@@ -115391,20 +115361,10 @@ var require_alerts_addon = __commonJS({
115391
115361
  },
115392
115362
  incoming: alert
115393
115363
  });
115394
- const stored = alertWithRetainedPush(toStore, existingRaw?.push, existingRaw?.status);
115395
115364
  const isNew = existingRaw === null;
115396
- await this.persistAlert(stored);
115365
+ const stored = await this.persistAlert(toStore);
115397
115366
  if (isNew) this.emitAlertCreatedEvent(stored);
115398
115367
  else this.emitAlertUpdatedEvent(alert.id, stored);
115399
- if (queuePush) {
115400
- const { key, line } = criticalPushPayload(stored);
115401
- this.queuePush({
115402
- alertId: stored.id,
115403
- key,
115404
- title: stored.title,
115405
- line
115406
- });
115407
- }
115408
115368
  await this.enforceMaxAlerts();
115409
115369
  await this.enforceRetention();
115410
115370
  }
@@ -115419,15 +115379,73 @@ var require_alerts_addon = __commonJS({
115419
115379
  * Read-merge-write: SettingsBackend.update() replaces the entire `data` blob,
115420
115380
  * so we must read the existing record, merge the patch, and write back.
115421
115381
  */
115422
- async mergeUpdate(id, patch) {
115382
+ /** Alert writes (stamp + store + announce) from any caller, one at a time. */
115383
+ writeChain = Promise.resolve();
115384
+ serializedWrite(write) {
115385
+ const run = this.writeChain.then(write, write);
115386
+ this.writeChain = run.catch(() => void 0);
115387
+ return run;
115388
+ }
115389
+ mergeUpdate(id, patch) {
115390
+ return this.serializedWrite(() => this.mergeUpdateNow(id, patch));
115391
+ }
115392
+ async mergeUpdateNow(id, patch) {
115423
115393
  const store = this.alertStore;
115424
115394
  if (store === null) return;
115425
- await store.mergeUpdate(id, patch);
115395
+ const previous = (await this.readAlertRaw(id))?.episode ?? null;
115396
+ const metadata = patch["metadata"];
115397
+ const keepEpisode = previous !== null && typeof metadata === "object" && metadata !== null ? { metadata: {
115398
+ ...metadata,
115399
+ episodeId: previous.episodeId
115400
+ } } : {};
115401
+ await store.mergeUpdate(id, {
115402
+ ...patch,
115403
+ ...keepEpisode
115404
+ });
115405
+ const after = await store.getData(id);
115406
+ const parsed = after === null ? null : require_dist10.AlertSchema.safeParse({
115407
+ id,
115408
+ ...after
115409
+ });
115410
+ if (parsed?.success === true) this.announceEpisode(previous, parsed.data);
115411
+ }
115412
+ /**
115413
+ * Store an alert, stamping its episode identity. The stored alert is what a
115414
+ * caller must use afterwards, and the opening or closing of a critical/error
115415
+ * episode is announced here, in the one place every write passes through.
115416
+ */
115417
+ persistAlert(alert) {
115418
+ return this.serializedWrite(() => this.persistAlertNow(alert));
115426
115419
  }
115427
- async persistAlert(alert) {
115420
+ async persistAlertNow(alert) {
115428
115421
  const store = this.alertStore;
115429
- if (store === null) return;
115430
- await store.persist(alert);
115422
+ if (store === null) return alert;
115423
+ const previous = (await this.readAlertRaw(alert.id))?.episode ?? null;
115424
+ const episodeId = nextEpisodeId({
115425
+ alertId: alert.id,
115426
+ previous,
115427
+ incomingKey: episodeKeyFromMetadata(alert.metadata),
115428
+ now: Date.now()
115429
+ });
115430
+ const stamped = {
115431
+ ...alert,
115432
+ metadata: {
115433
+ ...alert.metadata,
115434
+ episodeId
115435
+ }
115436
+ };
115437
+ await store.persist(stamped);
115438
+ this.announceEpisode(previous, stamped);
115439
+ return stamped;
115440
+ }
115441
+ /**
115442
+ * Announce an episode edge on the bus. Telemetry, and it may be dropped
115443
+ * (D8): Notification Center reconciles from the alerts store (D11, D49).
115444
+ */
115445
+ announceEpisode(previous, alert) {
115446
+ const edge = episodeEdge(previous, episodeViewOf(alert));
115447
+ if (edge === null) return;
115448
+ this.ctx.eventBus?.emit(require_dist10.createEvent(require_dist10.EventCategory.AlertEpisode, EVENT_SOURCE, require_dist10.alertEpisodePayload(alert, edge, Date.now())));
115431
115449
  }
115432
115450
  emitAlertCreatedEvent(alert) {
115433
115451
  this.ctx.eventBus?.emit(require_dist10.createEvent("alert.created", EVENT_SOURCE, {
@@ -115528,7 +115546,7 @@ var require_calendars_addon = __commonJS({
115528
115546
  [Symbol.toStringTag]: { value: "Module" }
115529
115547
  });
115530
115548
  require_chunk_Cek0wNdY();
115531
- var require_dist10 = require_dist_DyjNaeIj();
115549
+ var require_dist10 = require_dist_Bi4Ioxop();
115532
115550
  var require_settle_sources = require_settle_sources_Bhsy57y();
115533
115551
  var zod = require_zod();
115534
115552
  var CALENDAR_COLOR_PALETTE = [
@@ -115746,11 +115764,11 @@ var require_calendars = __commonJS({
115746
115764
  }
115747
115765
  });
115748
115766
 
115749
- // ../system/dist/composition-sources-DwEvAg5Z.js
115750
- var require_composition_sources_DwEvAg5Z = __commonJS({
115751
- "../system/dist/composition-sources-DwEvAg5Z.js"(exports) {
115767
+ // ../system/dist/composition-sources-Cukc78eu.js
115768
+ var require_composition_sources_Cukc78eu = __commonJS({
115769
+ "../system/dist/composition-sources-Cukc78eu.js"(exports) {
115752
115770
  "use strict";
115753
- var require_dist10 = require_dist_DyjNaeIj();
115771
+ var require_dist10 = require_dist_Bi4Ioxop();
115754
115772
  function normalizedNativeOwner(owner) {
115755
115773
  const trimmed = owner?.trim();
115756
115774
  return trimmed ? trimmed : null;
@@ -115865,8 +115883,8 @@ var require_composer_addon = __commonJS({
115865
115883
  [Symbol.toStringTag]: { value: "Module" }
115866
115884
  });
115867
115885
  require_chunk_Cek0wNdY();
115868
- var require_dist10 = require_dist_DyjNaeIj();
115869
- var require_composition_sources = require_composition_sources_DwEvAg5Z();
115886
+ var require_dist10 = require_dist_Bi4Ioxop();
115887
+ var require_composition_sources = require_composition_sources_Cukc78eu();
115870
115888
  var zod = require_zod();
115871
115889
  function isRecord$2(value) {
115872
115890
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -119676,7 +119694,7 @@ var require_console_logging = __commonJS({
119676
119694
  [Symbol.toStringTag]: { value: "Module" }
119677
119695
  });
119678
119696
  require_chunk_Cek0wNdY();
119679
- var require_dist10 = require_dist_DyjNaeIj();
119697
+ var require_dist10 = require_dist_Bi4Ioxop();
119680
119698
  var require_formatter = require_formatter_DqAKDlvN();
119681
119699
  var LEVEL_RANK = {
119682
119700
  debug: 0,
@@ -124069,9 +124087,9 @@ var require_core_blocks_addon = __commonJS({
124069
124087
  "use strict";
124070
124088
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
124071
124089
  var require_chunk = require_chunk_Cek0wNdY();
124072
- var require_dist10 = require_dist_DyjNaeIj();
124090
+ var require_dist10 = require_dist_Bi4Ioxop();
124073
124091
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
124074
- var require_composition_sources = require_composition_sources_DwEvAg5Z();
124092
+ var require_composition_sources = require_composition_sources_Cukc78eu();
124075
124093
  var require_prefix_range = require_prefix_range_D_KueXbQ();
124076
124094
  var require_prefix_walk = require_prefix_walk_Dv6MLZlM();
124077
124095
  var zod = require_zod();
@@ -126271,9 +126289,9 @@ var require_core_blocks = __commonJS({
126271
126289
  }
126272
126290
  });
126273
126291
 
126274
- // ../types/dist/event-category-Hoe-mqbd.js
126275
- var require_event_category_Hoe_mqbd = __commonJS({
126276
- "../types/dist/event-category-Hoe-mqbd.js"(exports) {
126292
+ // ../types/dist/event-category-eSqkye-K.js
126293
+ var require_event_category_eSqkye_K = __commonJS({
126294
+ "../types/dist/event-category-eSqkye-K.js"(exports) {
126277
126295
  "use strict";
126278
126296
  var EventCategory = /* @__PURE__ */ (function(EventCategory2) {
126279
126297
  EventCategory2["SystemBoot"] = "system.boot";
@@ -126294,6 +126312,7 @@ var require_event_category_Hoe_mqbd = __commonJS({
126294
126312
  EventCategory2["AddonRunnerRefused"] = "addon.runner-refused";
126295
126313
  EventCategory2["ProcessStateChanged"] = "process.state-changed";
126296
126314
  EventCategory2["SystemRunnerCheck"] = "system.runner-check";
126315
+ EventCategory2["AlertEpisode"] = "alert.episode";
126297
126316
  EventCategory2["AddonError"] = "addon.error";
126298
126317
  EventCategory2["AddonPageReady"] = "addon.page-ready";
126299
126318
  EventCategory2["AddonWidgetReady"] = "addon.widget-ready";
@@ -126475,11 +126494,11 @@ var require_event_category_Hoe_mqbd = __commonJS({
126475
126494
  }
126476
126495
  });
126477
126496
 
126478
- // ../types/dist/sleep-REHQvLV6.js
126479
- var require_sleep_REHQvLV6 = __commonJS({
126480
- "../types/dist/sleep-REHQvLV6.js"(exports) {
126497
+ // ../types/dist/sleep-BE7_6UnC.js
126498
+ var require_sleep_BE7_6UnC = __commonJS({
126499
+ "../types/dist/sleep-BE7_6UnC.js"(exports) {
126481
126500
  "use strict";
126482
- var require_event_category = require_event_category_Hoe_mqbd();
126501
+ var require_event_category = require_event_category_eSqkye_K();
126483
126502
  var zod = require_zod();
126484
126503
  var AUDIO_CHUNK_FORMATS = [
126485
126504
  "f32le",
@@ -131733,6 +131752,7 @@ var require_sleep_REHQvLV6 = __commonJS({
131733
131752
  const dishwasherInterface = {
131734
131753
  start: (input) => dispatch("dishwasher", "dishwasher", "start", "mutation", input),
131735
131754
  pause: (input) => dispatch("dishwasher", "dishwasher", "pause", "mutation", input),
131755
+ resume: (input) => dispatch("dishwasher", "dishwasher", "resume", "mutation", input),
131736
131756
  stop: (input) => dispatch("dishwasher", "dishwasher", "stop", "mutation", input),
131737
131757
  setProgram: (input) => dispatch("dishwasher", "dishwasher", "setProgram", "mutation", input),
131738
131758
  setPower: (input) => dispatch("dishwasher", "dishwasher", "setPower", "mutation", input),
@@ -131743,6 +131763,7 @@ var require_sleep_REHQvLV6 = __commonJS({
131743
131763
  setVarioSpeedPlus: (input) => dispatch("dishwasher", "dishwasher", "setVarioSpeedPlus", "mutation", input),
131744
131764
  setChildLock: (input) => dispatch("dishwasher", "dishwasher", "setChildLock", "mutation", input),
131745
131765
  setOption: (input) => dispatch("dishwasher", "dishwasher", "setOption", "mutation", input),
131766
+ getOptions: (input) => dispatch("dishwasher", "dishwasher", "getOptions", "query", input),
131746
131767
  getStatus: (input) => dispatch("dishwasher", "dishwasher", "getStatus", "query", input)
131747
131768
  };
131748
131769
  const doorbellInterface = { getStatus: (input) => dispatch("doorbell", "doorbell", "getStatus", "query", input) };
@@ -134040,8 +134061,8 @@ var require_addon = __commonJS({
134040
134061
  "../types/dist/addon.js"(exports) {
134041
134062
  "use strict";
134042
134063
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
134043
- var require_sleep = require_sleep_REHQvLV6();
134044
- var require_event_category = require_event_category_Hoe_mqbd();
134064
+ var require_sleep = require_sleep_BE7_6UnC();
134065
+ var require_event_category = require_event_category_eSqkye_K();
134045
134066
  var require_err_msg = require_err_msg_BhVbr0NX();
134046
134067
  var CAP_INPUT_DEFAULTS = Object.freeze({
134047
134068
  "addons": { "getLogs": { "limit": 100 } },
@@ -134945,6 +134966,15 @@ var require_addon = __commonJS({
134945
134966
  "valueType": "boolean"
134946
134967
  }],
134947
134968
  "dishwasher": [
134969
+ {
134970
+ "path": "availability",
134971
+ "valueType": "enum",
134972
+ "enumValues": [
134973
+ "available",
134974
+ "offline",
134975
+ "description-missing"
134976
+ ]
134977
+ },
134948
134978
  {
134949
134979
  "path": "state",
134950
134980
  "valueType": "enum",
@@ -134957,12 +134987,35 @@ var require_addon = __commonJS({
134957
134987
  "unavailable"
134958
134988
  ]
134959
134989
  },
134990
+ {
134991
+ "path": "operationState",
134992
+ "valueType": "enum",
134993
+ "enumValues": [
134994
+ "Inactive",
134995
+ "Ready",
134996
+ "DelayedStart",
134997
+ "Run",
134998
+ "Pause",
134999
+ "ActionRequired",
135000
+ "Finished",
135001
+ "Error",
135002
+ "Aborting"
135003
+ ]
135004
+ },
134960
135005
  {
134961
135006
  "path": "program",
134962
135007
  "valueType": "string"
134963
135008
  },
134964
135009
  {
134965
- "path": "remainingMinutes",
135010
+ "path": "selectedProgram",
135011
+ "valueType": "string"
135012
+ },
135013
+ {
135014
+ "path": "remainingSeconds",
135015
+ "valueType": "number"
135016
+ },
135017
+ {
135018
+ "path": "delayedStartSeconds",
134966
135019
  "valueType": "number"
134967
135020
  },
134968
135021
  {
@@ -134973,14 +135026,30 @@ var require_addon = __commonJS({
134973
135026
  "path": "doorOpen",
134974
135027
  "valueType": "boolean"
134975
135028
  },
135029
+ {
135030
+ "path": "doorLocked",
135031
+ "valueType": "boolean"
135032
+ },
134976
135033
  {
134977
135034
  "path": "remoteStartAllowed",
134978
135035
  "valueType": "boolean"
134979
135036
  },
135037
+ {
135038
+ "path": "remoteControlActive",
135039
+ "valueType": "boolean"
135040
+ },
135041
+ {
135042
+ "path": "localControlActive",
135043
+ "valueType": "boolean"
135044
+ },
134980
135045
  {
134981
135046
  "path": "powerOn",
134982
135047
  "valueType": "boolean"
134983
135048
  },
135049
+ {
135050
+ "path": "powerState",
135051
+ "valueType": "string"
135052
+ },
134984
135053
  {
134985
135054
  "path": "saltLevel",
134986
135055
  "valueType": "enum",
@@ -136638,7 +136707,7 @@ var require_device_manager_addon = __commonJS({
136638
136707
  [Symbol.toStringTag]: { value: "Module" }
136639
136708
  });
136640
136709
  require_chunk_Cek0wNdY();
136641
- var require_dist10 = require_dist_DyjNaeIj();
136710
+ var require_dist10 = require_dist_Bi4Ioxop();
136642
136711
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
136643
136712
  var require_prefix_range = require_prefix_range_D_KueXbQ();
136644
136713
  var require_prefix_walk = require_prefix_walk_Dv6MLZlM();
@@ -145712,7 +145781,7 @@ var require_hub_forwarder = __commonJS({
145712
145781
  [Symbol.toStringTag]: { value: "Module" }
145713
145782
  });
145714
145783
  require_chunk_Cek0wNdY();
145715
- var require_dist10 = require_dist_DyjNaeIj();
145784
+ var require_dist10 = require_dist_Bi4Ioxop();
145716
145785
  var require_formatter = require_formatter_DqAKDlvN();
145717
145786
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
145718
145787
  var HubForwarderDestination = class {
@@ -145849,7 +145918,7 @@ var require_liveness_monitor_addon = __commonJS({
145849
145918
  "use strict";
145850
145919
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
145851
145920
  require_chunk_Cek0wNdY();
145852
- var require_dist10 = require_dist_DyjNaeIj();
145921
+ var require_dist10 = require_dist_Bi4Ioxop();
145853
145922
  var NO_DEVICES = "liveness:no-devices";
145854
145923
  var ALL_OFFLINE = "liveness:all-devices-offline";
145855
145924
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -146212,12 +146281,12 @@ var require_local_auth_addon = __commonJS({
146212
146281
  [Symbol.toStringTag]: { value: "Module" }
146213
146282
  });
146214
146283
  var require_chunk = require_chunk_Cek0wNdY();
146215
- var require_dist10 = require_dist_DyjNaeIj();
146284
+ var require_dist10 = require_dist_Bi4Ioxop();
146216
146285
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
146217
146286
  var require_safe_buffer$1 = require_safe_buffer_D3aoa_7X();
146218
146287
  var require_ms$1 = require_ms_LB3giGoU();
146219
- var require_device_ca = require_device_ca_njpTMpJC();
146220
- var require_phase6_declare_gate = require_phase6_declare_gate_BlTDMI3t();
146288
+ var require_device_ca = require_device_ca_B3hUVfYk();
146289
+ var require_phase6_declare_gate = require_phase6_declare_gate_DwYAep7y();
146221
146290
  var zod = require_zod();
146222
146291
  var node_crypto = __require("crypto");
146223
146292
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -157012,7 +157081,7 @@ var require_loki_logging = __commonJS({
157012
157081
  [Symbol.toStringTag]: { value: "Module" }
157013
157082
  });
157014
157083
  require_chunk_Cek0wNdY();
157015
- var require_dist10 = require_dist_DyjNaeIj();
157084
+ var require_dist10 = require_dist_Bi4Ioxop();
157016
157085
  function sanitizeLabelName(raw) {
157017
157086
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
157018
157087
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -157684,7 +157753,7 @@ var require_native_metrics_addon = __commonJS({
157684
157753
  [Symbol.toStringTag]: { value: "Module" }
157685
157754
  });
157686
157755
  var require_chunk = require_chunk_Cek0wNdY();
157687
- var require_dist10 = require_dist_DyjNaeIj();
157756
+ var require_dist10 = require_dist_Bi4Ioxop();
157688
157757
  var node_fs_promises = __require("fs/promises");
157689
157758
  var node_util = __require("util");
157690
157759
  var node_os = __require("os");
@@ -160078,7 +160147,7 @@ var require_device_history_addon = __commonJS({
160078
160147
  [Symbol.toStringTag]: { value: "Module" }
160079
160148
  });
160080
160149
  require_chunk_Cek0wNdY();
160081
- var require_dist10 = require_dist_DyjNaeIj();
160150
+ var require_dist10 = require_dist_Bi4Ioxop();
160082
160151
  function subjectIdOf(subject) {
160083
160152
  return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
160084
160153
  }
@@ -162757,7 +162826,7 @@ var require_filesystem_storage_addon = __commonJS({
162757
162826
  [Symbol.toStringTag]: { value: "Module" }
162758
162827
  });
162759
162828
  var require_chunk = require_chunk_Cek0wNdY();
162760
- var require_dist10 = require_dist_DyjNaeIj();
162829
+ var require_dist10 = require_dist_Bi4Ioxop();
162761
162830
  var node_crypto = __require("crypto");
162762
162831
  var node_fs_promises = __require("fs/promises");
162763
162832
  var node_path = __require("path");
@@ -163867,12 +163936,12 @@ var require_lib = __commonJS({
163867
163936
  }
163868
163937
  });
163869
163938
 
163870
- // ../system/dist/wal-maintenance-i3GRvmQW.js
163871
- var require_wal_maintenance_i3GRvmQW = __commonJS({
163872
- "../system/dist/wal-maintenance-i3GRvmQW.js"(exports) {
163939
+ // ../system/dist/wal-maintenance-Bo_lo3Dh.js
163940
+ var require_wal_maintenance_Bo_lo3Dh = __commonJS({
163941
+ "../system/dist/wal-maintenance-Bo_lo3Dh.js"(exports) {
163873
163942
  "use strict";
163874
163943
  var require_chunk = require_chunk_Cek0wNdY();
163875
- var require_dist10 = require_dist_DyjNaeIj();
163944
+ var require_dist10 = require_dist_Bi4Ioxop();
163876
163945
  var require_prefix_range = require_prefix_range_D_KueXbQ();
163877
163946
  var node_crypto = __require("crypto");
163878
163947
  var node_module = __require("module");
@@ -164810,8 +164879,41 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
164810
164879
  if (this.schemaReady) {
164811
164880
  ensureRecordsSchema(db);
164812
164881
  for (const row of loadKinds(db)) this.kinds.set(keyOf(row.owner, row.kind), row);
164882
+ this.correctSequences();
164883
+ }
164884
+ }
164885
+ /**
164886
+ * Boot correction: every kind that ever allocated an id has its counter raised
164887
+ * above the highest numeric id it holds. Adopted legacy rows (or any caller
164888
+ * chosen numeric id) kept ids far above a counter that started at 1, and the
164889
+ * upsert would have overwritten another device's row on the first collision.
164890
+ * Runs on the engine's own connection, over the (kind_id, id) unique index.
164891
+ */
164892
+ correctSequences() {
164893
+ const seq = this.db.prepare("SELECT kind_id FROM _record_sequences");
164894
+ for (const { kind_id: kindId } of seq.all()) {
164895
+ const top = this.db.prepare("SELECT MAX(CAST(id AS INTEGER)) AS m FROM records WHERE kind_id = ? AND id GLOB '[0-9]*'").get(kindId);
164896
+ if (top?.m === null || top === void 0) continue;
164897
+ if (this.db.prepare("UPDATE _record_sequences SET next = ? WHERE kind_id = ? AND next < ?").run(top.m + 1, kindId, top.m + 1).changes > 0) this.options.logger.warn("records: id sequence raised above the highest stored id", { meta: {
164898
+ kindId,
164899
+ next: top.m + 1
164900
+ } });
164813
164901
  }
164814
164902
  }
164903
+ /** Ids handed out by allocateId and not yet written: their first write is insert-only. */
164904
+ pendingAllocated = /* @__PURE__ */ new Set();
164905
+ static PENDING_ALLOCATED_MAX = 4096;
164906
+ /** Raise a kind's counter above the numeric ids of a caller-supplied batch (adoption). */
164907
+ raiseSequenceFor(k, records) {
164908
+ let top = -1;
164909
+ for (const r of records) if (/^\d{1,15}$/.test(r.id)) top = Math.max(top, Number(r.id));
164910
+ if (top < 0) return;
164911
+ if (k.declaration.adoptsFrom !== void 0) {
164912
+ this.db.prepare("INSERT INTO _record_sequences(kind_id, next) VALUES (?, ?) ON CONFLICT(kind_id) DO UPDATE SET next = MAX(next, excluded.next)").run(k.kindId, top + 1);
164913
+ return;
164914
+ }
164915
+ this.db.prepare("UPDATE _record_sequences SET next = ? WHERE kind_id = ? AND next < ?").run(top + 1, k.kindId, top + 1);
164916
+ }
164815
164917
  schemaReady;
164816
164918
  kindOf(owner, kind) {
164817
164919
  const row = this.kinds.get(keyOf(owner, kind));
@@ -164950,6 +165052,17 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
164950
165052
  /** One row, inside the caller's transaction. */
164951
165053
  writeRecord(owner, k, rec, ifRev, nowMs) {
164952
165054
  const header = validateHeader(k.kind, k.declaration, rec, nowMs);
165055
+ const allocatedKey = `${k.kindId}:${rec.id}`;
165056
+ if (this.pendingAllocated.has(allocatedKey) && this.storedRow(k, rec.id) !== void 0) {
165057
+ this.options.logger.warn("records: an allocated id collided with a stored row; write refused", {
165058
+ tags: { deviceId: rec.deviceId },
165059
+ meta: {
165060
+ kind: k.kind,
165061
+ id: rec.id
165062
+ }
165063
+ });
165064
+ throw new require_dist10.RevConflictError(`${k.kind}/${rec.id}: an allocated id is insert-only and a row already holds it`);
165065
+ }
164953
165066
  const stored = k.declaration.secretPaths.length > 0 || ifRev !== void 0 ? this.storedRow(k, rec.id) : void 0;
164954
165067
  if (ifRev !== void 0) {
164955
165068
  if (ifRev === 0 && stored !== void 0) throw new require_dist10.RevConflictError(`${k.kind}/${rec.id}: the row exists (ifRev 0 means "must not exist")`);
@@ -164963,6 +165076,7 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
164963
165076
  } }), nowMs);
164964
165077
  const row = this.wrapWrite(k, () => this.db.prepare(this.upsertSql(k.kindId)).get(header.id, header.deviceId, header.at, header.expiresAt, nowMs, json));
164965
165078
  if (row === void 0) throw new Error("records: the upsert returned no revision");
165079
+ this.pendingAllocated.delete(allocatedKey);
164966
165080
  return {
164967
165081
  rev: row.rev,
164968
165082
  releasedRefs: checked.releasedRefs
@@ -164987,6 +165101,7 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
164987
165101
  const released = [];
164988
165102
  this.timed("putMany", input.kind, () => this.db.transaction(() => {
164989
165103
  for (const rec of input.records) released.push(...this.writeRecord(input.owner, k, rec, void 0, nowMs).releasedRefs);
165104
+ this.raiseSequenceFor(k, input.records);
164990
165105
  })());
164991
165106
  this.afterWrite(k, input.records.map((r) => r.deviceId));
164992
165107
  return {
@@ -165109,6 +165224,11 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
165109
165224
  const k = this.kindOf(input.owner, input.kind);
165110
165225
  const row = this.db.prepare("INSERT INTO _record_sequences(kind_id, next) VALUES (?, 2) ON CONFLICT(kind_id) DO UPDATE SET next = next + 1 RETURNING next - 1 AS id").get(k.kindId);
165111
165226
  if (row === void 0) throw new Error("records: the sequence returned no id");
165227
+ this.pendingAllocated.add(`${k.kindId}:${row.id}`);
165228
+ if (this.pendingAllocated.size > RecordsEngine2.PENDING_ALLOCATED_MAX) {
165229
+ const oldest = this.pendingAllocated.values().next();
165230
+ if (oldest.done !== true) this.pendingAllocated.delete(oldest.value);
165231
+ }
165112
165232
  return { id: row.id };
165113
165233
  }
165114
165234
  /**
@@ -168071,9 +168191,9 @@ var require_sqlite_settings_addon = __commonJS({
168071
168191
  [Symbol.toStringTag]: { value: "Module" }
168072
168192
  });
168073
168193
  var require_chunk = require_chunk_Cek0wNdY();
168074
- var require_dist10 = require_dist_DyjNaeIj();
168194
+ var require_dist10 = require_dist_Bi4Ioxop();
168075
168195
  var require_retired_settings_keys = require_retired_settings_keys_D_VwZiDt();
168076
- var require_wal_maintenance = require_wal_maintenance_i3GRvmQW();
168196
+ var require_wal_maintenance = require_wal_maintenance_Bo_lo3Dh();
168077
168197
  var require_sqlite_snapshot_protocol = require_sqlite_snapshot_protocol_B9OndqZv();
168078
168198
  var require_wal_checkpoint_policy = require_wal_checkpoint_policy_d2z2qPqx();
168079
168199
  var node_fs_promises = __require("fs/promises");
@@ -169930,7 +170050,7 @@ var require_sqlite_storage = __commonJS({
169930
170050
  });
169931
170051
  require_chunk_Cek0wNdY();
169932
170052
  var require_builtins_sqlite_storage_filesystem_storage_addon = require_filesystem_storage_addon();
169933
- var require_wal_maintenance = require_wal_maintenance_i3GRvmQW();
170053
+ var require_wal_maintenance = require_wal_maintenance_Bo_lo3Dh();
169934
170054
  var require_builtins_sqlite_storage_sqlite_settings_addon = require_sqlite_settings_addon();
169935
170055
  var ConfigStore = class {
169936
170056
  db;
@@ -170014,7 +170134,7 @@ var require_storage_orchestrator_addon = __commonJS({
170014
170134
  [Symbol.toStringTag]: { value: "Module" }
170015
170135
  });
170016
170136
  var require_chunk = require_chunk_Cek0wNdY();
170017
- var require_dist10 = require_dist_DyjNaeIj();
170137
+ var require_dist10 = require_dist_Bi4Ioxop();
170018
170138
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
170019
170139
  var zod = require_zod();
170020
170140
  var node_crypto = __require("crypto");
@@ -170038,6 +170158,7 @@ var require_storage_orchestrator_addon = __commonJS({
170038
170158
  mode: require_dist10.StorageLocationModeSchema.optional(),
170039
170159
  isDefault: zod.z.boolean().optional()
170040
170160
  });
170161
+ var StorageSeedMarkerBodySchema = zod.z.object({ seededAt: zod.z.number() });
170041
170162
  var StorageDrainStateBodySchema = zod.z.object({
170042
170163
  locationId: zod.z.string(),
170043
170164
  startedAtMs: zod.z.number(),
@@ -170062,6 +170183,14 @@ var require_storage_orchestrator_addon = __commonJS({
170062
170183
  retention: { none: "operator delete or declaration prune only" },
170063
170184
  adoptsFrom: STORAGE_LOCATIONS
170064
170185
  },
170186
+ "seed-markers": {
170187
+ schema: StorageSeedMarkerBodySchema,
170188
+ class: "config",
170189
+ writeTrigger: "a declared location type was seeded (or found already present on an existing install); the id is the type",
170190
+ device: "none",
170191
+ at: "none",
170192
+ retention: { none: "one row per location type ever seeded \u2014 what makes an operator delete stick" }
170193
+ },
170065
170194
  "drain-state": {
170066
170195
  schema: StorageDrainStateBodySchema,
170067
170196
  class: "config",
@@ -170141,9 +170270,27 @@ var require_storage_orchestrator_addon = __commonJS({
170141
170270
  store;
170142
170271
  declared = false;
170143
170272
  locationsKind;
170144
- constructor(store, locationsKind = null) {
170273
+ constructor(store, locationsKind = null, seedMarkersKind = null) {
170145
170274
  this.store = store;
170146
170275
  this.locationsKind = locationsKind;
170276
+ this.seedMarkersKind = seedMarkersKind;
170277
+ }
170278
+ seedMarkersKind;
170279
+ static seedMarkersOf(records) {
170280
+ return records.kind(storageOrchestratorRecordKinds["seed-markers"]);
170281
+ }
170282
+ async loadSeededTypes() {
170283
+ const kind = this.seedMarkersKind;
170284
+ if (kind === null) return [];
170285
+ return (await (0, import_read_all_rows.readAllRows)(kind)).map((r) => r.id);
170286
+ }
170287
+ async markTypeSeeded(type) {
170288
+ const kind = this.seedMarkersKind;
170289
+ if (kind === null) return;
170290
+ await kind.put({
170291
+ id: type,
170292
+ data: { seededAt: Date.now() }
170293
+ });
170147
170294
  }
170148
170295
  static recordStoresOf(records) {
170149
170296
  return records.kind(storageOrchestratorRecordKinds.locations);
@@ -171110,20 +171257,77 @@ var require_storage_orchestrator_addon = __commonJS({
171110
171257
  }
171111
171258
  };
171112
171259
  }
171113
- async function collectFiles(io, prefix) {
171260
+ var STORAGE_CLASS_SUBTREES = {
171261
+ eventMedia: ["events"],
171262
+ recordingsLow: ["low"],
171263
+ recordings: ["high", "mid"]
171264
+ };
171265
+ function isDotEntry(name) {
171266
+ return name.startsWith(".");
171267
+ }
171268
+ async function dirHasAnyEntry(dir) {
171269
+ let handle;
171270
+ try {
171271
+ handle = await (0, node_fs_promises.opendir)(dir);
171272
+ } catch (err) {
171273
+ if (err.code === "ENOENT") return false;
171274
+ throw err;
171275
+ }
171276
+ try {
171277
+ return await handle.read() !== null;
171278
+ } finally {
171279
+ await handle.close();
171280
+ }
171281
+ }
171282
+ async function probeLocalDirectoryOccupancy(input) {
171283
+ let names;
171284
+ try {
171285
+ names = await (0, node_fs_promises.readdir)(input.basePath);
171286
+ } catch (err) {
171287
+ if (err.code === "ENOENT") return "empty";
171288
+ return "unknown";
171289
+ }
171290
+ const visible = names.filter((name) => !isDotEntry(name));
171291
+ const subtrees = STORAGE_CLASS_SUBTREES[input.type];
171292
+ if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
171293
+ try {
171294
+ for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
171295
+ } catch {
171296
+ return "unknown";
171297
+ }
171298
+ return "empty";
171299
+ }
171300
+ var STORAGE_CLASS_ROOT_SUBTREES = { homekitClips: ["homekit-clips"] };
171301
+ function classifyPath(type, relativePath) {
171302
+ const rootSubtrees = STORAGE_CLASS_ROOT_SUBTREES[type];
171303
+ const cameraSubtrees = STORAGE_CLASS_SUBTREES[type];
171304
+ if (rootSubtrees === void 0 && cameraSubtrees === void 0) return "own";
171305
+ const segments = relativePath.split("/").filter((segment) => segment.length > 0);
171306
+ if (rootSubtrees !== void 0) {
171307
+ const top = segments[0];
171308
+ return top !== void 0 && rootSubtrees.includes(top) ? "own" : "foreign";
171309
+ }
171310
+ if (segments.length < 2) return "descend";
171311
+ const subtree = segments[1];
171312
+ return subtree !== void 0 && cameraSubtrees?.includes(subtree) === true ? "own" : "foreign";
171313
+ }
171314
+ async function collectFiles(io, scope, prefix) {
171114
171315
  const entries = await io.list(prefix);
171115
171316
  const files = [];
171116
171317
  for (const entry of entries) {
171318
+ const verdict = scope(entry);
171319
+ if (verdict === "foreign") continue;
171117
171320
  if ((await io.list(entry)).length > 0) {
171118
- files.push(...await collectFiles(io, entry));
171321
+ files.push(...await collectFiles(io, scope, entry));
171119
171322
  continue;
171120
171323
  }
171121
- files.push(entry);
171324
+ if (verdict === "own") files.push(entry);
171122
171325
  }
171123
171326
  return files;
171124
171327
  }
171125
- async function moveLocationTree(io) {
171126
- const candidates = await collectFiles(io);
171328
+ var WHOLE_ROOT = () => "own";
171329
+ async function moveLocationTree(io, scope = WHOLE_ROOT) {
171330
+ const candidates = await collectFiles(io, scope);
171127
171331
  const copied = [];
171128
171332
  let bytes = 0;
171129
171333
  for (const relativePath of candidates) {
@@ -171145,6 +171349,118 @@ var require_storage_orchestrator_addon = __commonJS({
171145
171349
  bytes
171146
171350
  };
171147
171351
  }
171352
+ var RepointRefusedError = class extends Error {
171353
+ reason;
171354
+ name = "RepointRefusedError";
171355
+ constructor(reason, message) {
171356
+ super(message);
171357
+ this.reason = reason;
171358
+ }
171359
+ };
171360
+ function rootsOverlap(a, b) {
171361
+ if (a === b) return true;
171362
+ const within = (inner, outer) => inner.startsWith(outer.endsWith("/") ? outer : `${outer}/`);
171363
+ return within(a, b) || within(b, a);
171364
+ }
171365
+ function basePathOf$1(config) {
171366
+ const raw = config["basePath"];
171367
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
171368
+ }
171369
+ var LocationRepointer = class {
171370
+ deps;
171371
+ claims = /* @__PURE__ */ new Set();
171372
+ constructor(deps) {
171373
+ this.deps = deps;
171374
+ }
171375
+ /**
171376
+ * Move `stored`'s own subtree to `next`'s root, then run `commit` (the row
171377
+ * save) while the claim is still held. Throws {@link RepointRefusedError}
171378
+ * with NOTHING copied or deleted when a mover is on the root.
171379
+ */
171380
+ async run(stored, next, commit) {
171381
+ const claim = await this.claim(stored, next);
171382
+ try {
171383
+ await this.refuseWhileMovers(claim, stored);
171384
+ const moved = await moveLocationTree(await this.deps.ioFor(stored, next), (path) => classifyPath(stored.type, path));
171385
+ this.logMoved(stored, next, moved);
171386
+ return await commit();
171387
+ } finally {
171388
+ this.claims.delete(claim);
171389
+ }
171390
+ }
171391
+ /**
171392
+ * May a mover that touches `locationIds` run? Refuses while a repoint holds
171393
+ * an overlapping root. Called by the owning addon AFTER its job is
171394
+ * registered, so the repoint's mover listing and this check cannot both miss.
171395
+ */
171396
+ async clearance(locationIds) {
171397
+ const keys = await this.keysOf(locationIds);
171398
+ for (const claim of this.claims) if (claim.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) return {
171399
+ clear: false,
171400
+ reason: "repoint-active",
171401
+ repointLocationId: claim.locationId
171402
+ };
171403
+ return { clear: true };
171404
+ }
171405
+ async claim(stored, next) {
171406
+ const paths = [basePathOf$1(stored.config), basePathOf$1(next.config)].filter((path) => path !== null);
171407
+ const keys = await Promise.all(paths.map((path) => this.deps.resolveKey(path)));
171408
+ for (const other of this.claims) if (other.keys.some((held) => keys.some((key) => rootsOverlap(held, key)))) this.refuse(stored, "repoint-active", `a repoint of "${other.locationId}" holds the same root`);
171409
+ const claim = {
171410
+ locationId: stored.id,
171411
+ keys
171412
+ };
171413
+ this.claims.add(claim);
171414
+ return claim;
171415
+ }
171416
+ async refuseWhileMovers(claim, stored) {
171417
+ let movers;
171418
+ try {
171419
+ movers = await this.deps.listMovers();
171420
+ } catch (err) {
171421
+ this.claims.delete(claim);
171422
+ this.refuse(stored, "movers-unreadable", `the active movers could not be listed (${err instanceof Error ? err.message : String(err)})`);
171423
+ }
171424
+ for (const mover of movers) {
171425
+ if (mover.job.state !== "queued" && mover.job.state !== "running") continue;
171426
+ const touched = await this.keysOf(this.locationsTouchedBy(mover));
171427
+ if (claim.keys.some((held) => touched.some((key) => rootsOverlap(held, key)))) {
171428
+ this.claims.delete(claim);
171429
+ this.refuse(stored, "mover-active", `${mover.lane} mover ${mover.job.jobId} (${mover.job.fromLocationId} -> ${mover.job.toLocationId}) touches the same physical root`, {
171430
+ jobId: mover.job.jobId,
171431
+ lane: mover.lane
171432
+ });
171433
+ }
171434
+ }
171435
+ }
171436
+ locationsTouchedBy(mover) {
171437
+ return [mover.job.fromLocationId, mover.job.toLocationId];
171438
+ }
171439
+ /** `*` is the media mover's "from wherever the rows are": every eventMedia location. */
171440
+ async keysOf(locationIds) {
171441
+ const paths = locationIds.flatMap((id) => id === "*" ? this.deps.locationIdsOfType("eventMedia") : [id]).map((id) => this.deps.locationBasePath(id)).filter((path) => path !== null);
171442
+ return Promise.all(paths.map((path) => this.deps.resolveKey(path)));
171443
+ }
171444
+ refuse(stored, reason, detail, meta = {}) {
171445
+ this.deps.logger.warn("storage-orchestrator: REFUSING to re-point a location \u2014 nothing was moved", { meta: {
171446
+ id: stored.id,
171447
+ type: stored.type,
171448
+ reason,
171449
+ detail,
171450
+ ...meta
171451
+ } });
171452
+ throw new RepointRefusedError(reason, `re-point of "${stored.id}" refused (${reason}): ${detail}`);
171453
+ }
171454
+ logMoved(stored, next, moved) {
171455
+ this.deps.logger.info("storage-orchestrator: singleton location re-pointed \u2014 its data was carried over", { meta: {
171456
+ id: stored.id,
171457
+ type: stored.type,
171458
+ from: basePathOf$1(stored.config),
171459
+ to: basePathOf$1(next.config),
171460
+ ...moved
171461
+ } });
171462
+ }
171463
+ };
171148
171464
  function resolveRefusalFor(location, locality) {
171149
171465
  if (locality !== false) return null;
171150
171466
  return `storage.resolve is a request for a path this node can open with node:fs, and location "${location.id}" is served by the remote provider "${location.providerId}" (nodeLocal: false) \u2014 the path it would return exists on the remote host, not here. Read and write this location through the storage cap (read/write, or beginUpload/writeChunk/finalizeUpload) instead.`;
@@ -171454,46 +171770,6 @@ var require_storage_orchestrator_addon = __commonJS({
171454
171770
  args: envelope.args
171455
171771
  }, { timeout: HUB_SETTINGS_FORWARD_TIMEOUT_MS }));
171456
171772
  }
171457
- var STORAGE_CLASS_SUBTREES = {
171458
- eventMedia: ["events"],
171459
- recordingsLow: ["low"],
171460
- recordings: ["high", "mid"]
171461
- };
171462
- function isDotEntry(name) {
171463
- return name.startsWith(".");
171464
- }
171465
- async function dirHasAnyEntry(dir) {
171466
- let handle;
171467
- try {
171468
- handle = await (0, node_fs_promises.opendir)(dir);
171469
- } catch (err) {
171470
- if (err.code === "ENOENT") return false;
171471
- throw err;
171472
- }
171473
- try {
171474
- return await handle.read() !== null;
171475
- } finally {
171476
- await handle.close();
171477
- }
171478
- }
171479
- async function probeLocalDirectoryOccupancy(input) {
171480
- let names;
171481
- try {
171482
- names = await (0, node_fs_promises.readdir)(input.basePath);
171483
- } catch (err) {
171484
- if (err.code === "ENOENT") return "empty";
171485
- return "unknown";
171486
- }
171487
- const visible = names.filter((name) => !isDotEntry(name));
171488
- const subtrees = STORAGE_CLASS_SUBTREES[input.type];
171489
- if (subtrees === void 0) return visible.length > 0 ? "occupied" : "empty";
171490
- try {
171491
- for (const camera of visible) for (const subtree of subtrees) if (await dirHasAnyEntry(node_path.default.join(input.basePath, camera, subtree))) return "occupied";
171492
- } catch {
171493
- return "unknown";
171494
- }
171495
- return "empty";
171496
- }
171497
171773
  var DEFAULT_STALE_AFTER_MS = 6e4;
171498
171774
  var LocationOccupancySlice = class {
171499
171775
  deps;
@@ -171994,6 +172270,44 @@ var require_storage_orchestrator_addon = __commonJS({
171994
172270
  getLocalNodeId() {
171995
172271
  return this.localNode;
171996
172272
  }
172273
+ /**
172274
+ * Whether a seed must wait for an ANSWERED read of the store (D49). True on a
172275
+ * node whose locations live in a store: seeding before that read lands writes
172276
+ * defaults the store's own rows (not yet adopted, or not yet read) then
172277
+ * contradict. False on a node that has no store by design — it seeds in memory
172278
+ * at every boot.
172279
+ */
172280
+ storeRequiredForSeed = false;
172281
+ setStoreRequiredForSeed(required) {
172282
+ this.storeRequiredForSeed = required;
172283
+ }
172284
+ /**
172285
+ * The location types a default was ever seeded for — null when the store
172286
+ * cannot remember (then a type with no row is seeded, as before). What makes
172287
+ * an operator's delete of a seeded default STICK: zero rows today is not
172288
+ * "fresh install", it is only fresh when nothing was ever seeded.
172289
+ */
172290
+ seededTypes = null;
172291
+ /** The store held locations but no seed memory: an install older than the memory. */
172292
+ installPredatesSeedMemory = false;
172293
+ async loadSeedMemory(store, rowCount) {
172294
+ if (!store.loadSeededTypes) return;
172295
+ const types = await store.loadSeededTypes();
172296
+ this.seededTypes = new Set(types);
172297
+ this.installPredatesSeedMemory = types.length === 0 && rowCount > 0;
172298
+ }
172299
+ rememberSeeded(type) {
172300
+ const seeded = this.seededTypes;
172301
+ const store = this.locationStore;
172302
+ if (seeded === null || seeded.has(type) || !store?.markTypeSeeded) return;
172303
+ seeded.add(type);
172304
+ store.markTypeSeeded(type).catch((err) => {
172305
+ this.logger.error("storage-orchestrator: persisting the seed memory failed", { meta: {
172306
+ type,
172307
+ error: err instanceof Error ? err.message : String(err)
172308
+ } });
172309
+ });
172310
+ }
171997
172311
  /** True once a persistence store is wired (constructor or {@link attachStore}). */
171998
172312
  hasStore() {
171999
172313
  return this.locationStore !== null;
@@ -172027,6 +172341,7 @@ var require_storage_orchestrator_addon = __commonJS({
172027
172341
  async attachStore(store) {
172028
172342
  if (this.locationStore) return;
172029
172343
  const rows = await store.loadAll();
172344
+ await this.loadSeedMemory(store, rows.length);
172030
172345
  this.locationStore = store;
172031
172346
  const dbIds = new Set(rows.map((r) => r.id));
172032
172347
  for (const loc of rows) this.locations.set(loc.id, loc);
@@ -172165,6 +172480,7 @@ var require_storage_orchestrator_addon = __commonJS({
172165
172480
  async initialize() {
172166
172481
  if (!this.locationStore) return;
172167
172482
  const rows = await this.locationStore.loadAll();
172483
+ await this.loadSeedMemory(this.locationStore, rows.length);
172168
172484
  for (const loc of rows) this.locations.set(loc.id, loc);
172169
172485
  this.logger.info("storage-orchestrator: hydrated locations from store", { meta: { loaded: this.locations.size } });
172170
172486
  this.migrateOntoTheWriteSet(rows);
@@ -172617,14 +172933,35 @@ var require_storage_orchestrator_addon = __commonJS({
172617
172933
  * directory for derivative slots like `recordingsLow` → `recordings`).
172618
172934
  */
172619
172935
  seedDefaults(input) {
172936
+ if (this.storeRequiredForSeed && this.locationStore === null) {
172937
+ this.logger.warn("storage-orchestrator: seed deferred \u2014 the location store has not answered a read yet (not wired, adoption pending, or the read failed); seeding now could shadow the operator's existing locations");
172938
+ return {
172939
+ added: 0,
172940
+ deferred: "store-not-answered"
172941
+ };
172942
+ }
172620
172943
  let added = 0;
172944
+ let deferredProvider = false;
172621
172945
  const declById = new Map(input.declarations.map((d) => [d.id, d]));
172622
172946
  const rootFor = (d) => {
172623
172947
  const dedicated = d.defaultRoot === "media" ? input.mediaBasePath : d.defaultRoot === "backup" ? input.backupBasePath : void 0;
172624
172948
  return node_path.join(dedicated ?? input.basePath, d.id);
172625
172949
  };
172626
172950
  for (const d of input.declarations) {
172627
- if (this.hasAnyLocationOfType(d.id)) continue;
172951
+ if (this.hasAnyLocationOfType(d.id)) {
172952
+ this.rememberSeeded(d.id);
172953
+ continue;
172954
+ }
172955
+ if (this.seededTypes?.has(d.id)) continue;
172956
+ if (this.installPredatesSeedMemory) {
172957
+ this.logger.info("storage-orchestrator: existing install \u2014 not re-creating a default the operator may have removed", { meta: { type: d.id } });
172958
+ this.rememberSeeded(d.id);
172959
+ continue;
172960
+ }
172961
+ if (this.nodeLocalResolver !== null && this.nodeLocalResolver(input.providerId) === void 0) {
172962
+ deferredProvider = true;
172963
+ continue;
172964
+ }
172628
172965
  const base = d.defaultsTo ? this.getLocationById(`${d.defaultsTo}:default`)?.config["basePath"] ?? rootFor(declById.get(d.defaultsTo) ?? d) : rootFor(d);
172629
172966
  this.upsertLocation({
172630
172967
  id: `${d.id}:default`,
@@ -172635,12 +172972,18 @@ var require_storage_orchestrator_addon = __commonJS({
172635
172972
  enabled: true,
172636
172973
  isSystem: false
172637
172974
  });
172975
+ this.rememberSeeded(d.id);
172638
172976
  added++;
172639
172977
  }
172640
172978
  if (added > 0) this.logger.info("storage-orchestrator: seeded default locations", { meta: {
172641
172979
  added,
172642
172980
  basePath: input.basePath
172643
172981
  } });
172982
+ if (deferredProvider) this.logger.info("storage-orchestrator: seed deferred for some types \u2014 provider not classified yet", { meta: { providerId: input.providerId } });
172983
+ return deferredProvider ? {
172984
+ added,
172985
+ deferred: "provider-unclassified"
172986
+ } : { added };
172644
172987
  }
172645
172988
  /**
172646
172989
  * The same-type physical-root refusal (D389).
@@ -172972,29 +173315,69 @@ var require_storage_orchestrator_addon = __commonJS({
172972
173315
  lowPercent: 15,
172973
173316
  criticalPercent: 5
172974
173317
  };
172975
- function bandOf(freePercent, config) {
172976
- if (freePercent === null) return {
172977
- band: "critical",
172978
- reason: "capacity-unreadable"
172979
- };
172980
- if (freePercent <= config.criticalPercent) return {
172981
- band: "critical",
172982
- reason: "free-below-critical"
172983
- };
172984
- if (freePercent <= config.lowPercent) return {
172985
- band: "low",
172986
- reason: "free-below-warn"
173318
+ var UNMANAGED_HYSTERESIS_POINTS = 2;
173319
+ var MANAGED_LOW_ENTER = 0.8;
173320
+ var MANAGED_LOW_EXIT = 0.9;
173321
+ var MANAGED_CRITICAL_ENTER = 0.5;
173322
+ var MANAGED_CRITICAL_EXIT = 0.6;
173323
+ var MANAGED_LOW_GRACE_MS = 10 * 6e4;
173324
+ function boundsOf(floorPercent, config) {
173325
+ if (floorPercent === void 0) return {
173326
+ lowEnter: config.lowPercent,
173327
+ lowExit: config.lowPercent + UNMANAGED_HYSTERESIS_POINTS,
173328
+ criticalEnter: config.criticalPercent,
173329
+ criticalExit: config.criticalPercent + UNMANAGED_HYSTERESIS_POINTS,
173330
+ lowGraceMs: 0,
173331
+ lowReason: "free-below-warn",
173332
+ criticalReason: "free-below-critical"
172987
173333
  };
172988
173334
  return {
172989
- band: "ok",
172990
- reason: "free-ok"
173335
+ lowEnter: floorPercent * MANAGED_LOW_ENTER,
173336
+ lowExit: floorPercent * MANAGED_LOW_EXIT,
173337
+ criticalEnter: floorPercent * MANAGED_CRITICAL_ENTER,
173338
+ criticalExit: floorPercent * MANAGED_CRITICAL_EXIT,
173339
+ lowGraceMs: MANAGED_LOW_GRACE_MS,
173340
+ lowReason: "free-below-floor",
173341
+ criticalReason: "free-below-hard-bound"
172991
173342
  };
172992
173343
  }
172993
173344
  var StorageLocationBandMonitor = class {
172994
173345
  config;
173346
+ now;
172995
173347
  band = /* @__PURE__ */ new Map();
172996
- constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG) {
173348
+ /** When free first went under `lowEnter` and has stayed there. */
173349
+ belowLowSince = /* @__PURE__ */ new Map();
173350
+ constructor(config = DEFAULT_STORAGE_LOCATION_BAND_CONFIG, now = Date.now) {
172997
173351
  this.config = config;
173352
+ this.now = now;
173353
+ }
173354
+ nextBand(locationId, previous, free, b) {
173355
+ if (free < b.criticalEnter || previous === "critical" && free < b.criticalExit) return {
173356
+ band: "critical",
173357
+ reason: b.criticalReason
173358
+ };
173359
+ const lowActive = previous === "low" || previous === "critical";
173360
+ if (free < b.lowEnter || lowActive && free < b.lowExit) {
173361
+ if (lowActive) return {
173362
+ band: "low",
173363
+ reason: b.lowReason
173364
+ };
173365
+ const since = this.belowLowSince.get(locationId) ?? this.now();
173366
+ this.belowLowSince.set(locationId, since);
173367
+ if (this.now() - since >= b.lowGraceMs) return {
173368
+ band: "low",
173369
+ reason: b.lowReason
173370
+ };
173371
+ return {
173372
+ band: previous ?? "ok",
173373
+ reason: "free-ok"
173374
+ };
173375
+ }
173376
+ this.belowLowSince.delete(locationId);
173377
+ return {
173378
+ band: "ok",
173379
+ reason: "free-ok"
173380
+ };
172998
173381
  }
172999
173382
  /**
173000
173383
  * Observe one sweep of managed locations. Locations missing from `samples`
@@ -173005,8 +173388,9 @@ var require_storage_orchestrator_addon = __commonJS({
173005
173388
  const out = [];
173006
173389
  for (const sample of samples) {
173007
173390
  seen.add(sample.locationId);
173008
- const { band, reason } = bandOf(sample.freePercent, this.config);
173391
+ if (sample.freePercent === null) continue;
173009
173392
  const previous = this.band.get(sample.locationId) ?? null;
173393
+ const { band, reason } = this.nextBand(sample.locationId, previous, sample.freePercent, boundsOf(sample.floorPercent, this.config));
173010
173394
  if (previous === band) continue;
173011
173395
  this.band.set(sample.locationId, band);
173012
173396
  if (previous === null && band === "ok") continue;
@@ -173016,11 +173400,17 @@ var require_storage_orchestrator_addon = __commonJS({
173016
173400
  ...sample.locationName !== void 0 ? { locationName: sample.locationName } : {},
173017
173401
  band,
173018
173402
  freePercent: sample.freePercent,
173403
+ ...sample.floorPercent !== void 0 ? { floorPercent: sample.floorPercent } : {},
173404
+ ...sample.freeBytes !== void 0 ? { freeBytes: sample.freeBytes } : {},
173405
+ ...sample.floorBytes !== void 0 ? { floorBytes: sample.floorBytes } : {},
173019
173406
  reason,
173020
173407
  previous
173021
173408
  });
173022
173409
  }
173023
- for (const id of [...this.band.keys()]) if (!seen.has(id)) this.band.delete(id);
173410
+ for (const id of [...this.band.keys()]) if (!seen.has(id)) {
173411
+ this.band.delete(id);
173412
+ this.belowLowSince.delete(id);
173413
+ }
173024
173414
  return out;
173025
173415
  }
173026
173416
  };
@@ -173033,6 +173423,7 @@ var require_storage_orchestrator_addon = __commonJS({
173033
173423
  var CAP_PROVIDER_REGISTERED_CATEGORY = "capability:provider-registered";
173034
173424
  var StorageOrchestratorAddon = class extends require_dist10.BaseAddon {
173035
173425
  locationsKind = null;
173426
+ seedMarkersKind = null;
173036
173427
  drainStateKind = null;
173037
173428
  service = null;
173038
173429
  pressureManager = null;
@@ -173058,6 +173449,8 @@ var require_storage_orchestrator_addon = __commonJS({
173058
173449
  pressureTimer = null;
173059
173450
  pressureSweepInFlight = false;
173060
173451
  migration = null;
173452
+ /** Guards every single-location repoint and arbitrates movers per physical root. */
173453
+ repointer = null;
173061
173454
  /**
173062
173455
  * Cached `providerId → nodeLocal` snapshot (SP1). Backs the orchestrator's
173063
173456
  * synchronous `NodeLocalResolver` — `getProviderInfo()` is async, so we
@@ -173094,6 +173487,7 @@ var require_storage_orchestrator_addon = __commonJS({
173094
173487
  if (records !== void 0) {
173095
173488
  await records.declare(storageOrchestratorRecordKinds);
173096
173489
  this.locationsKind = SqliteLocationStore.recordStoresOf(records);
173490
+ this.seedMarkersKind = SqliteLocationStore.seedMarkersOf(records);
173097
173491
  this.drainStateKind = SqliteDrainStateStore.recordStoresOf(records);
173098
173492
  }
173099
173493
  const getProviders = () => {
@@ -173113,6 +173507,7 @@ var require_storage_orchestrator_addon = __commonJS({
173113
173507
  return proxy;
173114
173508
  });
173115
173509
  this.service = service;
173510
+ service.setStoreRequiredForSeed(this.locationsKind !== null);
173116
173511
  service.setNodeLocalResolver((providerId) => this.nodeLocalByProvider.get(providerId));
173117
173512
  service.setOccupancyProbe((location) => this.locationOccupancy(location));
173118
173513
  await service.initialize();
@@ -173136,8 +173531,7 @@ var require_storage_orchestrator_addon = __commonJS({
173136
173531
  ...input,
173137
173532
  config: keychained
173138
173533
  };
173139
- await this.carryDataOnRepoint(stored, next);
173140
- const saved = service.upsertLocation(next);
173534
+ const saved = await this.upsertCarryingData(stored, next, () => service.upsertLocation(next));
173141
173535
  this.announceLocationsChanged(saved.id, saved.type, "upsert");
173142
173536
  return this.redacted(saved);
173143
173537
  },
@@ -173347,6 +173741,49 @@ var require_storage_orchestrator_addon = __commonJS({
173347
173741
  deviceKeyOf: (locationId) => this.locationDeviceKey(locationId)
173348
173742
  });
173349
173743
  this.migration = migration;
173744
+ this.repointer = new LocationRepointer({
173745
+ logger: this.ctx.logger,
173746
+ resolveKey: async (basePath) => {
173747
+ try {
173748
+ return await node_fs_promises.realpath(basePath);
173749
+ } catch {
173750
+ return node_path.resolve(basePath);
173751
+ }
173752
+ },
173753
+ locationBasePath: (id) => this.locationBasePath(id),
173754
+ locationIdsOfType: (type) => service.listLocations({ type }).map((loc) => loc.id),
173755
+ listMovers: () => migration.movers(),
173756
+ ioFor: async (stored, next) => {
173757
+ const target = {
173758
+ ...stored,
173759
+ config: next.config
173760
+ };
173761
+ const [source, destination] = await Promise.all([service.getProviderFor(stored), service.getProviderFor(target)]);
173762
+ return {
173763
+ list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
173764
+ location: stored,
173765
+ prefix
173766
+ }),
173767
+ read: (relativePath) => source.read({
173768
+ location: stored,
173769
+ relativePath
173770
+ }),
173771
+ write: (relativePath, data) => destination.write({
173772
+ location: target,
173773
+ relativePath,
173774
+ data
173775
+ }),
173776
+ readBack: (relativePath) => destination.read({
173777
+ location: target,
173778
+ relativePath
173779
+ }),
173780
+ deleteSource: (relativePath) => source.delete({
173781
+ location: stored,
173782
+ relativePath
173783
+ })
173784
+ };
173785
+ }
173786
+ });
173350
173787
  const cleanup = new StorageCleanupCoordinator({
173351
173788
  locations: service,
173352
173789
  state: this.state("storage-cleanup", require_dist10.StorageCleanupJobSchema.nullable(), null),
@@ -173382,6 +173819,13 @@ var require_storage_orchestrator_addon = __commonJS({
173382
173819
  status: ({ jobId }) => migration.status(jobId),
173383
173820
  cancel: async ({ jobId }) => ({ cancelled: await migration.cancel(jobId) }),
173384
173821
  movers: () => migration.movers(),
173822
+ moverClearance: async ({ locationIds }) => {
173823
+ const verdict = await this.requireRepointer().clearance(locationIds);
173824
+ return verdict.clear ? { clear: true } : {
173825
+ clear: false,
173826
+ repointLocationId: verdict.repointLocationId
173827
+ };
173828
+ },
173385
173829
  residue: () => migration.residue(),
173386
173830
  drain: async (input) => ({ jobId: await migration.drain(input) }),
173387
173831
  cleanupStart: async (input) => ({ jobId: await cleanup.start(input) }),
@@ -173509,7 +173953,7 @@ var require_storage_orchestrator_addon = __commonJS({
173509
173953
  else this.ctx.logger.info("storage-orchestrator: this node boots no data-store engine \u2014 storage locations are seeded from addon declarations at every boot, and node-pinned locations live on the hub");
173510
173954
  return null;
173511
173955
  }
173512
- return new SqliteLocationStore(engine, this.locationsKind);
173956
+ return new SqliteLocationStore(engine, this.locationsKind, this.seedMarkersKind);
173513
173957
  }
173514
173958
  /**
173515
173959
  * Late-wire the persistence store once a `data-store-provider` engine
@@ -173521,12 +173965,26 @@ var require_storage_orchestrator_addon = __commonJS({
173521
173965
  * restart — the service never hydrates from / persists to SQLite.
173522
173966
  */
173523
173967
  storeWireInFlight = null;
173968
+ /** True while the locations kind is empty and its legacy table still holds rows. */
173969
+ async adoptionIsPending() {
173970
+ const records = this.ctx.records;
173971
+ if (records === void 0 || this.locationsKind === null) return false;
173972
+ try {
173973
+ await records.declare(storageOrchestratorRecordKinds);
173974
+ return false;
173975
+ } catch (err) {
173976
+ const message = err instanceof Error ? err.message : String(err);
173977
+ this.ctx.logger.warn("storage-orchestrator: persistence NOT wired \u2014 the storage-locations kind is not authoritative yet (adoption pending or the check failed); seeding is deferred", { meta: { error: message } });
173978
+ return true;
173979
+ }
173980
+ }
173524
173981
  async ensureStoreWired() {
173525
173982
  const service = this.service;
173526
173983
  if (!service || service.hasStore()) return;
173527
173984
  if (this.storeWireInFlight) return this.storeWireInFlight;
173528
173985
  const store = this.resolveLocationStore(true);
173529
173986
  if (!store) return;
173987
+ if (await this.adoptionIsPending()) return;
173530
173988
  this.ctx.logger.info("storage-orchestrator: data-store engine now available \u2014 wiring persistence");
173531
173989
  this.storeWireInFlight = service.attachStore(store).then(() => this.seedFromDeclarations()).catch((err) => {
173532
173990
  this.ctx.logger.error("storage-orchestrator: attachStore failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
@@ -173555,7 +174013,7 @@ var require_storage_orchestrator_addon = __commonJS({
173555
174013
  }
173556
174014
  /**
173557
174015
  * Sample free space for every managed location and emit NC band edges.
173558
- * A failed capacity read stays `null` (critical) — never folded to 0 (D393).
174016
+ * A failed capacity read stays `null` and changes no band — never folded to 0 (D393).
173559
174017
  */
173560
174018
  async emitLocationBandEvents() {
173561
174019
  const svc = this.service;
@@ -173571,11 +174029,18 @@ var require_storage_orchestrator_addon = __commonJS({
173571
174029
  };
173572
174030
  const capacity = await this.volumeCapacityOf(locationId).catch(() => null);
173573
174031
  const freePercent = capacity === null ? null : freePercentOf(capacity);
174032
+ if (capacity === null) ctx.logger.debug("storage: location capacity unreadable \u2014 band unchanged", { meta: { locationId } });
174033
+ const floorPercent = thresholds.get(locationId);
173574
174034
  return {
173575
174035
  locationId,
173576
174036
  nodeId: loc.nodeId,
173577
174037
  locationName: loc.displayName,
173578
- freePercent
174038
+ freePercent,
174039
+ ...floorPercent !== void 0 ? { floorPercent } : {},
174040
+ ...capacity !== null ? {
174041
+ freeBytes: capacity.availableBytes,
174042
+ ...floorPercent !== void 0 ? { floorBytes: Math.round(capacity.totalBytes * floorPercent / 100) } : {}
174043
+ } : {}
173579
174044
  };
173580
174045
  }));
173581
174046
  for (const edge of this.locationBandMonitor.observe(samples)) {
@@ -173584,6 +174049,9 @@ var require_storage_orchestrator_addon = __commonJS({
173584
174049
  locationId: edge.locationId,
173585
174050
  band: edge.band,
173586
174051
  freePercent: edge.freePercent,
174052
+ floorPercent: edge.floorPercent ?? null,
174053
+ freeBytes: edge.freeBytes ?? null,
174054
+ floorBytes: edge.floorBytes ?? null,
173587
174055
  reason: edge.reason
173588
174056
  } });
173589
174057
  ctx.eventBus.emit({
@@ -173698,50 +174166,19 @@ var require_storage_orchestrator_addon = __commonJS({
173698
174166
  * A no-op for everything else — a rename, a mode change, a `multi` kind, or a
173699
174167
  * location being created for the first time.
173700
174168
  */
173701
- async carryDataOnRepoint(stored, next) {
173702
- if (stored === void 0) return;
174169
+ async upsertCarryingData(stored, next, commit) {
174170
+ if (stored === void 0) return commit();
173703
174171
  const service = this.service;
173704
- if (service === null) return;
173705
- if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return;
174172
+ if (service === null) return commit();
174173
+ if (service.listDeclarations().find((entry) => entry.id === next.type)?.cardinality !== "single") return commit();
173706
174174
  const from = stored.config["basePath"];
173707
174175
  const to = next.config["basePath"];
173708
- if (typeof from !== "string" || typeof to !== "string" || from === to) return;
173709
- const source = await service.getProviderFor(stored);
173710
- const target = {
173711
- ...stored,
173712
- config: next.config
173713
- };
173714
- const destination = await service.getProviderFor(target);
173715
- const moved = await moveLocationTree({
173716
- list: (prefix) => source.list(prefix === void 0 ? { location: stored } : {
173717
- location: stored,
173718
- prefix
173719
- }),
173720
- read: (relativePath) => source.read({
173721
- location: stored,
173722
- relativePath
173723
- }),
173724
- write: (relativePath, data) => destination.write({
173725
- location: target,
173726
- relativePath,
173727
- data
173728
- }),
173729
- readBack: (relativePath) => destination.read({
173730
- location: target,
173731
- relativePath
173732
- }),
173733
- deleteSource: (relativePath) => source.delete({
173734
- location: stored,
173735
- relativePath
173736
- })
173737
- });
173738
- this.ctx.logger.info("storage-orchestrator: singleton location re-pointed \u2014 its data was carried over", { meta: {
173739
- id: stored.id,
173740
- type: stored.type,
173741
- from,
173742
- to,
173743
- ...moved
173744
- } });
174176
+ if (typeof from !== "string" || typeof to !== "string" || from === to) return commit();
174177
+ return this.requireRepointer().run(stored, next, commit);
174178
+ }
174179
+ requireRepointer() {
174180
+ if (this.repointer === null) throw new Error("storage-orchestrator: not initialized");
174181
+ return this.repointer;
173745
174182
  }
173746
174183
  secretKeysFor(providerId, config) {
173747
174184
  return secretKeysForConfig(config, this.declaredSecretKeys(providerId));
@@ -174145,11 +174582,11 @@ var require_storage_orchestrator = __commonJS({
174145
174582
  }
174146
174583
  });
174147
174584
 
174148
- // ../system/dist/constants-B3fDUnJt.js
174149
- var require_constants_B3fDUnJt = __commonJS({
174150
- "../system/dist/constants-B3fDUnJt.js"(exports) {
174585
+ // ../system/dist/constants-DBGG6Zu5.js
174586
+ var require_constants_DBGG6Zu5 = __commonJS({
174587
+ "../system/dist/constants-DBGG6Zu5.js"(exports) {
174151
174588
  "use strict";
174152
- var MESH_CA_KEY_REF = require_dist_DyjNaeIj().buildSecretRef({
174589
+ var MESH_CA_KEY_REF = require_dist_Bi4Ioxop().buildSecretRef({
174153
174590
  owner: "system",
174154
174591
  scope: { kind: "global" },
174155
174592
  fieldPath: "meshCaKey"
@@ -174212,14 +174649,14 @@ var require_constants_B3fDUnJt = __commonJS({
174212
174649
  }
174213
174650
  });
174214
174651
 
174215
- // ../system/dist/agent-https-DL2cFOQv.js
174216
- var require_agent_https_DL2cFOQv = __commonJS({
174217
- "../system/dist/agent-https-DL2cFOQv.js"(exports) {
174652
+ // ../system/dist/agent-https-B7IjRRzT.js
174653
+ var require_agent_https_B7IjRRzT = __commonJS({
174654
+ "../system/dist/agent-https-B7IjRRzT.js"(exports) {
174218
174655
  "use strict";
174219
174656
  var require_chunk = require_chunk_Cek0wNdY();
174220
- var require_dist10 = require_dist_DyjNaeIj();
174657
+ var require_dist10 = require_dist_Bi4Ioxop();
174221
174658
  var require_x509_es = require_x509_es_C4cOo48M();
174222
- var require_constants15 = require_constants_B3fDUnJt();
174659
+ var require_constants15 = require_constants_DBGG6Zu5();
174223
174660
  var zod = require_zod();
174224
174661
  var node_crypto = __require("crypto");
174225
174662
  var _camstack_types_node = require_node();
@@ -192962,11 +193399,11 @@ var require_system_config_addon = __commonJS({
192962
193399
  [Symbol.toStringTag]: { value: "Module" }
192963
193400
  });
192964
193401
  var require_chunk = require_chunk_Cek0wNdY();
192965
- var require_dist10 = require_dist_DyjNaeIj();
193402
+ var require_dist10 = require_dist_Bi4Ioxop();
192966
193403
  var require_safe_buffer$1 = require_safe_buffer_D3aoa_7X();
192967
193404
  var require_x509_es = require_x509_es_C4cOo48M();
192968
- var require_constants$8 = require_constants_B3fDUnJt();
192969
- var require_agent_https = require_agent_https_DL2cFOQv();
193405
+ var require_constants$8 = require_constants_DBGG6Zu5();
193406
+ var require_agent_https = require_agent_https_B7IjRRzT();
192970
193407
  var require_src$1 = require_src_CnAqG6EF();
192971
193408
  var zod = require_zod();
192972
193409
  var node_crypto = __require("crypto");
@@ -272679,7 +273116,7 @@ var require_winston_logging = __commonJS({
272679
273116
  [Symbol.toStringTag]: { value: "Module" }
272680
273117
  });
272681
273118
  var require_chunk = require_chunk_Cek0wNdY();
272682
- var require_dist10 = require_dist_DyjNaeIj();
273119
+ var require_dist10 = require_dist_Bi4Ioxop();
272683
273120
  var require_formatter = require_formatter_DqAKDlvN();
272684
273121
  var node_path = __require("path");
272685
273122
  node_path = require_chunk.__toESM(node_path);
@@ -280329,14 +280766,14 @@ var require_lib_8oZrELLm = __commonJS({
280329
280766
  }
280330
280767
  });
280331
280768
 
280332
- // ../system/dist/hub-mdns-ciao-DpdFaLz-.js
280333
- var require_hub_mdns_ciao_DpdFaLz = __commonJS({
280334
- "../system/dist/hub-mdns-ciao-DpdFaLz-.js"(exports) {
280769
+ // ../system/dist/hub-mdns-ciao-2FzLWfbD.js
280770
+ var require_hub_mdns_ciao_2FzLWfbD = __commonJS({
280771
+ "../system/dist/hub-mdns-ciao-2FzLWfbD.js"(exports) {
280335
280772
  "use strict";
280336
280773
  var require_chunk = require_chunk_Cek0wNdY();
280337
- var require_dist10 = require_dist_DyjNaeIj();
280774
+ var require_dist10 = require_dist_Bi4Ioxop();
280338
280775
  var require_x509_es = require_x509_es_C4cOo48M();
280339
- var require_agent_https = require_agent_https_DL2cFOQv();
280776
+ var require_agent_https = require_agent_https_B7IjRRzT();
280340
280777
  var node_crypto = __require("crypto");
280341
280778
  var _camstack_types_node = require_node();
280342
280779
  var node_fs_promises = __require("fs/promises");
@@ -281335,12 +281772,12 @@ var require_platform_key_source_Di7HrlGR = __commonJS({
281335
281772
  }
281336
281773
  });
281337
281774
 
281338
- // ../system/dist/restore-archive-BeGP9IYs.js
281339
- var require_restore_archive_BeGP9IYs = __commonJS({
281340
- "../system/dist/restore-archive-BeGP9IYs.js"(exports) {
281775
+ // ../system/dist/restore-archive-C0ymYQk7.js
281776
+ var require_restore_archive_C0ymYQk7 = __commonJS({
281777
+ "../system/dist/restore-archive-C0ymYQk7.js"(exports) {
281341
281778
  "use strict";
281342
281779
  var require_chunk = require_chunk_Cek0wNdY();
281343
- var require_dist10 = require_dist_DyjNaeIj();
281780
+ var require_dist10 = require_dist_Bi4Ioxop();
281344
281781
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
281345
281782
  var zod = require_zod();
281346
281783
  var node_crypto = __require("crypto");
@@ -292661,9 +293098,9 @@ var require_dist3 = __commonJS({
292661
293098
  "use strict";
292662
293099
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
292663
293100
  var require_chunk = require_chunk_Cek0wNdY();
292664
- var require_dist10 = require_dist_DyjNaeIj();
293101
+ var require_dist10 = require_dist_Bi4Ioxop();
292665
293102
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
292666
- var require_device_ca = require_device_ca_njpTMpJC();
293103
+ var require_device_ca = require_device_ca_B3hUVfYk();
292667
293104
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
292668
293105
  require_alerts();
292669
293106
  var require_builtins_calendars_calendars_addon = require_calendars_addon();
@@ -292688,21 +293125,21 @@ var require_dist3 = __commonJS({
292688
293125
  var require_builtins_native_metrics_native_metrics_addon = require_native_metrics_addon();
292689
293126
  var require_builtins_device_history_device_history_addon = require_device_history_addon();
292690
293127
  var require_builtins_sqlite_storage_filesystem_storage_addon = require_filesystem_storage_addon();
292691
- var require_wal_maintenance = require_wal_maintenance_i3GRvmQW();
293128
+ var require_wal_maintenance = require_wal_maintenance_Bo_lo3Dh();
292692
293129
  var require_builtins_sqlite_storage_sqlite_settings_addon = require_sqlite_settings_addon();
292693
293130
  var require_builtins_sqlite_storage_index = require_sqlite_storage();
292694
293131
  var require_builtins_storage_orchestrator_storage_orchestrator_addon = require_storage_orchestrator_addon();
292695
293132
  require_storage_orchestrator();
292696
- var require_constants15 = require_constants_B3fDUnJt();
292697
- var require_agent_https = require_agent_https_DL2cFOQv();
293133
+ var require_constants15 = require_constants_DBGG6Zu5();
293134
+ var require_agent_https = require_agent_https_B7IjRRzT();
292698
293135
  var require_builtins_system_config_system_config_addon = require_system_config_addon();
292699
293136
  require_system_config();
292700
293137
  var require_builtins_winston_logging_index = require_winston_logging();
292701
293138
  var require_custom_action_registry = require_custom_action_registry_COyb49zm();
292702
- var require_hub_mdns_ciao = require_hub_mdns_ciao_DpdFaLz();
293139
+ var require_hub_mdns_ciao = require_hub_mdns_ciao_2FzLWfbD();
292703
293140
  var require_platform_key_source = require_platform_key_source_Di7HrlGR();
292704
293141
  var require_resource_monitor = require_resource_monitor_mio9dSCT();
292705
- var require_restore_archive = require_restore_archive_BeGP9IYs();
293142
+ var require_restore_archive = require_restore_archive_C0ymYQk7();
292706
293143
  var zod = require_zod();
292707
293144
  var node_crypto = __require("crypto");
292708
293145
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -316148,7 +316585,7 @@ var require_enums = __commonJS({
316148
316585
  "../types/dist/enums.js"(exports) {
316149
316586
  "use strict";
316150
316587
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
316151
- var require_event_category = require_event_category_Hoe_mqbd();
316588
+ var require_event_category = require_event_category_eSqkye_K();
316152
316589
  var EventSourceType = /* @__PURE__ */ (function(EventSourceType2) {
316153
316590
  EventSourceType2["Addon"] = "addon";
316154
316591
  EventSourceType2["Core"] = "core";
@@ -316168,8 +316605,8 @@ var require_dist4 = __commonJS({
316168
316605
  "../types/dist/index.js"(exports) {
316169
316606
  "use strict";
316170
316607
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
316171
- var require_sleep = require_sleep_REHQvLV6();
316172
- var require_event_category = require_event_category_Hoe_mqbd();
316608
+ var require_sleep = require_sleep_BE7_6UnC();
316609
+ var require_event_category = require_event_category_eSqkye_K();
316173
316610
  var require_composition = require_composition_eoYYVfOp();
316174
316611
  var require_node_color2 = require_node_color();
316175
316612
  var require_err_msg = require_err_msg_BhVbr0NX();
@@ -318580,6 +319017,31 @@ var require_dist4 = __commonJS({
318580
319017
  error: zod.z.string().nullable()
318581
319018
  });
318582
319019
  var StorageCleanupStatusInputSchema = zod.z.object({ jobId: zod.z.string().optional() });
319020
+ async function startMoverUnderClearance(deps) {
319021
+ const refuse = (detail) => {
319022
+ throw new Error(`mover refused: ${detail}`);
319023
+ };
319024
+ const ask = async () => {
319025
+ try {
319026
+ return await deps.clearance(deps.locationIds);
319027
+ } catch (err) {
319028
+ return refuse(`the storage orchestrator could not be asked (${err instanceof Error ? err.message : String(err)})`);
319029
+ }
319030
+ };
319031
+ const blocked = (answer) => `a re-point of "${answer.repointLocationId ?? "a location"}" holds an overlapping storage root`;
319032
+ const before = await ask();
319033
+ if (!before.clear) return refuse(blocked(before));
319034
+ const jobId = deps.start();
319035
+ const after = await ask().catch((err) => {
319036
+ deps.cancel(jobId);
319037
+ throw err;
319038
+ });
319039
+ if (!after.clear) {
319040
+ deps.cancel(jobId);
319041
+ return refuse(blocked(after));
319042
+ }
319043
+ return jobId;
319044
+ }
318583
319045
  var SUB_DETECTION_TYPES = ["face", "plate"];
318584
319046
  var RECOGNITION_TYPES = [
318585
319047
  "face",
@@ -321577,7 +322039,14 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
321577
322039
  ]);
321578
322040
  var AlertActionSchema = zod.z.object({
321579
322041
  label: zod.z.string(),
321580
- href: zod.z.string().optional()
322042
+ href: zod.z.string().optional(),
322043
+ /**
322044
+ * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
322045
+ * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
322046
+ * start) — the browser must leave the SPA with a full navigation, because
322047
+ * the SPA has no such route and would bounce to the home page.
322048
+ */
322049
+ navigation: zod.z.enum(["app", "server"]).optional()
321581
322050
  });
321582
322051
  var AlertStatusSchema = zod.z.enum([
321583
322052
  "active",
@@ -353092,6 +353561,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
353092
353561
  "server-updated",
353093
353562
  "export-completed",
353094
353563
  "backup-completed",
353564
+ "alert-critical",
353565
+ "alert-error",
353566
+ "alert-critical-resolved",
353095
353567
  "camera-online",
353096
353568
  "camera-offline",
353097
353569
  "camera-disabled",
@@ -354230,6 +354702,18 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
354230
354702
  value: "backup-completed",
354231
354703
  label: "Backup completed"
354232
354704
  },
354705
+ {
354706
+ value: "alert-critical",
354707
+ label: "Critical alert raised"
354708
+ },
354709
+ {
354710
+ value: "alert-error",
354711
+ label: "Error alert raised"
354712
+ },
354713
+ {
354714
+ value: "alert-critical-resolved",
354715
+ label: "Critical alert resolved"
354716
+ },
354233
354717
  {
354234
354718
  value: "alarm-arm-refused",
354235
354719
  label: "Alarm arm refused"
@@ -363216,6 +363700,18 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
363216
363700
  */
363217
363701
  movers: require_sleep.method(zod.z.object({}), zod.z.array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }),
363218
363702
  /**
363703
+ * May a mover that touches these locations run NOW? One claim per physical
363704
+ * root is owned by the storage orchestrator: a single-location repoint holds
363705
+ * it while it carries its subtree, and a mover (`relocateMedia`,
363706
+ * `relocateFootage`, a migration move) asks here AFTER registering its job and
363707
+ * cancels itself on `clear: false`. The repoint, in turn, refuses while a
363708
+ * registered mover touches its root. Asked, never polled (D3).
363709
+ */
363710
+ moverClearance: require_sleep.method(zod.z.object({ locationIds: zod.z.array(zod.z.string()).readonly() }), zod.z.object({
363711
+ clear: zod.z.boolean(),
363712
+ repointLocationId: zod.z.string().optional()
363713
+ }), { auth: "admin" }),
363714
+ /**
363219
363715
  * What each class's SOURCE still holds, from the archive — never from the
363220
363716
  * resident index (D295). Only classes with something left (or something
363221
363717
  * unknown) are listed, so an empty list means there is nothing to drain and
@@ -367555,43 +368051,139 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
367555
368051
  "error",
367556
368052
  "unavailable"
367557
368053
  ]);
368054
+ var DishwasherOperationStateSchema = zod.z.enum([
368055
+ "Inactive",
368056
+ "Ready",
368057
+ "DelayedStart",
368058
+ "Run",
368059
+ "Pause",
368060
+ "ActionRequired",
368061
+ "Finished",
368062
+ "Error",
368063
+ "Aborting"
368064
+ ]);
368065
+ var DishwasherAvailabilitySchema = zod.z.enum([
368066
+ "available",
368067
+ "offline",
368068
+ "description-missing"
368069
+ ]);
367558
368070
  var DishwasherConsumableLevelSchema = zod.z.enum([
367559
368071
  "ok",
367560
368072
  "low",
367561
368073
  "empty"
367562
368074
  ]);
367563
368075
  var DishwasherStatusSchema = zod.z.object({
368076
+ availability: DishwasherAvailabilitySchema,
368077
+ /** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
367564
368078
  state: DishwasherStateSchema,
367565
- /** Active program id/name, or null when none selected / unknown. */
368079
+ /** The appliance's operation state as reported; null when not known. */
368080
+ operationState: DishwasherOperationStateSchema.nullable(),
368081
+ /** Feature key of the ACTIVE (running) program, or null when none runs / not known. */
367566
368082
  program: zod.z.string().nullable(),
367567
- /** Program tokens the hardware accepts — drives the UI selector. */
368083
+ /** Feature key of the SELECTED program (what Start would run), or null when none / not known. */
368084
+ selectedProgram: zod.z.string().nullable(),
368085
+ /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
367568
368086
  availablePrograms: zod.z.array(zod.z.string()),
367569
- /** Minutes remaining on the active cycle, or null when not running. */
367570
- remainingMinutes: zod.z.number().int().nonnegative().nullable(),
368087
+ /** Seconds remaining on the active cycle, or null when not running / not known. */
368088
+ remainingSeconds: zod.z.number().int().nonnegative().nullable(),
368089
+ /** Seconds the delayed start is set to (time until the cycle starts), or null. */
368090
+ delayedStartSeconds: zod.z.number().int().nonnegative().nullable(),
367571
368091
  /** Cycle completion 0..100, or null when not applicable. */
367572
368092
  progressPercent: zod.z.number().min(0).max(100).nullable(),
367573
- /** True when the door is reported open. */
367574
- doorOpen: zod.z.boolean(),
367575
- /** True when remote start is allowed (door closed, power on, etc.). */
367576
- remoteStartAllowed: zod.z.boolean(),
367577
- /** Main power / switched-on state. */
367578
- powerOn: zod.z.boolean(),
368093
+ /** True when the door is reported open; null when the door state is not known. */
368094
+ doorOpen: zod.z.boolean().nullable(),
368095
+ /** True when the door is latched (locked) during a cycle; null when not reported. */
368096
+ doorLocked: zod.z.boolean().nullable(),
368097
+ /** True when remote start is allowed (door closed, power on, etc.); null when not known. */
368098
+ remoteStartAllowed: zod.z.boolean().nullable(),
368099
+ /** True when the appliance accepts remote control now; null when not known. */
368100
+ remoteControlActive: zod.z.boolean().nullable(),
368101
+ /** True while somebody operates the appliance at its panel; null when not reported. */
368102
+ localControlActive: zod.z.boolean().nullable(),
368103
+ /** Main power / switched-on state; null when not known. */
368104
+ powerOn: zod.z.boolean().nullable(),
368105
+ /** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
368106
+ powerState: zod.z.string().nullable(),
367579
368107
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
367580
368108
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
367581
- halfLoad: zod.z.boolean(),
367582
- hygienePlus: zod.z.boolean(),
368109
+ /** Option states: null when the appliance does not expose the option or has not said. */
368110
+ halfLoad: zod.z.boolean().nullable(),
368111
+ hygienePlus: zod.z.boolean().nullable(),
367583
368112
  /** Extra dry option when the appliance exposes it. */
367584
- extraDry: zod.z.boolean(),
368113
+ extraDry: zod.z.boolean().nullable(),
367585
368114
  /** Intensive zone (lower basket) when the appliance exposes it. */
367586
- intensiveZone: zod.z.boolean(),
368115
+ intensiveZone: zod.z.boolean().nullable(),
367587
368116
  /** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
367588
- varioSpeedPlus: zod.z.boolean(),
367589
- /** Child lock (controls disabled on the appliance). */
367590
- childLock: zod.z.boolean(),
368117
+ varioSpeedPlus: zod.z.boolean().nullable(),
368118
+ /** Child lock (controls disabled on the appliance); null when not known. */
368119
+ childLock: zod.z.boolean().nullable(),
368120
+ /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
368121
+ activeEvents: zod.z.array(zod.z.string()),
368122
+ /** Numeric error code when the appliance reports one; null otherwise. */
367591
368123
  errorCode: zod.z.number().nullable(),
368124
+ /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
367592
368125
  errorLabel: zod.z.string().nullable(),
368126
+ /** Ms epoch of the last value received from the appliance; 0 = never. */
367593
368127
  lastFetchedAt: zod.z.number()
367594
368128
  });
368129
+ var DeviceIdSchema = zod.z.number().int().nonnegative();
368130
+ var DishwasherValueSchema = zod.z.union([
368131
+ zod.z.boolean(),
368132
+ zod.z.string(),
368133
+ zod.z.number()
368134
+ ]);
368135
+ var DishwasherAccessSchema = zod.z.enum([
368136
+ "none",
368137
+ "read",
368138
+ "readWrite",
368139
+ "writeOnly",
368140
+ "readStatic"
368141
+ ]);
368142
+ var DishwasherOptionDescriptorSchema = zod.z.object({
368143
+ /** Feature key (`Dishcare.Dishwasher.Option.ExtraDry`). */
368144
+ key: zod.z.string(),
368145
+ /** Last segment of the key, for display. */
368146
+ name: zod.z.string(),
368147
+ type: zod.z.enum([
368148
+ "boolean",
368149
+ "number",
368150
+ "enum",
368151
+ "string"
368152
+ ]),
368153
+ access: DishwasherAccessSchema,
368154
+ /** The appliance reports this entity as currently available. */
368155
+ available: zod.z.boolean(),
368156
+ /** Unit the description declares for a number; null when it declares none. */
368157
+ unit: zod.z.string().nullable(),
368158
+ min: zod.z.number().nullable(),
368159
+ max: zod.z.number().nullable(),
368160
+ step: zod.z.number().nullable(),
368161
+ /** Allowed members of an enum; null for other types. */
368162
+ enumValues: zod.z.array(zod.z.string()).nullable(),
368163
+ defaultValue: DishwasherValueSchema.nullable()
368164
+ });
368165
+ var DishwasherProgramSchema = zod.z.object({
368166
+ /** Feature key (`Dishcare.Dishwasher.Program.Eco50`) — what `setProgram` / `start` take. */
368167
+ key: zod.z.string(),
368168
+ name: zod.z.string(),
368169
+ /** The appliance currently offers this program. */
368170
+ available: zod.z.boolean(),
368171
+ /** The options this program lists, with the program-level access / availability / default. */
368172
+ options: zod.z.array(DishwasherOptionDescriptorSchema)
368173
+ });
368174
+ var DishwasherOptionsSchema = zod.z.object({
368175
+ availability: DishwasherAvailabilitySchema,
368176
+ /** What the appliance can be told to do, from the commands its description declares. */
368177
+ supportsStart: zod.z.boolean(),
368178
+ supportsPause: zod.z.boolean(),
368179
+ supportsResume: zod.z.boolean(),
368180
+ supportsStop: zod.z.boolean(),
368181
+ supportsPower: zod.z.boolean(),
368182
+ /** Every program the description lists, with its options. */
368183
+ programs: zod.z.array(DishwasherProgramSchema),
368184
+ /** Every option and setting entity the description lists (programs reference these by key). */
368185
+ options: zod.z.array(DishwasherOptionDescriptorSchema)
368186
+ });
367595
368187
  var dishwasherCapability = {
367596
368188
  name: "dishwasher",
367597
368189
  scope: "device",
@@ -367599,86 +368191,102 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
367599
368191
  mode: "singleton",
367600
368192
  deviceTypes: [require_err_msg.DeviceType.Dishwasher],
367601
368193
  methods: {
367602
- start: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
368194
+ /**
368195
+ * Start a program. Without `program` the selected one starts. `options` are
368196
+ * option feature keys; each is checked against the program's constraints.
368197
+ */
368198
+ start: require_sleep.method(zod.z.object({
368199
+ deviceId: DeviceIdSchema,
368200
+ program: zod.z.string().min(1).optional(),
368201
+ options: zod.z.record(zod.z.string(), DishwasherValueSchema).optional()
368202
+ }), zod.z.void(), {
367603
368203
  kind: "mutation",
367604
368204
  auth: "protected"
367605
368205
  }),
367606
- pause: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
368206
+ pause: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
367607
368207
  kind: "mutation",
367608
368208
  auth: "protected"
367609
368209
  }),
367610
- stop: require_sleep.method(zod.z.object({ deviceId: zod.z.number().int().nonnegative() }), zod.z.void(), {
368210
+ resume: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
367611
368211
  kind: "mutation",
367612
368212
  auth: "protected"
367613
368213
  }),
368214
+ stop: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), zod.z.void(), {
368215
+ kind: "mutation",
368216
+ auth: "protected"
368217
+ }),
368218
+ /** Select (not start) a program; refused when the appliance does not offer it. */
367614
368219
  setProgram: require_sleep.method(zod.z.object({
367615
- deviceId: zod.z.number().int().nonnegative(),
368220
+ deviceId: DeviceIdSchema,
367616
368221
  program: zod.z.string().min(1)
367617
368222
  }), zod.z.void(), {
367618
368223
  kind: "mutation",
367619
368224
  auth: "protected"
367620
368225
  }),
367621
368226
  setPower: require_sleep.method(zod.z.object({
367622
- deviceId: zod.z.number().int().nonnegative(),
368227
+ deviceId: DeviceIdSchema,
367623
368228
  on: zod.z.boolean()
367624
368229
  }), zod.z.void(), {
367625
368230
  kind: "mutation",
367626
368231
  auth: "protected"
367627
368232
  }),
367628
368233
  setHalfLoad: require_sleep.method(zod.z.object({
367629
- deviceId: zod.z.number().int().nonnegative(),
368234
+ deviceId: DeviceIdSchema,
367630
368235
  on: zod.z.boolean()
367631
368236
  }), zod.z.void(), {
367632
368237
  kind: "mutation",
367633
368238
  auth: "protected"
367634
368239
  }),
367635
368240
  setHygienePlus: require_sleep.method(zod.z.object({
367636
- deviceId: zod.z.number().int().nonnegative(),
368241
+ deviceId: DeviceIdSchema,
367637
368242
  on: zod.z.boolean()
367638
368243
  }), zod.z.void(), {
367639
368244
  kind: "mutation",
367640
368245
  auth: "protected"
367641
368246
  }),
367642
368247
  setExtraDry: require_sleep.method(zod.z.object({
367643
- deviceId: zod.z.number().int().nonnegative(),
368248
+ deviceId: DeviceIdSchema,
367644
368249
  on: zod.z.boolean()
367645
368250
  }), zod.z.void(), {
367646
368251
  kind: "mutation",
367647
368252
  auth: "protected"
367648
368253
  }),
367649
368254
  setIntensiveZone: require_sleep.method(zod.z.object({
367650
- deviceId: zod.z.number().int().nonnegative(),
368255
+ deviceId: DeviceIdSchema,
367651
368256
  on: zod.z.boolean()
367652
368257
  }), zod.z.void(), {
367653
368258
  kind: "mutation",
367654
368259
  auth: "protected"
367655
368260
  }),
367656
368261
  setVarioSpeedPlus: require_sleep.method(zod.z.object({
367657
- deviceId: zod.z.number().int().nonnegative(),
368262
+ deviceId: DeviceIdSchema,
367658
368263
  on: zod.z.boolean()
367659
368264
  }), zod.z.void(), {
367660
368265
  kind: "mutation",
367661
368266
  auth: "protected"
367662
368267
  }),
367663
368268
  setChildLock: require_sleep.method(zod.z.object({
367664
- deviceId: zod.z.number().int().nonnegative(),
368269
+ deviceId: DeviceIdSchema,
367665
368270
  on: zod.z.boolean()
367666
368271
  }), zod.z.void(), {
367667
368272
  kind: "mutation",
367668
368273
  auth: "protected"
367669
368274
  }),
368275
+ /**
368276
+ * Write any option or setting by feature key. Refused when the description does
368277
+ * not list it, it is not writable or not available now, or the value is outside
368278
+ * its declared range / members.
368279
+ */
367670
368280
  setOption: require_sleep.method(zod.z.object({
367671
- deviceId: zod.z.number().int().nonnegative(),
368281
+ deviceId: DeviceIdSchema,
367672
368282
  key: zod.z.string().min(1),
367673
- value: zod.z.union([
367674
- zod.z.boolean(),
367675
- zod.z.string(),
367676
- zod.z.number()
367677
- ])
368283
+ value: DishwasherValueSchema
367678
368284
  }), zod.z.void(), {
367679
368285
  kind: "mutation",
367680
368286
  auth: "protected"
367681
- })
368287
+ }),
368288
+ /** What this appliance can do, from its own description — the UI is derived from it. */
368289
+ getOptions: require_sleep.method(zod.z.object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" })
367682
368290
  },
367683
368291
  status: {
367684
368292
  schema: DishwasherStatusSchema,
@@ -382698,6 +383306,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
382698
383306
  addonId: null,
382699
383307
  access: "view"
382700
383308
  },
383309
+ "dishwasher.getOptions": {
383310
+ capName: "dishwasher",
383311
+ capScope: "device",
383312
+ addonId: null,
383313
+ access: "view"
383314
+ },
382701
383315
  "dishwasher.getStatus": {
382702
383316
  capName: "dishwasher",
382703
383317
  capScope: "device",
@@ -382710,6 +383324,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
382710
383324
  addonId: null,
382711
383325
  access: "create"
382712
383326
  },
383327
+ "dishwasher.resume": {
383328
+ capName: "dishwasher",
383329
+ capScope: "device",
383330
+ addonId: null,
383331
+ access: "create"
383332
+ },
382713
383333
  "dishwasher.setChildLock": {
382714
383334
  capName: "dishwasher",
382715
383335
  capScope: "device",
@@ -387210,6 +387830,12 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
387210
387830
  addonId: null,
387211
387831
  access: "view"
387212
387832
  },
387833
+ "storageMigration.moverClearance": {
387834
+ capName: "storage-migration",
387835
+ capScope: "system",
387836
+ addonId: null,
387837
+ access: "view"
387838
+ },
387213
387839
  "storageMigration.movers": {
387214
387840
  capName: "storage-migration",
387215
387841
  capScope: "system",
@@ -392558,6 +393184,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
392558
393184
  form: "single",
392559
393185
  optional: false
392560
393186
  }],
393187
+ "dishwasher.getOptions": [{
393188
+ name: "deviceId",
393189
+ form: "single",
393190
+ optional: false
393191
+ }],
392561
393192
  "dishwasher.getStatus": [{
392562
393193
  name: "deviceId",
392563
393194
  form: "single",
@@ -392568,6 +393199,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
392568
393199
  form: "single",
392569
393200
  optional: false
392570
393201
  }],
393202
+ "dishwasher.resume": [{
393203
+ name: "deviceId",
393204
+ form: "single",
393205
+ optional: false
393206
+ }],
392571
393207
  "dishwasher.setChildLock": [{
392572
393208
  name: "deviceId",
392573
393209
  form: "single",
@@ -396202,6 +396838,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
396202
396838
  status: (input) => dispatch("storageMigration", "status", "query", input),
396203
396839
  cancel: (input) => dispatch("storageMigration", "cancel", "mutation", input),
396204
396840
  movers: (input) => dispatch("storageMigration", "movers", "query", input),
396841
+ moverClearance: (input) => dispatch("storageMigration", "moverClearance", "query", input),
396205
396842
  residue: (input) => dispatch("storageMigration", "residue", "query", input),
396206
396843
  drain: (input) => dispatch("storageMigration", "drain", "mutation", input),
396207
396844
  cleanupStart: (input) => dispatch("storageMigration", "cleanupStart", "mutation", input),
@@ -396739,7 +397376,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
396739
397376
  case "deviceTypes":
396740
397377
  return kind.startsWith("device-") || kind === "detection-blind";
396741
397378
  case "nodeIds":
396742
- return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "wrapper-update-available" || kind === "server-updated" || kind.startsWith("storage-location-");
397379
+ return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "wrapper-update-available" || kind === "server-updated" || kind.startsWith("storage-location-") || kind.startsWith("alert-");
396743
397380
  case "packageNames":
396744
397381
  return kind.endsWith("update-available") || kind === "addon-updated" || kind === "node-updating" || kind === "server-updated";
396745
397382
  case "consumableLowPercent":
@@ -396788,6 +397425,9 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
396788
397425
  "wrapper-update-available": "system",
396789
397426
  "backup-completed": "system",
396790
397427
  "export-completed": "system",
397428
+ "alert-critical": "system",
397429
+ "alert-error": "system",
397430
+ "alert-critical-resolved": "system",
396791
397431
  "storage-location-low": "system",
396792
397432
  "storage-location-critical": "system",
396793
397433
  "storage-location-ok": "system",
@@ -396923,6 +397563,16 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
396923
397563
  "storage-location-ok"
396924
397564
  ]
396925
397565
  },
397566
+ {
397567
+ id: "alerts",
397568
+ family: "system",
397569
+ label: "Alerts",
397570
+ kinds: [
397571
+ "alert-critical",
397572
+ "alert-error",
397573
+ "alert-critical-resolved"
397574
+ ]
397575
+ },
396926
397576
  {
396927
397577
  id: "alarm",
396928
397578
  family: "alarm",
@@ -397995,6 +398645,69 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
397995
398645
  return sensorKinds.every((kind) => NC_SENSOR_TRANSITION_KINDS.includes(normaliseKind(kind)));
397996
398646
  }
397997
398647
  var NC_SENSOR_TRANSITION_UNSUPPORTED_REASON = "Only a sensor that raises and clears (water, smoke, gas, CO, vibration, tamper, motion, contact) can fire on one edge. Pick only such sensor kinds, or the rule fires on every change.";
398648
+ var AlertEpisodePayloadSchema = zod.z.object({
398649
+ phase: zod.z.enum(["raised", "resolved"]),
398650
+ episodeId: zod.z.string().min(1),
398651
+ alertId: zod.z.string().min(1),
398652
+ severity: AlertSeveritySchema,
398653
+ category: zod.z.string(),
398654
+ title: zod.z.string(),
398655
+ message: zod.z.string(),
398656
+ sourceType: zod.z.string().optional(),
398657
+ sourceId: zod.z.string().optional(),
398658
+ nodeId: zod.z.string().optional(),
398659
+ /** The alert's own action link (where the operator acts on it), when it has one. */
398660
+ actionUrl: zod.z.string().optional(),
398661
+ at: zod.z.number()
398662
+ });
398663
+ function parseAlertEpisodePayload(data) {
398664
+ const parsed = AlertEpisodePayloadSchema.safeParse(data);
398665
+ return parsed.success ? parsed.data : null;
398666
+ }
398667
+ function isAlertEpisodeOpen(status) {
398668
+ return status !== "completed" && status !== "dismissed";
398669
+ }
398670
+ function isAlertEpisodeSeverity(severity) {
398671
+ return severity === "critical" || severity === "error";
398672
+ }
398673
+ function alertEpisodeId(alert) {
398674
+ const minted = alert.metadata?.["episodeId"];
398675
+ return typeof minted === "string" && minted.length > 0 ? minted : `${alert.id}@${String(alert.createdAt)}`;
398676
+ }
398677
+ function alertNodeId(alert) {
398678
+ const direct = alert.metadata?.["nodeId"];
398679
+ if (typeof direct === "string") return direct;
398680
+ const runner = alert.metadata?.["runner"];
398681
+ if (typeof runner === "object" && runner !== null && "nodeId" in runner) {
398682
+ const id = runner.nodeId;
398683
+ if (typeof id === "string") return id;
398684
+ }
398685
+ return alert.source?.type === "node" ? alert.source.id : void 0;
398686
+ }
398687
+ function alertEpisodeKind(severity, phase) {
398688
+ if (severity === "critical") return phase === "raised" ? "alert-critical" : "alert-critical-resolved";
398689
+ if (severity === "error" && phase === "raised") return "alert-error";
398690
+ return null;
398691
+ }
398692
+ function alertEpisodePayload(alert, phase, at) {
398693
+ const nodeId = alertNodeId(alert);
398694
+ return {
398695
+ phase,
398696
+ episodeId: alertEpisodeId(alert),
398697
+ alertId: alert.id,
398698
+ severity: alert.severity,
398699
+ category: alert.category,
398700
+ title: alert.title,
398701
+ message: alert.message,
398702
+ ...alert.source !== void 0 ? {
398703
+ sourceType: alert.source.type,
398704
+ sourceId: alert.source.id
398705
+ } : {},
398706
+ ...nodeId !== void 0 ? { nodeId } : {},
398707
+ ...alert.action?.href !== void 0 ? { actionUrl: alert.action.href } : {},
398708
+ at
398709
+ };
398710
+ }
397998
398711
  var RULE_BODY_ONLY = ["body"];
397999
398712
  var RULE_TEXT = ["title", "body"];
398000
398713
  var BATTERY = ["device-battery-low", "device-battery-normal"];
@@ -398004,6 +398717,11 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
398004
398717
  "device-tank-full",
398005
398718
  "device-tank-normal"
398006
398719
  ];
398720
+ var ALERT_EPISODE = [
398721
+ "alert-critical",
398722
+ "alert-error",
398723
+ "alert-critical-resolved"
398724
+ ];
398007
398725
  var UPDATES = [
398008
398726
  "addon-update-available",
398009
398727
  "server-update-available",
@@ -398525,7 +399243,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
398525
399243
  "server-updated",
398526
399244
  "storage-location-low",
398527
399245
  "storage-location-critical",
398528
- "storage-location-ok"
399246
+ "storage-location-ok",
399247
+ ...ALERT_EPISODE
398529
399248
  ]
398530
399249
  },
398531
399250
  {
@@ -398547,7 +399266,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
398547
399266
  "server-updated",
398548
399267
  "storage-location-low",
398549
399268
  "storage-location-critical",
398550
- "storage-location-ok"
399269
+ "storage-location-ok",
399270
+ ...ALERT_EPISODE
398551
399271
  ]
398552
399272
  },
398553
399273
  {
@@ -398614,7 +399334,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
398614
399334
  families: ["rule"],
398615
399335
  fields: [...RULE_TEXT],
398616
399336
  kinds: ["system"],
398617
- systemEventKinds: ["recording-degraded"]
399337
+ systemEventKinds: ["recording-degraded", ...ALERT_EPISODE]
398618
399338
  },
398619
399339
  {
398620
399340
  name: "locationName",
@@ -398811,13 +399531,53 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
398811
399531
  },
398812
399532
  {
398813
399533
  name: "title",
398814
- label: "Export title",
399534
+ label: "Export or alert title",
398815
399535
  example: "Ingresso 08:00",
398816
399536
  group: "system",
398817
399537
  families: ["rule"],
398818
399538
  fields: [...RULE_TEXT],
398819
399539
  kinds: ["system"],
398820
- systemEventKinds: ["export-completed"]
399540
+ systemEventKinds: ["export-completed", ...ALERT_EPISODE]
399541
+ },
399542
+ {
399543
+ name: "severity",
399544
+ label: "Alert severity",
399545
+ example: "critical",
399546
+ group: "system",
399547
+ families: ["rule"],
399548
+ fields: [...RULE_TEXT],
399549
+ kinds: ["system"],
399550
+ systemEventKinds: [...ALERT_EPISODE]
399551
+ },
399552
+ {
399553
+ name: "category",
399554
+ label: "Alert category",
399555
+ example: "addon.runner-failed",
399556
+ group: "system",
399557
+ families: ["rule"],
399558
+ fields: [...RULE_TEXT],
399559
+ kinds: ["system"],
399560
+ systemEventKinds: [...ALERT_EPISODE]
399561
+ },
399562
+ {
399563
+ name: "source",
399564
+ label: "Alert source (addon, node or runner)",
399565
+ example: "recorder",
399566
+ group: "system",
399567
+ families: ["rule"],
399568
+ fields: [...RULE_TEXT],
399569
+ kinds: ["system"],
399570
+ systemEventKinds: [...ALERT_EPISODE]
399571
+ },
399572
+ {
399573
+ name: "actionUrl",
399574
+ label: "Alert action link",
399575
+ example: "/system/addons",
399576
+ group: "system",
399577
+ families: ["rule"],
399578
+ fields: [...RULE_TEXT],
399579
+ kinds: ["system"],
399580
+ systemEventKinds: [...ALERT_EPISODE]
398821
399581
  },
398822
399582
  {
398823
399583
  name: "exportId",
@@ -399013,7 +399773,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
399013
399773
  "device-tank-full",
399014
399774
  "device-tank-normal",
399015
399775
  "node-restarting",
399016
- "node-updating"
399776
+ "node-updating",
399777
+ "alert-critical",
399778
+ "alert-error",
399779
+ "alert-critical-resolved"
399017
399780
  ];
399018
399781
  function servedTemplateVars() {
399019
399782
  return NC_TEMPLATE_VARS.filter((d) => !isHeldBackTemplateVar(d)).map((d) => d.systemEventKinds === void 0 ? d : {
@@ -402214,6 +402977,52 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
402214
402977
  }
402215
402978
  current[parts[parts.length - 1]] = value;
402216
402979
  }
402980
+ var OAUTH_LANDING_PATH = "/sso-complete";
402981
+ var OAUTH_LANDING_REASONS = [
402982
+ "access_denied",
402983
+ "invalid_state",
402984
+ "invalid_request",
402985
+ "unknown_broker",
402986
+ "not_configured",
402987
+ "exchange_failed",
402988
+ "no_refresh_token",
402989
+ "unavailable",
402990
+ "unknown"
402991
+ ];
402992
+ function isOauthLandingReason(value) {
402993
+ return OAUTH_LANDING_REASONS.some((reason) => reason === value);
402994
+ }
402995
+ function oauthLandingUrl(origin, outcome) {
402996
+ const params = new URLSearchParams({
402997
+ return: "oauth",
402998
+ status: outcome.status
402999
+ });
403000
+ if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
403001
+ if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
403002
+ if (outcome.integration !== void 0) params.set("integration", outcome.integration);
403003
+ if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
403004
+ if (outcome.action === "disconnect") params.set("action", "disconnect");
403005
+ return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
403006
+ }
403007
+ function parseOauthLandingFragment(hash) {
403008
+ if (!hash.startsWith("#")) return null;
403009
+ const params = new URLSearchParams(hash.slice(1));
403010
+ if (params.get("return") !== "oauth") return null;
403011
+ const status = params.get("status");
403012
+ if (status !== "success" && status !== "error") return null;
403013
+ const addonId = params.get("addon");
403014
+ const brokerId = params.get("broker");
403015
+ const integration = params.get("integration");
403016
+ const reasonRaw = params.get("reason");
403017
+ return {
403018
+ status,
403019
+ ...addonId !== null && addonId.length > 0 ? { addonId } : {},
403020
+ ...brokerId !== null && brokerId.length > 0 ? { brokerId } : {},
403021
+ ...integration !== null && integration.length > 0 ? { integration } : {},
403022
+ ...status === "error" ? { reason: isOauthLandingReason(reasonRaw) ? reasonRaw : "unknown" } : {},
403023
+ ...params.get("action") === "disconnect" ? { action: "disconnect" } : {}
403024
+ };
403025
+ }
402217
403026
  var MB = 1024 * 1024;
402218
403027
  var DEFAULT_POOL_MEMORY_POLICY = {
402219
403028
  sampleIntervalMs: 6e4,
@@ -403066,6 +403875,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
403066
403875
  exports.AlarmArmModeSchema = AlarmArmModeSchema;
403067
403876
  exports.AlarmPanelStatusSchema = AlarmPanelStatusSchema;
403068
403877
  exports.AlarmStateSchema = AlarmStateSchema;
403878
+ exports.AlertEpisodePayloadSchema = AlertEpisodePayloadSchema;
403069
403879
  exports.AlertSchema = AlertSchema;
403070
403880
  exports.AlertSeveritySchema = AlertSeveritySchema;
403071
403881
  exports.AlertSourceSchema = AlertSourceSchema;
@@ -403544,9 +404354,16 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
403544
404354
  exports.DiscoveredChildStatusSchema = DiscoveredChildStatusSchema;
403545
404355
  exports.DiscoveredDeviceSchema = DiscoveredDeviceSchema;
403546
404356
  exports.DiscoveredTargetSchema = DiscoveredTargetSchema;
404357
+ exports.DishwasherAccessSchema = DishwasherAccessSchema;
404358
+ exports.DishwasherAvailabilitySchema = DishwasherAvailabilitySchema;
403547
404359
  exports.DishwasherConsumableLevelSchema = DishwasherConsumableLevelSchema;
404360
+ exports.DishwasherOperationStateSchema = DishwasherOperationStateSchema;
404361
+ exports.DishwasherOptionDescriptorSchema = DishwasherOptionDescriptorSchema;
404362
+ exports.DishwasherOptionsSchema = DishwasherOptionsSchema;
404363
+ exports.DishwasherProgramSchema = DishwasherProgramSchema;
403548
404364
  exports.DishwasherStateSchema = DishwasherStateSchema;
403549
404365
  exports.DishwasherStatusSchema = DishwasherStatusSchema;
404366
+ exports.DishwasherValueSchema = DishwasherValueSchema;
403550
404367
  exports.DiskReconcileJobSchema = DiskReconcileJobSchema;
403551
404368
  exports.DisposerChain = require_sleep.DisposerChain;
403552
404369
  exports.DoorbellPressEventSchema = DoorbellPressEventSchema;
@@ -404214,6 +405031,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
404214
405031
  exports.NotificationSchema = NotificationSchema;
404215
405032
  exports.NotifierStatusSchema = NotifierStatusSchema;
404216
405033
  exports.NumericSensorStatusSchema = NumericSensorStatusSchema;
405034
+ exports.OAUTH_LANDING_PATH = OAUTH_LANDING_PATH;
405035
+ exports.OAUTH_LANDING_REASONS = OAUTH_LANDING_REASONS;
404217
405036
  exports.OPERATOR_WRITTEN_STALE_MS = OPERATOR_WRITTEN_STALE_MS;
404218
405037
  exports.OPS_LOG_DEFAULT_LIMIT = OPS_LOG_DEFAULT_LIMIT;
404219
405038
  exports.OPS_LOG_RING_DEFAULT_MAX = OPS_LOG_RING_DEFAULT_MAX;
@@ -404894,6 +405713,10 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
404894
405713
  exports.alarmPanelCapability = alarmPanelCapability;
404895
405714
  exports.alarmPatchLanded = alarmPatchLanded;
404896
405715
  exports.alarmProfileById = alarmProfileById;
405716
+ exports.alertEpisodeId = alertEpisodeId;
405717
+ exports.alertEpisodeKind = alertEpisodeKind;
405718
+ exports.alertEpisodePayload = alertEpisodePayload;
405719
+ exports.alertNodeId = alertNodeId;
404897
405720
  exports.alertsCapability = alertsCapability;
404898
405721
  exports.allDoActions = allDoActions;
404899
405722
  exports.allowedSuppliedEffect = allowedSuppliedEffect;
@@ -405250,6 +406073,8 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
405250
406073
  exports.invocationFromEncodeProfile = require_composition.invocationFromEncodeProfile;
405251
406074
  exports.isAgentOnlyPlacement = isAgentOnlyPlacement;
405252
406075
  exports.isAlarmControllableCap = isAlarmControllableCap;
406076
+ exports.isAlertEpisodeOpen = isAlertEpisodeOpen;
406077
+ exports.isAlertEpisodeSeverity = isAlertEpisodeSeverity;
405253
406078
  exports.isAppLinkBase = isAppLinkBase;
405254
406079
  exports.isAppStartRefusal = isAppStartRefusal;
405255
406080
  exports.isArrayOutputSchema = isArrayOutputSchema;
@@ -405285,6 +406110,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
405285
406110
  exports.isLocationEnabled = isLocationEnabled;
405286
406111
  exports.isMediaOwnerType = isMediaOwnerType;
405287
406112
  exports.isNode = isNode;
406113
+ exports.isOauthLandingReason = isOauthLandingReason;
405288
406114
  exports.isObjectInput = isObjectInput;
405289
406115
  exports.isOccupancyRule = isOccupancyRule;
405290
406116
  exports.isOwnerOnlyEventCategory = require_event_category.isOwnerOnlyEventCategory;
@@ -405408,6 +406234,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
405408
406234
  exports.notifierCapability = notifierCapability;
405409
406235
  exports.numericSensorCapability = numericSensorCapability;
405410
406236
  exports.oauthIntegrationCapability = oauthIntegrationCapability;
406237
+ exports.oauthLandingUrl = oauthLandingUrl;
405411
406238
  exports.objectInputDeclaresAddonId = objectInputDeclaresAddonId;
405412
406239
  exports.objectInputDeclaresRequiredDeviceId = objectInputDeclaresRequiredDeviceId;
405413
406240
  exports.occupancyScope = occupancyScope;
@@ -405420,6 +406247,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
405420
406247
  exports.overlayClusterStepSettings = overlayClusterStepSettings;
405421
406248
  exports.packagePickerItems = packagePickerItems;
405422
406249
  exports.parseAdminBase = parseAdminBase;
406250
+ exports.parseAlertEpisodePayload = parseAlertEpisodePayload;
405423
406251
  exports.parseAppAuthCallback = parseAppAuthCallback;
405424
406252
  exports.parseAppFlowRequest = parseAppFlowRequest;
405425
406253
  exports.parseAppPageLang = parseAppPageLang;
@@ -405430,6 +406258,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
405430
406258
  exports.parseJsonObject = require_sleep.parseJsonObject;
405431
406259
  exports.parseJsonUnknown = require_sleep.parseJsonUnknown;
405432
406260
  exports.parseMediaOwnerKey = parseMediaOwnerKey;
406261
+ exports.parseOauthLandingFragment = parseOauthLandingFragment;
405433
406262
  exports.parseOccupancyScope = parseOccupancyScope;
405434
406263
  exports.parseProcStatus = parseProcStatus;
405435
406264
  exports.parseProfileBrokerId = require_sleep.parseProfileBrokerId;
@@ -405632,6 +406461,7 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
405632
406461
  exports.snapshotCapability = snapshotCapability;
405633
406462
  exports.splitTranslationRef = splitTranslationRef;
405634
406463
  exports.ssoBridgeCapability = ssoBridgeCapability;
406464
+ exports.startMoverUnderClearance = startMoverUnderClearance;
405635
406465
  exports.startReachabilityPoll = startReachabilityPoll;
405636
406466
  exports.stateLabel = stateLabel;
405637
406467
  exports.stateVocabularyFor = stateVocabularyFor;
@@ -571830,6 +572660,10 @@ var require_app_page_shell = __commonJS({
571830
572660
  `.error:empty{display:none}`,
571831
572661
  `.btn:focus-visible,.link:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}`,
571832
572662
  `code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow-wrap:anywhere}`,
572663
+ `.field{display:flex;flex-direction:column;gap:var(--spacing-md)}`,
572664
+ `.field label{font-size:12px;line-height:16px;font-weight:500;color:var(--color-foreground-muted)}`,
572665
+ `input[type=url],input[type=text]{width:100%;min-height:44px;padding:10px 12px;border:1px solid var(--color-border);border-radius:var(--radius-lg);background:var(--color-background);color:var(--color-foreground);font:inherit}`,
572666
+ `input[type=url]:focus-visible,input[type=text]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}`,
571833
572667
  `form{display:contents}`,
571834
572668
  `.actions{display:flex;flex-direction:column;gap:var(--spacing-md)}`
571835
572669
  ].join("");
@@ -571973,6 +572807,7 @@ var require_oauth2_routes = __commonJS({
571973
572807
  exports.isRedirectUriAllowed = isRedirectUriAllowed;
571974
572808
  exports.summariseScopes = summariseScopes;
571975
572809
  exports.registerOauth2Routes = registerOauth2Routes;
572810
+ var types_1 = require_dist4();
571976
572811
  var session_cookie_js_1 = require_session_cookie();
571977
572812
  var app_page_strings_js_1 = require_app_page_strings();
571978
572813
  var consent_page_js_1 = require_consent_page();
@@ -572119,7 +572954,7 @@ var require_oauth2_routes = __commonJS({
572119
572954
  }
572120
572955
  return scopes.map((s) => s.type).join(", ") || "no permissions";
572121
572956
  }
572122
- function refuseAuthorize(reply, logger, method, call, refusal) {
572957
+ function refuseAuthorize(request, reply, logger, method, call, refusal) {
572123
572958
  logger.warn(`oauth2 authorize refused: ${refusal.error}`, {
572124
572959
  meta: {
572125
572960
  method,
@@ -572132,7 +572967,18 @@ var require_oauth2_routes = __commonJS({
572132
572967
  if (refusal.retryAfterSec !== void 0) {
572133
572968
  void reply.header("Retry-After", String(refusal.retryAfterSec));
572134
572969
  }
572135
- return reply.status(refusal.status).send({ error: refusal.error, ...refusal.detail });
572970
+ return answerRefusal(request, reply, refusal.status, { error: refusal.error, ...refusal.detail }, refusal.status === 503 ? "unavailable" : "invalid_request", typeof refusal.detail?.received === "string" ? refusal.detail.received : void 0);
572971
+ }
572972
+ function answerRefusal(request, reply, status, body, reason, integration) {
572973
+ const accept = request.headers.accept;
572974
+ if (typeof accept === "string" && accept.includes("text/html")) {
572975
+ return reply.redirect((0, types_1.oauthLandingUrl)(null, {
572976
+ status: "error",
572977
+ reason,
572978
+ ...integration !== void 0 && /^[A-Za-z0-9._-]{1,64}$/.test(integration) ? { integration } : {}
572979
+ }));
572980
+ }
572981
+ return reply.status(status).send(body);
572136
572982
  }
572137
572983
  function refuseUnauthenticated(request, reply, logger, call, reason) {
572138
572984
  const redirected = (0, session_cookie_js_1.shouldRedirectToLogin)(request.method, request.headers.accept);
@@ -572181,13 +573027,13 @@ var require_oauth2_routes = __commonJS({
572181
573027
  }
572182
573028
  const registry = deps.getRegistry();
572183
573029
  if (!registry) {
572184
- return reply.status(503).send({ error: "service_unavailable" });
573030
+ return answerRefusal(request, reply, 503, { error: "service_unavailable" }, "unavailable");
572185
573031
  }
572186
573032
  const { descriptors, pendingAddons } = await readIntegrationRegistry(registry);
572187
573033
  const query = request.query;
572188
573034
  const v = validateAuthorizeQuery(query, descriptors, pendingAddons);
572189
573035
  if (!v.ok) {
572190
- return refuseAuthorize(reply, deps.logger, "GET", call, v);
573036
+ return refuseAuthorize(request, reply, deps.logger, "GET", call, v);
572191
573037
  }
572192
573038
  const descriptor = descriptors.get(v.integration);
572193
573039
  if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
@@ -572201,7 +573047,7 @@ var require_oauth2_routes = __commonJS({
572201
573047
  allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? []
572202
573048
  }
572203
573049
  });
572204
- return reply.status(400).send({ error: "invalid_request \u2014 redirect_uri not allowed for this integration" });
573050
+ return answerRefusal(request, reply, 400, { error: "invalid_request \u2014 redirect_uri not allowed for this integration" }, "invalid_request", v.integration);
572205
573051
  }
572206
573052
  const html = (0, consent_page_js_1.renderConsentPage)({
572207
573053
  lang: (0, app_page_strings_js_1.resolveAppPageLang)({
@@ -572258,7 +573104,7 @@ var require_oauth2_routes = __commonJS({
572258
573104
  }
572259
573105
  const registry = deps.getRegistry();
572260
573106
  if (!registry) {
572261
- return reply.status(503).send({ error: "service_unavailable" });
573107
+ return answerRefusal(request, reply, 503, { error: "service_unavailable" }, "unavailable");
572262
573108
  }
572263
573109
  const { descriptors, pendingAddons } = await readIntegrationRegistry(registry);
572264
573110
  const body = request.body;
@@ -572272,7 +573118,7 @@ var require_oauth2_routes = __commonJS({
572272
573118
  };
572273
573119
  const v = validateAuthorizeQuery(formQuery, descriptors, pendingAddons);
572274
573120
  if (!v.ok) {
572275
- return refuseAuthorize(reply, deps.logger, "POST", call, v);
573121
+ return refuseAuthorize(request, reply, deps.logger, "POST", call, v);
572276
573122
  }
572277
573123
  const descriptor = descriptors.get(v.integration);
572278
573124
  if (!isRedirectUriAllowed(v.redirectUri, descriptor.allowedRedirectPrefixes, descriptor.allowedPrivateHostPaths ?? [])) {
@@ -572286,7 +573132,7 @@ var require_oauth2_routes = __commonJS({
572286
573132
  allowedPrivateHostPaths: descriptor.allowedPrivateHostPaths ?? []
572287
573133
  }
572288
573134
  });
572289
- return reply.status(400).send({ error: "invalid_request \u2014 redirect_uri not allowed for this integration" });
573135
+ return answerRefusal(request, reply, 400, { error: "invalid_request \u2014 redirect_uri not allowed for this integration" }, "invalid_request", v.integration);
572290
573136
  }
572291
573137
  if (body.consent !== "allow") {
572292
573138
  return reply.redirect(`${v.redirectUri}?error=access_denied&state=${encodeURIComponent(v.state)}`);
@@ -592273,7 +593119,7 @@ var require_manual_boot = __commonJS({
592273
593119
  throw new Error("the process service did not answer");
592274
593120
  return rows;
592275
593121
  },
592276
- // The verdict reaches the alert centre (one alert + the direct critical push).
593122
+ // The verdict reaches the alert centre (one critical alert).
592277
593123
  onResult: (check) => {
592278
593124
  eventBusService.emit({
592279
593125
  id: (0, node_crypto_1.randomUUID)(),