@camstack/addon-osd-manager 0.1.170 → 0.1.172

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 (19) hide show
  1. package/dist/{MotionZonesSettings-CTFqdJfp.mjs → MotionZonesSettings-3SEt9bVC.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-M3LLrSyZ.mjs → PrivacyMaskSettings-6XhCc_10.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-BWTOg6t0.mjs → SceneMonitorEditor-BOaJdiHv.mjs} +3 -3
  4. package/dist/_stub.js +4430 -4730
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-CGnGPNWo.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-BWC_vxZ4.mjs} +4 -4
  6. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DMeEVWZy.mjs +26 -0
  7. package/dist/addon-osd-manager.css +1 -1
  8. package/dist/{hostInit-CXC2ehtI.mjs → hostInit-CSG1YJE4.mjs} +3 -3
  9. package/dist/index.js +156 -94
  10. package/dist/index.mjs +156 -94
  11. package/dist/{player-overlays-DBi0aH1E.mjs → player-overlays-7D6iloq4.mjs} +1 -1
  12. package/dist/remoteEntry.js +1 -1
  13. package/dist/{responsive-BoQdxzOM.mjs → responsive-DFTpb0I2.mjs} +1 -1
  14. package/dist/{scene-monitor-copy-CwfDunLo.mjs → scene-monitor-copy-D6Ypp3Cr.mjs} +1 -1
  15. package/dist/{square-C-adTToZ.mjs → square-CyIowYkn.mjs} +1 -1
  16. package/dist/{trash-2-DJjBuL5Q.mjs → trash-2-B5AYx8ut.mjs} +1 -1
  17. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-CtQlS867.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-CzgrkKsK.mjs} +1 -1
  18. package/package.json +1 -1
  19. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B4GSezZ1.mjs +0 -26
