@7365admin1/core 3.86.0 → 3.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.86.0",
4
+ "version": "3.87.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -13,7 +13,7 @@
13
13
  "build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
14
14
  "release": "yarn run build && changeset publish",
15
15
  "lint": "tsc",
16
- "test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/png-encoder.util.ts src/services/demo.adapter.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
16
+ "test": "tsup src/config.ts src/utils/building-map.util.ts src/utils/robot-cloud.util.ts src/utils/robot-adapter.util.ts src/utils/png-encoder.util.ts src/services/demo.adapter.ts src/utils/robot-fault.util.ts src/utils/robot-tracking.util.ts src/utils/robot-apps.util.ts src/utils/robot-permission.util.ts src/utils/lift-contract.util.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/occurrence-book-create.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
17
17
  "test:camera": "yarn test",
18
18
  "test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
19
19
  },
@@ -162,3 +162,36 @@ test("the encoder refuses an impossible image rather than emitting a broken one"
162
162
  assert.throws(() => encodeGreyscalePng(new Uint8Array(4), 0, 4), /positive size/);
163
163
  assert.throws(() => encodeGreyscalePng(new Uint8Array(2), 10, 10), /one byte per pixel/);
164
164
  });
165
+
166
+ /* ------------------------------------------------ faults and spot cleaning */
167
+
168
+ import { demoFault, DEMO_FAULT_ROBOT } from "./.build/services/demo.adapter.mjs";
169
+
170
+ test("one demo robot faults two minutes in every thirty, keyed by when it started", async () => {
171
+ const windowStart = 30 * 60_000 * 1000;
172
+ const inside = await at(windowStart + 60_000).getRobot(DEMO_FAULT_ROBOT);
173
+ assert.equal(inside.mode, "MAINTENANCE");
174
+ assert.equal(inside.fault.code, "DEMO_BRUSH_JAM");
175
+ assert.equal(inside.fault.since, windowStart);
176
+ assert.match(inside.fault.message, /simulated/);
177
+
178
+ const after = await at(windowStart + 3 * 60_000).getRobot(DEMO_FAULT_ROBOT);
179
+ assert.equal(after.fault, null);
180
+ // Only the one robot, so a fleet screen is not a wall of red.
181
+ assert.equal(demoFault("DEMO-0001", windowStart), null);
182
+ });
183
+
184
+ test("the demo brand supports spot cleaning, and parks the robot on the spot", async () => {
185
+ assert.equal(commandSupported(DEMO_CAPABILITIES, "spot"), true);
186
+ const t = 5 * 60_000; // outside any fault window, battery well above flat
187
+ const demo = at(t);
188
+ await demo.sendCommand("DEMO-0001", {
189
+ kind: "spot",
190
+ points: [{ x: 90, y: 50 }, { x: 110, y: 50 }, { x: 110, y: 70 }, { x: 90, y: 70 }],
191
+ });
192
+ const robot = await demo.getRobot("DEMO-0001");
193
+ assert.equal(robot.mode, "WORKING");
194
+ // Pixels to metres at 0.05 m/px, y flipped (180 px tall plan).
195
+ assert.ok(Math.abs(robot.position.x - 5) < 1e-9);
196
+ assert.ok(Math.abs(robot.position.y - (180 - 60) * 0.05) < 1e-9);
197
+ });
@@ -874,10 +874,12 @@ function buildApp(nsu, core) {
874
874
  // mirrors iservice365-API-core/src/routes/robot.route.ts
875
875
  const robots = express.Router();
876
876
  {
877
- const { createRobot, getRobots } = core.useRobotController();
877
+ const { createRobot, getRobots, updateRobot, deleteRobot } = core.useRobotController();
878
878
 
879
879
  robots.post("/", requireAuth, createRobot);
880
880
  robots.get("/organization/:organization/site/:site", requireAuth, getRobots);
881
+ robots.patch("/:id([0-9a-fA-F]{24})", requireAuth, updateRobot);
882
+ robots.delete("/:id([0-9a-fA-F]{24})", requireAuth, deleteRobot);
881
883
  }
882
884
 
883
885
  // mirrors iservice365-API-core/src/routes/attendance-settings.route.ts
@@ -290,6 +290,73 @@ describe("robots and their credentials are scoped to their estate", { concurrenc
290
290
  assert.deepEqual(stranger.body, insider.body);
291
291
  });
292
292
 
293
+ // ---- edit and remove: the estate comes off the STORED robot ---------------
294
+
295
+ const seeded = async (name) => (await byName(name))._id;
296
+ const audits = async (action, target) =>
297
+ await h.db.collection("console-audit").find({ action, target: String(target) }).toArray();
298
+
299
+ record("14. an outsider cannot EDIT or REMOVE another estate's robot by quoting its id", async () => {
300
+ const robotB = await seeded("rb-seeded-b");
301
+ const edit = await h.api(`/robots/${robotB}`, {
302
+ method: "PATCH", sid: outsiderSid, body: { name: "rb-hijacked" },
303
+ });
304
+ const remove = await h.api(`/robots/${robotB}`, { method: "DELETE", sid: outsiderSid });
305
+
306
+ assert.equal(edit.status, 404);
307
+ assert.equal(remove.status, 404);
308
+ const stored = await h.db.collection(COLL).findOne({ _id: robotB });
309
+ assert.equal(stored.name, "rb-seeded-b");
310
+ assert.ok(!stored.deletedAt);
311
+ });
312
+
313
+ record("15. the estate's own manager edits a robot, and it is audited without credentials", async () => {
314
+ const robotB = await seeded("rb-seeded-b");
315
+ const res = await h.api(`/robots/${robotB}`, {
316
+ method: "PATCH", sid: managerSid, body: { name: "rb-renamed-b", robotSn: "SN-1" },
317
+ });
318
+ assert.equal(res.status, 200);
319
+
320
+ const stored = await h.db.collection(COLL).findOne({ _id: robotB });
321
+ assert.equal(stored.name, "rb-renamed-b");
322
+ assert.equal(stored.robotSn, "SN-1");
323
+
324
+ const [row] = await audits("robot.updated", robotB);
325
+ assert.equal(row?.after?.name, "rb-renamed-b");
326
+ assert.equal(row?.before?.name, "rb-seeded-b");
327
+ assert.equal(JSON.stringify(row).includes("e2e-fake"), false);
328
+ });
329
+
330
+ record("16. an empty edit is a 400", async () => {
331
+ const robotB = await seeded("rb-renamed-b");
332
+ const res = await h.api(`/robots/${robotB}`, { method: "PATCH", sid: managerSid, body: {} });
333
+ assert.equal(res.status, 400);
334
+ });
335
+
336
+ record("17. a removed robot stops listing and answers as not found, and the row stays", async () => {
337
+ const robotB = await seeded("rb-renamed-b");
338
+ const remove = await h.api(`/robots/${robotB}`, { method: "DELETE", sid: managerSid });
339
+ assert.equal(remove.status, 200);
340
+
341
+ const list = await h.api(listUrl(id.orgB, id.siteB), { sid: managerSid });
342
+ assert.equal((list.body?.items ?? []).some((r) => r.name === "rb-renamed-b"), false);
343
+
344
+ const again = await h.api(`/robots/${robotB}`, { method: "DELETE", sid: managerSid });
345
+ assert.equal(again.status, 404);
346
+
347
+ const stored = await h.db.collection(COLL).findOne({ _id: robotB });
348
+ assert.ok(stored.deletedAt, "soft delete keeps the row");
349
+ assert.equal((await audits("robot.removed", robotB)).length, 1);
350
+ });
351
+
352
+ record("18. creating a robot writes an audit row, without credentials", async () => {
353
+ const robot = await byName("rb-allowed");
354
+ const [row] = await audits("robot.added", robot._id);
355
+ assert.equal(row?.after?.name, "rb-allowed");
356
+ assert.equal(String(row?.org), String(id.orgB));
357
+ assert.equal(JSON.stringify(row).includes("e2e-fake"), false);
358
+ });
359
+
293
360
  record("13. no session is still a 401 on both routes", async () => {
294
361
  const list = await h.api(listUrl(id.orgB, id.siteB));
295
362
  const create = await h.api("/robots", {
@@ -299,6 +366,9 @@ describe("robots and their credentials are scoped to their estate", { concurrenc
299
366
 
300
367
  assert.equal(list.status, 401);
301
368
  assert.equal(create.status, 401);
369
+ const robotA = await seeded("rb-seeded-a");
370
+ assert.equal((await h.api(`/robots/${robotA}`, { method: "PATCH", body: { name: "x" } })).status, 401);
371
+ assert.equal((await h.api(`/robots/${robotA}`, { method: "DELETE" })).status, 401);
302
372
  });
303
373
  });
304
374
 
@@ -129,6 +129,15 @@ test("each command maps to the capability it actually needs", () => {
129
129
  assert.equal(commandSupported(caps("navigateToPoint"), "navigate"), true);
130
130
  });
131
131
 
132
+ test("a spot clean needs spotClean, not just startProcess - a click needs a verified frame", () => {
133
+ assert.equal(commandSupported(caps("startProcess"), "spot"), false);
134
+ assert.equal(commandSupported(caps("spotClean"), "spot"), true);
135
+ });
136
+
137
+ test("spotClean is ours, not Open-RMF's, and moves no brand's tier", () => {
138
+ assert.equal(robotTier(caps("readLocation", "spotClean")), robotTier(caps("readLocation")));
139
+ });
140
+
132
141
  test("an unknown command kind is refused, not defaulted", () => {
133
142
  for (const kind of ["selfDestruct", "", null, undefined, "constructor"]) {
134
143
  assert.equal(commandSupported(ALL, kind), false, String(kind));
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import {
5
+ claimFaultOnce,
6
+ robotFaultKey,
7
+ robotFaultNotifyEnabled,
8
+ robotFaultText,
9
+ } from "./.build/utils/robot-fault.util.mjs";
10
+
11
+ test("the same fault occurrence is claimed once per process", async () => {
12
+ const local = new Set();
13
+ const key = robotFaultKey("r1", { code: "JAM", since: 10 });
14
+ assert.equal(await claimFaultOnce(key, { local }), true);
15
+ assert.equal(await claimFaultOnce(key, { local }), false);
16
+ // A second occurrence of the same fault notifies again.
17
+ assert.equal(await claimFaultOnce(robotFaultKey("r1", { code: "JAM", since: 20 }), { local }), true);
18
+ });
19
+
20
+ test("another instance holding the claim suppresses it; an unreachable store does not", async () => {
21
+ assert.equal(await claimFaultOnce("k1", { local: new Set(), claimShared: async () => false }), false);
22
+ assert.equal(await claimFaultOnce("k2", { local: new Set(), claimShared: async () => null }), true);
23
+ assert.equal(
24
+ await claimFaultOnce("k3", { local: new Set(), claimShared: async () => { throw new Error("down"); } }),
25
+ true,
26
+ );
27
+ });
28
+
29
+ test("ROBOT_FAULT_NOTIFY=off silences it; anything else leaves it on", () => {
30
+ assert.equal(robotFaultNotifyEnabled({}), true);
31
+ assert.equal(robotFaultNotifyEnabled({ ROBOT_FAULT_NOTIFY: " OFF " }), false);
32
+ assert.equal(robotFaultNotifyEnabled({ ROBOT_FAULT_NOTIFY: "on" }), true);
33
+ });
34
+
35
+ test("the text names the robot and the fault", () => {
36
+ const { body } = robotFaultText("Cleaner 02", { code: "JAM", message: "Side brush jammed" });
37
+ assert.match(body, /Cleaner 02/);
38
+ assert.match(body, /Side brush jammed/);
39
+ });