@7365admin1/core 3.52.4 → 3.52.5-staging.250
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/.changeset/expired-vehicle-cron-per-site-fail-closed.md +27 -0
- package/.changeset/visitor-dahua-cron-fail-closed.md +27 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +183 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -83
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/expired-vehicle-sweep.test.mjs +175 -0
- package/test/visitor-dahua-sweep.test.mjs +191 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Revoke each expired vehicle's plate only from its own site's cameras, and fail closed
|
|
6
|
+
|
|
7
|
+
The hourly expired-vehicle sweep declared its `siteCameras` array outside the
|
|
8
|
+
per-vehicle loop and only ever pushed to it, so the list grew as the run went
|
|
9
|
+
on: the second vehicle was revoked from its own site's cameras AND from the
|
|
10
|
+
first vehicle's. `recNo` is a per-device sequence number, so those extra calls
|
|
11
|
+
did not simply miss - they addressed whatever record carried that number on the
|
|
12
|
+
other site's camera and took an unrelated, still-valid plate off the barrier.
|
|
13
|
+
Cameras are now resolved per vehicle, cached by site id so each site is still
|
|
14
|
+
paged only once per run.
|
|
15
|
+
|
|
16
|
+
The sweep also marked every expired vehicle deleted with one blanket
|
|
17
|
+
`updateMany`, regardless of what the cameras answered, so a record could read
|
|
18
|
+
"deleted" while its plate still opened the gate. Only vehicles whose every
|
|
19
|
+
camera confirmed are now marked deleted; the rest are left expired, logged at
|
|
20
|
+
error level naming the vehicle, plate, site and camera, and retried on the next
|
|
21
|
+
hourly run. `deleteExpiredVehicles` now requires the list of ids.
|
|
22
|
+
|
|
23
|
+
Also: each camera is asked for its own record id before the removal (the same
|
|
24
|
+
thing manual delete does), the ANPR list is chosen from the vehicle's type
|
|
25
|
+
instead of always TRAFFIC_REDLIST, a site with no ANPR camera no longer aborts
|
|
26
|
+
the whole run, and the transaction that wrapped nothing - the only write was
|
|
27
|
+
issued without the session - has been removed.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fail closed in the visitor ANPR sweep, and remove the dead invitation transaction
|
|
6
|
+
|
|
7
|
+
`processTransactionDahuaStatus`, the hourly job that takes an expired or
|
|
8
|
+
checked-out visitor's plate off the site's ANPR camera, marked the transaction
|
|
9
|
+
`dahuaSyncStatus: "removed"` on the line after the camera call and only skipped
|
|
10
|
+
on a thrown error. `removePlateNumber` does not throw for a device failure - it
|
|
11
|
+
returns an outcome object - so a camera answering 401 or 500, or not answering
|
|
12
|
+
at all, still had the transaction recorded as removed while the visitor's plate
|
|
13
|
+
was still on the barrier. Nothing retried it either, because the query that
|
|
14
|
+
feeds the sweep skips anything already marked removed.
|
|
15
|
+
|
|
16
|
+
It now uses the same decision logic as the expired-vehicle sweep: only a
|
|
17
|
+
transaction every camera confirmed is marked removed, and the rest are logged at
|
|
18
|
+
error level naming the plate, the site and the camera, then retried on the next
|
|
19
|
+
run. Each camera is also asked for its own record id before the removal, which
|
|
20
|
+
is what the old "not found" message sniff was reaching for and what stops a
|
|
21
|
+
transaction whose record is already gone from being retried for ever.
|
|
22
|
+
|
|
23
|
+
`checkExpiredInvitation` opened a session and a transaction and then issued
|
|
24
|
+
every write without that session, never committing: the transaction protected no
|
|
25
|
+
write and the rollback rolled back nothing. Each invitation is independent, so
|
|
26
|
+
there is no invariant spanning them and nothing for a transaction to protect -
|
|
27
|
+
it is removed rather than wired up.
|
package/dist/index.d.ts
CHANGED
|
@@ -2818,7 +2818,7 @@ declare function useVehicleRepo(): {
|
|
|
2818
2818
|
pages: number;
|
|
2819
2819
|
pageRange: string;
|
|
2820
2820
|
}>;
|
|
2821
|
-
deleteExpiredVehicles: (session?: ClientSession) => Promise<number>;
|
|
2821
|
+
deleteExpiredVehicles: (ids: Array<string | ObjectId>, session?: ClientSession) => Promise<number>;
|
|
2822
2822
|
getAllVehiclesByUnitId: ({ unitId, page, limit, sort, }: {
|
|
2823
2823
|
unitId: string | ObjectId;
|
|
2824
2824
|
page?: number | undefined;
|
package/dist/index.js
CHANGED
|
@@ -16202,27 +16202,21 @@ function useVerificationService() {
|
|
|
16202
16202
|
}
|
|
16203
16203
|
}
|
|
16204
16204
|
async function checkExpiredInvitation() {
|
|
16205
|
-
const session = import_node_server_utils34.useAtlas.getClient()?.startSession();
|
|
16206
|
-
session?.startTransaction();
|
|
16207
16205
|
try {
|
|
16208
16206
|
const verifications = await _getByStatus("pending");
|
|
16207
|
+
const now = Date.now();
|
|
16209
16208
|
for (const verification of verifications) {
|
|
16210
|
-
|
|
16211
|
-
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
16212
|
-
if (now > expiration) {
|
|
16209
|
+
if (now > new Date(verification.expireAt).getTime()) {
|
|
16213
16210
|
await _updateStatusById(verification._id.toString(), "expired");
|
|
16214
16211
|
}
|
|
16215
16212
|
}
|
|
16216
16213
|
return "Successfully checked for expired invitations.";
|
|
16217
16214
|
} catch (error) {
|
|
16218
|
-
await session?.abortTransaction();
|
|
16219
16215
|
import_node_server_utils34.logger.log({
|
|
16220
16216
|
level: "info",
|
|
16221
16217
|
message: `Error checking expired user invitation: ${error}`
|
|
16222
16218
|
});
|
|
16223
16219
|
throw error;
|
|
16224
|
-
} finally {
|
|
16225
|
-
session?.endSession();
|
|
16226
16220
|
}
|
|
16227
16221
|
}
|
|
16228
16222
|
return {
|
|
@@ -22041,11 +22035,15 @@ function useVehicleRepo() {
|
|
|
22041
22035
|
throw error;
|
|
22042
22036
|
}
|
|
22043
22037
|
}
|
|
22044
|
-
async function deleteExpiredVehicles(session) {
|
|
22038
|
+
async function deleteExpiredVehicles(ids2, session) {
|
|
22045
22039
|
try {
|
|
22046
22040
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22041
|
+
const _ids = (ids2 ?? []).map((id) => (0, import_node_server_utils50.toObjectId)(id)).filter((id) => Boolean(id));
|
|
22042
|
+
if (!_ids.length)
|
|
22043
|
+
return 0;
|
|
22047
22044
|
const res = await collection.updateMany(
|
|
22048
22045
|
{
|
|
22046
|
+
_id: { $in: _ids },
|
|
22049
22047
|
status: { $ne: "deleted" },
|
|
22050
22048
|
end: { $exists: true, $lte: now }
|
|
22051
22049
|
// check only end
|
|
@@ -22528,6 +22526,10 @@ function anprRevokeThrew(error) {
|
|
|
22528
22526
|
detail: message || "the request threw with no message"
|
|
22529
22527
|
};
|
|
22530
22528
|
}
|
|
22529
|
+
var ANPR_NOTHING_TO_REVOKE = {
|
|
22530
|
+
ok: true,
|
|
22531
|
+
reason: ""
|
|
22532
|
+
};
|
|
22531
22533
|
function cameraFailureSentence(options) {
|
|
22532
22534
|
const { failures, operation, outcome, consequence } = options;
|
|
22533
22535
|
return `The plate number could not be ${operation} ${cameraCount(failures.length)}: ${cameraDetail(failures)}. The vehicle has NOT been ${outcome} \u2014 ${consequence}. Please check the ${failures.length === 1 ? "camera" : "cameras"} and try again.`;
|
|
@@ -23340,7 +23342,7 @@ function useDahuaService() {
|
|
|
23340
23342
|
}
|
|
23341
23343
|
try {
|
|
23342
23344
|
value.owner = String(value.owner ?? "").replace(/['"]/g, "").replace(/[\/\\]/g, " - ").substring(0, 15).trim() || "unknown";
|
|
23343
|
-
const
|
|
23345
|
+
const formatDahuaDate3 = (dateStr, fallbackYearsAhead = 0) => {
|
|
23344
23346
|
const date = dateStr ? new Date(dateStr) : /* @__PURE__ */ new Date();
|
|
23345
23347
|
if (!dateStr) {
|
|
23346
23348
|
date.setMinutes(date.getMinutes() - 10);
|
|
@@ -23351,8 +23353,8 @@ function useDahuaService() {
|
|
|
23351
23353
|
const pad = (num) => String(num).padStart(2, "0");
|
|
23352
23354
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
23353
23355
|
};
|
|
23354
|
-
const formattedStart =
|
|
23355
|
-
const formattedEnd =
|
|
23356
|
+
const formattedStart = formatDahuaDate3(value.start);
|
|
23357
|
+
const formattedEnd = formatDahuaDate3(value.end, 10);
|
|
23356
23358
|
const beginTime = encodeURIComponent(formattedStart);
|
|
23357
23359
|
const cancelTime = encodeURIComponent(formattedEnd);
|
|
23358
23360
|
const plateNumber = encodeURIComponent(value.plateNumber);
|
|
@@ -35317,6 +35319,70 @@ function useBuildingUnitRepo() {
|
|
|
35317
35319
|
};
|
|
35318
35320
|
}
|
|
35319
35321
|
|
|
35322
|
+
// src/utils/expired-vehicle-sweep.util.ts
|
|
35323
|
+
async function sweepExpiredVehicles(vehicles, deps) {
|
|
35324
|
+
const { camerasForSite, revoke, cameraLabel: cameraLabel2 } = deps;
|
|
35325
|
+
const deletableIds = [];
|
|
35326
|
+
const skipped = [];
|
|
35327
|
+
const bySite = /* @__PURE__ */ new Map();
|
|
35328
|
+
for (const vehicle of vehicles) {
|
|
35329
|
+
const id = String(vehicle?._id ?? "");
|
|
35330
|
+
const site = String(vehicle?.site ?? "");
|
|
35331
|
+
const plateNumber = String(vehicle?.plateNumber ?? "");
|
|
35332
|
+
if (!site) {
|
|
35333
|
+
skipped.push({
|
|
35334
|
+
id,
|
|
35335
|
+
site,
|
|
35336
|
+
plateNumber,
|
|
35337
|
+
failures: [
|
|
35338
|
+
{ camera: "unknown", reason: "the vehicle has no site on record" }
|
|
35339
|
+
]
|
|
35340
|
+
});
|
|
35341
|
+
continue;
|
|
35342
|
+
}
|
|
35343
|
+
let cameras = bySite.get(site);
|
|
35344
|
+
if (!cameras) {
|
|
35345
|
+
try {
|
|
35346
|
+
cameras = await camerasForSite(site);
|
|
35347
|
+
} catch (error) {
|
|
35348
|
+
skipped.push({
|
|
35349
|
+
id,
|
|
35350
|
+
site,
|
|
35351
|
+
plateNumber,
|
|
35352
|
+
failures: [
|
|
35353
|
+
{
|
|
35354
|
+
camera: "unknown",
|
|
35355
|
+
reason: "the site's cameras could not be listed"
|
|
35356
|
+
}
|
|
35357
|
+
]
|
|
35358
|
+
});
|
|
35359
|
+
continue;
|
|
35360
|
+
}
|
|
35361
|
+
bySite.set(site, cameras);
|
|
35362
|
+
}
|
|
35363
|
+
const failures = [];
|
|
35364
|
+
for (const camera of cameras) {
|
|
35365
|
+
const outcome = await revoke(camera, vehicle);
|
|
35366
|
+
if (!outcome?.ok) {
|
|
35367
|
+
failures.push({
|
|
35368
|
+
camera: cameraLabel2(camera),
|
|
35369
|
+
reason: outcome?.reason || "the camera could not be reached"
|
|
35370
|
+
});
|
|
35371
|
+
}
|
|
35372
|
+
}
|
|
35373
|
+
if (failures.length) {
|
|
35374
|
+
skipped.push({ id, site, plateNumber, failures });
|
|
35375
|
+
continue;
|
|
35376
|
+
}
|
|
35377
|
+
deletableIds.push(id);
|
|
35378
|
+
}
|
|
35379
|
+
return { deletableIds, skipped };
|
|
35380
|
+
}
|
|
35381
|
+
function sweepSkipLine(entry) {
|
|
35382
|
+
const detail = entry.failures.map((failure) => `${failure.camera} (${failure.reason})`).join(", ");
|
|
35383
|
+
return `processDeletingExpiredVehicles NOT deleting vehicle ${entry.id} (plate ${JSON.stringify(entry.plateNumber)}, site ${entry.site}): ${detail}. The plate may still open the barrier, so the record is left expired and will be retried on the next run.`;
|
|
35384
|
+
}
|
|
35385
|
+
|
|
35320
35386
|
// src/services/vehicle.service.ts
|
|
35321
35387
|
function formatDahuaDate(date) {
|
|
35322
35388
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -35793,17 +35859,10 @@ function useVehicleService() {
|
|
|
35793
35859
|
}
|
|
35794
35860
|
}
|
|
35795
35861
|
async function processDeletingExpiredVehicles() {
|
|
35796
|
-
const
|
|
35797
|
-
|
|
35798
|
-
|
|
35799
|
-
|
|
35800
|
-
try {
|
|
35801
|
-
session.startTransaction();
|
|
35802
|
-
const vehicles = await _getAllExpiredVehicles();
|
|
35803
|
-
let siteCameras = [];
|
|
35804
|
-
for (const vehicle of vehicles) {
|
|
35805
|
-
const site = vehicle.site;
|
|
35806
|
-
const recno = vehicle.recNo;
|
|
35862
|
+
const vehicles = await _getAllExpiredVehicles();
|
|
35863
|
+
const { deletableIds, skipped } = await sweepExpiredVehicles(vehicles, {
|
|
35864
|
+
camerasForSite: async (site) => {
|
|
35865
|
+
const cameras = [];
|
|
35807
35866
|
let page = 1;
|
|
35808
35867
|
let pages = 1;
|
|
35809
35868
|
const limit = 20;
|
|
@@ -35811,44 +35870,72 @@ function useVehicleService() {
|
|
|
35811
35870
|
const siteCameraReq = await _getAllSiteCameras({
|
|
35812
35871
|
site,
|
|
35813
35872
|
type: "anpr",
|
|
35814
|
-
direction: ["both", "entry"],
|
|
35815
35873
|
page,
|
|
35816
35874
|
limit
|
|
35817
35875
|
});
|
|
35818
35876
|
pages = siteCameraReq.pages || 1;
|
|
35819
|
-
|
|
35877
|
+
cameras.push(...siteCameraReq.items);
|
|
35820
35878
|
page++;
|
|
35821
35879
|
} while (page <= pages);
|
|
35822
|
-
|
|
35823
|
-
|
|
35880
|
+
return cameras;
|
|
35881
|
+
},
|
|
35882
|
+
revoke: async (camera, vehicle) => {
|
|
35883
|
+
const host = camera.host;
|
|
35884
|
+
const username = camera.username;
|
|
35885
|
+
const password = camera.password;
|
|
35886
|
+
const mode = vehicle?.type !== "whitelist" /* WHITELIST */ ? "TrafficBlackList" /* TRAFFIC_BLACKLIST */ : "TrafficRedList" /* TRAFFIC_REDLIST */;
|
|
35887
|
+
const plateNumber = String(vehicle?.plateNumber ?? "");
|
|
35888
|
+
let recnoForCamera = String(vehicle?.recNo ?? "");
|
|
35889
|
+
if (plateNumber) {
|
|
35890
|
+
try {
|
|
35891
|
+
const found = parseDahuaFind(
|
|
35892
|
+
await _getPlateNumber({
|
|
35893
|
+
host,
|
|
35894
|
+
username,
|
|
35895
|
+
password,
|
|
35896
|
+
mode,
|
|
35897
|
+
plateNumber,
|
|
35898
|
+
requireOk: true
|
|
35899
|
+
}) ?? ""
|
|
35900
|
+
);
|
|
35901
|
+
if (!found.exists)
|
|
35902
|
+
return ANPR_NOTHING_TO_REVOKE;
|
|
35903
|
+
if (found.recNo)
|
|
35904
|
+
recnoForCamera = found.recNo;
|
|
35905
|
+
} catch (error) {
|
|
35906
|
+
const outcome2 = anprRevokeThrew(error);
|
|
35907
|
+
logCameraFailure("expiredVehicle lookup", camera, outcome2.detail);
|
|
35908
|
+
return outcome2;
|
|
35909
|
+
}
|
|
35824
35910
|
}
|
|
35825
|
-
|
|
35826
|
-
|
|
35827
|
-
|
|
35828
|
-
|
|
35829
|
-
|
|
35830
|
-
|
|
35831
|
-
|
|
35832
|
-
|
|
35833
|
-
|
|
35834
|
-
|
|
35835
|
-
|
|
35836
|
-
|
|
35911
|
+
if (!recnoForCamera)
|
|
35912
|
+
return ANPR_NOTHING_TO_REVOKE;
|
|
35913
|
+
const outcome = await _removePlateNumber({
|
|
35914
|
+
host,
|
|
35915
|
+
username,
|
|
35916
|
+
password,
|
|
35917
|
+
recno: recnoForCamera,
|
|
35918
|
+
mode
|
|
35919
|
+
}).catch((error) => anprRevokeThrew(error));
|
|
35920
|
+
if (!outcome?.ok) {
|
|
35921
|
+
logCameraFailure(
|
|
35922
|
+
"expiredVehicle revoke",
|
|
35923
|
+
camera,
|
|
35924
|
+
outcome?.detail ?? "no outcome returned"
|
|
35925
|
+
);
|
|
35837
35926
|
}
|
|
35838
|
-
|
|
35839
|
-
|
|
35840
|
-
|
|
35841
|
-
|
|
35842
|
-
|
|
35843
|
-
import_node_server_utils99.logger.error(
|
|
35844
|
-
"Error in vehicle service process deleting expired vehicles:",
|
|
35845
|
-
error
|
|
35846
|
-
);
|
|
35847
|
-
await session.abortTransaction();
|
|
35848
|
-
throw error;
|
|
35849
|
-
} finally {
|
|
35850
|
-
session.endSession();
|
|
35927
|
+
return outcome ?? anprRevokeThrew("no outcome returned");
|
|
35928
|
+
},
|
|
35929
|
+
cameraLabel
|
|
35930
|
+
});
|
|
35931
|
+
for (const entry of skipped) {
|
|
35932
|
+
import_node_server_utils99.logger.error(sweepSkipLine(entry));
|
|
35851
35933
|
}
|
|
35934
|
+
const deleted = deletableIds.length ? await _deleteExpiredVehicles(deletableIds) : 0;
|
|
35935
|
+
import_node_server_utils99.logger.info(
|
|
35936
|
+
`processDeletingExpiredVehicles: ${vehicles.length} expired, ${deleted} deleted, ${skipped.length} left for the next run.`
|
|
35937
|
+
);
|
|
35938
|
+
return `Expired Vehicle plate numbers deleted successfully.`;
|
|
35852
35939
|
}
|
|
35853
35940
|
async function reactivateVehicleById(id, orgId, siteId) {
|
|
35854
35941
|
const session = import_node_server_utils99.useAtlas.getClient()?.startSession();
|
|
@@ -51921,42 +52008,55 @@ function useVisitorTransactionService() {
|
|
|
51921
52008
|
);
|
|
51922
52009
|
if (!transactions.length)
|
|
51923
52010
|
continue;
|
|
51924
|
-
const
|
|
51925
|
-
|
|
51926
|
-
|
|
51927
|
-
|
|
51928
|
-
|
|
51929
|
-
|
|
51930
|
-
|
|
51931
|
-
|
|
51932
|
-
|
|
51933
|
-
|
|
51934
|
-
|
|
51935
|
-
|
|
51936
|
-
|
|
51937
|
-
|
|
51938
|
-
|
|
51939
|
-
|
|
51940
|
-
|
|
51941
|
-
|
|
51942
|
-
|
|
51943
|
-
|
|
51944
|
-
|
|
51945
|
-
`Dahua record already missing for transaction ${transaction._id}, marking as removed.`
|
|
52011
|
+
const { deletableIds, skipped } = await sweepExpiredVehicles(
|
|
52012
|
+
transactions,
|
|
52013
|
+
{
|
|
52014
|
+
camerasForSite: async () => [camera],
|
|
52015
|
+
revoke: async (anprCamera, transaction) => {
|
|
52016
|
+
const credentials = {
|
|
52017
|
+
host: anprCamera.host,
|
|
52018
|
+
username: anprCamera.username,
|
|
52019
|
+
password: anprCamera.password,
|
|
52020
|
+
mode: "TrafficRedList" /* TRAFFIC_REDLIST */
|
|
52021
|
+
};
|
|
52022
|
+
let recno = String(transaction?.recNo ?? "");
|
|
52023
|
+
const plateNumber = String(transaction?.plateNumber ?? "");
|
|
52024
|
+
if (plateNumber) {
|
|
52025
|
+
try {
|
|
52026
|
+
const found = parseDahuaFind(
|
|
52027
|
+
await _getPlateNumber({
|
|
52028
|
+
...credentials,
|
|
52029
|
+
plateNumber,
|
|
52030
|
+
requireOk: true
|
|
52031
|
+
}) ?? ""
|
|
51946
52032
|
);
|
|
51947
|
-
|
|
51948
|
-
|
|
52033
|
+
if (!found.exists)
|
|
52034
|
+
return ANPR_NOTHING_TO_REVOKE;
|
|
52035
|
+
if (found.recNo)
|
|
52036
|
+
recno = found.recNo;
|
|
52037
|
+
} catch (error) {
|
|
52038
|
+
return anprRevokeThrew(error);
|
|
51949
52039
|
}
|
|
51950
|
-
import_node_server_utils134.logger.error(
|
|
51951
|
-
`Failed to remove plate for transaction ${transaction._id}`,
|
|
51952
|
-
error
|
|
51953
|
-
);
|
|
51954
52040
|
}
|
|
51955
|
-
|
|
52041
|
+
if (!recno)
|
|
52042
|
+
return ANPR_NOTHING_TO_REVOKE;
|
|
52043
|
+
return await _removePlateNumber({
|
|
52044
|
+
...credentials,
|
|
52045
|
+
recno
|
|
52046
|
+
}).catch((error) => anprRevokeThrew(error));
|
|
52047
|
+
},
|
|
52048
|
+
cameraLabel
|
|
52049
|
+
}
|
|
52050
|
+
);
|
|
52051
|
+
for (const entry of skipped) {
|
|
52052
|
+
import_node_server_utils134.logger.error(
|
|
52053
|
+
`processTransactionDahuaStatus NOT marking transaction ${entry.id} removed (plate ${JSON.stringify(
|
|
52054
|
+
entry.plateNumber
|
|
52055
|
+
)}, site ${entry.site}): ` + entry.failures.map((failure) => `${failure.camera} (${failure.reason})`).join(", ") + `. The plate may still open the barrier, so the record is left unsynced and will be retried on the next run.`
|
|
51956
52056
|
);
|
|
51957
52057
|
}
|
|
51958
|
-
if (
|
|
51959
|
-
await _updateManyDahuaSyncStatus(
|
|
52058
|
+
if (deletableIds.length > 0) {
|
|
52059
|
+
await _updateManyDahuaSyncStatus(deletableIds, "removed");
|
|
51960
52060
|
}
|
|
51961
52061
|
}
|
|
51962
52062
|
page++;
|