@7365admin1/core 3.42.3 → 3.43.0

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.
@@ -0,0 +1,191 @@
1
+ # Camera integration — configuration reference
2
+
3
+ How to switch each camera capability on, and who has to approve it.
4
+
5
+ Everything here is **configuration**. Nothing in this document requires a code
6
+ change, which is the point of the integration layer: the transports, the Dahua
7
+ adapter and the capability descriptor are already built and tested, and each
8
+ capability starts answering `supported` when the variables below are set.
9
+
10
+ ---
11
+
12
+ ## 1. What works with no configuration at all
13
+
14
+ | Capability | Transport | Needs |
15
+ |---|---|---|
16
+ | `liveVideo` | `RELAY_PLAYER` | nothing — the camera record's `host` is the player-page URL, and a client renders it in a WebView |
17
+ | `digitalZoom` | `RELAY_PLAYER` / `RTSP_FRAME` | nothing — it is client-side, on whichever picture arrived |
18
+
19
+ ## 2. What needs the RTSP recorder (already set on staging)
20
+
21
+ | Capability | Transport | Needs |
22
+ |---|---|---|
23
+ | `stillFrame` | `RTSP_FRAME` | `ffmpeg` on the API host, and `CAMERA_RTSP_DEVICES` |
24
+
25
+ ```
26
+ CAMERA_RTSP_DEVICES={"<relay-authority>":{"host":"<recorder-host>","port":554,"username":"<user>","password":"<password>"}}
27
+ CAMERA_FFMPEG_PATH=/usr/bin/ffmpeg # optional; "ffmpeg" on PATH by default
28
+ ```
29
+
30
+ Keyed by **relay authority** — the host part of `site.cameras.host` — because
31
+ that is the only stable identifier a camera record carries. The numeric last
32
+ path segment of the stored host is the **channel** on that recorder.
33
+
34
+ A camera whose relay has no entry here reports
35
+ `stillFrame: unsupported / no-recorder-configured`, which is a deployment fix. A
36
+ camera whose address carries no channel reports `no-channel-in-address`, which is
37
+ a data fix. The two are deliberately different answers.
38
+
39
+ ### 2.1 The same entry is what makes camera SETUP self-service
40
+
41
+ `CAMERA_RTSP_DEVICES` is read in both directions now.
42
+
43
+ - **Viewing** asks "given this relay, which recorder?" — the direction the stored
44
+ records need.
45
+ - **Setting a camera up** asks the opposite: the person at the form has the
46
+ recorder in front of them and no idea what a relay is, so the server looks the
47
+ entry up by recorder host and builds `https://<relay-authority>/<channel>`
48
+ itself. There is deliberately no second map.
49
+
50
+ So **one entry per recorder unlocks every channel on it** for the site's own
51
+ staff. A recorder with no entry is refused at the form with *"This recorder is
52
+ not connected to the video service yet…"* — the same deployment fix as
53
+ `no-recorder-configured`, said to a property manager instead of to a lead.
54
+
55
+ The **"Test this camera"** button in the form reads the same entry. It is
56
+ rationed before it can reach the device — one test per camera per 15 s, and 4
57
+ per recorder per 5 minutes — and it checks the shared authentication budget
58
+ first, so pressing it cannot walk a recorder into its own
59
+ 3-failures-in-30-seconds lockout.
60
+
61
+ ## 3. What needs the camera's own HTTP interface — OFF today
62
+
63
+ **Blocked on infrastructure, not on code.** Measured 2026-08-10 from this
64
+ workstation and from the staging API host: the recorder answers on **RTSP/554
65
+ only**. Ports 80, 443 and the Dahua native port 37777 time out. Until that
66
+ changes, every variable in this section can be set and nothing will answer.
67
+
68
+ | Capability | Needs |
69
+ |---|---|
70
+ | `deviceInfo` | §2 not required · `CAMERA_DEVICE_HTTP` + `CAMERA_DEVICE_HTTP_ENABLED` + reachability |
71
+ | `playback` | the same |
72
+ | `events` | the same |
73
+ | `stillFrame` (fallback) | the same — used only when no RTSP recorder is configured |
74
+ | `ptz` | the above **plus** `CAMERA_DEVICE_CONTROL_ENABLED` **plus** a PTZ-capable unit **plus** `CAMERA_PTZ_ENABLED` |
75
+ | `presets` | the same as `ptz` |
76
+
77
+ ```
78
+ # 1. Where the recorder's HTTP interface is. The credential is REFERENCED by
79
+ # environment-variable NAME and never written here, so this value is safe to
80
+ # paste into a PR or a ticket.
81
+ CAMERA_DEVICE_HTTP={"<relay-authority>":{"baseUrl":"https://<recorder-host>:443","credentialRef":"CAMERA_DEVICE_CRED_MAIN"}}
82
+
83
+ # 2. The secret itself, set separately, alongside the host's other secrets.
84
+ CAMERA_DEVICE_CRED_MAIN=<username>:<password>
85
+
86
+ # 3. Reads. Off by default. Nothing contacts a device while this is unset.
87
+ CAMERA_DEVICE_HTTP_ENABLED=true
88
+
89
+ # 4. Optional tuning.
90
+ CAMERA_DEVICE_HTTP_TIMEOUT_MS=8000
91
+ CAMERA_DEVICE_HTTP_PROBE_TTL_SECONDS=900
92
+ ```
93
+
94
+ ### Validation, and what happens when it fails
95
+
96
+ `CAMERA_DEVICE_HTTP` is validated at startup and every rejection is logged with
97
+ the variable name and the reason. A rejected entry is **dropped, never
98
+ half-used**, and its cameras then report `device-http-not-configured` truthfully
99
+ rather than hanging on a request.
100
+
101
+ - `baseUrl` must be an `http(s)` **origin** — scheme, host, optional port. A path,
102
+ query or fragment is rejected.
103
+ - `credentialRef` must look like an environment variable name (`A-Z`, `0-9`, `_`).
104
+ A credential pasted into this field is **rejected, not used**.
105
+ - The referenced variable must exist and be `username:password`. The split is on
106
+ the first colon only, so a password may contain colons.
107
+ - Malformed JSON yields no recorders at all.
108
+
109
+ ---
110
+
111
+ ## 4. Arming camera control — needs a named human
112
+
113
+ ```
114
+ CAMERA_DEVICE_CONTROL_ENABLED=true
115
+ ```
116
+
117
+ **This is the only variable in this document that can move physical hardware, and
118
+ it must not be set without a backend lead's sign-off** (Benar or Kyle). It is a
119
+ separate variable from `CAMERA_DEVICE_HTTP_ENABLED` on purpose: turning reads on
120
+ must not arm motors.
121
+
122
+ Three independent switches must all be on before a PTZ command can be sent —
123
+ `CAMERA_PTZ_ENABLED`, `CAMERA_DEVICE_HTTP_ENABLED` and
124
+ `CAMERA_DEVICE_CONTROL_ENABLED` — and the unit must have told us through §8.1.3
125
+ that it actually pans. All three ship off, and no device has been moved.
126
+
127
+ While control is off, the adapter's `control` object is `null`: the mutating
128
+ functions are not merely guarded, they do not exist to be called.
129
+
130
+ **Never implemented, and it must stay that way:** any `configManager.cgi` write,
131
+ `setCurrentTime`, `SetPreset` / `ClearPreset`, tours, patterns, reboot, and any
132
+ barrier or gate command. This integration issues no configuration writes to any
133
+ device.
134
+
135
+ ---
136
+
137
+ ## 5. The lockout budget
138
+
139
+ A Dahua unit locks an account for **1800 s after 3 failed logins in 30 s**
140
+ (V3.37 §4.7.x). On a unit that also runs ANPR and a barrier, that is half an hour
141
+ of a site's access control gone.
142
+
143
+ The failure budget is therefore **not configurable**. It counts failures per
144
+ device in the shared cache, stops at **2**, and once spent refuses to send
145
+ anything until the device's own lock time has elapsed — reporting
146
+ `device-http-locked-out` instead of retrying. It is shared across API workers
147
+ (the limit belongs to the device, not to a process) and persistent across
148
+ restarts (a deploy must not hand the budget back).
149
+
150
+ If the budget is spent, the cause is almost always a wrong credential in
151
+ `CAMERA_DEVICE_CRED_*`. Fix the credential; do not shorten the cooldown.
152
+
153
+ ---
154
+
155
+ ## 6. Reading the descriptor
156
+
157
+ Each camera in the wall and status responses carries:
158
+
159
+ ```json
160
+ "capabilities": {
161
+ "liveVideo": { "state": "supported", "transport": "RELAY_PLAYER", "reason": null, "detail": null },
162
+ "stillFrame": { "state": "supported", "transport": "RTSP_FRAME", "reason": null, "detail": null },
163
+ "ptz": { "state": "unsupported", "transport": null,
164
+ "reason": "device-http-not-configured",
165
+ "detail": "No direct connection to this camera's recorder is configured on this server." }
166
+ }
167
+ ```
168
+
169
+ **A client should switch on `reason`, never on `detail`.** `detail` is there so a
170
+ surface with nothing better to show has something honest to show.
171
+
172
+ Reason codes: `not-patrol-cctv-camera`, `camera-inactive`, `no-address`,
173
+ `invalid-address`, `not-a-relay-player-url`, `no-recorder-configured`,
174
+ `no-channel-in-address`, `device-http-not-configured`, `device-http-disabled`,
175
+ `device-http-unreachable`, `device-http-locked-out`, `device-not-probed`,
176
+ `device-no-ptz`, `control-not-enabled`, `no-transport`.
177
+
178
+ ## 7. Adding a transport later
179
+
180
+ Register one — `registerCameraTransport({ id, provides, evaluate })` — and list
181
+ the capabilities it serves. Registration order is preference order. **No service,
182
+ controller or descriptor change is needed**, which is how an HLS relay, a MediaMTX
183
+ playback URL or an audio path would land: a capability no transport claims already
184
+ answers `no-transport`, and starts answering `supported` the moment something
185
+ claims it.
186
+
187
+ ---
188
+
189
+ _Written 2026-08-10 alongside the integration layer. Update when the recorder's
190
+ HTTP interface becomes reachable, when a transport is added, or when control is
191
+ armed anywhere._
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.42.3",
4
+ "version": "3.43.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -14,7 +14,8 @@
14
14
  "release": "yarn run build && changeset publish",
15
15
  "lint": "tsc",
16
16
  "test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\"",
17
- "test:anpr": "tsup src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts --format esm --out-dir test/.build/utils --no-dts --silent && node --test \"test/camera-alert.util.test.mjs\" \"test/dahua-protocol.util.test.mjs\""
17
+ "test:anpr": "tsup src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts --format esm --out-dir test/.build/utils --no-dts --silent && node --test \"test/camera-alert.util.test.mjs\" \"test/dahua-protocol.util.test.mjs\"",
18
+ "test:camera": "tsup src/utils/camera-view.util.ts src/utils/camera-capability.util.ts src/services/camera-device-http.service.ts --format esm --out-dir test/.build --no-dts --silent && node --test \"test/camera-view.util.test.mjs\" \"test/camera-capability.util.test.mjs\" \"test/camera-device-http.test.mjs\" \"test/camera-write-gate.test.mjs\" \"test/camera-entitlement-wiring.test.mjs\""
18
19
  },
19
20
  "devDependencies": {
20
21
  "@changesets/cli": "^2.26.0",