@7365admin1/core 3.79.1-staging.350 → 3.79.1-staging.352

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,199 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.89.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d3afd05: Robots: a network-wide listing for the Seven365 console (`useRobotNetworkController` - `listNetworkRobots`, `getNetworkSummary`). Every robot at every client site, with client and site names, paginated, searchable, filterable (client, site, class, brand) and sortable; soft-deleted robots excluded; no credentials; no live vendor calls. Gated on the super-admin membership with the cross-client grant.
8
+
9
+ ## 3.88.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 81cb7d9: Robots: building maps (per-floor doors, lift shafts, levels, drawn walls/zones; versioned saves, soft delete) and a durable lift queue (one holder per lift, lease/timeout, release; simulated car for demo robots). New collections `robot-building-maps` and `robot-lift-sessions` with index helpers.
14
+
15
+ ## 3.87.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 07d82bd: Robots: edit and remove, an audit trail, fault notifications, and spot cleaning
20
+
21
+ - `useRobotController` gains `updateRobot` (name, serial) and `deleteRobot` (soft
22
+ delete via the existing `deletedAt` field; removed robots stop listing and
23
+ answer as not found). The estate is read off the stored robot.
24
+ - Robot create, edit, remove and every accepted command write a console-audit
25
+ row (`robot.*` actions). The credential pair is not on the allow-list.
26
+ - A robot reporting a fault notifies Seven365 staff once per occurrence
27
+ (`platform` category), noticed on a state read. `ROBOT_FAULT_NOTIFY=off`
28
+ silences it. The demo brand faults two minutes in every thirty.
29
+ - New `spotClean` capability; a spot command needs it. The demo brand has it;
30
+ Ounitech does not until its pixel frame is verified.
31
+ - The state response carries `fault` and `capabilities`.
32
+
33
+ ## 3.86.0
34
+
35
+ ### Minor Changes
36
+
37
+ - f6b10f4: Route the fleet through the adapter, and add a demo brand you can actually see
38
+
39
+ The vendor-neutral contract shipped but nothing used it: the fleet service still
40
+ called Ounitech directly, so a second brand could not have worked end to end.
41
+ Everything now goes through `TRobotAdapter`, and the registry holds a FACTORY
42
+ per brand — an adapter is built per robot from that robot's own credentials, so
43
+ one estate's appid can never sign another estate's request.
44
+
45
+ Adds a demo brand: three simulated robots on two floors that move, drain and
46
+ recharge their batteries, respond to commands, and carry generated floor plans
47
+ as PNG data URIs. It reaches no network and writes nothing.
48
+
49
+ It exists because until a vendor supplies credentials every robot screen is
50
+ empty and no button can be pressed — and because it is a second brand on the
51
+ same contract, declaring Full Control where Ounitech reaches no tier at all. If
52
+ the screens draw different controls for the two without being told which is
53
+ which, the platform really is brand-agnostic.
54
+
55
+ Adds `GET /robots/brands` so the Add Robot form can offer them, each with its
56
+ Open-RMF tier.
57
+
58
+ ## 3.85.0
59
+
60
+ ### Minor Changes
61
+
62
+ - 8afb16f: The robot's live position, streamed
63
+
64
+ Adds `robot-tracking.service.ts` and a `trackRobot` SSE endpoint. The parsing,
65
+ staleness and backoff shipped earlier were pure and tested but nothing opened a
66
+ connection, so nothing was ever live — the screens read `get-robot-list`, which
67
+ carries no position at all, and the 3D layout could not draw the robot.
68
+
69
+ The stream follows a viewer rather than running as a fleet-wide daemon: a screen
70
+ opens it, the API opens one upstream, and both close when the viewer leaves.
71
+
72
+ Events are ours, not the vendor's frames: a normalised pose, and an explicit
73
+ `lost` when the stream goes quiet — a screen has to tell "the robot is here"
74
+ from "we no longer know where the robot is", and the vendor's stream cannot say
75
+ the second thing.
76
+
77
+ ## 3.84.0
78
+
79
+ ### Minor Changes
80
+
81
+ - 3c562a9: Building map: a floor plan, and the 3D layout generated from it
82
+
83
+ Adds `building-map.util.ts` — the Open-RMF building-map model (levels, vertices,
84
+ walls, floors, doors, measurements), the scale handling, and the generation step
85
+ that turns an annotated level into the geometry a 3D view draws.
86
+
87
+ The shape is taken from a real `office.building.yaml` in `open-rmf/rmf_demos`,
88
+ and the wall defaults are `traffic_editor`'s own (10cm thick, 2.5m high, 1px =
89
+ 5cm before a measurement), so a map made here can be exported to RMF and one
90
+ made in `traffic_editor` read here.
91
+
92
+ `wallsFromOccupancyGrid` traces walls straight out of a floor-plan image, so
93
+ uploading a plan produces a layout without anyone drawing walls by hand — a SLAM
94
+ occupancy grid already marks walls as dark pixels.
95
+
96
+ `buildingMapFromRobotMaps` reads the cleaning robot's own floor maps into that
97
+ model: one level per floor, the vendor's resolution as the scale, its origin as
98
+ the level offset, its cleaning areas as RMF cleaning-zone waypoints and its dock
99
+ as a charger.
100
+
101
+ ## 3.83.0
102
+
103
+ ### Minor Changes
104
+
105
+ - b9d7f9c: Robot fleet: a vendor-neutral adapter contract, so any brand can be integrated
106
+
107
+ Adds `robot-adapter.util.ts` — the capability model, the tier calculation and the
108
+ brand registry — and `ounitech.adapter.ts`, the first brand behind it.
109
+
110
+ The capability names and tier requirements are Open-RMF's, verbatim from the
111
+ Fleet Adapters table: Full Control, Traffic Light, Read Only, No Interface. That
112
+ means a brand's datasheet reads straight into a tier, and a vendor conversation
113
+ gets a name ("your API is Read Only, we need Traffic Light") rather than a list
114
+ of features we would like.
115
+
116
+ Screens ask the adapter what the robot can do rather than assuming, so a brand
117
+ that cannot pause a process is never shown a Pause button that would fail.
118
+ `commandSupported` refuses an unsupported command at the service too, because a
119
+ command can also arrive from an API caller or a scheduled job.
120
+
121
+ `uncontrollableFleetWarning` enforces Open-RMF's rule that a shared space may
122
+ contain at most one fleet it cannot control — a read-only cleaning fleet plus a
123
+ security robot with no interface is the exact pair the book says makes deadlock
124
+ avoidance nearly impossible.
125
+
126
+ Adds `brand` to the robot record so a row says whose protocol it answers.
127
+ Defaulted to `ounitech` because every existing row is one; a second brand makes
128
+ it a required choice rather than a fallback.
129
+
130
+ ## 3.82.0
131
+
132
+ ### Minor Changes
133
+
134
+ - dbc439a: Cleaning robot: fleet endpoints, and the lift contract
135
+
136
+ Adds `robot-fleet.controller.ts` — live state, floors and areas, map image URLs,
137
+ the cleaning schedule, and one command endpoint covering full, area, spot and
138
+ return. Every handler loads the robot first and authorises the estate named on
139
+ the STORED record, never one named in the request, so a robot cannot be reached
140
+ by quoting its id; credentials are read server-side and never enter a response.
141
+
142
+ Adds `parseRobotCommand`, pure, so the command rules can be asserted without a
143
+ robot. Fields are refused on commands that do not use them rather than ignored —
144
+ silently dropping a spot polygon from an area command would clean the wrong
145
+ thing and report success.
146
+
147
+ Adds `getRobotById` and `setRobotSn` to the robot repository. `setRobotSn` only
148
+ ever fills a blank serial, so a later discovery cannot re-point a nickname at a
149
+ different machine.
150
+
151
+ Adds `lift-contract.util.ts` — the lift vocabulary, with field names and enum
152
+ values taken verbatim from Open-RMF's `rmf_lift_msgs`, plus an in-memory lift
153
+ simulator. No Modbus and no register numbers: the Inovance open-protocol
154
+ register map has not been supplied, so the driver sits behind an interface and
155
+ the queue and session logic can be built and tested against the simulator first.
156
+
157
+ ## 3.81.0
158
+
159
+ ### Minor Changes
160
+
161
+ - fde0167: Cleaning robot: robot cloud connector, and stop projecting robot credentials
162
+
163
+ Adds the signed client for the cleaning-robot cloud (`robot-cloud.util.ts`,
164
+ `robot-cloud.service.ts`): request signing, the status vocabulary, the
165
+ pixel/metre transform and the area colour conversions, plus the nine
166
+ request/response endpoints. Credentials are passed in by the caller and are
167
+ never read from the environment, logged, or placed in a request body.
168
+
169
+ Also fixes a credential disclosure in the existing robot listing: `getRobots`
170
+ projected `appKey` and `appToken` — the pair that signs every command sent to
171
+ that estate's robots — into the response, so anyone holding `see-all-robots`
172
+ received them. They are no longer returned.
173
+
174
+ Fixes `POST /robots`, which had never worked: the service assigned an ObjectId
175
+ where the model's schema wanted a string, so every create ended in
176
+ `400 "createdBy" must be a string` and no robot could be registered at all.
177
+
178
+ Adds the server-side enforcement of the five robot permissions, which have
179
+ existed in the client catalogue but were never checked by the API. `"*"` allows
180
+ everything; an empty list allows everything only on a `type: "admin"` role (the
181
+ Seven365 staff role is seeded with `[]`, so enforcing it would refuse the
182
+ platform owner); a non-empty list is enforced exactly.
183
+
184
+ Adds `robotSn` to the robot record (optional, discovered from `get-robot-list`
185
+ when absent) and `robot-fleet.service.ts`, which bridges a stored robot to the
186
+ vendor cloud: live state, maps, map image URLs minted at use, and dispatch.
187
+
188
+ Adds a table of which application may set up and monitor which robot type, so
189
+ cleaning robots and security robots share one module while each app sees only
190
+ its own.
191
+
192
+ Adds an optional `type` ("cleaning" | "security") to the robot record with an
193
+ optional filter, so both device classes share one module and one collection
194
+ while each app lists only its own. Existing rows are untouched and an
195
+ unfiltered query behaves exactly as before.
196
+
3
197
  ## 3.80.0
