@camstack/system 1.2.287 → 1.2.288

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-config-contribution.d.ts +1 -0
  18. package/dist/builtins/device-manager/device-manager.addon.js +472 -3
  19. package/dist/builtins/device-manager/device-manager.addon.mjs +472 -3
  20. package/dist/builtins/device-manager/recording-onboard-config-schema.d.ts +41 -0
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  22. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  23. package/dist/builtins/hub-forwarder/index.js +1 -1
  24. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  26. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  28. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  29. package/dist/builtins/local-network/local-network.addon.js +1 -1
  30. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  31. package/dist/builtins/loki-logging/index.js +1 -1
  32. package/dist/builtins/loki-logging/index.mjs +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  35. package/dist/builtins/platform-probe/index.js +1 -1
  36. package/dist/builtins/platform-probe/index.mjs +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  39. package/dist/builtins/snapshot/index.js +1 -1
  40. package/dist/builtins/snapshot/index.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/system-config/system-config.addon.js +1 -1
  48. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  49. package/dist/builtins/winston-logging/index.js +1 -1
  50. package/dist/builtins/winston-logging/index.mjs +1 -1
  51. package/dist/{dist-C51Z6L6t.js → dist-BYnHJU96.js} +563 -2
  52. package/dist/{dist-jv1aoo9E.mjs → dist-DKFb2936.mjs} +546 -3
  53. package/dist/index.js +1 -1
  54. package/dist/index.mjs +1 -1
  55. package/dist/{retired-settings-keys-BFeJh5E7.js → retired-settings-keys-C6rFqHR7.js} +1 -1
  56. package/dist/{retired-settings-keys-Bulcwqkz.mjs → retired-settings-keys-tuNxNNpR.mjs} +1 -1
  57. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
- import { $ as deviceStateCapability, Bt as resolveContainerPrimaryChild, D as STREAM_PROFILE_META, En as isDeviceConfigCap, Fn as EventCategory, H as buildStreamParamsConfigSchema, Jt as siteZoneKey, Pn as sleep$1, Pt as parseStreamParamsFormPatch, Q as deviceManagerCapability, St as locationIconKey, Ut as runtimeStatePolicyFor, _n as WELL_KNOWN_TAB_MAP, cn as BaseAddon, et as deviceStatusCapability, fn as DeviceFeature, it as enumerateSchemaFields, jt as normalizeUnit, m as DeviceStatusSchema, mn as DeviceType, o as CAMERA_SWITCH_ORDER, on as errMsg, pn as DeviceRole, rt as enumerateItemArrayFields, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, v as LocationIconIdSchema } from "../../dist-jv1aoo9E.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Bulcwqkz.mjs";
1
+ import { Kt as runtimeStatePolicyFor, Ln as sleep$1, Lt as parseStreamParamsFormPatch, Pt as normalizeUnit, Rn as EventCategory, Tt as locationIconKey, Ut as resolveContainerPrimaryChild, W as buildStreamParamsConfigSchema, Zt as siteZoneKey, _n as DeviceType, at as enumerateItemArrayFields, bn as WELL_KNOWN_TAB_MAP, d as DAY_NAMES, dn as BaseAddon, et as deviceManagerCapability, ft as formatMinutes, gn as DeviceRole, h as DeviceStatusSchema, hn as DeviceFeature, k as STREAM_PROFILE_META, kn as isDeviceConfigCap, ln as errMsg, nt as deviceStatusCapability, o as CAMERA_SWITCH_ORDER, ot as enumerateSchemaFields, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, tt as deviceStateCapability, x as MINUTES_PER_DAY, y as LocationIconIdSchema } from "../../dist-DKFb2936.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-tuNxNNpR.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -1286,6 +1286,468 @@ function parseImageSettingsFormPatch(patch) {
1286
1286
  return out;
1287
1287
  }
1288
1288
  //#endregion
