@7365admin1/core 3.42.1 → 3.42.3
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.
- package/CHANGELOG.md +96 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +217 -56
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +217 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/test/camera-alert.util.test.mjs +109 -0
- package/test/dahua-protocol.util.test.mjs +217 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
# @iservice365/core
|
|
2
2
|
|
|
3
|
+
## 3.42.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 854dd11: Editing a building no longer creates duplicate levels or orphaned units
|
|
8
|
+
|
|
9
|
+
Updating a block inserted a brand-new level document for every level name in the
|
|
10
|
+
payload, without checking whether that name was already a level on the block. A
|
|
11
|
+
client that posted the block's existing level names back — which the block
|
|
12
|
+
editor's own full-levels payload does — got a second "Level 1" document, a
|
|
13
|
+
second entry in `building.levels`, and units still pointing at the first one.
|
|
14
|
+
|
|
15
|
+
The submitted names are now reconciled against the block's existing ACTIVE
|
|
16
|
+
levels first. A name that already matches an existing level is left alone and
|
|
17
|
+
keeps its `_id`, so nothing that references it is orphaned; only genuinely new
|
|
18
|
+
names are inserted.
|
|
19
|
+
|
|
20
|
+
Matching is case- and whitespace-insensitive, using the same normalisation the
|
|
21
|
+
import path already uses, so "Level 1", "level 1" and "Level 1" are
|
|
22
|
+
one level. Only the match key is normalised — the stored name keeps its
|
|
23
|
+
original spelling, so two genuinely different levels are never merged.
|
|
24
|
+
|
|
25
|
+
Removals are deliberately not inferred from this payload; deleting a level keeps
|
|
26
|
+
its own guarded path.
|
|
27
|
+
|
|
28
|
+
Worth a QA pass on building edit — this changes what a block update writes.
|
|
29
|
+
|
|
30
|
+
## 3.42.2
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 7078e6f: Make ANPR "Connection Lost" alerts readable, paced, and self-clearing
|
|
35
|
+
|
|
36
|
+
The alert an operator actually saw was `Camera [Seventh Condominium-both]
|
|
37
|
+
Connection Lost. Retrying in 10 seconds. Check the camera if this issue
|
|
38
|
+
persists, Or set the camera to Inactive to stop this notification.` Three
|
|
39
|
+
things were wrong with it:
|
|
40
|
+
|
|
41
|
+
- `[<site>-<direction>]` glued a database enum onto the site name and showed it
|
|
42
|
+
as if it were a place. "both" is the camera's direction setting, not
|
|
43
|
+
somewhere you can go. The camera is now named the way the setup form names
|
|
44
|
+
it — by its own name where it has one, otherwise "the entry and exit ANPR
|
|
45
|
+
camera at <site>".
|
|
46
|
+
- It told the operator to set the camera Inactive to stop the alert. On a
|
|
47
|
+
camera driving a barrier, that is advice to switch off plate reading in order
|
|
48
|
+
to silence the warning that plate reading is off. Removed.
|
|
49
|
+
- It was emitted on every reconnect attempt — once every 10 seconds with no
|
|
50
|
+
ceiling — into a snackbar with a 20-second timeout, so a camera that stayed
|
|
51
|
+
down held a red toast on screen permanently, on every page. One hour of
|
|
52
|
+
downtime produced 360 alerts.
|
|
53
|
+
|
|
54
|
+
Alerts are now paced: the first failure, then roughly 1, 5 and 15 minutes, then
|
|
55
|
+
every 15 minutes for as long as it stays down. When the camera answers again,
|
|
56
|
+
an operator who was told it was down is told it is back; if they were never
|
|
57
|
+
alerted, nothing is sent.
|
|
58
|
+
|
|
59
|
+
**The reconnect loop itself is unchanged and still retries every 10 seconds** —
|
|
60
|
+
only the alerting is paced. A barrier camera has to come back the moment it can.
|
|
61
|
+
|
|
62
|
+
Recovery and fault alerts also carry `event` and `camera` fields so a client can
|
|
63
|
+
group or clear alerts per camera rather than matching on message text. Existing
|
|
64
|
+
clients ignore them and render `message` as they always have.
|
|
65
|
+
|
|
66
|
+
- a5d2b88: Stop logging camera and mailer credentials
|
|
67
|
+
|
|
68
|
+
Six places wrote a secret or a resident's details to stdout on an ordinary code
|
|
69
|
+
path. `getTrafficJunction` dumped the whole `TSiteCamera` object — including the
|
|
70
|
+
camera's `username` and plaintext `password` — on every reconnect, and
|
|
71
|
+
`addPlateNumber` logged its argument, which carries the same password plus a
|
|
72
|
+
resident's plate number and owner name. `checkOutBySiteAndPlate` logged the
|
|
73
|
+
matched visitor transaction. `siteCameraRepo.updateById` logged the re-read
|
|
74
|
+
camera document, and `vehicleService.add` logged the whole array of a site's
|
|
75
|
+
cameras, both with credentials in them. `sendEmail` printed `MAILER_PASSWORD`
|
|
76
|
+
itself on every message sent.
|
|
77
|
+
|
|
78
|
+
All now log identifiers, counts and outcomes only, through the module's logger
|
|
79
|
+
rather than `console.log`; the mailer line is removed outright because it
|
|
80
|
+
carried nothing but the secret.
|
|
81
|
+
|
|
82
|
+
- b472360: Fix three Dahua HTTP API defects found by checking our integration against the
|
|
83
|
+
vendor specification (V3.37):
|
|
84
|
+
|
|
85
|
+
- The ANPR reconnect loop retried authentication ten times at one-second
|
|
86
|
+
intervals. The device locks the account for 1800 seconds after three failed
|
|
87
|
+
logins in thirty, so our own recovery could take ANPR down at a site for half
|
|
88
|
+
an hour. Authentication now backs off well past the device's counting window,
|
|
89
|
+
and a network fault is distinguished from an authentication failure.
|
|
90
|
+
- 401 and 403 were handled backwards. Per spec §3.4, 401 is the digest
|
|
91
|
+
challenge (retry) and 403 is a credentials rejection (stop). We treated 403 as
|
|
92
|
+
an account lock and killed the listener, and counted 401s toward a "wrong
|
|
93
|
+
password" warning.
|
|
94
|
+
- `updatePlateNumber`, `bulkInsertPlateNumber` and `removePlateNumber`
|
|
95
|
+
interpolated values into the CGI query string unencoded, against the spec's
|
|
96
|
+
stated RFC 3986 requirement. A plate or owner name containing a space or "&"
|
|
97
|
+
broke the request or appended extra parameters to it.
|
|
98
|
+
|
|
3
99
|
## 3.42.1
|
|
4
100
|
|
|
5
101
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -7846,6 +7846,7 @@ declare function useBuildingLevelRepo(): {
|
|
|
7846
7846
|
message: string;
|
|
7847
7847
|
data: bson.Document[];
|
|
7848
7848
|
}>;
|
|
7849
|
+
getActiveLevelsByIds: (ids: (string | ObjectId)[], session?: ClientSession) => Promise<TBuildingLevel[]>;
|
|
7849
7850
|
};
|
|
7850
7851
|
|
|
7851
7852
|
declare function useBuildingLevelService(): {
|
package/dist/index.js
CHANGED
|
@@ -22212,7 +22212,6 @@ async function sendEmail({ to, subject, text, html }) {
|
|
|
22212
22212
|
pass: MAILER_PASSWORD
|
|
22213
22213
|
}
|
|
22214
22214
|
});
|
|
22215
|
-
console.log(MAILER_EMAIL, MAILER_PASSWORD, MAILER_TRANSPORT_HOST, MAILER_TRANSPORT_PORT);
|
|
22216
22215
|
const mailOptions = {
|
|
22217
22216
|
from: `Seven 365 <${MAILER_EMAIL}>`,
|
|
22218
22217
|
to,
|
|
@@ -24249,6 +24248,86 @@ var loggerDahua = winston.createLogger({
|
|
|
24249
24248
|
]
|
|
24250
24249
|
});
|
|
24251
24250
|
|
|
24251
|
+
// src/utils/camera-alert.util.ts
|
|
24252
|
+
var DIRECTION_WORDS = {
|
|
24253
|
+
entry: "entry",
|
|
24254
|
+
exit: "exit",
|
|
24255
|
+
both: "entry and exit",
|
|
24256
|
+
visitors: "visitor",
|
|
24257
|
+
residents: "resident"
|
|
24258
|
+
};
|
|
24259
|
+
function describeCamera(camera) {
|
|
24260
|
+
const site = String(camera?.siteName ?? "").trim();
|
|
24261
|
+
const name = String(camera?.name ?? "").trim();
|
|
24262
|
+
const at = site ? ` at ${site}` : "";
|
|
24263
|
+
if (name)
|
|
24264
|
+
return `Camera "${name}"${at}`;
|
|
24265
|
+
const words = DIRECTION_WORDS[String(camera?.direction ?? "").trim().toLowerCase()];
|
|
24266
|
+
return words ? `The ${words} ANPR camera${at}` : `The ANPR camera${at}`;
|
|
24267
|
+
}
|
|
24268
|
+
function cameraUnreachableMessage(camera) {
|
|
24269
|
+
return `${describeCamera(camera)} is not responding. Plate reads and automatic barrier opening are stopped while it is down. The server keeps trying to reconnect on its own. If it does not come back, ask your site technician or the security-system installer to check the camera's power and network.`;
|
|
24270
|
+
}
|
|
24271
|
+
function cameraRecoveredMessage(camera) {
|
|
24272
|
+
return `${describeCamera(camera)} is responding again. Plate reads have resumed.`;
|
|
24273
|
+
}
|
|
24274
|
+
function cameraCredentialsMessage(camera) {
|
|
24275
|
+
return `${describeCamera(camera)} refused the login, so it cannot read plates. The username or password saved for it is wrong. An administrator can correct them in Site Settings > Cameras; if they are already correct, the camera's own account needs checking by your site technician.`;
|
|
24276
|
+
}
|
|
24277
|
+
function cameraNotAuthorisedMessage(camera) {
|
|
24278
|
+
return `${describeCamera(camera)} rejected our connection, so it cannot read plates. This usually means its account is not allowed to use the plate-reading feed. Please ask your site technician or the security-system installer to check the camera's account permissions.`;
|
|
24279
|
+
}
|
|
24280
|
+
var CAMERA_ALERT_AT_ATTEMPT = [1, 6, 30, 90];
|
|
24281
|
+
var CAMERA_ALERT_EVERY_AFTER = 90;
|
|
24282
|
+
function shouldAlertOnFailure(consecutiveFailures) {
|
|
24283
|
+
if (!Number.isInteger(consecutiveFailures) || consecutiveFailures < 1)
|
|
24284
|
+
return false;
|
|
24285
|
+
if (CAMERA_ALERT_AT_ATTEMPT.includes(consecutiveFailures))
|
|
24286
|
+
return true;
|
|
24287
|
+
if (consecutiveFailures < CAMERA_ALERT_EVERY_AFTER)
|
|
24288
|
+
return false;
|
|
24289
|
+
return (consecutiveFailures - CAMERA_ALERT_EVERY_AFTER) % CAMERA_ALERT_EVERY_AFTER === 0;
|
|
24290
|
+
}
|
|
24291
|
+
|
|
24292
|
+
// src/utils/dahua-protocol.util.ts
|
|
24293
|
+
var DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT = 2;
|
|
24294
|
+
var AUTH_BACKOFF_MS = [6e4, 3e5, 9e5];
|
|
24295
|
+
function dahuaAuthBackoffMs(consecutiveAuthFailures) {
|
|
24296
|
+
const index = Math.min(
|
|
24297
|
+
Math.max(consecutiveAuthFailures, 1),
|
|
24298
|
+
AUTH_BACKOFF_MS.length
|
|
24299
|
+
);
|
|
24300
|
+
return AUTH_BACKOFF_MS[index - 1];
|
|
24301
|
+
}
|
|
24302
|
+
function classifyDahuaStatus(statusCode) {
|
|
24303
|
+
if (statusCode === 401)
|
|
24304
|
+
return "challenge";
|
|
24305
|
+
if (statusCode === 403)
|
|
24306
|
+
return "rejected";
|
|
24307
|
+
if (statusCode === 400 || statusCode === 500)
|
|
24308
|
+
return "fatal";
|
|
24309
|
+
return "proceed";
|
|
24310
|
+
}
|
|
24311
|
+
function classifyDahuaFailure(message) {
|
|
24312
|
+
const text = String(message ?? "");
|
|
24313
|
+
if (/\b403\b|forbidden/i.test(text))
|
|
24314
|
+
return "rejected";
|
|
24315
|
+
if (/\b401\b|unauthorized|invalid authority|digest/i.test(text)) {
|
|
24316
|
+
return "challenge";
|
|
24317
|
+
}
|
|
24318
|
+
return "transient";
|
|
24319
|
+
}
|
|
24320
|
+
var enc = (value) => encodeURIComponent(String(value));
|
|
24321
|
+
function buildPlateUpdateEndpoint(value) {
|
|
24322
|
+
return `/cgi-bin/recordUpdater.cgi?action=update&name=${enc(value.mode)}&recno=${enc(value.recno)}&PlateNumber=${enc(value.plateNumber)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
|
|
24323
|
+
}
|
|
24324
|
+
function buildPlateInsertEndpoint(value) {
|
|
24325
|
+
return `/cgi-bin/recordUpdater.cgi?action=insert&name=${enc(value.mode)}&PlateNumber=${enc(value.plateNumber)}&VehicleType=${enc(value.vehicleType)}&VehicleColor=${enc(value.vehicleColor)}&BeginTime=${enc(value.start)}&CancelTime=${enc(value.end)}&+OpenGate=${value.openGate}&MasterOfCar=${enc(value.owner)}`;
|
|
24326
|
+
}
|
|
24327
|
+
function buildPlateRemoveEndpoint(value) {
|
|
24328
|
+
return `/cgi-bin/recordUpdater.cgi?action=remove&recno=${enc(value.recno)}&name=${enc(value.mode)}`;
|
|
24329
|
+
}
|
|
24330
|
+
|
|
24252
24331
|
// src/services/dahua.service.ts
|
|
24253
24332
|
var cameraRegistry = /* @__PURE__ */ new Map();
|
|
24254
24333
|
var _savedOnDetected;
|
|
@@ -24294,7 +24373,8 @@ async function useDahuaDigestWithRetry({
|
|
|
24294
24373
|
retryDelayMs = 0
|
|
24295
24374
|
}) {
|
|
24296
24375
|
let lastError;
|
|
24297
|
-
|
|
24376
|
+
const effectiveRetries = Math.min(retries, DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT);
|
|
24377
|
+
for (let attempt = 1; attempt <= effectiveRetries; attempt++) {
|
|
24298
24378
|
try {
|
|
24299
24379
|
const effectiveTimeout = streaming ? 0 : timeout;
|
|
24300
24380
|
const response = await useDahuaDigest({
|
|
@@ -24321,9 +24401,9 @@ async function useDahuaDigestWithRetry({
|
|
|
24321
24401
|
}
|
|
24322
24402
|
const errorText = streaming ? `HTTP ${statusCode}` : getDahuaResponseText(response);
|
|
24323
24403
|
const shouldRetry = isDahuaInvalidAuthority(errorText);
|
|
24324
|
-
if (shouldRetry && attempt <
|
|
24404
|
+
if (shouldRetry && attempt < effectiveRetries) {
|
|
24325
24405
|
loggerDahua.warn(
|
|
24326
|
-
`[${host}] Dahua Invalid Authority. Retrying ${attempt}/${
|
|
24406
|
+
`[${host}] Dahua Invalid Authority. Retrying ${attempt}/${effectiveRetries}`
|
|
24327
24407
|
);
|
|
24328
24408
|
if (retryDelayMs > 0) {
|
|
24329
24409
|
await sleep(retryDelayMs);
|
|
@@ -24335,9 +24415,9 @@ async function useDahuaDigestWithRetry({
|
|
|
24335
24415
|
lastError = error;
|
|
24336
24416
|
const message = error?.message || String(error);
|
|
24337
24417
|
const shouldRetry = isDahuaInvalidAuthority(message);
|
|
24338
|
-
if (shouldRetry && attempt <
|
|
24418
|
+
if (shouldRetry && attempt < effectiveRetries) {
|
|
24339
24419
|
loggerDahua.warn(
|
|
24340
|
-
`[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${
|
|
24420
|
+
`[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${effectiveRetries}`
|
|
24341
24421
|
);
|
|
24342
24422
|
if (retryDelayMs > 0) {
|
|
24343
24423
|
await sleep(retryDelayMs);
|
|
@@ -24441,7 +24521,9 @@ function useDahuaService() {
|
|
|
24441
24521
|
return;
|
|
24442
24522
|
try {
|
|
24443
24523
|
const result = await _checkOutBySiteAndPlate(site, plateNumber2);
|
|
24444
|
-
|
|
24524
|
+
loggerDahua.info(
|
|
24525
|
+
`checkOutBySiteAndPlate matched=${Boolean(result)} type=${result?.type ?? "none"}`
|
|
24526
|
+
);
|
|
24445
24527
|
if (onDetected2 && result?.type != "resident" && result?.site) {
|
|
24446
24528
|
onDetected2({ reload: true, site: result?.site?.toString(), siteName: camera?.siteName, cameraDirection: camera?.direction, direction });
|
|
24447
24529
|
}
|
|
@@ -24740,9 +24822,12 @@ function useDahuaService() {
|
|
|
24740
24822
|
);
|
|
24741
24823
|
}
|
|
24742
24824
|
async function getTrafficJunction(camera, signal, onDetected) {
|
|
24743
|
-
|
|
24825
|
+
loggerDahua.info(
|
|
24826
|
+
`getTrafficJunction starting for camera ${camera?._id} [${camera?.siteName}-${camera?.direction}] host ${camera?.host}`
|
|
24827
|
+
);
|
|
24744
24828
|
let authFailureCount = 0;
|
|
24745
|
-
|
|
24829
|
+
let connectionFailureCount = 0;
|
|
24830
|
+
let alertedFailureCount = 0;
|
|
24746
24831
|
while (!signal.aborted) {
|
|
24747
24832
|
let bufferQueue = null;
|
|
24748
24833
|
let response = null;
|
|
@@ -24757,31 +24842,48 @@ function useDahuaService() {
|
|
|
24757
24842
|
streaming: true
|
|
24758
24843
|
});
|
|
24759
24844
|
const statusCode = getDahuaStatusCode(response);
|
|
24760
|
-
|
|
24761
|
-
|
|
24762
|
-
|
|
24763
|
-
throw new Error("401 Unauthorized -
|
|
24764
|
-
} else if (
|
|
24845
|
+
const verdict = classifyDahuaStatus(statusCode);
|
|
24846
|
+
if (verdict === "challenge") {
|
|
24847
|
+
loggerDahua.warn(`[${camera?.siteName}-${camera?.direction}] 401 - digest challenge not completed; will re-authenticate after backoff.`);
|
|
24848
|
+
throw new Error("401 Unauthorized - digest challenge not completed");
|
|
24849
|
+
} else if (verdict === "rejected") {
|
|
24765
24850
|
loggerDahua.error(
|
|
24766
|
-
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden
|
|
24851
|
+
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden - credentials rejected by the device. Not retrying.`
|
|
24767
24852
|
);
|
|
24768
24853
|
if (onDetected) {
|
|
24769
24854
|
onDetected({
|
|
24770
24855
|
site: camera?.site,
|
|
24771
24856
|
direction: camera?.direction,
|
|
24772
|
-
|
|
24857
|
+
// Superseded #1803's own wording: cameraCredentialsMessage already
|
|
24858
|
+
// says the login was refused and where to correct it, and no longer
|
|
24859
|
+
// claims a ten-minute lockout.
|
|
24860
|
+
messagePermanent: cameraCredentialsMessage(camera)
|
|
24773
24861
|
});
|
|
24774
24862
|
}
|
|
24775
24863
|
return;
|
|
24776
|
-
} else if (
|
|
24864
|
+
} else if (verdict === "fatal") {
|
|
24777
24865
|
loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] Connection error: ${statusCode}`);
|
|
24778
24866
|
if (onDetected) {
|
|
24779
|
-
onDetected({ site: camera?.site, messagePermanent:
|
|
24867
|
+
onDetected({ site: camera?.site, messagePermanent: cameraNotAuthorisedMessage(camera) });
|
|
24780
24868
|
}
|
|
24781
24869
|
return;
|
|
24782
24870
|
}
|
|
24783
24871
|
authFailureCount = 0;
|
|
24784
24872
|
isAuthFailure = false;
|
|
24873
|
+
if (alertedFailureCount > 0 && onDetected) {
|
|
24874
|
+
onDetected({
|
|
24875
|
+
site: camera?.site,
|
|
24876
|
+
direction: camera?.direction,
|
|
24877
|
+
// `event` and `camera` let a client group or clear an alert per
|
|
24878
|
+
// camera instead of matching on the message text. Older clients
|
|
24879
|
+
// ignore them and just render `message`, as they always have.
|
|
24880
|
+
event: "camera-recovered",
|
|
24881
|
+
camera: camera?._id?.toString(),
|
|
24882
|
+
message: cameraRecoveredMessage(camera)
|
|
24883
|
+
});
|
|
24884
|
+
}
|
|
24885
|
+
connectionFailureCount = 0;
|
|
24886
|
+
alertedFailureCount = 0;
|
|
24785
24887
|
loggerDahua.info(`[${camera?.siteName}-${camera?.direction}] Successfully connected to ANPR.`);
|
|
24786
24888
|
const contentType = response.res.headers["content-type"];
|
|
24787
24889
|
const boundaryMatch = contentType?.match(/boundary=(.*)$/i);
|
|
@@ -24827,50 +24929,53 @@ function useDahuaService() {
|
|
|
24827
24929
|
break;
|
|
24828
24930
|
}
|
|
24829
24931
|
const errMsg = String(error?.message || error);
|
|
24830
|
-
|
|
24932
|
+
const failure = classifyDahuaFailure(errMsg);
|
|
24933
|
+
if (failure === "rejected") {
|
|
24831
24934
|
loggerDahua.error(
|
|
24832
|
-
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client
|
|
24935
|
+
`[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client - credentials rejected. Not retrying.`
|
|
24833
24936
|
);
|
|
24834
24937
|
if (onDetected) {
|
|
24835
24938
|
onDetected({
|
|
24836
24939
|
site: camera?.site,
|
|
24837
24940
|
direction: camera?.direction,
|
|
24838
|
-
messagePermanent:
|
|
24941
|
+
messagePermanent: cameraCredentialsMessage(camera)
|
|
24839
24942
|
});
|
|
24840
24943
|
}
|
|
24841
24944
|
return;
|
|
24842
24945
|
}
|
|
24843
|
-
|
|
24844
|
-
if (isAuthError) {
|
|
24946
|
+
if (failure === "challenge") {
|
|
24845
24947
|
authFailureCount++;
|
|
24846
24948
|
isAuthFailure = true;
|
|
24847
24949
|
loggerDahua.warn(
|
|
24848
|
-
`[${camera?.siteName}-${camera?.direction}]
|
|
24950
|
+
`[${camera?.siteName}-${camera?.direction}] Authentication attempt ${authFailureCount} failed; next attempt in ${dahuaAuthBackoffMs(authFailureCount) / 1e3}s`
|
|
24849
24951
|
);
|
|
24850
|
-
if (authFailureCount
|
|
24952
|
+
if (authFailureCount === DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT) {
|
|
24851
24953
|
loggerDahua.error(
|
|
24852
|
-
`[${camera?.siteName}-${camera?.direction}]
|
|
24954
|
+
`[${camera?.siteName}-${camera?.direction}] ${authFailureCount} consecutive authentication failures. Slowing down and alerting the operator.`
|
|
24853
24955
|
);
|
|
24854
24956
|
if (onDetected) {
|
|
24855
24957
|
onDetected({
|
|
24856
24958
|
site: camera?.site,
|
|
24857
24959
|
direction: camera?.direction,
|
|
24858
|
-
messagePermanent:
|
|
24960
|
+
messagePermanent: cameraCredentialsMessage(camera)
|
|
24859
24961
|
});
|
|
24860
24962
|
}
|
|
24861
|
-
return;
|
|
24862
24963
|
}
|
|
24863
24964
|
} else {
|
|
24864
24965
|
authFailureCount = 0;
|
|
24865
24966
|
isAuthFailure = false;
|
|
24967
|
+
connectionFailureCount++;
|
|
24866
24968
|
loggerDahua.error(
|
|
24867
|
-
`[${camera?.siteName}-${camera?.direction}] Connection lost or error: ${error.message || error}. Retrying in 10 seconds
|
|
24969
|
+
`[${camera?.siteName}-${camera?.direction}] Connection lost or error: ${error.message || error}. Retrying in 10 seconds... (failure ${connectionFailureCount})`
|
|
24868
24970
|
);
|
|
24869
|
-
if (onDetected) {
|
|
24971
|
+
if (onDetected && shouldAlertOnFailure(connectionFailureCount)) {
|
|
24972
|
+
alertedFailureCount = connectionFailureCount;
|
|
24870
24973
|
onDetected({
|
|
24871
24974
|
site: camera?.site,
|
|
24872
24975
|
direction: camera?.direction,
|
|
24873
|
-
|
|
24976
|
+
event: "camera-fault",
|
|
24977
|
+
camera: camera?._id?.toString(),
|
|
24978
|
+
message: cameraUnreachableMessage(camera)
|
|
24874
24979
|
});
|
|
24875
24980
|
}
|
|
24876
24981
|
}
|
|
@@ -24894,7 +24999,7 @@ function useDahuaService() {
|
|
|
24894
24999
|
}
|
|
24895
25000
|
}
|
|
24896
25001
|
if (!signal.aborted) {
|
|
24897
|
-
const waitMs = isAuthFailure ?
|
|
25002
|
+
const waitMs = isAuthFailure ? dahuaAuthBackoffMs(authFailureCount) : 1e4;
|
|
24898
25003
|
await new Promise((res) => setTimeout(res, waitMs));
|
|
24899
25004
|
}
|
|
24900
25005
|
}
|
|
@@ -24907,7 +25012,9 @@ function useDahuaService() {
|
|
|
24907
25012
|
return match ? match[1] : null;
|
|
24908
25013
|
}
|
|
24909
25014
|
async function addPlateNumber(value) {
|
|
24910
|
-
|
|
25015
|
+
loggerDahua.info(
|
|
25016
|
+
`addPlateNumber called for host ${value?.host} mode ${value?.mode}`
|
|
25017
|
+
);
|
|
24911
25018
|
let recno = null;
|
|
24912
25019
|
const validation = import_joi42.default.object({
|
|
24913
25020
|
host: import_joi42.default.string().required(),
|
|
@@ -25052,7 +25159,15 @@ function useDahuaService() {
|
|
|
25052
25159
|
const openGateString = String(value.isOpenGate);
|
|
25053
25160
|
_isOpenGate = openGateString ? openGateString : "true";
|
|
25054
25161
|
}
|
|
25055
|
-
const endpoint =
|
|
25162
|
+
const endpoint = buildPlateUpdateEndpoint({
|
|
25163
|
+
mode: value.mode,
|
|
25164
|
+
recno: value.recno,
|
|
25165
|
+
plateNumber: value.plateNumber,
|
|
25166
|
+
start: value.start,
|
|
25167
|
+
end: value.end,
|
|
25168
|
+
owner: value.owner,
|
|
25169
|
+
openGate: _isOpenGate
|
|
25170
|
+
});
|
|
25056
25171
|
try {
|
|
25057
25172
|
const response = await useDahuaDigest({
|
|
25058
25173
|
host: value.host,
|
|
@@ -25085,7 +25200,10 @@ function useDahuaService() {
|
|
|
25085
25200
|
host: value.host,
|
|
25086
25201
|
username: value.username,
|
|
25087
25202
|
password: value.password,
|
|
25088
|
-
endpoint:
|
|
25203
|
+
endpoint: buildPlateRemoveEndpoint({
|
|
25204
|
+
recno: value.recno,
|
|
25205
|
+
mode: value.mode
|
|
25206
|
+
})
|
|
25089
25207
|
});
|
|
25090
25208
|
return response;
|
|
25091
25209
|
} catch (error2) {
|
|
@@ -25141,7 +25259,16 @@ function useDahuaService() {
|
|
|
25141
25259
|
value.vehicleColor = String(value.vehicleColor ?? "").substring(0, 31) || "unknown";
|
|
25142
25260
|
const _openGate = String(value.isOpenGate);
|
|
25143
25261
|
const isOpenGateString = _openGate && _openGate !== "undefined" ? _openGate : "true";
|
|
25144
|
-
const endpoint =
|
|
25262
|
+
const endpoint = buildPlateInsertEndpoint({
|
|
25263
|
+
mode: value.mode,
|
|
25264
|
+
plateNumber: value.plateNumber,
|
|
25265
|
+
vehicleType: value.vehicleType,
|
|
25266
|
+
vehicleColor: value.vehicleColor,
|
|
25267
|
+
start: value.start,
|
|
25268
|
+
end: value.end,
|
|
25269
|
+
owner: value.owner,
|
|
25270
|
+
openGate: isOpenGateString
|
|
25271
|
+
});
|
|
25145
25272
|
try {
|
|
25146
25273
|
const response = await useDahuaDigest({
|
|
25147
25274
|
host: value.host,
|
|
@@ -25567,7 +25694,7 @@ function useSiteCameraRepo() {
|
|
|
25567
25694
|
}
|
|
25568
25695
|
if (result?._id) {
|
|
25569
25696
|
const result2 = await findOne({ _id: new import_mongodb47.ObjectId(result._id) }, void 0, { session });
|
|
25570
|
-
|
|
25697
|
+
import_node_server_utils74.logger.info(`Site camera ${result2?._id} re-read after update; reconnecting listener`);
|
|
25571
25698
|
if (result2?._id) {
|
|
25572
25699
|
const { listenToCamera } = useDahuaService();
|
|
25573
25700
|
await listenToCamera(result2);
|
|
@@ -27857,7 +27984,9 @@ function useVehicleService() {
|
|
|
27857
27984
|
siteCameras.push(...siteCameraReq.items);
|
|
27858
27985
|
page++;
|
|
27859
27986
|
} while (page <= pages);
|
|
27860
|
-
|
|
27987
|
+
import_node_server_utils79.logger.info(
|
|
27988
|
+
`addVehicle resolved ${siteCameras.length} site camera(s) for site ${value?.site}`
|
|
27989
|
+
);
|
|
27861
27990
|
}
|
|
27862
27991
|
for (const plateNumber of plateNumbers) {
|
|
27863
27992
|
const vehicleValue = {
|
|
@@ -30163,6 +30292,20 @@ function useBuildingLevelRepo() {
|
|
|
30163
30292
|
}
|
|
30164
30293
|
}
|
|
30165
30294
|
}
|
|
30295
|
+
async function getActiveLevelsByIds(ids, session) {
|
|
30296
|
+
if (!ids || ids.length === 0)
|
|
30297
|
+
return [];
|
|
30298
|
+
let objectIds;
|
|
30299
|
+
try {
|
|
30300
|
+
objectIds = ids.map((id) => new import_mongodb56.ObjectId(id));
|
|
30301
|
+
} catch {
|
|
30302
|
+
throw new import_node_server_utils88.BadRequestError("Invalid level ID format.");
|
|
30303
|
+
}
|
|
30304
|
+
return collection.find(
|
|
30305
|
+
{ _id: { $in: objectIds }, status: "active" /* ACTIVE */ },
|
|
30306
|
+
{ projection: { _id: 1, name: 1 }, session }
|
|
30307
|
+
).toArray();
|
|
30308
|
+
}
|
|
30166
30309
|
async function updateLevelById(_id, value, session) {
|
|
30167
30310
|
try {
|
|
30168
30311
|
_id = new import_mongodb56.ObjectId(_id);
|
|
@@ -30361,7 +30504,8 @@ function useBuildingLevelRepo() {
|
|
|
30361
30504
|
deleteById,
|
|
30362
30505
|
bulkWriteLevels,
|
|
30363
30506
|
batchUpdateByIds,
|
|
30364
|
-
getBuildingLevelList
|
|
30507
|
+
getBuildingLevelList,
|
|
30508
|
+
getActiveLevelsByIds
|
|
30365
30509
|
};
|
|
30366
30510
|
}
|
|
30367
30511
|
|
|
@@ -30382,7 +30526,10 @@ function useBuildingService() {
|
|
|
30382
30526
|
} = useBuildingUnitRepo();
|
|
30383
30527
|
const { updateStatusById } = useFileRepo();
|
|
30384
30528
|
const { deleteFile } = useFileService();
|
|
30385
|
-
const {
|
|
30529
|
+
const {
|
|
30530
|
+
bulkWriteLevels: _bulkWriteLevels,
|
|
30531
|
+
getActiveLevelsByIds: _getActiveLevelsByIds
|
|
30532
|
+
} = useBuildingLevelRepo();
|
|
30386
30533
|
function normalizeImportKey(value) {
|
|
30387
30534
|
return String(value ?? "").trim().replace(/\s+/g, " ").toLowerCase();
|
|
30388
30535
|
}
|
|
@@ -30476,23 +30623,37 @@ function useBuildingService() {
|
|
|
30476
30623
|
);
|
|
30477
30624
|
}
|
|
30478
30625
|
if (data.levels) {
|
|
30479
|
-
const
|
|
30480
|
-
|
|
30481
|
-
|
|
30482
|
-
|
|
30483
|
-
|
|
30484
|
-
|
|
30485
|
-
|
|
30486
|
-
|
|
30487
|
-
|
|
30488
|
-
|
|
30489
|
-
|
|
30490
|
-
|
|
30491
|
-
|
|
30492
|
-
|
|
30493
|
-
|
|
30494
|
-
|
|
30626
|
+
const submittedNames = data.levels ?? [];
|
|
30627
|
+
const existingLevels = await _getActiveLevelsByIds(
|
|
30628
|
+
building.levels,
|
|
30629
|
+
session
|
|
30630
|
+
);
|
|
30631
|
+
const seenKeys = new Set(
|
|
30632
|
+
existingLevels.map((level) => normalizeImportKey(level.name))
|
|
30633
|
+
);
|
|
30634
|
+
const newNames = submittedNames.filter((name) => {
|
|
30635
|
+
const key = normalizeImportKey(name);
|
|
30636
|
+
if (seenKeys.has(key))
|
|
30637
|
+
return false;
|
|
30638
|
+
seenKeys.add(key);
|
|
30639
|
+
return true;
|
|
30640
|
+
});
|
|
30641
|
+
if (newNames.length > 0) {
|
|
30642
|
+
const levelsPayload = newNames.map((name) => ({
|
|
30643
|
+
blockId: id,
|
|
30644
|
+
site: building.site.toString(),
|
|
30645
|
+
name,
|
|
30646
|
+
status: "active" /* ACTIVE */,
|
|
30647
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
30648
|
+
updatedAt: null,
|
|
30649
|
+
deletedAt: null
|
|
30650
|
+
}));
|
|
30651
|
+
const levels = await _bulkWriteLevels(levelsPayload, session);
|
|
30652
|
+
if (levels) {
|
|
30653
|
+
building.levels.push(...Object.values(levels.insertedIds));
|
|
30654
|
+
}
|
|
30495
30655
|
}
|
|
30656
|
+
data.levels = building.levels.map((levelId) => levelId.toString());
|
|
30496
30657
|
}
|
|
30497
30658
|
const buildingUnitData = {};
|
|
30498
30659
|
if (data.name && building.name !== data.name) {
|