@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 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
- for (let attempt = 1; attempt <= retries; attempt++) {
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 < retries) {
24404
+ if (shouldRetry && attempt < effectiveRetries) {
24325
24405
  loggerDahua.warn(
24326
- `[${host}] Dahua Invalid Authority. Retrying ${attempt}/${retries}`
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 < retries) {
24418
+ if (shouldRetry && attempt < effectiveRetries) {
24339
24419
  loggerDahua.warn(
24340
- `[${host}] Dahua Invalid Authority error. Retrying ${attempt}/${retries}`
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
- console.log("checkOutBySiteAndPlate result", result);
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
- console.log(`getTrafficJunction camera object`, camera);
24825
+ loggerDahua.info(
24826
+ `getTrafficJunction starting for camera ${camera?._id} [${camera?.siteName}-${camera?.direction}] host ${camera?.host}`
24827
+ );
24744
24828
  let authFailureCount = 0;
24745
- const MAX_AUTH_RETRIES = 10;
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
- if (statusCode === 401) {
24761
- loggerDahua.error(`[${camera?.siteName}-${camera?.direction}] 401 Unauthorized - Handshake or Wrong Credentials`);
24762
- console.log(`[${camera?.siteName}-${camera?.direction}] 401 Unauthorized - Handshake or Wrong Credentials`);
24763
- throw new Error("401 Unauthorized - Handshake or Wrong Credentials");
24764
- } else if (statusCode === 403) {
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. Account locked or invalid permissions.`
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
- messagePermanent: `Camera [${camera?.siteName}-${camera?.direction}]. Connection Error. Check username and password and try after 10 minutes.`
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 ([400, 500].includes(statusCode)) {
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: `Camera [${camera?.siteName}-${camera?.direction}]. Connection Error. Possible Not Authorized.` });
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
- if (errMsg.includes("403") || errMsg.includes("Forbidden")) {
24932
+ const failure = classifyDahuaFailure(errMsg);
24933
+ if (failure === "rejected") {
24831
24934
  loggerDahua.error(
24832
- `[${camera?.siteName}-${camera?.direction}] 403 Forbidden thrown by Dahua client. Account locked or invalid credentials.`
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: `Camera [${camera?.siteName}-${camera?.direction}]. Connection Error. Check username and password and try after 10 minutes.`
24941
+ messagePermanent: cameraCredentialsMessage(camera)
24839
24942
  });
24840
24943
  }
24841
24944
  return;
24842
24945
  }
24843
- const isAuthError = errMsg.includes("401") || errMsg.includes("Unauthorized") || errMsg.includes("Invalid Authority");
24844
- if (isAuthError) {
24946
+ if (failure === "challenge") {
24845
24947
  authFailureCount++;
24846
24948
  isAuthFailure = true;
24847
24949
  loggerDahua.warn(
24848
- `[${camera?.siteName}-${camera?.direction}] Auth attempt ${authFailureCount}/${MAX_AUTH_RETRIES}`
24950
+ `[${camera?.siteName}-${camera?.direction}] Authentication attempt ${authFailureCount} failed; next attempt in ${dahuaAuthBackoffMs(authFailureCount) / 1e3}s`
24849
24951
  );
24850
- if (authFailureCount >= MAX_AUTH_RETRIES) {
24952
+ if (authFailureCount === DAHUA_AUTH_ATTEMPTS_BEFORE_ALERT) {
24851
24953
  loggerDahua.error(
24852
- `[${camera?.siteName}-${camera?.direction}] Max auth failures reached (${MAX_AUTH_RETRIES}). Wrong credentials.`
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: `Camera [${camera?.siteName}-${camera?.direction}]. Connection Error. Check username and password.`
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
- message: `Camera in [${camera?.siteName}-${camera?.direction}] Connection Lost. Retrying in 10 seconds. Check the camera if this issue persists, Or set the camera to Inactive to stop this notification.`
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 ? 1e3 : 1e4;
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
- console.log("addPlateNumber called with value:", value);
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 = `/cgi-bin/recordUpdater.cgi?action=update&name=${value.mode}&recno=${value.recno}&PlateNumber=${value.plateNumber}&BeginTime=${value.start}&CancelTime=${value.end}&+OpenGate=${_isOpenGate}&MasterOfCar=${value.owner}`;
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: `/cgi-bin/recordUpdater.cgi?action=remove&recno=${value.recno}&name=${value.mode}`
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 = `/cgi-bin/recordUpdater.cgi?action=insert&name=${value.mode}&PlateNumber=${value.plateNumber}&VehicleType=${value.vehicleType}&VehicleColor=${value.vehicleColor}&BeginTime=${value.start}&CancelTime=${value.end}&+OpenGate=${isOpenGateString}&MasterOfCar=${value.owner}`;
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
- console.log("updateById result2", result2);
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
- console.log("add vehicle service siteCameras", siteCameras);
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 { bulkWriteLevels: _bulkWriteLevels } = useBuildingLevelRepo();
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 levelsPayload = (data.levels ?? []).map((levelName) => ({
30480
- blockId: id,
30481
- site: building.site.toString(),
30482
- name: levelName,
30483
- status: "active" /* ACTIVE */,
30484
- createdAt: /* @__PURE__ */ new Date(),
30485
- updatedAt: null,
30486
- deletedAt: null
30487
- }));
30488
- let levels;
30489
- if (levelsPayload.length > 0) {
30490
- levels = await _bulkWriteLevels(levelsPayload, session);
30491
- }
30492
- if (levels) {
30493
- building.levels.push(...Object.values(levels.insertedIds));
30494
- data.levels = building.levels.map((id2) => id2.toString());
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) {