@7365admin1/core 3.82.0 → 3.83.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/dist/index.js CHANGED
@@ -215,6 +215,8 @@ __export(src_exports, {
215
215
  ORG_OWNER_REPAIR_CLAIM_TAKEN_REASON: () => ORG_OWNER_REPAIR_CLAIM_TAKEN_REASON,
216
216
  ORG_OWNER_REPAIR_DEFAULT: () => ORG_OWNER_REPAIR_DEFAULT,
217
217
  ORG_OWNER_REPAIR_OFF_REASON: () => ORG_OWNER_REPAIR_OFF_REASON,
218
+ OUNITECH_BRAND: () => OUNITECH_BRAND,
219
+ OUNITECH_CAPABILITIES: () => OUNITECH_CAPABILITIES,
218
220
  OrgNature: () => OrgNature,
219
221
  OvernightParkingRequestSort: () => OvernightParkingRequestSort,
220
222
  OvernightParkingRequestStatus: () => OvernightParkingRequestStatus,
@@ -242,6 +244,7 @@ __export(src_exports, {
242
244
  REALTIME_MAX_FANOUT: () => REALTIME_MAX_FANOUT,
243
245
  REGISTERED_VEHICLE_CATEGORIES: () => REGISTERED_VEHICLE_CATEGORIES,
244
246
  ROBOT_APP_SCOPES: () => ROBOT_APP_SCOPES,
247
+ ROBOT_CAPABILITIES: () => ROBOT_CAPABILITIES,
245
248
  ROBOT_CLOUD_DOC_CONFIRMATIONS: () => ROBOT_CLOUD_DOC_CONFIRMATIONS,
246
249
  ROBOT_CLOUD_PATHS: () => ROBOT_CLOUD_PATHS,
247
250
  ROBOT_MODULE: () => ROBOT_MODULE,
@@ -330,13 +333,16 @@ __export(src_exports, {
330
333
  chatSchema: () => chatSchema,
331
334
  claimReadsAsStale: () => claimReadsAsStale,
332
335
  clampPtzSpeed: () => clampPtzSpeed,
336
+ clearRobotAdapters: () => clearRobotAdapters,
333
337
  clockDriftSeconds: () => clockDriftSeconds,
338
+ commandSupported: () => commandSupported,
334
339
  consoleRoleAllows: () => consoleRoleAllows,
335
340
  consoleRoleAllowsAll: () => consoleRoleAllowsAll,
336
341
  consoleSpellings: () => consoleSpellings,
337
342
  console_audit_namespace_collection: () => console_audit_namespace_collection,
338
343
  createLiftSimulator: () => createLiftSimulator,
339
344
  createManpowerRemarksDaily: () => createManpowerRemarksDaily,
345
+ createOunitechAdapter: () => createOunitechAdapter,
340
346
  createSseParser: () => createSseParser,
341
347
  customerSchema: () => customerSchema,
342
348
  customerSitePropertyFields: () => customerSitePropertyFields,
@@ -418,6 +424,7 @@ __export(src_exports, {
418
424
  maskNric: () => maskNric,
419
425
  memberModulesHandlers: () => memberModulesHandlers,
420
426
  metresToPixel: () => metresToPixel,
427
+ missingForTier: () => missingForTier,
421
428
  moduleDeniedForCaller: () => moduleDeniedForCaller,
422
429
  moduleGateEnforceKeys: () => moduleGateEnforceKeys,
423
430
  moduleGateMode: () => moduleGateMode,
@@ -472,6 +479,9 @@ __export(src_exports, {
472
479
  recordConsoleAction: () => recordConsoleAction,
473
480
  refuseServiceProviderInviteAction: () => refuseServiceProviderInviteAction,
474
481
  registerCameraTransport: () => registerCameraTransport,
482
+ registerOunitechBrand: () => registerOunitechBrand,
483
+ registerRobotAdapter: () => registerRobotAdapter,
484
+ registeredRobotBrands: () => registeredRobotBrands,
475
485
  relayForRecorder: () => relayForRecorder,
476
486
  remarksSchema: () => remarksSchema,
477
487
  renderPagePdf: () => renderPagePdf,
@@ -489,6 +499,7 @@ __export(src_exports, {
489
499
  resolveSiteTimezone: () => resolveSiteTimezone,
490
500
  resolveStaffRole: () => resolveStaffRole,
491
501
  robotActionsFor: () => robotActionsFor,
502
+ robotAdapterFor: () => robotAdapterFor,
492
503
  robotCloudBody: () => robotCloudBody,
493
504
  robotCloudSign: () => robotCloudSign,
494
505
  robotCloudSigningString: () => robotCloudSigningString,
@@ -497,6 +508,7 @@ __export(src_exports, {
497
508
  robotModeFromList: () => robotModeFromList,
498
509
  robotSchema: () => robotSchema,
499
510
  robotScopeForApp: () => robotScopeForApp,
511
+ robotTier: () => robotTier,
500
512
  robotTypesVisibleTo: () => robotTypesVisibleTo,
501
513
  rtspUrl: () => rtspUrl,
502
514
  schema: () => schema,
@@ -670,7 +682,9 @@ __export(src_exports, {
670
682
  stripFacialImageMetadata: () => stripFacialImageMetadata,
671
683
  subscriptionPlanSchema: () => subscriptionPlanSchema,
672
684
  summariseBulkCameraPlan: () => summariseBulkCameraPlan,
685
+ supports: () => supports,
673
686
  trackingBackoffMs: () => trackingBackoffMs,
687
+ uncontrollableFleetWarning: () => uncontrollableFleetWarning,
674
688
  updateRemarksStatusEod: () => updateRemarksStatusEod,
675
689
  updateRemarksisAcknowledged: () => updateRemarksisAcknowledged,
676
690
  updateSiteSchema: () => updateSiteSchema,
@@ -59003,6 +59017,7 @@ var robotSchema = import_joi74.default.object({
59003
59017
  // Optional, so every caller written before this field existed still validates.
59004
59018
  type: import_joi74.default.string().valid(...ROBOT_TYPES).optional().allow("", null),
59005
59019
  robotSn: import_joi74.default.string().optional().allow("", null),
59020
+ brand: import_joi74.default.string().optional().allow("", null),
59006
59021
  appKey: import_joi74.default.string().required(),
59007
59022
  appToken: import_joi74.default.string().required(),
59008
59023
  createdBy: import_joi74.default.string().hex().required(),
@@ -59064,6 +59079,7 @@ function MRobot(value) {
59064
59079
  */
59065
59080
  type: value.type || "cleaning",
59066
59081
  robotSn: value.robotSn ?? "",
59082
+ brand: value.brand || "ounitech",
59067
59083
  appKey: value.appKey ?? "",
59068
59084
  appToken: value.appToken ?? "",
59069
59085
  createdBy: value.createdBy ?? "",
@@ -59218,6 +59234,9 @@ function useRobotRepo() {
59218
59234
  // Not a secret - it is the vendor's device serial, and the apps
59219
59235
  // need it to address a robot. The CREDENTIALS stay out.
59220
59236
  robotSn: 1,
59237
+ // Not a secret - it names which protocol the robot speaks, and a
59238
+ // screen needs it to know which controls to draw.
59239
+ brand: 1,
59221
59240
  createdByName: 1,
59222
59241
  createdAt: 1,
59223
59242
  status: 1
@@ -59611,30 +59630,30 @@ function parseRobotCommand(body) {
59611
59630
  if (kind !== "full" && kind !== "area" && kind !== "spot" && kind !== "return") {
59612
59631
  return { ok: false, message: 'kind must be one of "full", "area", "spot", "return".' };
59613
59632
  }
59614
- const has = (k) => Object.prototype.hasOwnProperty.call(b, k);
59633
+ const has2 = (k) => Object.prototype.hasOwnProperty.call(b, k);
59615
59634
  const optionKeys = ["cleanTimes", "modeFlag", "subModeFlag", "levelFlag"];
59616
59635
  if (kind === "return") {
59617
59636
  for (const k of [...optionKeys, "areas", "points"]) {
59618
- if (has(k))
59637
+ if (has2(k))
59619
59638
  return { ok: false, message: `${k} is not valid for a return command.` };
59620
59639
  }
59621
59640
  return { ok: true, command: { kind: "return" } };
59622
59641
  }
59623
- if (kind !== "area" && has("areas")) {
59642
+ if (kind !== "area" && has2("areas")) {
59624
59643
  return { ok: false, message: "areas is only valid for an area command." };
59625
59644
  }
59626
- if (kind !== "spot" && has("points")) {
59645
+ if (kind !== "spot" && has2("points")) {
59627
59646
  return { ok: false, message: "points is only valid for a spot command." };
59628
59647
  }
59629
59648
  const int = (k, allowed, fallback) => {
59630
- if (!has(k))
59649
+ if (!has2(k))
59631
59650
  return fallback;
59632
59651
  const v = b[k];
59633
59652
  if (typeof v !== "number" || !Number.isInteger(v) || !allowed.includes(v))
59634
59653
  return null;
59635
59654
  return v;
59636
59655
  };
59637
- const cleanTimes = has("cleanTimes") ? typeof b.cleanTimes === "number" && Number.isInteger(b.cleanTimes) && b.cleanTimes >= 0 && b.cleanTimes <= 99 ? b.cleanTimes : null : DEFAULTS.cleanTimes;
59656
+ const cleanTimes = has2("cleanTimes") ? typeof b.cleanTimes === "number" && Number.isInteger(b.cleanTimes) && b.cleanTimes >= 0 && b.cleanTimes <= 99 ? b.cleanTimes : null : DEFAULTS.cleanTimes;
59638
59657
  if (cleanTimes === null)
59639
59658
  return { ok: false, message: "cleanTimes must be an integer 0-99." };
59640
59659
  const modeFlag = int("modeFlag", [1, 2, 3], DEFAULTS.modeFlag);
@@ -59850,6 +59869,125 @@ function robotActionsFor(actor) {
59850
59869
  );
59851
59870
  }
59852
59871
 
59872
+ // src/utils/robot-adapter.util.ts
59873
+ var ROBOT_CAPABILITIES = [
59874
+ /** Read the current location of the robot [x, y, yaw]. */
59875
+ "readLocation",
59876
+ /** Request the robot move to an [x, y, yaw] coordinate. */
59877
+ "navigateToPoint",
59878
+ /** Pause a robot while it is navigating, and resume it. */
59879
+ "pauseResume",
59880
+ /** Read the route/path taken, or the ETA to the destination. */
59881
+ "pathAndEta",
59882
+ /** Read the battery level. */
59883
+ "battery",
59884
+ /** Send the robot to its docking/charging station. */
59885
+ "sendToDock",
59886
+ /** Switch the on-board map and re-localise the robot. */
59887
+ "relocalize",
59888
+ /** Start a process — "clean zone A". */
59889
+ "startProcess",
59890
+ /** Pause, resume or stop a running process. */
59891
+ "pauseProcess",
59892
+ /** Know, rather than infer, when a process has finished. */
59893
+ "processCompletion",
59894
+ /** Report which floor the robot is on. */
59895
+ "floorReporting",
59896
+ /** Ride a lift between floors under our coordination. */
59897
+ "multiFloor"
59898
+ ];
59899
+ var TIER_REQUIREMENTS = {
59900
+ "full-control": [
59901
+ "readLocation",
59902
+ "navigateToPoint",
59903
+ "pauseResume",
59904
+ "pathAndEta",
59905
+ "battery",
59906
+ "sendToDock",
59907
+ "relocalize",
59908
+ "startProcess",
59909
+ "pauseProcess",
59910
+ "processCompletion"
59911
+ ],
59912
+ "traffic-light": [
59913
+ "readLocation",
59914
+ "pauseResume",
59915
+ "battery",
59916
+ "sendToDock",
59917
+ "startProcess",
59918
+ "pauseProcess",
59919
+ "processCompletion"
59920
+ ],
59921
+ "read-only": ["readLocation", "pathAndEta", "battery", "processCompletion"]
59922
+ };
59923
+ var has = (caps, c) => Object.prototype.hasOwnProperty.call(caps, c) && caps[c] === true;
59924
+ function robotTier(capabilities) {
59925
+ const caps = capabilities ?? {};
59926
+ for (const tier of ["full-control", "traffic-light", "read-only"]) {
59927
+ if (TIER_REQUIREMENTS[tier].every((c) => has(caps, c)))
59928
+ return tier;
59929
+ }
59930
+ return "no-interface";
59931
+ }
59932
+ function missingForTier(capabilities, tier) {
59933
+ const caps = capabilities ?? {};
59934
+ return TIER_REQUIREMENTS[tier].filter((c) => !has(caps, c));
59935
+ }
59936
+ function supports(capabilities, capability) {
59937
+ return has(capabilities ?? {}, capability);
59938
+ }
59939
+ function uncontrollableFleetWarning(tiers) {
59940
+ const count = tiers.filter(
59941
+ (t) => t === "read-only" || t === "no-interface"
59942
+ ).length;
59943
+ if (count <= 1) {
59944
+ return { ok: true, count, message: "" };
59945
+ }
59946
+ return {
59947
+ ok: false,
59948
+ count,
59949
+ message: `This site has ${count} robot fleets that cannot be controlled (read-only or no interface). A shared space supports at most one: beyond that, robots cannot reliably be kept out of each other's way.`
59950
+ };
59951
+ }
59952
+ var COMMAND_NEEDS = {
59953
+ full: "startProcess",
59954
+ area: "startProcess",
59955
+ spot: "startProcess",
59956
+ return: "sendToDock",
59957
+ pause: "pauseProcess",
59958
+ resume: "pauseProcess",
59959
+ cancel: "pauseProcess",
59960
+ navigate: "navigateToPoint",
59961
+ relocalize: "relocalize"
59962
+ };
59963
+ function commandSupported(capabilities, kind) {
59964
+ const needed = Object.prototype.hasOwnProperty.call(COMMAND_NEEDS, kind) ? COMMAND_NEEDS[kind] : void 0;
59965
+ if (!needed)
59966
+ return false;
59967
+ return supports(capabilities, needed);
59968
+ }
59969
+ var adapters = /* @__PURE__ */ new Map();
59970
+ function registerRobotAdapter(adapter) {
59971
+ if (!adapter?.brand)
59972
+ throw new Error("A robot adapter must name its brand.");
59973
+ adapters.set(adapter.brand, adapter);
59974
+ }
59975
+ function robotAdapterFor(brand) {
59976
+ if (typeof brand !== "string" || !brand)
59977
+ return null;
59978
+ return adapters.get(brand) ?? null;
59979
+ }
59980
+ function registeredRobotBrands() {
59981
+ return [...adapters.values()].map((a) => ({
59982
+ brand: a.brand,
59983
+ displayName: a.displayName,
59984
+ tier: robotTier(a.capabilities)
59985
+ }));
59986
+ }
59987
+ function clearRobotAdapters() {
59988
+ adapters.clear();
59989
+ }
59990
+
59853
59991
  // src/utils/lift-contract.util.ts
59854
59992
  var LIFT_DOOR = {
59855
59993
  CLOSED: 0,
@@ -60246,6 +60384,129 @@ function useRobotFleetService(deps) {
60246
60384
  };
60247
60385
  }
60248
60386
 
60387
+ // src/services/ounitech.adapter.ts
60388
+ var OUNITECH_BRAND = "ounitech";
60389
+ var OUNITECH_CAPABILITIES = {
60390
+ // The tracking stream carries x, y, theta.
60391
+ readLocation: true,
60392
+ // `/portal/robots/return-to-base`.
60393
+ sendToDock: true,
60394
+ // `start-full-cleaning`, `start-area-cleaning`, `start-spot-cleaning` — RMF's
60395
+ // "start a process (such as clean Zone_A)", and their areas are RMF's zones.
60396
+ startProcess: true,
60397
+ // Everything below is FALSE on purpose, and each one is a question already put
60398
+ // to the vendor. Their app can do most of it; the API cannot.
60399
+ navigateToPoint: false,
60400
+ // app: "Move to Target Point in standby mode"
60401
+ relocalize: false,
60402
+ // app: "Set Origin"
60403
+ pauseResume: false,
60404
+ // handlebar + app: Pause/Resume, long-press cancel
60405
+ pauseProcess: false,
60406
+ // app: "End work"
60407
+ battery: false,
60408
+ // nothing in the API carries it
60409
+ pathAndEta: false,
60410
+ // nothing in the API carries it
60411
+ processCompletion: false,
60412
+ // no task id is returned, so completion is inferred
60413
+ floorReporting: false,
60414
+ // the tracking stream has no floorNo
60415
+ multiFloor: false
60416
+ // their app does cross-floor; the API does not expose it
60417
+ };
60418
+ function createOunitechAdapter(credentials, options) {
60419
+ const cloud = useRobotCloudService(credentials, options);
60420
+ function toAdapterRobot(raw) {
60421
+ return {
60422
+ robotSn: raw.robotSn,
60423
+ name: raw.robotName,
60424
+ online: raw.onlineFlag !== false,
60425
+ mode: robotModeFromList(raw),
60426
+ statusName: raw.statusName,
60427
+ // Null, not 0 or a guess. A screen must be able to tell "we do not know"
60428
+ // from "empty", and `floorKnown: false` is what stops a robot being drawn
60429
+ // on a storey nobody confirmed.
60430
+ batteryPercent: null,
60431
+ floorNo: null,
60432
+ floorKnown: false,
60433
+ position: null
60434
+ };
60435
+ }
60436
+ return {
60437
+ brand: OUNITECH_BRAND,
60438
+ displayName: "Ounitech (Q3-G / Q3-W)",
60439
+ capabilities: OUNITECH_CAPABILITIES,
60440
+ async listRobots() {
60441
+ const fleet = await cloud.getRobots();
60442
+ return fleet.map(toAdapterRobot);
60443
+ },
60444
+ async getRobot(robotSn) {
60445
+ const fleet = await cloud.getRobots();
60446
+ const match = fleet.find((r) => r.robotSn === robotSn);
60447
+ if (!match) {
60448
+ return {
60449
+ robotSn,
60450
+ online: false,
60451
+ mode: "UNAVAILABLE",
60452
+ statusName: "Not found in the robot cloud",
60453
+ batteryPercent: null,
60454
+ floorNo: null,
60455
+ floorKnown: false,
60456
+ position: null
60457
+ };
60458
+ }
60459
+ return toAdapterRobot(match);
60460
+ },
60461
+ async sendCommand(robotSn, command) {
60462
+ const options2 = {
60463
+ cleanTimes: 1,
60464
+ modeFlag: 1,
60465
+ subModeFlag: 0,
60466
+ levelFlag: 1
60467
+ };
60468
+ switch (command.kind) {
60469
+ case "full":
60470
+ await cloud.startFullCleaning(robotSn, options2);
60471
+ return;
60472
+ case "area":
60473
+ await cloud.startAreaCleaning(robotSn, command.areas, options2);
60474
+ return;
60475
+ case "spot":
60476
+ await cloud.startSpotCleaning(robotSn, command.points, options2);
60477
+ return;
60478
+ case "return":
60479
+ await cloud.returnToBase(robotSn);
60480
+ return;
60481
+ default:
60482
+ throw new Error(
60483
+ `Ounitech robots do not support "${command.kind}" through the API yet.`
60484
+ );
60485
+ }
60486
+ },
60487
+ async getMaps(robotSn) {
60488
+ return cloud.getMaps(robotSn);
60489
+ },
60490
+ async getPlans(robotSn) {
60491
+ return cloud.getPlans(robotSn);
60492
+ }
60493
+ };
60494
+ }
60495
+ function registerOunitechBrand() {
60496
+ const unconfigured = () => {
60497
+ throw new Error(
60498
+ "No Ounitech credentials for this robot. Add the app key and token on the robot's record."
60499
+ );
60500
+ };
60501
+ registerRobotAdapter({
60502
+ brand: OUNITECH_BRAND,
60503
+ displayName: "Ounitech (Q3-G / Q3-W)",
60504
+ capabilities: OUNITECH_CAPABILITIES,
60505
+ listRobots: async () => unconfigured(),
60506
+ getRobot: async () => unconfigured()
60507
+ });
60508
+ }
60509
+
60249
60510
  // src/controllers/robot-fleet.controller.ts
60250
60511
  var import_joi76 = __toESM(require("joi"));
60251
60512
  var import_node_server_utils158 = require("@7365admin1/node-server-utils");
@@ -93821,6 +94082,8 @@ function usePersonalEmergencyChainController() {
93821
94082
  ORG_OWNER_REPAIR_CLAIM_TAKEN_REASON,
93822
94083
  ORG_OWNER_REPAIR_DEFAULT,
93823
94084
  ORG_OWNER_REPAIR_OFF_REASON,
94085
+ OUNITECH_BRAND,
94086
+ OUNITECH_CAPABILITIES,
93824
94087
  OrgNature,
93825
94088
  OvernightParkingRequestSort,
93826
94089
  OvernightParkingRequestStatus,
@@ -93848,6 +94111,7 @@ function usePersonalEmergencyChainController() {
93848
94111
  REALTIME_MAX_FANOUT,
93849
94112
  REGISTERED_VEHICLE_CATEGORIES,
93850
94113
  ROBOT_APP_SCOPES,
94114
+ ROBOT_CAPABILITIES,
93851
94115
  ROBOT_CLOUD_DOC_CONFIRMATIONS,
93852
94116
  ROBOT_CLOUD_PATHS,
93853
94117
  ROBOT_MODULE,
@@ -93936,13 +94200,16 @@ function usePersonalEmergencyChainController() {
93936
94200
  chatSchema,
93937
94201
  claimReadsAsStale,
93938
94202
  clampPtzSpeed,
94203
+ clearRobotAdapters,
93939
94204
  clockDriftSeconds,
94205
+ commandSupported,
93940
94206
  consoleRoleAllows,
93941
94207
  consoleRoleAllowsAll,
93942
94208
  consoleSpellings,
93943
94209
  console_audit_namespace_collection,
93944
94210
  createLiftSimulator,
93945
94211
  createManpowerRemarksDaily,
94212
+ createOunitechAdapter,
93946
94213
  createSseParser,
93947
94214
  customerSchema,
93948
94215
  customerSitePropertyFields,
@@ -94024,6 +94291,7 @@ function usePersonalEmergencyChainController() {
94024
94291
  maskNric,
94025
94292
  memberModulesHandlers,
94026
94293
  metresToPixel,
94294
+ missingForTier,
94027
94295
  moduleDeniedForCaller,
94028
94296
  moduleGateEnforceKeys,
94029
94297
  moduleGateMode,
@@ -94078,6 +94346,9 @@ function usePersonalEmergencyChainController() {
94078
94346
  recordConsoleAction,
94079
94347
  refuseServiceProviderInviteAction,
94080
94348
  registerCameraTransport,
94349
+ registerOunitechBrand,
94350
+ registerRobotAdapter,
94351
+ registeredRobotBrands,
94081
94352
  relayForRecorder,
94082
94353
  remarksSchema,
94083
94354
  renderPagePdf,
@@ -94095,6 +94366,7 @@ function usePersonalEmergencyChainController() {
94095
94366
  resolveSiteTimezone,
94096
94367
  resolveStaffRole,
94097
94368
  robotActionsFor,
94369
+ robotAdapterFor,
94098
94370
  robotCloudBody,
94099
94371
  robotCloudSign,
94100
94372
  robotCloudSigningString,
@@ -94103,6 +94375,7 @@ function usePersonalEmergencyChainController() {
94103
94375
  robotModeFromList,
94104
94376
  robotSchema,
94105
94377
  robotScopeForApp,
94378
+ robotTier,
94106
94379
  robotTypesVisibleTo,
94107
94380
  rtspUrl,
94108
94381
  schema,
@@ -94276,7 +94549,9 @@ function usePersonalEmergencyChainController() {
94276
94549
  stripFacialImageMetadata,
94277
94550
  subscriptionPlanSchema,
94278
94551
  summariseBulkCameraPlan,
94552
+ supports,
94279
94553
  trackingBackoffMs,
94554
+ uncontrollableFleetWarning,
94280
94555
  updateRemarksStatusEod,
94281
94556
  updateRemarksisAcknowledged,
94282
94557
  updateSiteSchema,