@7365admin1/core 3.79.1-staging.335 → 3.79.1-staging.337

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.
@@ -35,6 +35,12 @@ and moves it to the form's tab (off every camera list, onto the list of its new
35
35
  kind; pending for a security agency without the approve permission). Its
36
36
  history records "moved".
37
37
 
38
+ **Sync.** `syncVehicleToCameras` puts a live vehicle's plate on every active
39
+ camera of its list that lacks it (a plate saved while its cameras were
40
+ inactive), camera by camera, and records the cameras that took it;
41
+ `activeCameraCounts` gives the active cameras per list so the screens can offer
42
+ it.
43
+
38
44
  **One plate per site.** Adding a plate the site already has - deleted,
39
45
  pending, temporary, blocked or unblocked included - is refused with its status
40
46
  and "edit the existing vehicle instead" (restore, for a deleted one). The same
package/dist/index.d.ts CHANGED
@@ -4123,6 +4123,15 @@ declare function useVehicleService(): {
4123
4123
  view: string;
4124
4124
  }[]>;
4125
4125
  moveVehicle: (_id: string, value: TVehicle, user?: string) => Promise<string>;
4126
+ activeCameraCounts: (site: string) => Promise<{
4127
+ allowed: number;
4128
+ blocked: number;
4129
+ }>;
4130
+ syncVehicleToCameras: (_id: string, user?: string) => Promise<{
4131
+ message: string;
4132
+ synced: number;
4133
+ failed: number;
4134
+ }>;
4126
4135
  fillCameraLists: (site: unknown, camera: {
4127
4136
  id?: string;
4128
4137
  host?: string;
@@ -4188,6 +4197,8 @@ declare function useVehicleController(): {
4188
4197
  removeFromBlocklist: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4189
4198
  existingPlates: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4190
4199
  moveVehicle: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4200
+ activeCameraCounts: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4201
+ syncVehicleToCameras: (req: Request, res: Response, next: NextFunction) => Promise<void>;
4191
4202
  };
4192
4203
 
4193
4204
  declare function useSiteCameraRepo(): {
package/dist/index.js CHANGED
@@ -18525,6 +18525,7 @@ function useVehicleRepo() {
18525
18525
  seasonPassType: 1,
18526
18526
  start: 1,
18527
18527
  end: 1,
18528
+ remarks: 1,
18528
18529
  nric: 1,
18529
18530
  plateNumber: 1,
18530
18531
  site: 1,
@@ -18571,7 +18572,9 @@ function useVehicleRepo() {
18571
18572
  // A season pass's expiry, shown under its number.
18572
18573
  seasonPassType: "$seasonPassType",
18573
18574
  start: "$start",
18574
- end: "$end"
18575
+ end: "$end",
18576
+ // Each record's own remarks (e.g. why it was blocked).
18577
+ remarks: "$remarks"
18575
18578
  }
18576
18579
  }
18577
18580
  }
@@ -35160,25 +35163,44 @@ function useVehicleService() {
35160
35163
  }
35161
35164
  return failures;
35162
35165
  }
35166
+ const CAMERA_WRITE_ROUNDS = 3;
35167
+ const CAMERA_WRITE_RETRY_DELAY_MS = 3e3;
35163
35168
  async function writeToCameras(cameras, write, journal, options = {}) {
35164
35169
  const failures = [];
35165
35170
  let recNo = "";
35166
35171
  for (const camera of cameras) {
35167
- try {
35168
- if (options.onlyIfMissing) {
35169
- const { inserted } = await ensurePlateOnCamera(camera, write);
35170
- if (inserted)
35172
+ for (let round = 1; ; round++) {
35173
+ try {
35174
+ if (options.onlyIfMissing || round > 1) {
35175
+ const { inserted } = await ensurePlateOnCamera(camera, write);
35176
+ if (inserted)
35177
+ journal.inserted(camera, write.mode, write.plateNumber);
35178
+ } else {
35179
+ recNo = await insertPlateOnCamera(camera, write) || recNo;
35171
35180
  journal.inserted(camera, write.mode, write.plateNumber);
35172
- } else {
35173
- recNo = await insertPlateOnCamera(camera, write) || recNo;
35174
- journal.inserted(camera, write.mode, write.plateNumber);
35181
+ }
35182
+ if (round > 1) {
35183
+ import_node_server_utils109.logger.info(
35184
+ `[${cameraLabel(camera)}] write ${write.mode} went through on round ${round}`
35185
+ );
35186
+ }
35187
+ break;
35188
+ } catch (error) {
35189
+ const transient = classifyDahuaFailure(error?.message ?? error) === "transient";
35190
+ if (transient && round < CAMERA_WRITE_ROUNDS) {
35191
+ logCameraFailure(`write ${write.mode} (round ${round}, retrying)`, camera, error);
35192
+ await new Promise(
35193
+ (resolve) => setTimeout(resolve, CAMERA_WRITE_RETRY_DELAY_MS * round)
35194
+ );
35195
+ continue;
35196
+ }
35197
+ logCameraFailure(`write ${write.mode}`, camera, error);
35198
+ failures.push({
35199
+ camera: cameraLabel(camera),
35200
+ reason: anprWriteReason(error)
35201
+ });
35202
+ break;
35175
35203
  }
35176
- } catch (error) {
35177
- logCameraFailure(`write ${write.mode}`, camera, error);
35178
- failures.push({
35179
- camera: cameraLabel(camera),
35180
- reason: anprWriteReason(error)
35181
- });
35182
35204
  }
35183
35205
  }
35184
35206
  return { failures, recNo };
@@ -35565,6 +35587,9 @@ function useVehicleService() {
35565
35587
  }
35566
35588
  const _plateNumber = String(vehicle.plateNumber ?? "");
35567
35589
  const owner = vehicle.name;
35590
+ const approveOutcome = ["deleted", "inactive", "rejected"].includes(
35591
+ String(vehicle.status ?? "")
35592
+ ) ? "restored" : "approved";
35568
35593
  const isBlocklist = vehicle.type === "blocklist" /* BLOCKLIST */;
35569
35594
  const mode = listMode(vehicle.type);
35570
35595
  const hasStart = typeof vehicle.start === "string" ? vehicle.start.trim() !== "" : !!vehicle.start;
@@ -35593,7 +35618,7 @@ function useVehicleService() {
35593
35618
  const cameraSite = vehicle.site ?? siteId;
35594
35619
  const siteCameras = isBlocklist ? await blockCamerasAt(cameraSite) : await allowCamerasAt(cameraSite);
35595
35620
  if (!siteCameras.length) {
35596
- throw new import_node_server_utils109.BadRequestError(anprNoCameraRefusal("approved"));
35621
+ throw new import_node_server_utils109.BadRequestError(anprNoCameraRefusal(approveOutcome));
35597
35622
  }
35598
35623
  const { failures } = await writeToCameras(
35599
35624
  siteCameras,
@@ -35611,7 +35636,7 @@ function useVehicleService() {
35611
35636
  cameraFailureSentence({
35612
35637
  failures,
35613
35638
  operation: "added to",
35614
- outcome: "approved",
35639
+ outcome: approveOutcome,
35615
35640
  consequence: isBlocklist ? "it would still open the barrier" : "it would not open the barrier"
35616
35641
  })
35617
35642
  );
@@ -35722,7 +35747,11 @@ function useVehicleService() {
35722
35747
  const ownEnd = validDate(vehicle.end);
35723
35748
  let startDate;
35724
35749
  let endDate;
35725
- if (ownEnd) {
35750
+ if (isBlocklist) {
35751
+ const block = blockWindow();
35752
+ startDate = new Date(block.start);
35753
+ endDate = new Date(block.end);
35754
+ } else if (ownEnd) {
35726
35755
  if (ownEnd.getTime() <= Date.now()) {
35727
35756
  throw new import_node_server_utils109.BadRequestError(
35728
35757
  `This vehicle's expiry date (${dahuaWallClock(
@@ -36128,6 +36157,76 @@ function useVehicleService() {
36128
36157
  throw error;
36129
36158
  }
36130
36159
  }
36160
+ async function activeCameraCounts(site) {
36161
+ const [allowed, blocked] = await Promise.all([
36162
+ allowCamerasAt(site),
36163
+ blockCamerasAt(site)
36164
+ ]);
36165
+ return { allowed: allowed.length, blocked: blocked.length };
36166
+ }
36167
+ async function syncVehicleToCameras(_id, user) {
36168
+ const vehicle = await findOne({ _id: new import_mongodb76.ObjectId(_id) });
36169
+ if (!vehicle)
36170
+ throw new import_node_server_utils109.NotFoundError("Vehicle not found.");
36171
+ if (!isLiveVehicleStatus(vehicle.status)) {
36172
+ throw new import_node_server_utils109.BadRequestError(
36173
+ `This vehicle is ${String(vehicle.status || "not live")}, so it is not put on any camera.`
36174
+ );
36175
+ }
36176
+ const blocked = vehicle.type === "blocklist" /* BLOCKLIST */;
36177
+ const cameras = blocked ? await blockCamerasAt(vehicle.site) : await allowCamerasAt(vehicle.site);
36178
+ if (!cameras.length) {
36179
+ throw new import_node_server_utils109.BadRequestError(
36180
+ "There is no active ANPR camera to sync to. Switch a camera on first."
36181
+ );
36182
+ }
36183
+ const plateNumber = String(vehicle.plateNumber ?? "");
36184
+ const write = {
36185
+ plateNumber,
36186
+ mode: blocked ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */,
36187
+ owner: String(vehicle.name ?? ""),
36188
+ ...blocked ? blockWindow() : cameraWindow(vehicle.start, vehicle.end)
36189
+ };
36190
+ const journal = cameraJournal();
36191
+ const took = [];
36192
+ const failures = [];
36193
+ for (const camera of cameras) {
36194
+ const result = await writeToCameras([camera], write, journal, {
36195
+ onlyIfMissing: true
36196
+ });
36197
+ if (result.failures.length)
36198
+ failures.push(...result.failures);
36199
+ else
36200
+ took.push(camera);
36201
+ }
36202
+ const hostKey = (value) => String(value ?? "").trim().toLowerCase().replace(/\/+$/, "");
36203
+ const tookHosts = new Set(took.map((camera) => hostKey(camera.host)));
36204
+ const current = Array.isArray(vehicle.anprCameras) ? vehicle.anprCameras : [];
36205
+ try {
36206
+ await _setAnprCameras(_id, [
36207
+ ...current.filter((entry) => !tookHosts.has(hostKey(entry.host))),
36208
+ ...anprCameraSummary(took, blocked ? "blocked" : "allowed")
36209
+ ]);
36210
+ if (took.length) {
36211
+ await _pushHistory(_id, vehicleHistoryEntry("synced", user, ["anprCameras"]));
36212
+ }
36213
+ } catch (error) {
36214
+ import_node_server_utils109.logger.error(`syncVehicleToCameras could not record ${_id}: ${error?.message ?? error}`);
36215
+ }
36216
+ const list = blocked ? "blocklist" : "allowed list";
36217
+ if (!failures.length) {
36218
+ return {
36219
+ message: `Vehicle ${plateNumber} is on the ${list} of all ${took.length} active camera${took.length === 1 ? "" : "s"}.`,
36220
+ synced: took.length,
36221
+ failed: 0
36222
+ };
36223
+ }
36224
+ return {
36225
+ message: `Vehicle ${plateNumber} was synced to ${took.length} of ${cameras.length} active cameras. Not synced: ${failures.map((f) => `${f.camera} (${f.reason})`).join("; ")}.`,
36226
+ synced: took.length,
36227
+ failed: failures.length
36228
+ };
36229
+ }
36131
36230
  async function existingPlatesAtSite(site, plateNumbers) {
36132
36231
  const seen = /* @__PURE__ */ new Set();
36133
36232
  const items = [];
@@ -36253,12 +36352,12 @@ function useVehicleService() {
36253
36352
  ...value.phoneNumber !== void 0 && { phoneNumber: value.phoneNumber },
36254
36353
  ...value.nric !== void 0 && { nric: value.nric },
36255
36354
  ...value.remarks !== void 0 && { remarks: value.remarks },
36256
- ...toBlocklist ? {} : {
36257
- ...value.block && { block: asId(value.block) },
36258
- ...value.level && { level: asId(value.level) },
36259
- ...value.unit && { unit: asId(value.unit) },
36260
- ...value.peopleId && { peopleId: asId(value.peopleId) }
36261
- }
36355
+ // Block / level / unit whenever the form sends them - a Block Listed
36356
+ // vehicle blocked from a visitor transaction brings the visit's.
36357
+ ...value.block && { block: asId(value.block) },
36358
+ ...value.level && { level: asId(value.level) },
36359
+ ...value.unit && { unit: asId(value.unit) },
36360
+ ...toBlocklist ? {} : { ...value.peopleId && { peopleId: asId(value.peopleId) } }
36262
36361
  };
36263
36362
  const changed = changedFields(vehicle, formattedValue);
36264
36363
  await _updateVehicleById(_id, formattedValue, session);
@@ -36564,6 +36663,8 @@ function useVehicleService() {
36564
36663
  removeFromBlocklist,
36565
36664
  existingPlatesAtSite,
36566
36665
  moveVehicle,
36666
+ activeCameraCounts,
36667
+ syncVehicleToCameras,
36567
36668
  fillCameraLists,
36568
36669
  newCameraPlateRows,
36569
36670
  writeVehicleToNewCamera
@@ -40139,7 +40240,9 @@ function useVehicleController() {
40139
40240
  blocklistVehicles: _blocklistVehicles,
40140
40241
  removeFromBlocklist: _removeFromBlocklist,
40141
40242
  existingPlatesAtSite: _existingPlatesAtSite,
40142
- moveVehicle: _moveVehicle
40243
+ moveVehicle: _moveVehicle,
40244
+ activeCameraCounts: _activeCameraCounts,
40245
+ syncVehicleToCameras: _syncVehicleToCameras
40143
40246
  } = useVehicleService();
40144
40247
  const {
40145
40248
  getSeasonPassTypes: _getSeasonPassTypes,
@@ -40168,6 +40271,10 @@ function useVehicleController() {
40168
40271
  message: "You do not have permission to view vehicles."
40169
40272
  };
40170
40273
  const VEHICLE_DETAILS = {
40274
+ permissions: ["vehicle-mgmt:see-vehicle-details"],
40275
+ message: "You do not have permission to view vehicle details."
40276
+ };
40277
+ const VEHICLE_EDIT_READ = {
40171
40278
  permissions: [
40172
40279
  "vehicle-mgmt:see-vehicle-details",
40173
40280
  "vehicle-mgmt:update-vehicle"
@@ -40574,7 +40681,7 @@ function useVehicleController() {
40574
40681
  }
40575
40682
  const { _id } = value;
40576
40683
  try {
40577
- await requireVehicleReach(req, _id, VEHICLE_DETAILS);
40684
+ await requireVehicleReach(req, _id, VEHICLE_EDIT_READ);
40578
40685
  const site = await _getSpecificVehicleById(_id);
40579
40686
  res.json(site);
40580
40687
  return;
@@ -40970,6 +41077,42 @@ function useVehicleController() {
40970
41077
  return;
40971
41078
  }
40972
41079
  }
41080
+ async function activeCameraCounts(req, res, next) {
41081
+ const { error, value } = import_joi58.default.object({
41082
+ site: import_joi58.default.string().hex().length(24).required()
41083
+ }).validate({ site: req.params.site });
41084
+ if (error) {
41085
+ next(new import_node_server_utils123.BadRequestError(error.message));
41086
+ return;
41087
+ }
41088
+ try {
41089
+ await requireSiteReach(req, value.site, VEHICLE_LIST);
41090
+ res.json(await _activeCameraCounts(value.site));
41091
+ return;
41092
+ } catch (error2) {
41093
+ import_node_server_utils123.logger.log({ level: "error", message: error2.message });
41094
+ next(error2);
41095
+ return;
41096
+ }
41097
+ }
41098
+ async function syncVehicleToCameras(req, res, next) {
41099
+ const { error, value } = import_joi58.default.object({
41100
+ _id: import_joi58.default.string().hex().length(24).required()
41101
+ }).validate({ _id: req.params.id });
41102
+ if (error) {
41103
+ next(new import_node_server_utils123.BadRequestError(error.message));
41104
+ return;
41105
+ }
41106
+ try {
41107
+ await requireVehicleReach(req, value._id, VEHICLE_UPDATE);
41108
+ res.json(await _syncVehicleToCameras(value._id, actingUser(req)));
41109
+ return;
41110
+ } catch (error2) {
41111
+ import_node_server_utils123.logger.log({ level: "error", message: error2.message });
41112
+ next(error2);
41113
+ return;
41114
+ }
41115
+ }
40973
41116
  return {
40974
41117
  add,
40975
41118
  addBulk,
@@ -40988,7 +41131,9 @@ function useVehicleController() {
40988
41131
  blocklistVehicles,
40989
41132
  removeFromBlocklist,
40990
41133
  existingPlates,
40991
- moveVehicle
41134
+ moveVehicle,
41135
+ activeCameraCounts,
41136
+ syncVehicleToCameras
40992
41137
  };
40993
41138
  }
40994
41139