1289
+ //#region src/builtins/device-manager/recording-onboard-config-schema.ts
1290
+ var KEY_PREFIX = "recordingOnboard_";
1291
+ var RECORDING_ONBOARD_KEYS = {
1292
+ storageBanner: `${KEY_PREFIX}storageBanner`,
1293
+ volumes: `${KEY_PREFIX}volumes`,
1294
+ tracks: `${KEY_PREFIX}tracks`,
1295
+ enabled: `${KEY_PREFIX}enabled`,
1296
+ overwriteWhenFull: `${KEY_PREFIX}overwriteWhenFull`,
1297
+ preRecordSec: `${KEY_PREFIX}preRecordSec`,
1298
+ postRecordSec: `${KEY_PREFIX}postRecordSec`,
1299
+ segmentMinutes: `${KEY_PREFIX}segmentMinutes`,
1300
+ windows: `${KEY_PREFIX}windows`
1301
+ };
1302
+ /** Row keys inside the windows `editable-array`. Local, not prefixed. */
1303
+ var WINDOW_ROW_KEYS = {
1304
+ trigger: "trigger",
1305
+ day: "day",
1306
+ startMinute: "startMinute",
1307
+ endMinute: "endMinute"
1308
+ };
1309
+ var TRIGGER_LABELS = {
1310
+ continuous: "Continuous",
1311
+ motion: "Motion",
1312
+ person: "Person",
1313
+ vehicle: "Vehicle",
1314
+ animal: "Animal",
1315
+ lineCrossing: "Line crossing",
1316
+ intrusion: "Intrusion",
1317
+ loitering: "Loitering",
1318
+ alarmInput: "Alarm input"
1319
+ };
1320
+ /**
1321
+ * Append a support reason to a description, so a disabled control always
1322
+ * says WHY. Returns undefined when there is nothing to say, because an
1323
+ * empty description renders as a stray blank line.
1324
+ */
1325
+ function describeSupport(base, support) {
1326
+ if (support.writable) return base.length > 0 ? base : void 0;
1327
+ const suffix = `Read-only on this camera: ${support.reason ?? "this camera does not expose it"}`;
1328
+ return base.length > 0 ? `${base} ${suffix}` : suffix;
1329
+ }
1330
+ function storageBanner(status) {
1331
+ if (!status) return {
1332
+ type: "info",
1333
+ key: RECORDING_ONBOARD_KEYS.storageBanner,
1334
+ label: "Onboard storage",
1335
+ variant: "info",
1336
+ content: "Not read from the camera yet."
1337
+ };
1338
+ if (status.recordingToNowhere) return {
1339
+ type: "info",
1340
+ key: RECORDING_ONBOARD_KEYS.storageBanner,
1341
+ label: "Onboard storage",
1342
+ variant: "danger",
1343
+ content: "This camera is scheduled to record and has no usable storage. Everything it records is discarded."
1344
+ };
1345
+ if (status.storage.kind === "absent") return {
1346
+ type: "info",
1347
+ key: RECORDING_ONBOARD_KEYS.storageBanner,
1348
+ label: "Onboard storage",
1349
+ variant: "warning",
1350
+ content: `No usable storage. ${status.storage.reason}`
1351
+ };
1352
+ if (status.storage.kind === "unknown") return {
1353
+ type: "info",
1354
+ key: RECORDING_ONBOARD_KEYS.storageBanner,
1355
+ label: "Onboard storage",
1356
+ variant: "warning",
1357
+ content: `CamStack could not read this camera's storage, so its state is unknown — not empty. ${status.storage.reason}`
1358
+ };
1359
+ return {
1360
+ type: "info",
1361
+ key: RECORDING_ONBOARD_KEYS.storageBanner,
1362
+ label: "Onboard storage",
1363
+ variant: "info",
1364
+ content: "Free space is not a measure of how much footage is held: a card that has wrapped once sits at a fixed reserve forever."
1365
+ };
1366
+ }
1367
+ function volumesField(status) {
1368
+ const rows = status && status.storage.kind === "present" ? status.storage.volumes.map((volume) => ({
1369
+ id: volume.id,
1370
+ label: volume.label ?? null,
1371
+ status: volume.status === "ok" ? "ok" : "warn",
1372
+ capacity: volume.capacityMb === null ? null : `${String(volume.capacityMb)} MB`,
1373
+ free: volume.freeMb === null ? null : `${String(volume.freeMb)} MB`
1374
+ })) : [];
1375
+ return {
1376
+ type: "object-array",
1377
+ key: RECORDING_ONBOARD_KEYS.volumes,
1378
+ label: "Cards",
1379
+ columns: [
1380
+ {
1381
+ key: "id",
1382
+ label: "Id",
1383
+ kind: "monospace",
1384
+ width: "70px"
1385
+ },
1386
+ {
1387
+ key: "label",
1388
+ label: "Name",
1389
+ kind: "monospace"
1390
+ },
1391
+ {
1392
+ key: "status",
1393
+ label: "Status",
1394
+ kind: "status"
1395
+ },
1396
+ {
1397
+ key: "capacity",
1398
+ label: "Capacity"
1399
+ },
1400
+ {
1401
+ key: "free",
1402
+ label: "Free"
1403
+ }
1404
+ ],
1405
+ value: rows,
1406
+ emptyMessage: "The camera reports no storage."
1407
+ };
1408
+ }
1409
+ function tracksField(status) {
1410
+ const rows = (status?.tracks ?? []).map((track) => ({
1411
+ id: track.id,
1412
+ primary: track.id === status?.primaryTrackId ? "yes" : "",
1413
+ enabled: track.enabled ? "ok" : "idle",
1414
+ codec: track.codec,
1415
+ resolution: track.resolution
1416
+ }));
1417
+ return {
1418
+ type: "object-array",
1419
+ key: RECORDING_ONBOARD_KEYS.tracks,
1420
+ label: "Recording tracks",
1421
+ description: "Some cameras keep several tracks with independent schedules. Settings below apply to the primary one. Frame rate is deliberately absent — the value these cameras declare for it is wrong.",
1422
+ columns: [
1423
+ {
1424
+ key: "id",
1425
+ label: "Track",
1426
+ kind: "monospace",
1427
+ width: "80px"
1428
+ },
1429
+ {
1430
+ key: "primary",
1431
+ label: "Primary",
1432
+ width: "80px"
1433
+ },
1434
+ {
1435
+ key: "enabled",
1436
+ label: "Enabled",
1437
+ kind: "status",
1438
+ width: "90px"
1439
+ },
1440
+ {
1441
+ key: "codec",
1442
+ label: "Codec"
1443
+ },
1444
+ {
1445
+ key: "resolution",
1446
+ label: "Resolution"
1447
+ }
1448
+ ],
1449
+ value: rows,
1450
+ emptyMessage: "The camera reports no recording track."
1451
+ };
1452
+ }
1453
+ function booleanField(key, label, description, support, current) {
1454
+ const field = {
1455
+ type: "boolean",
1456
+ key,
1457
+ label,
1458
+ default: current ?? false,
1459
+ disabled: !support.writable
1460
+ };
1461
+ const text = describeSupport(description, support);
1462
+ return text === void 0 ? field : {
1463
+ ...field,
1464
+ description: text
1465
+ };
1466
+ }
1467
+ /**
1468
+ * Label one member of an allowed SET.
1469
+ *
1470
+ * Zero is "Off" in every field that carries a set here — no pre-roll and
1471
+ * no post-roll are both the feature switched off, and "0 s" is a
1472
+ * duration an operator would read as a very short one. A sentinel is
1473
+ * named by its meaning rather than printed: Hikvision's no-limit member
1474
+ * is `2147483647`, which as a number on a form is nonsense.
1475
+ *
1476
+ * Whole minutes are shown as minutes because that is how the vendor's
1477
+ * own UI offers them — 120, 300 and 600 read back to the operator as
1478
+ * "2 min", "5 min", "10 min", which is the list they picked from.
1479
+ */
1480
+ function describeChoice(value, unit, allowed) {
1481
+ const sentinel = allowed.sentinel;
1482
+ if (sentinel !== void 0 && sentinel.value === value) return sentinel.meaning === "no-limit" ? "No limit" : "Off";
1483
+ if (value === 0) return "Off";
1484
+ if (unit === "s" && value >= 60 && value % 60 === 0) return `${String(value / 60)} min`;
1485
+ return `${String(value)} ${unit}`;
1486
+ }
1487
+ /**
1488
+ * A numeric camera setting.
1489
+ *
1490
+ * An allowed SET is preferred over a range whenever the camera stated
1491
+ * one, and it renders a SELECT rather than a number input. A spinner
1492
+ * over a set is a control that offers values the camera answers 200 to
1493
+ * and then discards — measured on both vendors, and the reason
1494
+ * `*Allowed` exists at all. The cap says the set wins; before this, the
1495
+ * builder read only the range and the set was declared and rendered by
1496
+ * nobody.
1497
+ */
1498
+ function numberField(key, label, unit, description, support, range, current, allowed) {
1499
+ const text = describeSupport(description, support);
1500
+ if (allowed !== void 0) {
1501
+ const options = allowed.values.map((value) => ({
1502
+ value: String(value),
1503
+ label: describeChoice(value, unit, allowed)
1504
+ }));
1505
+ const fallback = allowed.values[0];
1506
+ const selected = current !== null && allowed.values.includes(current) ? current : current ?? fallback;
1507
+ const field = {
1508
+ type: "select",
1509
+ key,
1510
+ label,
1511
+ options: current !== null && !allowed.values.includes(current) ? [...options, {
1512
+ value: String(current),
1513
+ label: describeChoice(current, unit, allowed)
1514
+ }] : options,
1515
+ default: String(selected),
1516
+ disabled: !support.writable
1517
+ };
1518
+ return text === void 0 ? field : {
1519
+ ...field,
1520
+ description: text
1521
+ };
1522
+ }
1523
+ const field = {
1524
+ type: "number",
1525
+ key,
1526
+ label,
1527
+ unit,
1528
+ default: current ?? range?.min ?? 0,
1529
+ disabled: !support.writable,
1530
+ ...range ? {
1531
+ min: range.min,
1532
+ max: range.max,
1533
+ step: range.step
1534
+ } : {}
1535
+ };
1536
+ return text === void 0 ? field : {
1537
+ ...field,
1538
+ description: text
1539
+ };
1540
+ }
1541
+ /**
1542
+ * The time-of-day offer for a window edge.
1543
+ *
1544
+ * Stepped at the camera's own granularity when that is coarse (Reolink's
1545
+ * 60), and at 15 minutes when it is fine (Hikvision's 1) — a 1-minute
1546
+ * offer would be 1 441 options. 15 is a multiple of 1, so every value
1547
+ * offered is one the camera accepts; the converse is not true, and a
1548
+ * window the camera already holds that falls between the offered values
1549
+ * still round-trips because it is carried in the row's value, not
1550
+ * re-derived from the offer.
1551
+ */
1552
+ function timeOptions(granularityMinutes) {
1553
+ const step = granularityMinutes >= 30 ? granularityMinutes : 15;
1554
+ const out = [];
1555
+ for (let minute = 0; minute < MINUTES_PER_DAY; minute += step) out.push({
1556
+ value: String(minute),
1557
+ label: formatMinutes(minute)
1558
+ });
1559
+ out.push({
1560
+ value: String(MINUTES_PER_DAY),
1561
+ label: "24:00"
1562
+ });
1563
+ return out;
1564
+ }
1565
+ function windowsField(options, status) {
1566
+ const schedule = options.schedule;
1567
+ const triggerOptions = schedule.triggers.map((trigger) => ({
1568
+ value: trigger,
1569
+ label: TRIGGER_LABELS[trigger]
1570
+ }));
1571
+ const dayOptions = DAY_NAMES.map((name, index) => ({
1572
+ value: String(index),
1573
+ label: name
1574
+ }));
1575
+ const times = timeOptions(schedule.granularityMinutes);
1576
+ const itemFields = [
1577
+ {
1578
+ type: "select",
1579
+ key: WINDOW_ROW_KEYS.trigger,
1580
+ label: "Record on",
1581
+ options: triggerOptions,
1582
+ default: schedule.triggers[0] ?? ""
1583
+ },
1584
+ {
1585
+ type: "select",
1586
+ key: WINDOW_ROW_KEYS.day,
1587
+ label: "Day",
1588
+ options: dayOptions,
1589
+ default: "0"
1590
+ },
1591
+ {
1592
+ type: "select",
1593
+ key: WINDOW_ROW_KEYS.startMinute,
1594
+ label: "From",
1595
+ options: times,
1596
+ default: "0"
1597
+ },
1598
+ {
1599
+ type: "select",
1600
+ key: WINDOW_ROW_KEYS.endMinute,
1601
+ label: "To",
1602
+ options: times,
1603
+ default: String(MINUTES_PER_DAY)
1604
+ }
1605
+ ];
1606
+ const rows = (status?.windows ?? []).map((window) => ({
1607
+ [WINDOW_ROW_KEYS.trigger]: window.trigger,
1608
+ [WINDOW_ROW_KEYS.day]: String(window.day),
1609
+ [WINDOW_ROW_KEYS.startMinute]: String(window.startMinute),
1610
+ [WINDOW_ROW_KEYS.endMinute]: String(window.endMinute)
1611
+ }));
1612
+ const description = describeSupport(`${schedule.granularityMinutes > 1 ? `This camera's schedule moves in ${String(schedule.granularityMinutes)}-minute steps.` : ""}${schedule.supportsOverlappingTriggers ? "" : " It stores one trigger per time range, so two triggers cannot cover the same minutes on one day."}`.trim(), schedule.support);
1613
+ const field = {
1614
+ type: "editable-array",
1615
+ key: RECORDING_ONBOARD_KEYS.windows,
1616
+ label: "Weekly schedule",
1617
+ itemFields,
1618
+ defaultItem: {
1619
+ [WINDOW_ROW_KEYS.trigger]: schedule.triggers[0] ?? "",
1620
+ [WINDOW_ROW_KEYS.day]: "0",
1621
+ [WINDOW_ROW_KEYS.startMinute]: "0",
1622
+ [WINDOW_ROW_KEYS.endMinute]: String(MINUTES_PER_DAY)
1623
+ },
1624
+ default: rows,
1625
+ disabled: !schedule.support.writable,
1626
+ addLabel: "Add window",
1627
+ emptyMessage: "The camera records on no schedule.",
1628
+ rowTitleTemplate: "{day} · {trigger}"
1629
+ };
1630
+ return description === void 0 ? field : {
1631
+ ...field,
1632
+ description
1633
+ };
1634
+ }
1635
+ /**
1636
+ * Build the `recording-onboard` `ConfigUISchema`.
1637
+ *
1638
+ * Never returns null. The storage read works on every camera in the
1639
+ * fleet, so the section always has something true to say — and a camera
1640
+ * CamStack has not reached yet says exactly that, rather than
1641
+ * disappearing (D315: not-yet-known must not look like not-installed).
1642
+ */
1643
+ function buildRecordingOnboardConfigSchema(options, status) {
1644
+ return { sections: [{
1645
+ id: "recording-onboard-storage",
1646
+ tab: "recording",
1647
+ title: "Camera storage",
1648
+ description: "The card in the camera, as the camera itself describes it.",
1649
+ columns: 1,
1650
+ fields: [
1651
+ storageBanner(status),
1652
+ volumesField(status),
1653
+ tracksField(status)
1654
+ ]
1655
+ }, {
1656
+ id: "recording-onboard-recording",
1657
+ tab: "recording",
1658
+ title: "Camera recording",
1659
+ description: "What the camera records to its own card. This writes the camera, not CamStack's recorder.",
1660
+ columns: 2,
1661
+ fields: [
1662
+ booleanField(RECORDING_ONBOARD_KEYS.enabled, "Record to card", "The camera-side master switch.", options.enabled, status?.enabled ?? null),
1663
+ booleanField(RECORDING_ONBOARD_KEYS.overwriteWhenFull, "Overwrite when full", "Keep recording by discarding the oldest footage.", options.overwriteWhenFull, status?.overwriteWhenFull ?? null),
1664
+ numberField(RECORDING_ONBOARD_KEYS.preRecordSec, "Pre-record", "s", "Seconds kept from before the trigger.", options.preRecordSec, options.preRecordSecRange, status?.preRecordSec ?? null, options.preRecordSecAllowed),
1665
+ numberField(RECORDING_ONBOARD_KEYS.postRecordSec, "Post-record", "s", "Seconds kept after the trigger clears.", options.postRecordSec, options.postRecordSecRange, status?.postRecordSec ?? null, options.postRecordSecAllowed),
1666
+ numberField(RECORDING_ONBOARD_KEYS.segmentMinutes, "File length", "min", "How long one recorded file runs before the camera starts another.", options.segmentMinutes, options.segmentMinutesRange, status?.segmentMinutes ?? null, options.segmentMinutesAllowed),
1667
+ windowsField(options, status)
1668
+ ]
1669
+ }] };
1670
+ }
1671
+ function readBoolean$1(patch, key) {
1672
+ if (!(key in patch)) return void 0;
1673
+ const value = patch[key];
1674
+ if (typeof value === "boolean") return value;
1675
+ if (value === "true") return true;
1676
+ if (value === "false") return false;
1677
+ }
1678
+ function readNumber$1(patch, key) {
1679
+ if (!(key in patch)) return void 0;
1680
+ const value = Number(patch[key]);
1681
+ return Number.isFinite(value) ? value : void 0;
1682
+ }
1683
+ /** Is this string one of the cap's triggers? A guard, never a cast. */
1684
+ function isRecordTrigger(value) {
1685
+ return typeof value === "string" && value in TRIGGER_LABELS;
1686
+ }
1687
+ /**
1688
+ * Read one row of the windows `editable-array`.
1689
+ *
1690
+ * Returns null for a row that is not a window. Validation of whether
1691
+ * THIS camera accepts the window is deliberately not done here: the
1692
+ * provider refuses it by name through `describeOnboardRefusal`, which is
1693
+ * the one refusal vocabulary, and duplicating a weaker copy of it in the
1694
+ * form parser would give the operator two different answers.
1695
+ */
1696
+ function readWindowRow(row) {
1697
+ if (typeof row !== "object" || row === null) return null;
1698
+ const record = { ...row };
1699
+ const trigger = record[WINDOW_ROW_KEYS.trigger];
1700
+ if (!isRecordTrigger(trigger)) return null;
1701
+ const day = Number(record[WINDOW_ROW_KEYS.day]);
1702
+ const startMinute = Number(record[WINDOW_ROW_KEYS.startMinute]);
1703
+ const endMinute = Number(record[WINDOW_ROW_KEYS.endMinute]);
1704
+ if (!Number.isInteger(day) || day < 0 || day > 6) return null;
1705
+ if (!Number.isInteger(startMinute) || !Number.isInteger(endMinute)) return null;
1706
+ return {
1707
+ trigger,
1708
+ day,
1709
+ startMinute,
1710
+ endMinute
1711
+ };
1712
+ }
1713
+ /**
1714
+ * Re-parse a flat `ConfigFormBuilder` patch into a
1715
+ * `RecordingOnboardPatch`-shaped object.
1716
+ *
1717
+ * Returns only the keys the patch actually carried. The windows key is
1718
+ * all-or-nothing on purpose: the field is a single editable array, so
1719
+ * its presence means the operator submitted the COMPLETE window set. A
1720
+ * row that cannot be read is DROPPED and the remaining set is still
1721
+ * submitted — the provider then refuses or applies a set the operator
1722
+ * can see in the form, rather than the form and the camera silently
1723
+ * disagreeing about a row neither shows.
1724
+ */
1725
+ function parseRecordingOnboardFormPatch(patch) {
1726
+ const out = {};
1727
+ const enabled = readBoolean$1(patch, RECORDING_ONBOARD_KEYS.enabled);
1728
+ if (enabled !== void 0) out.enabled = enabled;
1729
+ const overwrite = readBoolean$1(patch, RECORDING_ONBOARD_KEYS.overwriteWhenFull);
1730
+ if (overwrite !== void 0) out.overwriteWhenFull = overwrite;
1731
+ const pre = readNumber$1(patch, RECORDING_ONBOARD_KEYS.preRecordSec);
1732
+ if (pre !== void 0) out.preRecordSec = pre;
1733
+ const post = readNumber$1(patch, RECORDING_ONBOARD_KEYS.postRecordSec);
1734
+ if (post !== void 0) out.postRecordSec = post;
1735
+ const segment = readNumber$1(patch, RECORDING_ONBOARD_KEYS.segmentMinutes);
1736
+ if (segment !== void 0) out.segmentMinutes = segment;
1737
+ if (RECORDING_ONBOARD_KEYS.windows in patch) {
1738
+ const raw = patch[RECORDING_ONBOARD_KEYS.windows];
1739
+ if (Array.isArray(raw)) {
1740
+ const windows = [];
1741
+ for (const row of raw) {
1742
+ const parsed = readWindowRow(row);
1743
+ if (parsed) windows.push(parsed);
1744
+ }
1745
+ out.windows = windows;
1746
+ }
1747
+ }
1748
+ return out;
1749
+ }
1750
+ //#endregion
1289
1751
  //#region src/builtins/device-manager/device-config-contribution.ts
1290
1752
  /**
1291
1753
  * D14 device-config archetype — framework-side contribution derivation.
@@ -1313,6 +1775,7 @@ function parseImageSettingsFormPatch(patch) {
1313
1775
  var STREAM_PARAMS_BUILDER_ID = "stream-params";
1314
1776
  var DAY_NIGHT_BUILDER_ID = "day-night";
1315
1777
  var IMAGE_SETTINGS_BUILDER_ID = "image-settings";
1778
+ var RECORDING_ONBOARD_BUILDER_ID = "recording-onboard";
1316
1779
  var STREAM_PARAMS_REDUCER = {
1317
1780
  kind: "profile",
1318
1781
  buildSchema: (options, status) => buildStreamParamsConfigSchema(options, status ?? null),
@@ -1333,10 +1796,16 @@ var IMAGE_SETTINGS_REDUCER = {
1333
1796
  buildSchema: (options, status) => buildImageSettingsConfigSchema(options, status ?? null),
1334
1797
  parsePatch: (patch) => parseImageSettingsFormPatch(patch)
1335
1798
  };
1799
+ var RECORDING_ONBOARD_REDUCER = {
1800
+ kind: "settings",
1801
+ buildSchema: (options, status) => buildRecordingOnboardConfigSchema(options, status ?? null),
1802
+ parsePatch: (patch) => parseRecordingOnboardFormPatch(patch)
1803
+ };
1336
1804
  var BUILDER_REDUCERS = {
1337
1805
  [STREAM_PARAMS_BUILDER_ID]: STREAM_PARAMS_REDUCER,
1338
1806
  [DAY_NIGHT_BUILDER_ID]: DAY_NIGHT_REDUCER,
1339
- [IMAGE_SETTINGS_BUILDER_ID]: IMAGE_SETTINGS_REDUCER
1807
+ [IMAGE_SETTINGS_BUILDER_ID]: IMAGE_SETTINGS_REDUCER,
1808
+ [RECORDING_ONBOARD_BUILDER_ID]: RECORDING_ONBOARD_REDUCER
1340
1809
  };
1341
1810
  function resolveReducer(builderId) {
1342
1811
  const reducer = BUILDER_REDUCERS[builderId];
@@ -0,0 +1,41 @@
1
+ import { ConfigUISchema, RecordingOnboardOptions, RecordingOnboardStatus } from '@camstack/types';
2
+ export declare const RECORDING_ONBOARD_KEYS: {
3
+ readonly storageBanner: "recordingOnboard_storageBanner";
4
+ readonly volumes: "recordingOnboard_volumes";
5
+ readonly tracks: "recordingOnboard_tracks";
6
+ readonly enabled: "recordingOnboard_enabled";
7
+ readonly overwriteWhenFull: "recordingOnboard_overwriteWhenFull";
8
+ readonly preRecordSec: "recordingOnboard_preRecordSec";
9
+ readonly postRecordSec: "recordingOnboard_postRecordSec";
10
+ readonly segmentMinutes: "recordingOnboard_segmentMinutes";
11
+ readonly windows: "recordingOnboard_windows";
12
+ };
13
+ /** Row keys inside the windows `editable-array`. Local, not prefixed. */
14
+ export declare const WINDOW_ROW_KEYS: {
15
+ readonly trigger: "trigger";
16
+ readonly day: "day";
17
+ readonly startMinute: "startMinute";
18
+ readonly endMinute: "endMinute";
19
+ };
20
+ /**
21
+ * Build the `recording-onboard` `ConfigUISchema`.
22
+ *
23
+ * Never returns null. The storage read works on every camera in the
24
+ * fleet, so the section always has something true to say — and a camera
25
+ * CamStack has not reached yet says exactly that, rather than
26
+ * disappearing (D315: not-yet-known must not look like not-installed).
27
+ */
28
+ export declare function buildRecordingOnboardConfigSchema(options: RecordingOnboardOptions, status: RecordingOnboardStatus | null): ConfigUISchema | null;
29
+ /**
30
+ * Re-parse a flat `ConfigFormBuilder` patch into a
31
+ * `RecordingOnboardPatch`-shaped object.
32
+ *
33
+ * Returns only the keys the patch actually carried. The windows key is
34
+ * all-or-nothing on purpose: the field is a single editable array, so
35
+ * its presence means the operator submitted the COMPLETE window set. A
36
+ * row that cannot be read is DROPPED and the remaining set is still
37
+ * submitted — the provider then refuses or applies a set the operator
38
+ * can see in the form, rather than the form and the camera silently
39
+ * disagreeing about a row neither shows.
40
+ */
41
+ export declare function parseRecordingOnboardFormPatch(patch: Record<string, unknown>): Record<string, unknown>;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C51Z6L6t.js");
6
+ const require_dist = require("../../dist-BYnHJU96.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { B as bareAddonId, Fn as EventCategory, T as SOURCE_DEVICE_TYPES, at as evaluateSensorEdge, cn as BaseAddon, gt as isSourceCap, ht as isSameAddonId, mn as DeviceType, on as errMsg, tt as doorbellCapability, w as SOURCE_CAPS, xn as createEvent } from "../../dist-jv1aoo9E.mjs";
1
+ import { D as SOURCE_DEVICE_TYPES, E as SOURCE_CAPS, H as bareAddonId, Rn as EventCategory, _n as DeviceType, dn as BaseAddon, ln as errMsg, rt as doorbellCapability, st as evaluateSensorEdge, vt as isSameAddonId, wn as createEvent, yt as isSourceCap } from "../../dist-DKFb2936.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C51Z6L6t.js");
6
+ const require_dist = require("../../dist-BYnHJU96.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
9
9
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, cn as BaseAddon } from "../../dist-jv1aoo9E.mjs";
1
+ import { Et as logDestinationCapability, dn as BaseAddon } from "../../dist-DKFb2936.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
4
4
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-C51Z6L6t.js");
3
+ const require_dist = require("../../dist-BYnHJU96.js");
4
4
  //#region src/builtins/liveness-monitor/liveness-checks.ts
5
5
  var NO_DEVICES = "liveness:no-devices";
6
6
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -1,4 +1,4 @@
1
- import { C as RecordingStatusSchema, Fn as EventCategory, cn as BaseAddon, on as errMsg, xn as createEvent } from "../../dist-jv1aoo9E.mjs";
1
+ import { Rn as EventCategory, T as RecordingStatusSchema, dn as BaseAddon, ln as errMsg, wn as createEvent } from "../../dist-DKFb2936.mjs";
2
2
  //#region src/builtins/liveness-monitor/liveness-checks.ts
3
3
  var NO_DEVICES = "liveness:no-devices";
4
4
  var ALL_OFFLINE = "liveness:all-devices-offline";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C51Z6L6t.js");
6
+ const require_dist = require("../../dist-BYnHJU96.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { N as UserRecordSchema, O as ScopedTokenSchema, R as authProviderCapability, cn as BaseAddon, nn as userManagementCapability, r as ApiKeyRecordSchema } from "../../dist-jv1aoo9E.mjs";
2
+ import { A as ScopedTokenSchema, B as authProviderCapability, F as UserRecordSchema, an as userManagementCapability, dn as BaseAddon, r as ApiKeyRecordSchema } from "../../dist-DKFb2936.mjs";
3
3
  import * as crypto$2 from "node:crypto";
4
4
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-C51Z6L6t.js");
3
+ const require_dist = require("../../dist-BYnHJU96.js");
4
4
  const require_tls = require("../../tls-D_wUeOTD.js");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Fn as EventCategory, cn as BaseAddon, xt as localNetworkCapability } from "../../dist-jv1aoo9E.mjs";
1
+ import { Rn as EventCategory, dn as BaseAddon, wt as localNetworkCapability } from "../../dist-DKFb2936.mjs";
2
2
  import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-IM-v7JV4.mjs";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { join } from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C51Z6L6t.js");
6
+ const require_dist = require("../../dist-BYnHJU96.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { Ct as logDestinationCapability, cn as BaseAddon } from "../../dist-jv1aoo9E.mjs";
1
+ import { Et as logDestinationCapability, dn as BaseAddon } from "../../dist-DKFb2936.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-C51Z6L6t.js");
6
+ const require_dist = require("../../dist-BYnHJU96.js");
7
7
  let node_fs_promises = require("node:fs/promises");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Fn as EventCategory, Ot as metricsProviderCapability, Rt as reducePoints, cn as BaseAddon, lt as foldSnapshotByFunction, on as errMsg, xn as createEvent, zt as resolveBucketMs } from "../../dist-jv1aoo9E.mjs";
1
+ import { Ht as resolveBucketMs, Rn as EventCategory, Vt as reducePoints, dn as BaseAddon, dt as foldSnapshotByFunction, jt as metricsProviderCapability, ln as errMsg, wn as createEvent } from "../../dist-DKFb2936.mjs";
2
2
  import { access, readFile, readdir } from "node:fs/promises";
3
3
  import * as fs from "node:fs";
4
4
  import { constants, readFileSync } from "node:fs";