package/dist/index.js CHANGED
@@ -10635,11 +10635,11 @@ var ModelVariantGroupSchema = object({
10635
10635
  /** Quantization axis. Omit ⇒ the fp32 base build. */
10636
10636
  precision: _enum(["fp32", "int8"]).optional(),
10637
10637
  /**
10638
- * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
10639
- * latency-optimized export (e.g. ReLU-activation variant) — the slot the
10640
- * future performance variants plug into.
10638
+ * Training / latency-variant axis. Omit ⇒ the standard build. `test` marks
10639
+ * an alternate training of the same family (Scrypted `relu_test`); chip
10640
+ * label is "Test".
10641
10641
  */
10642
- optimization: _enum(["standard", "fast"]).optional(),
10642
+ optimization: _enum(["standard", "test"]).optional(),
10643
10643
  /**
10644
10644
  * Input-resolution axis (square input side, px). Omit ⇒ the family's native
10645
10645
  * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
@@ -10662,9 +10662,10 @@ var ModelProviderIdSchema = _enum([
10662
10662
  * are per upstream. A persisted custom-model row written when this field was a
10663
10663
  * string still parses — the string is lifted into `{ weights }`.
10664
10664
  *
10665
- * Every licence surface (the admin "Models and licenses" page,
10666
- * `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
10667
- * these records by `scripts/gen-model-licenses.ts` (D661).
10665
+ * Every machine licence surface (`THIRD_PARTY_MODELS.md`, the Hugging Face
10666
+ * model table) is GENERATED from these records by
10667
+ * `scripts/gen-model-licenses.ts` (D661). Operator-facing docs live in
10668
+ * `camstack/docs` → `/docs/detection/models`.
10668
10669
  */
10669
10670
  var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
10670
10671
  /** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
@@ -15481,7 +15482,7 @@ var detectionPipelineCapability = {
15481
15482
  * This matters here more than anywhere else in the repo, because every provider
15482
15483
  * that knows a device's host knows its PASSWORD too: `{ host, port, username,
15483
15484
  * password }` sit in one object on Hikvision, Amcrest, Reolink and ONVIF alike,
15484
- * and `http://admin:hunter2@192.168.x.x/` is a URL a browser accepts. The
15485
+ * and `http://admin:hunter2@192.168.60.139/` is a URL a browser accepts. The
15485
15486
  * camera's own page will ask for its own login. That is correct, and pre-
15486
15487
  * filling it is the operator's business, not ours.
15487
15488
  *
@@ -19767,7 +19768,7 @@ var modelDistributorCapability = {
19767
19768
  mode: "singleton",
19768
19769
  internal: true,
19769
19770
  methods: { distributeModel: method(object({
19770
- nodeId: string(),
19771
+ targetNodeId: string(),
19771
19772
  modelId: string(),
19772
19773
  format: _enum(MODEL_FORMATS),
19773
19774
  entry: ModelCatalogEntrySchema
@@ -22347,8 +22348,9 @@ var NcRuleSchema = NcRuleInputSchema.extend({
22347
22348
  disabledTargetIds: array(string()).default([])
22348
22349
  });
22349
22350
  /**
22350
- * The state-summary SECTIONS (D647 / D698): the D637 kind groups that report a
22351
- * still-true state. `backups-exports` and `alarm` are not sections — a completed
22351
+ * The state-summary SECTIONS (D647 / D698 / D708): the D637 kind groups that
22352
+ * report a still-true state. `addons` is the crash-looped addons only and
22353
+ * `addon-updates` the addons that are behind (D708 split them). `backups-exports` and `alarm` are not sections — a completed
22352
22354
  * job is not an open state, and the alarm stays instant.
22353
22355
  */
22354
22356
  var NcStateSummarySectionIdSchema = _enum([
@@ -22360,6 +22362,7 @@ var NcStateSummarySectionIdSchema = _enum([
22360
22362
  "detection-blind",
22361
22363
  "nodes",
22362
22364
  "addons",
22365
+ "addon-updates",
22363
22366
  "updates"
22364
22367
  ]);
22365
22368
  /** One section of a previewed state summary. `unavailable` = its state could not be read. */
@@ -23376,7 +23379,19 @@ var NcAlarmModeDelaysListSchema = array(object({
23376
23379
  * When this mode may be armed. Absent / null = always allowed (same as a
23377
23380
  * rule with no `schedule`). Evaluated with {@link isScheduleActive}.
23378
23381
  */
23379
- schedule: NcScheduleSchema.optional()
23382
+ schedule: NcScheduleSchema.optional(),
23383
+ /**
23384
+ * D706: while this mode's ENTRY DELAY runs, do the alarm rules' own pushes go out (as an
23385
+ * intrusion naming the locations)? Absent = false — the entry-delay notification is the
23386
+ * only one, and the matches ride the triggered notification's mosaic (D705).
23387
+ */
23388
+ notifyDuringEntryDelay: boolean().optional(),
23389
+ /**
23390
+ * D706: notify "watched again" when a sensor this arm bypassed closes and opens again.
23391
+ * Absent = false (logged, not sent). Even when true it is not sent when that same sensor
23392
+ * is the one that starts the entry delay — the entry-delay notification already names it.
23393
+ */
23394
+ notifySensorRewatched: boolean().optional()
23380
23395
  })).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
23381
23396
  var NcAlarmModeDelaysEditListSchema = array(object({
23382
23397
  mode: AlarmArmModeSchema,
@@ -23385,7 +23400,11 @@ var NcAlarmModeDelaysEditListSchema = array(object({
23385
23400
  triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional(),
23386
23401
  postTriggeredAction: NcAlarmPostTriggeredActionSchema.nullable().optional(),
23387
23402
  /** Set a schedule, or `null` to clear (mode always allowed again). */
23388
- schedule: NcScheduleSchema.nullable().optional()
23403
+ schedule: NcScheduleSchema.nullable().optional(),
23404
+ /** D706: set the switch, or `null` to clear it (back to the default, false). */
23405
+ notifyDuringEntryDelay: boolean().nullable().optional(),
23406
+ /** D706: set the switch, or `null` to clear it (back to the default, false). */
23407
+ notifySensorRewatched: boolean().nullable().optional()
23389
23408
  })).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
23390
23409
  /** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
23391
23410
  var NcAlarmNonBlockingEditSchema = object({
@@ -24562,39 +24581,19 @@ Object.freeze(Object.fromEntries([{
24562
24581
  label: "Face recognition model",
24563
24582
  defaultModelId: "auraface-r100",
24564
24583
  indexName: "the enrolled face gallery",
24565
- options: [
24566
- {
24567
- id: "auraface-r100",
24568
- label: "AuraFace R100"
24569
- },
24570
- {
24571
- id: "arcface-r100",
24572
- label: "ArcFace (ResNet34)"
24573
- },
24574
- {
24575
- id: "inception-resnet-v1",
24576
- label: "Inception ResNet V1"
24577
- }
24578
- ]
24584
+ options: [{
24585
+ id: "auraface-r100",
24586
+ label: "AuraFace R100"
24587
+ }]
24579
24588
  }, {
24580
24589
  stepId: "clip-embedding",
24581
24590
  label: "Semantic search model",
24582
- defaultModelId: "mobileclip-s1",
24591
+ defaultModelId: "siglip2-b16-224",
24583
24592
  indexName: "the object semantic-search index",
24584
- options: [
24585
- {
24586
- id: "mobileclip-s1",
24587
- label: "MobileCLIP S1"
24588
- },
24589
- {
24590
- id: "mobileclip-s2",
24591
- label: "MobileCLIP S2"
24592
- },
24593
- {
24594
- id: "siglip2-b16-224",
24595
- label: "SigLIP2 B/16 (Apache-2.0)"
24596
- }
24597
- ]
24593
+ options: [{
24594
+ id: "siglip2-b16-224",
24595
+ label: "SigLIP2 B/16 (Apache-2.0)"
24596
+ }]
24598
24597
  }].map((s) => [s.stepId, s.defaultModelId])));
24599
24598
  var CLUSTER_STEP_SETTING_FIELDS = [{
24600
24599
  stepId: "face-embedding",
@@ -28338,6 +28337,22 @@ var MotionSourceEnum = _enum([
28338
28337
  "device-activity"
28339
28338
  ]);
28340
28339
  /**
28340
+ * What opened an on-motion detection SESSION: the motion source that passed the
28341
+ * camera's `motionSources` gate, and what raised it (the camera itself, or a
28342
+ * sensor the operator attached — D672). `sensorName` is the attached sensor's
28343
+ * device name as the orchestrator's camera-sensors resolution last read it;
28344
+ * absent when unknown or when the camera itself triggered.
28345
+ *
28346
+ * Carried on the session attach ONLY so the runner's `DetectionPhaseTransition`
28347
+ * can say what started the session (D672 rule 11, amended 2026-10-06). The
28348
+ * runner never decides anything on it.
28349
+ */
28350
+ var SessionTriggerSchema = object({
28351
+ source: MotionSourceEnum,
28352
+ triggeredBy: MotionTriggeredBySchema,
28353
+ sensorName: string().optional()
28354
+ });
28355
+ /**
28341
28356
  * List of motion sources active on a camera. Empty array is valid:
28342
28357
  * "no source" — happens for battery cams without firmware motion when
28343
28358
  * the operator hasn't opted into the SW analyzer (would hold the
@@ -28589,6 +28604,14 @@ var RunnerCameraConfigSchema = object({
28589
28604
  */
28590
28605
  motionSources: MotionSourcesSchema.default(["analyzer"]),
28591
28606
  /**
28607
+ * What opened this attach, on a SESSION attach only (see
28608
+ * {@link SessionTriggerSchema}). OPTIONAL with no default: absent on a
28609
+ * standing attach, an occupancy burst, a re-dispatch with no motion event
28610
+ * behind it, and every attach from an orchestrator older than the field — the
28611
+ * runner then names no source on the phase event.
28612
+ */
28613
+ sessionTrigger: SessionTriggerSchema.optional(),
28614
+ /**
28592
28615
  * WHICH root detector runs for this camera. The detection counterpart of
28593
28616
  * {@link motionSources}, same shape and same discipline: a per-camera list
28594
28617
  * of sources the system understands, vendor-agnostic, owned here.
@@ -37689,7 +37712,7 @@ var LocalInterfaceSchema = object({
37689
37712
  name: string(),
37690
37713
  family: _enum(["IPv4", "IPv6"]),
37691
37714
  address: string(),
37692
- /** CIDR shorthand (e.g. `192.168.x.x/24`). Empty when OS doesn't expose. */
37715
+ /** CIDR shorthand (e.g. `192.168.10.10/24`). Empty when OS doesn't expose. */
37693
37716
  cidr: string(),
37694
37717
  /** OS netmask. Stable across calls so the SDK can hash on it. */
37695
37718
  netmask: string(),
@@ -37743,7 +37766,8 @@ var ConnectionEndpointSchema = object({
37743
37766
  "lan-ipv4",
37744
37767
  "lan-ipv6",
37745
37768
  "public",
37746
- "loopback"
37769
+ "loopback",
37770
+ "app-link"
37747
37771
  ]),
37748
37772
  /**
37749
37773
  * Originating-interface kind — `lan`/`wifi`/`docker`/`vpn`/`other`/
@@ -37812,15 +37836,27 @@ var GetConnectionEndpointsResultSchema = object({
37812
37836
  localPort: AdvertisedLocalPortSchema
37813
37837
  });
37814
37838
  /**
37815
- * The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
37816
- * AUTO (resolved from the candidate ranking at send time); `resolved` reports
37817
- * what AUTO currently picks, so the UI can show the effective value either way.
37839
+ * The address flagged for notification links. `baseUrl: null` = AUTO, which
37840
+ * since D709 is the CamStack app link; `resolved` reports the effective value,
37841
+ * so the UI can show it either way.
37818
37842
  */
37819
37843
  var NotificationEndpointSchema = object({
37820
37844
  /** The operator's explicit choice, or null for AUTO. */
37821
37845
  baseUrl: string().nullable(),
37822
- /** What the ranking currently resolves to (null when nothing is reachable). */
37823
- resolved: string().nullable()
37846
+ /**
37847
+ * The effective flagged address: the explicit choice, else (AUTO) the
37848
+ * CamStack app link `https://camstack.zentik.app` (D709). A pre-D709 hub
37849
+ * answered the public tunnel / mesh here, or null.
37850
+ */
37851
+ resolved: string().nullable(),
37852
+ /**
37853
+ * The hub origin an app link carries as `hub=` (D709): the hub's best
37854
+ * public HTTPS address — the tunnel, then the mesh (Tailscale MagicDNS) —
37855
+ * picked from this cap's own palette by `pickAppLinkHub`. `null` when the
37856
+ * hub advertises neither; the app link is then minted without `hub=`.
37857
+ * Absent on a pre-D709 hub.
37858
+ */
37859
+ appLinkHub: string().nullable()
37824
37860
  });
37825
37861
  /**
37826
37862
  * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
@@ -37888,7 +37924,7 @@ var localNetworkCapability = {
37888
37924
  * every client a second authority on a socket only the hub binds. A viewer
37889
37925
  * configured with a port-less public URL (`https://camstack.example.top`)
37890
37926
  * infers 443 from the scheme, asks with 443, and was handed
37891
- * `https://192.168.x.x:443` for a hub listening on 4443. That candidate
37927
+ * `https://192.168.10.9:443` for a hub listening on 4443. That candidate
37892
37928
  * cannot ever answer, and losing its race reads in the log exactly like
37893
37929
  * "the LAN was tried and it was slower" — so an operator sitting on his own
37894
37930
  * WiFi stayed on the public tunnel for weeks, with the LAN candidate born
@@ -37921,13 +37957,21 @@ var localNetworkCapability = {
37921
37957
  * Pass `'https'` when the caller is itself loaded over HTTPS
37922
37958
  * to avoid mixed-content blocks in the browser. The public
37923
37959
  * tunnel always emits `https://` regardless. */
37924
- scheme: _enum(["http", "https"]).optional()
37960
+ scheme: _enum(["http", "https"]).optional(),
37961
+ /** Append the CamStack app link entry (`kind: 'app-link'`, D709) —
37962
+ * an address notification links can be flagged with, not a hub
37963
+ * route. Default `false`: racers and rankers never see it. */
37964
+ includeAppLink: boolean().optional()
37925
37965
  }), GetConnectionEndpointsResultSchema),
37926
37966
  /**
37927
- * The endpoint OUTBOUND artifacts must be addressed by — notification
37928
- * attachment links and deep links. `baseUrl: null` means AUTO: the first
37929
- * public candidate (a configured origin, then a connected tunnel, then the
37930
- * LAN), never loopback.
37967
+ * The address notification links are flagged with. `baseUrl: null` means
37968
+ * AUTO, which is the CamStack app link (D709): viewer taps go through
37969
+ * `https://camstack.zentik.app/…?hub=<appLinkHub>` and open the app. Any
37970
+ * hub address the palette lists can be flagged instead; then viewer taps,
37971
+ * attachment links and action buttons all use it. Signed media and
37972
+ * buttons never use the app link — with it flagged (or AUTO) they keep
37973
+ * the ranked hub ladder (a configured origin, a connected tunnel, then
37974
+ * the LAN), never loopback.
37931
37975
  *
37932
37976
  * Why an explicit marker: the automatic ranking cannot know which of
37933
37977
  * several reachable addresses the RECIPIENT can actually open. A link is
@@ -38320,7 +38364,7 @@ var MeshPeerSchema = object({
38320
38364
  tags: array(string()).readonly(),
38321
38365
  /** True when the peer advertises itself as an exit-node candidate. */
38322
38366
  exitNodeOption: boolean(),
38323
- /** Subnet routes the peer advertises (e.g. `["192.168.x.x/24"]`). */
38367
+ /** Subnet routes the peer advertises (e.g. `["192.168.10.0/24"]`). */
38324
38368
  advertisedRoutes: array(string()).readonly(),
38325
38369
  /** Owner account login (e.g. `foo@example.com`). Null for tagged peers. */
38326
38370
  userLogin: string().nullable(),
@@ -39768,7 +39812,13 @@ var OsdStatusSchema = object({ overlays: array(object({
39768
39812
  */
39769
39813
  availablePositions: array(OsdPositionEnum).readonly(),
39770
39814
  /** Firmware does not allow editing the `text` field (e.g. timestamp). */
39771
- readOnly: boolean().optional()
39815
+ readOnly: boolean().optional(),
39816
+ /**
39817
+ * Firmware text ceiling for this overlay. Absent = no known limit.
39818
+ * The OSD manager truncates here on every write — never stores its own
39819
+ * copy on the binding.
39820
+ */
39821
+ maxCharacters: number().int().positive().optional()
39772
39822
  })).readonly() });
39773
39823
  var OsdOverlayPatchSchema = object({
39774
39824
  enabled: boolean().optional(),
@@ -39865,7 +39915,9 @@ var OsdTemplateBindingSchema = object({
39865
39915
  valuePath: string().min(1).max(64),
39866
39916
  /** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
39867
39917
  displayUnit: string().max(16).optional(),
39868
- maxDecimals: number().int().min(0).max(4).default(1)
39918
+ maxDecimals: number().int().min(0).max(4).default(1),
39919
+ /** Raw value → display text (same shape as slot binding). Empty string hides. */
39920
+ valueMap: record(string(), string()).optional()
39869
39921
  });
39870
39922
  /**
39871
39923
  * A named template: bindings (alias → leaf field) + a parser that
@@ -39915,18 +39967,11 @@ var OsdSlotBindingSchema = object({
39915
39967
  source: OsdSourceSchema,
39916
39968
  /** `${value}` and `${unit}` are substituted; every occurrence. */
39917
39969
  template: string().max(96).default("${value}"),
39918
- /** Truncate with an ellipsis past this length. Absent = no limit. */
39919
- maxCharacters: number().int().min(4).max(64).optional(),
39920
39970
  /**
39921
39971
  * Decimal places for a numeric value. `0` yields an integer — the
39922
39972
  * documented workaround for firmwares that reject `.` in overlay text.
39923
39973
  */
39924
39974
  maxDecimals: number().int().min(0).max(4).default(1),
39925
- /**
39926
- * Appended via `${unit}` when no unit conversion produces a label.
39927
- * Prefer `source.displayUnit` for converted device-state values.
39928
- */
39929
- unitLabel: string().max(8).optional(),
39930
39975
  /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
39931
39976
  valueMap: record(string(), string()).optional(),
39932
39977
  /** Time windows in which the slot is shown. Absent = always. */
@@ -39935,9 +39980,7 @@ var OsdSlotBindingSchema = object({
39935
39980
  * Display gate, in the notification centre's condition vocabulary.
39936
39981
  * Only the keys reported by `getConditionSupport` are accepted.
39937
39982
  */
39938
- conditions: NcConditionsSchema.optional(),
39939
- /** Rendered when the gate is closed or the value unreadable. Empty = hide. */
39940
- fallbackText: string().max(64).default("")
39983
+ conditions: NcConditionsSchema.optional()
39941
39984
  });
39942
39985
  /** One camera slot, as the operator sees it: firmware truth + our binding. */
39943
39986
  var OsdSlotViewSchema = object({
@@ -57819,12 +57862,14 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
57819
57862
  {
57820
57863
  id: "addons",
57821
57864
  family: "system",
57822
- label: "Addons",
57823
- kinds: [
57824
- "addon-crash-loop",
57825
- "addon-update-available",
57826
- "addon-updated"
57827
- ]
57865
+ label: "Addons stopped",
57866
+ kinds: ["addon-crash-loop"]
57867
+ },
57868
+ {
57869
+ id: "addon-updates",
57870
+ family: "system",
57871
+ label: "Addon updates",
57872
+ kinds: ["addon-update-available", "addon-updated"]
57828
57873
  },
57829
57874
  {
57830
57875
  id: "updates",
@@ -58064,13 +58109,15 @@ var STATE_SUMMARY_SECTION_VARS = [
58064
58109
  },
58065
58110
  {
58066
58111
  name: "summaryAddons",
58067
- label: "Summary: addons",
58068
- example: "@camstack/addon-recorder 1.2.3 → 1.2.4",
58069
- kinds: [
58070
- "addon-crash-loop",
58071
- "addon-update-available",
58072
- "addon-updated"
58073
- ]
58112
+ label: "Summary: addons stopped",
58113
+ example: "hub — recorder stopped after repeated crashes",
58114
+ kinds: ["addon-crash-loop"]
58115
+ },
58116
+ {
58117
+ name: "summaryAddonUpdates",
58118
+ label: "Summary: addon updates",
58119
+ example: "hub:\n• @camstack/addon-recorder 1.2.3 → 1.2.4",
58120
+ kinds: ["addon-update-available", "addon-updated"]
58074
58121
  },
58075
58122
  {
58076
58123
  name: "summaryUpdates",
@@ -58154,6 +58201,7 @@ var STATE_SUMMARY_VARS = [
58154
58201
  [...RULE_BODY_ONLY],
58155
58202
  [...RULE_TEXT],
58156
58203
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
58204
+ ([...RULE_TEXT], [...COMBINED_ALARM]),
58157
58205
  ([...RULE_TEXT], [...DEVICE_EVENTS]),
58158
58206
  ([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
58159
58207
  ([...RULE_TEXT], [...CONSUMABLE]),
@@ -58194,6 +58242,8 @@ var STATE_SUMMARY_VARS = [
58194
58242
  [...RULE_TEXT],
58195
58243
  [...RULE_TEXT],
58196
58244
  [...RULE_TEXT],
58245
+ [...RULE_TEXT],
58246
+ [...RULE_TEXT],
58197
58247
  ...STATE_SUMMARY_VARS
58198
58248
  ];
58199
58249
  new Set([
@@ -58986,8 +59036,8 @@ function applyTemplate(template, value, unit) {
58986
59036
  /** The whole formatting chain: raw value → the text to burn into the slot. */
58987
59037
  function formatSlotText(raw, binding, options = {}) {
58988
59038
  const value = displayValue(options.displayRaw ?? raw, binding);
58989
- const unit = options.unit ?? binding.unitLabel ?? "";
58990
- return limitText(applyTemplate(binding.template, value, unit).trim(), binding.maxCharacters);
59039
+ const unit = options.unit ?? "";
59040
+ return limitText(applyTemplate(binding.template, value, unit).trim(), options.maxCharacters);
58991
59041
  }
58992
59042
  //#endregion
58993
59043
  //#region src/state-leaves.ts
@@ -59252,6 +59302,12 @@ function resolveBindingLive(binding, facts) {
59252
59302
  if (slice === void 0) return { reason: `device ${deviceId} publishes no "${binding.capName}" state` };
59253
59303
  const raw = readStatePath(slice, binding.valuePath);
59254
59304
  if (raw === void 0) return { reason: `"${binding.capName}.${binding.valuePath}" is absent on device ${deviceId}` };
59305
+ const mapped = binding.valueMap?.[String(raw)];
59306
+ if (mapped !== void 0) return {
59307
+ value: mapped,
59308
+ unit: "",
59309
+ formatted: applyTemplateTokenKind(mapped, "", "full")
59310
+ };
59255
59311
  let value;
59256
59312
  let unit = "";
59257
59313
  if (typeof raw === "number") {
@@ -59324,7 +59380,7 @@ function resolveSource(binding, cameraDeviceId, facts) {
59324
59380
  return { value: {
59325
59381
  raw,
59326
59382
  displayRaw: converted.value,
59327
- unit: converted.unit || binding.unitLabel
59383
+ ...converted.unit !== void 0 && converted.unit !== "" ? { unit: converted.unit } : {}
59328
59384
  } };
59329
59385
  }
59330
59386
  return { value: { raw } };
@@ -59335,7 +59391,7 @@ function resolveSource(binding, cameraDeviceId, facts) {
59335
59391
  * write — a slot rewritten every tick is one firmware round-trip per tick
59336
59392
  * per slot, for a string that did not move.
59337
59393
  */
59338
- function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
59394
+ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText, maxCharacters) {
59339
59395
  if (binding === null) return {
59340
59396
  slotId,
59341
59397
  outcome: "unbound",
@@ -59348,7 +59404,6 @@ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
59348
59404
  text: "",
59349
59405
  reason: "binding is switched off"
59350
59406
  };
59351
- const fallback = limitText(binding.fallbackText, binding.maxCharacters);
59352
59407
  const gate = evaluateGate({
59353
59408
  cameraDeviceId,
59354
59409
  atMs: facts.atMs,
@@ -59356,14 +59411,14 @@ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
59356
59411
  conditions: binding.conditions,
59357
59412
  deviceState: facts.deviceState
59358
59413
  });
59359
- if (!gate.open) return settle(slotId, fallback, previousText, "gated", `condition "${gate.failedCondition}"`);
59414
+ if (!gate.open) return settle(slotId, "", previousText, "gated", `condition "${gate.failedCondition}"`);
59360
59415
  const resolved = resolveSource(binding, cameraDeviceId, facts);
59361
- if ("reason" in resolved) return settle(slotId, fallback, previousText, "unreadable", resolved.reason);
59362
- if (resolved.value.preformatted !== void 0 && binding.source.kind === "template") return settle(slotId, limitText(resolved.value.preformatted.trim(), binding.maxCharacters), previousText, "written");
59363
- const unit = resolved.value.unit ?? binding.unitLabel;
59416
+ if ("reason" in resolved) return settle(slotId, "", previousText, "unreadable", resolved.reason);
59417
+ if (resolved.value.preformatted !== void 0 && binding.source.kind === "template") return settle(slotId, limitText(resolved.value.preformatted.trim(), maxCharacters), previousText, "written");
59364
59418
  return settle(slotId, formatSlotText(resolved.value.raw, binding, {
59365
59419
  ...resolved.value.displayRaw !== void 0 ? { displayRaw: resolved.value.displayRaw } : {},
59366
- ...unit !== void 0 ? { unit } : {}
59420
+ ...resolved.value.unit !== void 0 ? { unit: resolved.value.unit } : {},
59421
+ ...maxCharacters !== void 0 ? { maxCharacters } : {}
59367
59422
  }), previousText, "written");
59368
59423
  }
59369
59424
  function settle(slotId, text, previousText, outcome, reason) {
@@ -59882,7 +59937,7 @@ var OsdManager = class {
59882
59937
  previewSlot = async ({ deviceId, slotId, binding }) => {
59883
59938
  if (binding !== void 0) assertSupportedConditions(binding.conditions);
59884
59939
  const effective = binding ?? (await this.readBindings(deviceId))[slotId] ?? null;
59885
- return renderSlot(slotId, effective, deviceId, await this.collectFacts(deviceId, effective === null ? {} : { [slotId]: effective }), void 0);
59940
+ return renderSlot(slotId, effective, deviceId, await this.collectFacts(deviceId, effective === null ? {} : { [slotId]: effective }), void 0, await this.maxCharactersForSlot(deviceId, slotId));
59886
59941
  };
59887
59942
  renderDevice = async ({ deviceId }) => {
59888
59943
  return { results: [...await this.renderDeviceInternal(deviceId, null)] };
@@ -59917,17 +59972,24 @@ var OsdManager = class {
59917
59972
  });
59918
59973
  }
59919
59974
  }
59975
+ /** Firmware text ceiling for one overlay, from the live `osd` status. */
59976
+ async maxCharactersForSlot(deviceId, slotId) {
59977
+ return (await this.deps.readOsdStatus(deviceId))?.overlays.find((o) => o.id === slotId)?.maxCharacters;
59978
+ }
59920
59979
  /** Render (and write) `onlySlots`, or every bound slot when `null`. */
59921
59980
  async renderDeviceInternal(deviceId, onlySlots) {
59922
59981
  const bindings = await this.readBindings(deviceId);
59923
59982
  const slotIds = onlySlots ?? Object.keys(bindings);
59924
59983
  if (slotIds.length === 0) return [];
59925
59984
  const facts = await this.collectFacts(deviceId, bindings);
59985
+ const status = await this.deps.readOsdStatus(deviceId);
59986
+ const maxBySlot = /* @__PURE__ */ new Map();
59987
+ for (const overlay of status?.overlays ?? []) if (overlay.maxCharacters !== void 0) maxBySlot.set(overlay.id, overlay.maxCharacters);
59926
59988
  const results = [];
59927
59989
  for (const slotId of slotIds) {
59928
59990
  const binding = bindings[slotId] ?? null;
59929
59991
  const key = cacheKey(deviceId, slotId);
59930
- const result = renderSlot(slotId, binding, deviceId, facts, this.lastWritten.get(key));
59992
+ const result = renderSlot(slotId, binding, deviceId, facts, this.lastWritten.get(key), maxBySlot.get(slotId));
59931
59993
  results.push(result);
59932
59994
  if (result.outcome === "unchanged") continue;
59933
59995
  if (result.outcome === "unbound" || result.outcome === "disabled") continue;
@@ -60295,7 +60357,7 @@ function toOsdPickerDevices(devices) {
60295
60357
  return devices.map((d) => ({
60296
60358
  id: d.id,
60297
60359
  name: d.name,
60298
- type: d.type ?? null,
60360
+ ...typeof d.type === "string" ? { type: d.type } : {},
60299
60361
  location: d.location ?? null,
60300
60362
  parentDeviceId: d.parentDeviceId ?? null
60301
60363
  }));