4
198
 
5
199
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -10951,11 +10951,17 @@ declare function UseAccessManagementRepo(): {
10951
10951
  }>;
10952
10952
  assignedAccessCardsByUnitRepo: (params: {
10953
10953
  site: string;
10954
- unitId: string;
10954
+ unitId?: string;
10955
10955
  userType: string;
10956
10956
  type: string;
10957
10957
  search: string;
10958
- }) => Promise<bson.Document>;
10958
+ page?: number;
10959
+ limit?: number;
10960
+ }) => Promise<{
10961
+ items: any[];
10962
+ pages: number;
10963
+ pageRange: string;
10964
+ }>;
10959
10965
  acknowlegdeCardRepo: (params: {
10960
10966
  userId: string[];
10961
10967
  cardId: string[];
package/dist/index.js CHANGED
@@ -55827,26 +55827,50 @@ function UseAccessManagementRepo() {
55827
55827
  async function assignedAccessCardsByUnitRepo(params) {
55828
55828
  try {
55829
55829
  const site = new import_mongodb97.ObjectId(params.site);
55830
- const unitId = new import_mongodb97.ObjectId(params.unitId);
55830
+ const unitId = params.unitId ? new import_mongodb97.ObjectId(params.unitId) : null;
55831
55831
  const userType = params.userType;
55832
55832
  const type = params.type;
55833
- const search = params.search;
55833
+ const search = params.search ? params.search.trim() : "";
55834
+ const page = params.page ? Number(params.page) - 1 : 0;
55835
+ const limit = Number(params.limit) || 10;
55834
55836
  const typeFilter = type.toLowerCase() === "all" ? [] : [{ $eq: ["$type", type] }];
55837
+ const matchesSearch = (field) => ({
55838
+ $regexMatch: {
55839
+ input: {
55840
+ $cond: [{ $eq: [{ $type: field }, "string"] }, field, ""]
55841
+ },
55842
+ regex: search,
55843
+ options: "i"
55844
+ }
55845
+ });
55846
+ const unitNameMatch = matchesSearch("$$unitName");
55835
55847
  const searchFilter = search ? [
55836
55848
  {
55837
55849
  $or: [
55838
- { cardNo: { $regex: search.trim(), $options: "i" } },
55839
- { accessLevel: { $regex: search.trim(), $options: "i" } },
55840
- { doorName: { $regex: search.trim(), $options: "i" } },
55841
- { liftName: { $regex: search.trim(), $options: "i" } }
55850
+ unitNameMatch,
55851
+ matchesSearch("$cardNo"),
55852
+ matchesSearch("$accessLevel"),
55853
+ matchesSearch("$doorName"),
55854
+ matchesSearch("$liftName")
55842
55855
  ]
55843
55856
  }
55844
55857
  ] : [];
55858
+ const unitFilters = [
55859
+ ...unitId ? [] : [{ $gt: [{ $size: "$accessCards" }, 0] }],
55860
+ ...search ? [
55861
+ {
55862
+ $or: [
55863
+ matchesSearch("$name"),
55864
+ { $gt: [{ $size: "$accessCards" }, 0] }
55865
+ ]
55866
+ }
55867
+ ] : []
55868
+ ];
55845
55869
  const result = await collectionName("building-units").aggregate(
55846
55870
  [
55847
55871
  {
55848
55872
  $match: {
55849
- _id: unitId,
55873
+ ...unitId ? { _id: unitId } : {},
55850
55874
  site,
55851
55875
  status: { $ne: "deleted" }
55852
55876
  }
@@ -55884,7 +55908,7 @@ function UseAccessManagementRepo() {
55884
55908
  {
55885
55909
  $lookup: {
55886
55910
  from: "access-cards",
55887
- let: { unit: "$_id" },
55911
+ let: { unit: "$_id", unitName: "$name" },
55888
55912
  pipeline: [
55889
55913
  {
55890
55914
  $match: {
@@ -55908,10 +55932,10 @@ function UseAccessManagementRepo() {
55908
55932
  ...typeFilter,
55909
55933
  // Cards actually held by a user only - a card sitting
55910
55934
  // unassigned on the unit (`userId: null`) is left out.
55911
- { $ne: ["$userId", null] }
55935
+ { $ne: ["$userId", null] },
55936
+ ...searchFilter
55912
55937
  ]
55913
- },
55914
- ...searchFilter[0]
55938
+ }
55915
55939
  }
55916
55940
  },
55917
55941
  { $sort: { _id: -1 } },
@@ -55941,20 +55965,48 @@ function UseAccessManagementRepo() {
55941
55965
  as: "accessCards"
55942
55966
  }
55943
55967
  },
55968
+ // Listing mode hides units with nothing assigned, the same rule
55969
+ // assignedAccessCardsRepo applies. A directly requested unitId is
55970
+ // exempt, so the single-unit call still returns the unit with an
55971
+ // empty `accessCards` list.
55972
+ // The search keeps a unit whose own name matches even when none of
55973
+ // its cards do.
55974
+ ...unitFilters.length ? [{ $match: { $expr: { $and: unitFilters } } }] : [],
55944
55975
  {
55945
- $project: {
55946
- _id: 0,
55947
- unitId: "$_id",
55948
- block: { $ifNull: ["$buildingInfo.name", "$buildingName"] },
55949
- level: "$levelInfo.name",
55950
- unit: "$name",
55951
- accessCards: 1
55976
+ $sort: {
55977
+ "buildingInfo.name": 1,
55978
+ "levelInfo.name": 1,
55979
+ name: 1
55980
+ }
55981
+ },
55982
+ {
55983
+ $facet: {
55984
+ totalCount: [{ $count: "count" }],
55985
+ items: [
55986
+ { $skip: page * limit },
55987
+ { $limit: limit },
55988
+ {
55989
+ $project: {
55990
+ _id: 0,
55991
+ unitId: "$_id",
55992
+ block: {
55993
+ $ifNull: ["$buildingInfo.name", "$buildingName"]
55994
+ },
55995
+ level: "$levelInfo.name",
55996
+ unit: "$name",
55997
+ accessCards: 1
55998
+ }
55999
+ }
56000
+ ]
55952
56001
  }
55953
56002
  }
55954
56003
  ],
55955
56004
  { allowDiskUse: true }
55956
56005
  ).toArray();
55957
- return result[0] ?? null;
56006
+ const totalCount = result[0]?.totalCount?.[0]?.count ?? 0;
56007
+ const items = result[0]?.items ?? [];
56008
+ const paginatedResult = (0, import_node_server_utils145.paginate)(items, page, limit, totalCount);
56009
+ return paginatedResult;
55958
56010
  } catch (error) {
55959
56011
  throw new Error(error.message);
55960
56012
  }
@@ -74394,21 +74446,27 @@ function useAccessManagementController() {
74394
74446
  unitId,
74395
74447
  userType,
74396
74448
  type,
74397
- search = ""
74449
+ search = "",
74450
+ page,
74451
+ limit
74398
74452
  } = req.query;
74399
74453
  const schema2 = import_joi106.default.object({
74400
74454
  site: import_joi106.default.string().hex().required(),
74401
- unitId: import_joi106.default.string().hex().required(),
74455
+ unitId: import_joi106.default.string().hex().optional().allow("", null),
74402
74456
  userType: import_joi106.default.string().required(),
74403
74457
  type: import_joi106.default.string().valid("all", ...Object.values(EAccessCardTypes)).required(),
74404
- search: import_joi106.default.string().optional().allow("", null)
74458
+ search: import_joi106.default.string().optional().allow("", null),
74459
+ page: import_joi106.default.number().optional().default(1),
74460
+ limit: import_joi106.default.number().optional().default(10)
74405
74461
  });
74406
74462
  const { error } = schema2.validate({
74407
74463
  site,
74408
74464
  unitId,
74409
74465
  userType,
74410
74466
  type,
74411
- search
74467
+ search,
74468
+ page,
74469
+ limit
74412
74470
  });
74413
74471
  if (error) {
74414
74472
  return res.status(400).json({ message: error.message });
@@ -74420,7 +74478,9 @@ function useAccessManagementController() {
74420
74478
  unitId,
74421
74479
  userType,
74422
74480
  type,
74423
- search
74481
+ search,
74482
+ page,
74483
+ limit
74424
74484
  });
74425
74485
  return res.status(200).json({ message: "Success", data: result });
74426
74486
  } catch (error) {