@7365admin1/core 3.137.0 → 3.139.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.mjs CHANGED
@@ -3200,6 +3200,53 @@ function sameAppType(a, b) {
3200
3200
  return !!a && !!b && serviceAppType(a) === serviceAppType(b);
3201
3201
  }
3202
3202
 
3203
+ // src/utils/member-lookup.util.ts
3204
+ var idOf = (v) => v == null ? "" : String(v?._id ?? v).toLowerCase();
3205
+ function isLiveMembership(m) {
3206
+ return m.status !== "deleted" && !m.deletedAt;
3207
+ }
3208
+ function pickSiteMembership(rows, type, site, org = "") {
3209
+ site = site.toLowerCase();
3210
+ org = org.toLowerCase();
3211
+ if (!site)
3212
+ return null;
3213
+ const live2 = rows.filter(
3214
+ (m) => sameAppType(m.type, type) && isLiveMembership(m) && (!org || idOf(m.org) === org)
3215
+ );
3216
+ const atSite = live2.find((m) => idOf(m.siteId) === site);
3217
+ if (atSite)
3218
+ return atSite;
3219
+ if (!org)
3220
+ return null;
3221
+ return live2.find((m) => !idOf(m.siteId)) ?? live2[0] ?? null;
3222
+ }
3223
+ function pickLookupMembership(rows, type, site = "", org = "") {
3224
+ const list = Array.isArray(rows) ? rows : [];
3225
+ const atSite = site ? pickSiteMembership(list, type, site, org) : null;
3226
+ if (atSite)
3227
+ return atSite;
3228
+ const inOrg = org ? list.find((m) => sameAppType(m.type, type) && idOf(m.org) === org.toLowerCase()) : void 0;
3229
+ if (inOrg && isLiveMembership(inOrg))
3230
+ return inOrg;
3231
+ return preferWorkingMembership(list.filter((m) => sameAppType(m.type, type)));
3232
+ }
3233
+ var WORKING = /* @__PURE__ */ new Set(["active", "approved"]);
3234
+ function preferWorkingMembership(rows, defaultOrg = "") {
3235
+ const list = Array.isArray(rows) ? rows : [];
3236
+ const working = list.filter((m) => WORKING.has(String(m.status ?? "")) && !m.deletedAt);
3237
+ const home = idOf(defaultOrg);
3238
+ return (home ? working.find((m) => idOf(m.org) === home) : void 0) ?? working[0] ?? list.find(isLiveMembership) ?? list[0] ?? null;
3239
+ }
3240
+ function memberListSiteScope(memberships, org) {
3241
+ const inOrg = (memberships ?? []).filter((m) => idOf(m.org) === idOf(org));
3242
+ const here = inOrg.filter((m) => m.type !== "resident");
3243
+ if (!inOrg.length)
3244
+ return null;
3245
+ if (here.some((m) => !idOf(m.siteId) || m.orgLevelRole))
3246
+ return null;
3247
+ return [...new Set(here.map((m) => idOf(m.siteId)))];
3248
+ }
3249
+
3203
3250
  // src/repositories/member.repo.ts
3204
3251
  import {
3205
3252
  AppError as AppError2,
@@ -3332,10 +3379,10 @@ import { ObjectId as ObjectId13 } from "mongodb";
3332
3379
  function crossSiteRoleMode() {
3333
3380
  return (process.env.CROSS_SITE_ROLE_MODE ?? "").trim().toLowerCase() === "enforce" ? "enforce" : "log";
3334
3381
  }
3335
- var idOf = (value) => value === null || value === void 0 ? "" : String(value?.toString?.() ?? value);
3382
+ var idOf2 = (value) => value === null || value === void 0 ? "" : String(value?.toString?.() ?? value);
3336
3383
  function isCrossSiteRole(role, memberSite) {
3337
- const roleSite = idOf(role?.site);
3338
- return !!roleSite && roleSite !== idOf(memberSite);
3384
+ const roleSite = idOf2(role?.site);
3385
+ return !!roleSite && roleSite !== idOf2(memberSite);
3339
3386
  }
3340
3387
  var THROTTLE_MS = 10 * 60 * 1e3;
3341
3388
  var lastLogged = /* @__PURE__ */ new Map();
@@ -3344,7 +3391,7 @@ function crossSiteRoleGrants(input) {
3344
3391
  return true;
3345
3392
  const mode = crossSiteRoleMode();
3346
3393
  const granted = mode !== "enforce";
3347
- const key = [mode, idOf(input.member) || idOf(input.user), idOf(input.role?._id), input.where].join("|");
3394
+ const key = [mode, idOf2(input.member) || idOf2(input.user), idOf2(input.role?._id), input.where].join("|");
3348
3395
  const now = Date.now();
3349
3396
  if ((lastLogged.get(key) ?? 0) + THROTTLE_MS <= now) {
3350
3397
  lastLogged.set(key, now);
@@ -3352,7 +3399,7 @@ function crossSiteRoleGrants(input) {
3352
3399
  lastLogged.clear();
3353
3400
  logger7.log({
3354
3401
  level: "warn",
3355
- message: `cross-site-role mode=${mode} ${granted ? "granted" : "refused"} where=${input.where} member=${idOf(input.member) || "-"} user=${idOf(input.user) || "-"} site=${idOf(input.memberSite) || "-"} role=${idOf(input.role?._id) || "-"} roleSite=${idOf(input.role?.site)}`
3402
+ message: `cross-site-role mode=${mode} ${granted ? "granted" : "refused"} where=${input.where} member=${idOf2(input.member) || "-"} user=${idOf2(input.user) || "-"} site=${idOf2(input.memberSite) || "-"} role=${idOf2(input.role?._id) || "-"} roleSite=${idOf2(input.role?.site)}`
3356
3403
  });
3357
3404
  }
3358
3405
  return granted;
@@ -3363,7 +3410,7 @@ function crossSiteRoleRefusal(roleName, roleSiteName) {
3363
3410
  return `${role} belongs to ${site}. Choose a role for this site, or an org-wide role.`;
3364
3411
  }
3365
3412
  async function requireRoleFitsSite(roleId, siteId) {
3366
- const id = idOf(roleId);
3413
+ const id = idOf2(roleId);
3367
3414
  if (!id || !ObjectId13.isValid(id))
3368
3415
  return;
3369
3416
  const db2 = useAtlas7.getDb();
@@ -3372,7 +3419,7 @@ async function requireRoleFitsSite(roleId, siteId) {
3372
3419
  const role = await db2.collection("roles").findOne({ _id: new ObjectId13(id) }, { projection: { name: 1, site: 1 } });
3373
3420
  if (!isCrossSiteRole(role, siteId))
3374
3421
  return;
3375
- const site = ObjectId13.isValid(idOf(role?.site)) ? await db2.collection("sites").findOne({ _id: new ObjectId13(idOf(role?.site)) }, { projection: { name: 1 } }) : null;
3422
+ const site = ObjectId13.isValid(idOf2(role?.site)) ? await db2.collection("sites").findOne({ _id: new ObjectId13(idOf2(role?.site)) }, { projection: { name: 1 } }) : null;
3376
3423
  throw new BadRequestError13(crossSiteRoleRefusal(role?.name, site?.name));
3377
3424
  }
3378
3425
  async function otherSiteRoleClauses(roles, org) {
@@ -3380,7 +3427,7 @@ async function otherSiteRoleClauses(roles, org) {
3380
3427
  { ...org ? { org } : { org: { $nin: ["", null] } }, site: { $nin: ["", null] }, status: "active" },
3381
3428
  { projection: { site: 1, org: 1 } }
3382
3429
  ).toArray();
3383
- const both3 = (v) => ObjectId13.isValid(idOf(v)) ? [new ObjectId13(idOf(v)), idOf(v)] : [v];
3430
+ const both3 = (v) => ObjectId13.isValid(idOf2(v)) ? [new ObjectId13(idOf2(v)), idOf2(v)] : [v];
3384
3431
  const clauses = (list ?? []).map((r) => ({
3385
3432
  role: r._id,
3386
3433
  org: { $in: both3(r.org) },
@@ -3563,9 +3610,12 @@ function useMemberRepo() {
3563
3610
  } catch (error) {
3564
3611
  throw new BadRequestError14("Invalid user ID format.");
3565
3612
  }
3613
+ const account = await db2?.collection("users").findOne({ _id: user }, { projection: { defaultOrg: 1 } }).catch(() => null);
3614
+ const defaultOrg = String(account?.defaultOrg ?? "");
3566
3615
  const cacheKey = makeCacheKey5(namespace_collection3, {
3567
3616
  user: user.toString(),
3568
- type
3617
+ type,
3618
+ defaultOrg
3569
3619
  });
3570
3620
  const cachedData = await getCache(cacheKey);
3571
3621
  if (cachedData) {
@@ -3573,7 +3623,10 @@ function useMemberRepo() {
3573
3623
  return cachedData;
3574
3624
  }
3575
3625
  try {
3576
- const data = await collection.findOne({ user, type: appTypeQuery(type) });
3626
+ const data = preferWorkingMembership(
3627
+ await collection.find({ user, type: appTypeQuery(type) }).toArray(),
3628
+ defaultOrg
3629
+ );
3577
3630
  if (!data) {
3578
3631
  throw new NotFoundError5("Member not found.");
3579
3632
  }
@@ -4036,7 +4089,12 @@ function useMemberRepo() {
4036
4089
  const query2 = {
4037
4090
  user,
4038
4091
  org: { $nin: [null, ""] },
4039
- type: appTypeQuery(type)
4092
+ type: appTypeQuery(type),
4093
+ // The organisation switcher. A membership that was taken away (Repair
4094
+ // sets it inactive, removal deletes it) must not keep the organisation
4095
+ // on the person's list: latitude.sec@ still saw JLL Singapore beside its
4096
+ // own company after the repair (owner report 2026-09-30).
4097
+ status: { $nin: ["inactive", "deleted"] }
4040
4098
  };
4041
4099
  const cacheOptions = {
4042
4100
  page: page2,
@@ -10280,7 +10338,7 @@ function robotFaultText(robotName, fault) {
10280
10338
  }
10281
10339
  return {
10282
10340
  title: "Robot fault",
10283
- body: `${robotName || "A robot"} reports a fault: ${fault.message || fault.code}.`
10341
+ body: `${robotName || "A robot"} reports a fault: ${String(fault.message || fault.code).replace(/\.\s*$/, "")}.`
10284
10342
  };
10285
10343
  }
10286
10344
  function robotConnectionText(robotName, online, minutes) {
@@ -10541,7 +10599,7 @@ function enforcedModuleList(stored, save) {
10541
10599
  return NO_LIST;
10542
10600
  return { list, since: typeof save?.createdAt === "string" ? save.createdAt : "" };
10543
10601
  }
10544
- var idOf2 = (v) => v == null ? "" : String(v?._id ?? v).toLowerCase();
10602
+ var idOf3 = (v) => v == null ? "" : String(v?._id ?? v).toLowerCase();
10545
10603
  function deniedForMembership({
10546
10604
  membership,
10547
10605
  org,
@@ -10549,12 +10607,12 @@ function deniedForMembership({
10549
10607
  site,
10550
10608
  siteSave
10551
10609
  }) {
10552
- const memberOrg = idOf2(membership?.org);
10610
+ const memberOrg = idOf3(membership?.org);
10553
10611
  if (!membership || membership.type === "resident" || !memberOrg) {
10554
10612
  return { denied: [], since: "" };
10555
10613
  }
10556
10614
  const orgTier = enforcedModuleList(org?.modules, orgSave);
10557
- const siteTier = site && idOf2(site.orgId) === memberOrg ? enforcedModuleList(site.metadata?.modules, siteSave) : NO_LIST;
10615
+ const siteTier = site && idOf3(site.orgId) === memberOrg ? enforcedModuleList(site.metadata?.modules, siteSave) : NO_LIST;
10558
10616
  const denied = deniedModules(orgTier.list, siteTier.list);
10559
10617
  if (!denied.length)
10560
10618
  return { denied: [], since: "" };
@@ -11128,46 +11186,6 @@ function staffMayBypass(actor, grant = CROSS_CLIENT_GRANT) {
11128
11186
  return consoleRoleAllows(actor.staffPermissions, grant.resource, grant.action);
11129
11187
  }
11130
11188
 
11131
- // src/utils/member-lookup.util.ts
11132
- var idOf3 = (v) => v == null ? "" : String(v?._id ?? v).toLowerCase();
11133
- function isLiveMembership(m) {
11134
- return m.status !== "deleted" && !m.deletedAt;
11135
- }
11136
- function pickSiteMembership(rows, type, site, org = "") {
11137
- site = site.toLowerCase();
11138
- org = org.toLowerCase();
11139
- if (!site)
11140
- return null;
11141
- const live2 = rows.filter(
11142
- (m) => sameAppType(m.type, type) && isLiveMembership(m) && (!org || idOf3(m.org) === org)
11143
- );
11144
- const atSite = live2.find((m) => idOf3(m.siteId) === site);
11145
- if (atSite)
11146
- return atSite;
11147
- if (!org)
11148
- return null;
11149
- return live2.find((m) => !idOf3(m.siteId)) ?? live2[0] ?? null;
11150
- }
11151
- function pickLookupMembership(rows, type, site = "", org = "") {
11152
- const list = Array.isArray(rows) ? rows : [];
11153
- const atSite = site ? pickSiteMembership(list, type, site, org) : null;
11154
- if (atSite)
11155
- return atSite;
11156
- const inOrg = org ? list.find((m) => sameAppType(m.type, type) && idOf3(m.org) === org.toLowerCase()) : void 0;
11157
- if (inOrg && isLiveMembership(inOrg))
11158
- return inOrg;
11159
- return list.find((m) => sameAppType(m.type, type)) ?? null;
11160
- }
11161
- function memberListSiteScope(memberships, org) {
11162
- const inOrg = (memberships ?? []).filter((m) => idOf3(m.org) === idOf3(org));
11163
- const here = inOrg.filter((m) => m.type !== "resident");
11164
- if (!inOrg.length)
11165
- return null;
11166
- if (here.some((m) => !idOf3(m.siteId) || m.orgLevelRole))
11167
- return null;
11168
- return [...new Set(here.map((m) => idOf3(m.siteId)))];
11169
- }
11170
-
11171
11189
  // src/services/module-gate.service.ts
11172
11190
  var idOf4 = (v) => v == null ? "" : String(v?._id ?? v).toLowerCase();
11173
11191
  var isId = (v) => /^[0-9a-f]{24}$/i.test(v);
@@ -24560,7 +24578,7 @@ function useServiceProviderInviteService() {
24560
24578
  );
24561
24579
  }
24562
24580
  const existing = await getByCreatorAndNature(actor.id, nature).catch(() => null);
24563
- if (existing) {
24581
+ if (existing && String(existing.invitedFrom ?? "") !== String(invite._id)) {
24564
24582
  throw new BadRequestError47(
24565
24583
  `You already have a ${serviceTypeLabel(nature)} company on iService365 (${existing.name}). Accept the invitation from that company instead.`
24566
24584
  );
@@ -24591,12 +24609,21 @@ function useServiceProviderInviteService() {
24591
24609
  country
24592
24610
  }) {
24593
24611
  const inviteeId = String(user?._id ?? "");
24594
- const name = sanitizeCompanyName(companyName) || sanitizeCompanyName(invite.metadata?.name) || sanitizeCompanyName(user?.name);
24612
+ let name = sanitizeCompanyName(companyName) || sanitizeCompanyName(invite.metadata?.name) || sanitizeCompanyName(user?.name);
24595
24613
  if (!name)
24596
24614
  throw new BadRequestError47("Enter your company's name.");
24597
24615
  const email = String(invite.email ?? "").trim().toLowerCase();
24598
24616
  const actor = { id: inviteeId };
24599
- const orgId = String(
24617
+ const db2 = useAtlas38.getDb();
24618
+ const actorIds = [actor.id, ...ObjectId56.isValid(actor.id) ? [new ObjectId56(actor.id)] : []];
24619
+ const made = db2 ? await db2.collection("organizations").findOne({
24620
+ invitedFrom: { $in: [String(invite._id), invite._id] },
24621
+ createdBy: { $in: actorIds },
24622
+ status: { $ne: "deleted" }
24623
+ }) : null;
24624
+ if (made)
24625
+ name = String(made.name ?? name);
24626
+ const orgId = made ? String(made._id) : String(
24600
24627
  await addOrg({
24601
24628
  name,
24602
24629
  type: nature,
@@ -24608,8 +24635,17 @@ function useServiceProviderInviteService() {
24608
24635
  invitedFrom: String(invite._id)
24609
24636
  })
24610
24637
  );
24611
- const roles = await seedDefaultOrgRoles(useRoleRepo().addRole, orgId, nature);
24612
24638
  const appType = serviceAppType(nature);
24639
+ const owners = made && db2 ? await db2.collection("roles").find({
24640
+ org: { $in: [new ObjectId56(orgId), orgId] },
24641
+ name: "owner",
24642
+ status: { $ne: "deleted" }
24643
+ }).toArray() : [];
24644
+ const ownerRole = (type) => {
24645
+ const r = owners.find((x) => x.type === type);
24646
+ return r ? { id: String(r._id), type, name: "owner" } : null;
24647
+ };
24648
+ const roles = ownerRole("organization") && ownerRole(appType) ? [ownerRole("organization"), ownerRole(appType)] : await seedDefaultOrgRoles(useRoleRepo().addRole, orgId, nature);
24613
24649
  const orgOwner = roles.find((r) => r.type === "organization" && r.name === "owner");
24614
24650
  const appOwner = roles.find((r) => r.type === appType && r.name === "owner");
24615
24651
  if (!orgOwner || !appOwner) {
@@ -24632,8 +24668,18 @@ function useServiceProviderInviteService() {
24632
24668
  siteName: "",
24633
24669
  onboardingRequired: false
24634
24670
  };
24635
- await addMember({ ...base, role: orgOwner.id, type: "organization" });
24636
- await addMember({ ...base, role: appOwner.id, type: appType });
24671
+ const hasOwnerRow = async (type) => !!made && !!await db2?.collection("members").findOne({
24672
+ user: { $in: actorIds },
24673
+ org: { $in: [new ObjectId56(orgId), orgId] },
24674
+ type,
24675
+ status: "active"
24676
+ });
24677
+ if (!await hasOwnerRow("organization")) {
24678
+ await addMember({ ...base, role: orgOwner.id, type: "organization" });
24679
+ }
24680
+ if (!await hasOwnerRow(appType)) {
24681
+ await addMember({ ...base, role: appOwner.id, type: appType });
24682
+ }
24637
24683
  await useUserRepo().updateUserFieldById({ _id: actor.id, field: "defaultOrg", value: orgId }).catch(() => void 0);
24638
24684
  logger36.log({
24639
24685
  level: "info",
@@ -24689,10 +24735,60 @@ function useServiceProviderInviteService() {
24689
24735
  const last = [...history].reverse().find((e) => ["wrong-join-repaired", "wrong-join-undone"].includes(e?.action));
24690
24736
  return last?.action === "wrong-join-repaired" ? last : null;
24691
24737
  }
24738
+ async function leftoverRoles(invite, senderOrg, user) {
24739
+ const db2 = useAtlas38.getDb();
24740
+ if (!db2 || !user?._id || !ObjectId56.isValid(senderOrg))
24741
+ return [];
24742
+ const since = new Date(String(invite.createdAt ?? 0)).getTime();
24743
+ const rows = await db2.collection("members").find({
24744
+ user: { $in: [user._id, String(user._id)] },
24745
+ org: { $in: [new ObjectId56(senderOrg), senderOrg] }
24746
+ }).project({ role: 1 }).toArray();
24747
+ const roleIds = [
24748
+ ...new Set(
24749
+ rows.map((m) => m.role?.toString?.() ?? "").filter((r) => ObjectId56.isValid(r))
24750
+ )
24751
+ ].map((r) => new ObjectId56(r));
24752
+ if (!roleIds.length)
24753
+ return [];
24754
+ const roles = await db2.collection("roles").find({
24755
+ _id: { $in: roleIds },
24756
+ org: { $in: [new ObjectId56(senderOrg), senderOrg] },
24757
+ status: { $ne: "deleted" }
24758
+ }).project({ name: 1, permissions: 1, createdAt: 1 }).toArray();
24759
+ const out = [];
24760
+ for (const role of roles) {
24761
+ if (!(Array.isArray(role.permissions) && role.permissions.includes("*")))
24762
+ continue;
24763
+ if (role.createdAt && Number.isFinite(since) && new Date(role.createdAt).getTime() < since)
24764
+ continue;
24765
+ const inUse = await db2.collection("members").countDocuments({
24766
+ role: { $in: [role._id, role._id.toString()] },
24767
+ status: "active"
24768
+ });
24769
+ if (inUse)
24770
+ continue;
24771
+ out.push({ _id: role._id.toString(), name: String(role.name ?? "") });
24772
+ }
24773
+ return out;
24774
+ }
24775
+ async function setRoleStatus(ids2, status) {
24776
+ const db2 = useAtlas38.getDb();
24777
+ const valid = ids2.filter((r) => ObjectId56.isValid(r)).map((r) => new ObjectId56(r));
24778
+ if (!db2 || !valid.length)
24779
+ return;
24780
+ const now = (/* @__PURE__ */ new Date()).toISOString();
24781
+ await db2.collection("roles").updateMany(
24782
+ { _id: { $in: valid } },
24783
+ { $set: { status, deletedAt: status === "deleted" ? now : "", updatedAt: now } }
24784
+ );
24785
+ await useCache21("roles").delNamespace().catch(() => void 0);
24786
+ }
24692
24787
  async function wrongJoinPreview({ id, userId }) {
24693
24788
  const { invite, senderOrg, user } = await wrongJoinContext(id, userId);
24694
24789
  const org = await getOrgById(senderOrg).catch(() => null);
24695
24790
  const memberships = await wrongJoinRows(invite, senderOrg, user);
24791
+ const roles = await leftoverRoles(invite, senderOrg, user);
24696
24792
  const repaired = lastRepair(invite);
24697
24793
  return {
24698
24794
  invitationId: id,
@@ -24701,6 +24797,8 @@ function useServiceProviderInviteService() {
24701
24797
  senderOrgId: senderOrg,
24702
24798
  senderOrgName: String(org?.name ?? ""),
24703
24799
  memberships,
24800
+ // Left in the sender's role list by the old sign-up; Repair retires them.
24801
+ leftoverRoles: roles,
24704
24802
  defaultOrgIsSender: String(user?.defaultOrg ?? "") === senderOrg,
24705
24803
  repaired: repaired ? { at: repaired.at, by: repaired.byName, memberIds: repaired.memberIds ?? [] } : null
24706
24804
  };
@@ -24709,15 +24807,17 @@ function useServiceProviderInviteService() {
24709
24807
  const { actor, invite, senderOrg, user } = await wrongJoinContext(id, userId);
24710
24808
  const rows = await wrongJoinRows(invite, senderOrg, user);
24711
24809
  const defaultOrgIsSender = !!user?._id && String(user?.defaultOrg ?? "") === senderOrg;
24712
- if (!rows.length && !defaultOrgIsSender) {
24810
+ const { updateMemberStatus } = useMemberRepo();
24811
+ for (const row of rows)
24812
+ await updateMemberStatus(row._id, { status: "inactive" });
24813
+ const roles = await leftoverRoles(invite, senderOrg, user);
24814
+ if (!rows.length && !defaultOrgIsSender && !roles.length) {
24713
24815
  return {
24714
24816
  message: "Nothing to repair: this account is not in the sender's organisation.",
24715
24817
  changed: 0
24716
24818
  };
24717
24819
  }
24718
- const { updateMemberStatus } = useMemberRepo();
24719
- for (const row of rows)
24720
- await updateMemberStatus(row._id, { status: "inactive" });
24820
+ await setRoleStatus(roles.map((r) => r._id), "deleted");
24721
24821
  if (defaultOrgIsSender)
24722
24822
  await setDefaultOrg(String(user._id), "");
24723
24823
  await _transition({
@@ -24726,6 +24826,7 @@ function useServiceProviderInviteService() {
24726
24826
  event: {
24727
24827
  ...event("wrong-join-repaired", actor),
24728
24828
  memberIds: rows.map((row) => row._id),
24829
+ roleIds: roles.map((r) => r._id),
24729
24830
  ...defaultOrgIsSender ? { defaultOrgWas: senderOrg } : {}
24730
24831
  }
24731
24832
  });
@@ -24734,8 +24835,8 @@ function useServiceProviderInviteService() {
24734
24835
  message: `[sp-invite] wrong-join repaired invite=${id} memberships=${rows.length} by=${actor.id}`
24735
24836
  });
24736
24837
  return {
24737
- message: `Repaired: ${rows.length} membership${rows.length === 1 ? "" : "s"} in the sender's organisation set inactive. Undo is available.`,
24738
- changed: rows.length
24838
+ message: `Repaired: ${rows.length} membership${rows.length === 1 ? "" : "s"} in the sender's organisation set inactive` + (roles.length ? `, ${roles.length} unused full-access role${roles.length === 1 ? "" : "s"} removed from its role list` : "") + ". Undo is available.",
24839
+ changed: rows.length + roles.length
24739
24840
  };
24740
24841
  }
24741
24842
  async function undoWrongJoin({ id, userId }) {
@@ -24747,6 +24848,7 @@ function useServiceProviderInviteService() {
24747
24848
  const ids2 = (repaired.memberIds ?? []).filter((m) => ObjectId56.isValid(m));
24748
24849
  for (const m of ids2)
24749
24850
  await updateMemberStatus(m, { status: "active" }).catch(() => 0);
24851
+ await setRoleStatus(repaired.roleIds ?? [], "active");
24750
24852
  const { user } = await wrongJoinContext(id, userId);
24751
24853
  if (repaired.defaultOrgWas && user?._id && !String(user.defaultOrg ?? "")) {
24752
24854
  await setDefaultOrg(String(user._id), String(repaired.defaultOrgWas));
@@ -64008,7 +64110,7 @@ function robotManifest(brand, reportedModel) {
64008
64110
  const b = robotBrandFor(brand);
64009
64111
  const reported = typeof reportedModel === "string" ? reportedModel.trim().slice(0, 60) : "";
64010
64112
  if (!b)
64011
- return { capabilities: {}, profile: { model: reported, modelKnown: false, alarms: [], makerApp: "", makerAppOnly: [] } };
64113
+ return { capabilities: {}, profile: { model: reported, modelKnown: false, alarms: [], makerApp: "", makerAppOnly: [], clearFault: [] } };
64012
64114
  const prefix = modelKey(b.brand);
64013
64115
  let key = modelKey(reported);
64014
64116
  if (prefix && key.startsWith(prefix) && key !== prefix)
@@ -64021,7 +64123,8 @@ function robotManifest(brand, reportedModel) {
64021
64123
  modelKnown: Boolean(m),
64022
64124
  alarms: [...m?.alarms ?? b.info?.alarms ?? []],
64023
64125
  makerApp: m?.makerApp ?? b.info?.makerApp ?? "",
64024
- makerAppOnly: [...m?.makerAppOnly ?? b.info?.makerAppOnly ?? []]
64126
+ makerAppOnly: [...m?.makerAppOnly ?? b.info?.makerAppOnly ?? []],
64127
+ clearFault: [...m?.clearFault ?? b.info?.clearFault ?? []]
64025
64128
  }
64026
64129
  };
64027
64130
  }
@@ -64854,6 +64957,246 @@ function useRobotCloudService(credentials, options = {}) {
64854
64957
  // src/services/ounitech.adapter.ts
64855
64958
  import axios4 from "axios";
64856
64959
  import { BadRequestError as BadRequestError137 } from "@7365admin1/node-server-utils";
64960
+
64961
+ // src/utils/ounitech-codes.util.ts
64962
+ var CLEAR = "Clear the fault (press and release the emergency stop)";
64963
+ var SUPPORT = "If it keeps happening, call Ounitech support";
64964
+ var DOCK = "Push it onto its charging dock so it can find its position again";
64965
+ var WAIT = "Wait until it is ready, then try again";
64966
+ var WHEEL = "Clear anything caught in the wheel and check the floor for holes";
64967
+ var NONE = "Normal when the emergency stop is pressed or released - no action needed";
64968
+ var T = {
64969
+ "100": ["Charging dock is over-heating", "Unplug the dock and let it cool down"],
64970
+ "101": ["Charging dock's clean-water tank is empty", "Fill the dock's clean-water tank"],
64971
+ "102": ["Charging dock's waste-water tank is full", "Drain the dock (unlock the dock, long-press Drain)"],
64972
+ "103": ["Charging dock valve or pump fault", `Check the pump connections. ${SUPPORT}`],
64973
+ "104": ["Charging dock valve or pump fault", `Check the pump connections. ${SUPPORT}`],
64974
+ "105": ["Charging dock valve or pump fault", `Check the pump connections. ${SUPPORT}`],
64975
+ "106": ["Charging dock valve or pump fault", `Check the pump connections. ${SUPPORT}`],
64976
+ "107": ["Charging dock valve or pump fault", `Check the pump connections. ${SUPPORT}`],
64977
+ "200": ["Battery not responding (flat for a long time)", "Charge it on the dock, or take the battery out and put it back"],
64978
+ "204": ["Wheel driver board offline", `Check the wheel wiring. ${SUPPORT}`],
64979
+ "205": ["Side-brush driver offline (side brush won't turn)", "Check the side-brush motor wire"],
64980
+ "206": ["Button panel offline (handle lights off)", `Check the handle's cable contact. ${SUPPORT}`],
64981
+ "207": ["Vacuum fan stalled", "Clean the vacuum pipe, HEPA filter, dust bag and tray"],
64982
+ "209": ["Vacuum fan fault", `${CLEAR}. ${SUPPORT}`],
64983
+ "210": ["Roller mop motor fault", "Take out the roller and clear anything tangled in it"],
64984
+ "211": ["Roller brush motor fault", "Take out the roller and clear anything tangled in it"],
64985
+ "212": ["Left side-brush motor fault", "Clear anything tangled in the side brush"],
64986
+ "213": ["Right side-brush motor fault", "Clear anything tangled in the side brush"],
64987
+ "214": ["Clean-water pump fault", `Re-plug the pump board. ${SUPPORT}`],
64988
+ "218": ["Waste-water pump fault", `Re-plug the pump board. ${SUPPORT}`],
64989
+ "219": ["Mop lift motor fault", `Restart the robot (power switch at the back). ${SUPPORT}`],
64990
+ "220": ["Brush lift motor fault", `Restart the robot (power switch at the back). ${SUPPORT}`],
64991
+ "221": ["Wheel fault", WHEEL],
64992
+ "222": ["Wheel fault", WHEEL],
64993
+ "223": ["Wheel stalled", WHEEL],
64994
+ "224": ["Wheel stalled", WHEEL],
64995
+ "228": ["Cooling fan fault", "Call Ounitech support"],
64996
+ "233": ["Battery short circuit", "Stop using the robot and call Ounitech support to replace the battery"],
64997
+ "234": ["Battery over-charged", "Take it off charge and call Ounitech support"],
64998
+ "245": ["Water-suction fan stalled", "Unclog the suction pipe and replace the filter"],
64999
+ "248": ["Squeegee motor fault", `Check the squeegee cable and motor. ${SUPPORT}`],
65000
+ "250": ["Battery full"],
65001
+ "251": ["Battery low"],
65002
+ "252": ["Battery too low to charge", "Call Ounitech support"],
65003
+ "255": ["Battery charging current high"],
65004
+ "256": ["Battery over-charged", "Take it off charge and call Ounitech support"],
65005
+ "257": ["Battery over 70 \xB0C", "Stop charging and let it cool down"],
65006
+ "302": ["Robot can't load its map", "Call Ounitech support"],
65007
+ "303": ["The map's area layout is damaged", "Re-edit the areas in the Ounitech app and save"],
65008
+ "304": ["The area layout doesn't match the map", "In the Ounitech app, switch to another floor map and back"],
65009
+ "305": ["Lost contact with its motion controller", "Restart the robot (power switch at the back)"],
65010
+ "306": ["Robot couldn't receive the chosen area", `${CLEAR}, then send the task again`],
65011
+ "307": ["Robot is finding its position again", "If the site has changed, push it onto its charging dock"],
65012
+ "308": ["Robot couldn't find its position", DOCK],
65013
+ "310": ["No signal from the charging dock", `Check the dock is powered. ${SUPPORT}`],
65014
+ "312": ["Couldn't dock", "Check the dock hasn't been moved"],
65015
+ "314": ["Couldn't dock - trying again", "Check the dock hasn't been moved"],
65016
+ "315": ["Couldn't dock - trying again", "Check the dock hasn't been moved"],
65017
+ "316": ["Lost the wireless link to its charging dock", "Check the dock is powered"],
65018
+ "319": ["Robot was moved while charging", "Press the emergency stop and push it back onto the dock"],
65019
+ "401": ["Battery below 10% - going to charge"],
65020
+ "403": ["Robot has left its charging dock"],
65021
+ "404": ["Charging dock reports a fault", "See the dock's other alerts"],
65022
+ "405": ["Couldn't call the lift"],
65023
+ "407": ["Lift is full"],
65024
+ "409": ["Couldn't switch map", "Call Ounitech support"],
65025
+ "410": ["Internal communication problem", SUPPORT],
65026
+ "415": ["Gyro data fault", `Check the cables. ${SUPPORT}`],
65027
+ "416": ["Wheel odometer data fault", `Check the cables. ${SUPPORT}`],
65028
+ "417": ["Laser scanner data fault", `Check the cables. ${SUPPORT}`],
65029
+ "418": ["Couldn't dock - trying again", "Check the dock hasn't been moved"],
65030
+ "419": ["Couldn't dock - trying again", "Check the dock hasn't been moved"],
65031
+ "420": ["Docking failed several times", "Check the dock's position and the robot's position"],
65032
+ "423": ["Dust box blocked", "Empty the dust box and clear the pipe"],
65033
+ "431": ["Robot's waste-water tank is high - returning to empty"],
65034
+ "432": ["Robot's clean water is low - returning to refill"],
65035
+ "433": ["Robot over-heating while charging", "Let it cool down"],
65036
+ "443": ["No charging current", "Seat the battery and clean the charging contacts"],
65037
+ "445": ["Area laser fault", "Call Ounitech support"],
65038
+ "446": ["Area laser board offline", "Call Ounitech support"],
65039
+ "448": ["Wheel drawing too much current", WHEEL],
65040
+ "449": ["Wheel over-current fault", `${WHEEL}, then clear the fault (press and release the emergency stop)`],
65041
+ "450": ["Wheels slipping", "Check the wheels"],
65042
+ "453": ["Robot tried to leave a no-go area", DOCK],
65043
+ "454": ["Robot hasn't moved for a long time", "Clear its path and wipe its sensors"],
65044
+ "457": ["Left bumper hit something", "Tap the bumper if it stays stuck"],
65045
+ "458": ["Right bumper hit something", "Tap the bumper if it stays stuck"],
65046
+ "459": ["Water suction blocked", "Clean the suction pipe and filters"],
65047
+ "460": ["Front camera fault", `Check the camera cable. ${SUPPORT}`],
65048
+ "462": ["Left camera fault", `Check the camera cable. ${SUPPORT}`],
65049
+ "464": ["Right camera fault", `Check the camera cable. ${SUPPORT}`],
65050
+ "469": ["The map's floor doesn't match the floor it senses", "Check it is on the right floor; the air-pressure sensor may need recalibrating"],
65051
+ "470": ["Gyro offline", "Call Ounitech support"],
65052
+ "471": ["Roller motor needs servicing", "Book a service"],
65053
+ "472": ["Fan needs servicing", "Book a service"],
65054
+ "500": ["Scheduled job skipped: floor can't be reached", "Cross-floor jobs need lift control"],
65055
+ "501": ["Scheduled job skipped: robot not ready to clean", "Check battery, water, tanks and dust box"],
65056
+ "502": ["Waited over 20 minutes for the lift - going to charge"],
65057
+ "503": ["Scheduled job done - going to charge"],
65058
+ "504": ["Job done - going to charge"],
65059
+ "505": ["Job cancelled - going to charge"],
65060
+ "506": ["Couldn't leave the charging dock", "Check the wheels"],
65061
+ "507": ["Clean water low - returning to refill, then continuing"],
65062
+ "508": ["Waste water high - returning to empty, then continuing"],
65063
+ "509": ["Dust box missing", "Fit the dust box"],
65064
+ "510": ["Clean-water tank missing", "Fit the clean-water tank"],
65065
+ "511": ["Waste-water tank missing", "Fit the waste-water tank"],
65066
+ "512": ["Tank cover missing", "Fit the tank cover"],
65067
+ "513": ["Top cover open", "Close the top cover"],
65068
+ "514": ["Handle is up (manual mode) - the app can't start a job", "Lower the handle"],
65069
+ "515": ["Battery below 10% - can't clean", "Let it charge"],
65070
+ "517": ["Robot is starting up - not ready yet", WAIT],
65071
+ "518": ["Robot is shutting down"],
65072
+ "519": ["Robot is resuming - not ready yet", WAIT],
65073
+ "520": ["Robot is finding its position - not ready yet", WAIT],
65074
+ "521": ["Robot is downloading its map - not ready yet", WAIT],
65075
+ "522": ["Couldn't resume: clean water below half", "Wait for it to refill"],
65076
+ "523": ["Couldn't resume: waste water too high", "Wait for it to empty"],
65077
+ "524": ["Charging dock doesn't match this robot", "Re-scan the dock's QR code in the Ounitech app"],
65078
+ "525": ["Scheduled time ended before the job finished", "Give the schedule a longer time window"],
65079
+ "527": ["Software updating", "Don't use the robot until it finishes"],
65080
+ "528": ["Software updating", "Don't use the robot until it finishes"],
65081
+ "529": ["Software updating", "Don't use the robot until it finishes"],
65082
+ "530": ["Software updating", "Don't use the robot until it finishes"],
65083
+ "531": ["Manual mode - the app's command was refused", "Lower the handle"],
65084
+ "532": ["Finding its starting position"],
65085
+ "534": ["Finding its starting position after a map switch"],
65086
+ "535": ["Areas updated"],
65087
+ "536": ["Virtual walls updated"],
65088
+ "537": ["Safe zones updated"],
65089
+ "538": ["Map saved"],
65090
+ "539": ["Couldn't save the map", "Save it again"],
65091
+ "540": ["Cliff sensor switched on"],
65092
+ "541": ["Cliff sensor switched off"],
65093
+ "542": ["Stopped at a drop (cliff sensor)", `Pull it back by the handle, ${CLEAR.charAt(0).toLowerCase()}${CLEAR.slice(1)}, and add a virtual wall there`],
65094
+ "543": ["Slowing down near a drop (cliff sensor)", "Add a virtual wall there if it keeps happening"],
65095
+ "544": ["Scheduled shutdown soon - returning to the dock"],
65096
+ "545": ["Edge cleaning started"],
65097
+ "546": ["Edge cleaning finished"],
65098
+ "549": ["Asking to switch map"],
65099
+ "552": ["No floor switch needed"],
65100
+ "553": ["Area can't be reached - job ended", "In the Ounitech app, switch map and back, or re-save an area"],
65101
+ "554": ["Sensor calibration"],
65102
+ "555": ["Sensor calibration - clear obstacles around the robot"],
65103
+ "556": ["Sensor calibration - clear obstacles around the robot"],
65104
+ "557": ["Slow zones updated"],
65105
+ "560": ["Fan stalled - returning to the dock", "Press and release the emergency stop, then check the fan"],
65106
+ "561": ["Couldn't reconnect to the charging dock", "Check the dock"],
65107
+ "562": ["Scheduled shutdown soon - no new jobs"],
65108
+ "563": ["Robot is in a fault state", "Find the cause, clear it (press and release the emergency stop), and check the robot"],
65109
+ "564": ["Fault cleared - robot on standby"],
65110
+ "565": ["Handling waste water"],
65111
+ "566": ["Waste water handled"],
65112
+ "567": ["Fan stalled", "Clear the fan, then resume"],
65113
+ "568": ["No charging dock on this floor", "Set the dock's floor in the Ounitech app"],
65114
+ "570": ["Finding its starting position - can't start yet", WAIT],
65115
+ "571": ["Stopped too long - side brush switched off", "Pause, then resume"],
65116
+ "572": ["Lift ride took too long - job cancelled", "Push it back onto its charging dock"],
65117
+ "573": ["Camera saw a drop", "Add a virtual wall 1 m from the drop"],
65118
+ "574": ["Map updating", WAIT],
65119
+ "575": ["Air-pressure sensor reading invalid", "Recalibrate the air-pressure sensor"],
65120
+ "576": ["Air-pressure sensor calibration"],
65121
+ "577": ["Air-pressure sensor calibration"],
65122
+ "578": ["Air-pressure sensor calibration"],
65123
+ "581": ["Taught route started"],
65124
+ "582": ["Taught route ended"],
65125
+ "583": ["Taught route cleared"],
65126
+ "585": ["An area's positioning mode changed"],
65127
+ "587": ["Battery discharging while on charge", "Seat the battery and clean the charging contacts"],
65128
+ "588": ["Switched off with the power button"],
65129
+ "589": ["Switched off by the timed power schedule"],
65130
+ "590": ["Switched on"],
65131
+ "591": ["Switched on with the power button"],
65132
+ "592": ["Switched on by the timed power schedule"],
65133
+ "593": ["Still finding its position - can't take commands yet", WAIT],
65134
+ "600": ["Left wheel driver offline", NONE],
65135
+ "601": ["Left wheel driver reset", NONE],
65136
+ "603": ["Left wheel drawing too much current", WHEEL],
65137
+ "604": ["Left wheel drawing too much current", WHEEL],
65138
+ "605": ["Left wheel stalled", WHEEL],
65139
+ "620": ["Right wheel driver offline", NONE],
65140
+ "621": ["Right wheel driver reset", NONE],
65141
+ "622": ["Right wheel driver error", "Call Ounitech support"],
65142
+ "623": ["Right wheel drawing too much current", WHEEL],
65143
+ "624": ["Right wheel drawing too much current", WHEEL],
65144
+ "625": ["Right wheel stalled", WHEEL],
65145
+ "640": ["A part is overdue for replacement - won't clean", "Replace the part"],
65146
+ "641": ["A running job is blocking the schedule", "Cancel the job or wait"],
65147
+ "642": ["Can't find its position while charging", "Put it on standby first"],
65148
+ "643": ["Can't reset the dock while charging", "Put it on standby first"],
65149
+ "644": ["Can't set its starting position while charging", "Put it on standby first"],
65150
+ "700": ["Vacuum button pressed"],
65151
+ "701": ["Standard button pressed"],
65152
+ "702": ["Dust-mop button pressed"],
65153
+ "703": ["Cancel button pressed"],
65154
+ "704": ["Strong button pressed"],
65155
+ "705": ["Mop button pressed"],
65156
+ "706": ["Pause button pressed"],
65157
+ "707": ["Recall button pressed"],
65158
+ "708": ["Pause button held"],
65159
+ "709": ["Pause button held"],
65160
+ "710": ["Wheel drawing too much current", WHEEL],
65161
+ "711": ["Wheel over-heating", "Let it cool and check the wheel"],
65162
+ "712": ["Wheel kept drawing too much current", `${WHEEL}, then clear the fault (press and release the emergency stop)`],
65163
+ "713": ["Switching lift - timed power paused"],
65164
+ "731": ["Robot locked by the dealer", "Ask the dealer"],
65165
+ "732": ["Voice file updated"],
65166
+ "733": ["Switched off from the app"],
65167
+ "737": ["Laser scanner motion-sensor fault", "Call Ounitech support"],
65168
+ "738": ["Can't start: clean water low", "Fill the clean-water tank"],
65169
+ "739": ["Can't start: waste water high", "Empty the waste-water tank"],
65170
+ "760": ["Map file missing", "Call Ounitech support"],
65171
+ "761": ["Map file damaged", "Call Ounitech support"],
65172
+ "762": ["Started from the wrong place (it was pushed)", "Push it back onto its charging dock"],
65173
+ "775": ["Moved by a person during a lift ride", "Cancel the job and push it back onto its charging dock"],
65174
+ "776": ["Boxed in", `Clear its path, wipe its sensors. ${DOCK}`],
65175
+ "777": ["Can't reach its target", `Clear its path, wipe its sensors. ${DOCK}`],
65176
+ "778": ["Couldn't dock - recovering by itself", "Check the dock's sensor and that the dock hasn't moved"],
65177
+ "779": ["Robot settings changed"],
65178
+ "780": ["Top cover open", "Close the top cover"],
65179
+ "781": ["Dust box not fitted", "Fit the dust box"],
65180
+ "782": ["Waste-water tank not fitted", "Fit the waste-water tank"],
65181
+ "785": ["Position ready"]
65182
+ };
65183
+ var _a;
65184
+ for (const c of [...range(740, 759), 763, 764, 783, 784])
65185
+ T[_a = String(c)] ?? (T[_a] = ["Sensor or camera calibration", "Recalibrate if it failed"]);
65186
+ var _a2;
65187
+ for (const c of range(765, 773))
65188
+ T[_a2 = String(c)] ?? (T[_a2] = ["QR code return-point / position scan", "Rescan if it failed"]);
65189
+ T["774"] ?? (T["774"] = ["Taught route saved"]);
65190
+ function range(a, b) {
65191
+ return Array.from({ length: b - a + 1 }, (_, i) => a + i);
65192
+ }
65193
+ var OUNITECH_CODES = T;
65194
+ function ounitechCodeWords(code) {
65195
+ const c = OUNITECH_CODES[code];
65196
+ return c ? c[1] ? `${c[0]}. ${c[1]}.` : c[0] : null;
65197
+ }
65198
+
65199
+ // src/services/ounitech.adapter.ts
64857
65200
  var OUNITECH_BRAND = "ounitech";
64858
65201
  var OUNITECH_CAPABILITIES = {
64859
65202
  // Pose in the chapter-6 robot list (metres) and the tracking stream.
@@ -64882,8 +65225,9 @@ var OUNITECH_CAPABILITIES = {
64882
65225
  // taskState carries the job and its %, shown in the status words; there is
64883
65226
  // no command id to follow a job WE started, so completion is not claimed.
64884
65227
  processCompletion: false,
65228
+ // The manual: "Combined cleaning" across floors exists in the Ounitech app and needs its
65229
+ // lift-control module (code 500 = floor unreachable); no API call starts one.
64885
65230
  multiFloor: false,
64886
- // plans can name floors; no API starts a cross-floor job
64887
65231
  // `start-spot-cleaning` takes a PIXEL polygon while poses are metres, and
64888
65232
  // whether y is flipped between the two is not documented. Hidden until one
64889
65233
  // real robot confirms the frame.
@@ -65039,9 +65383,9 @@ function ounitechFault(list) {
65039
65383
  }
65040
65384
  function faultWords(a) {
65041
65385
  if (!a.code)
65042
- return { code: "error", message: a.message.slice(0, 200) };
65043
- const words = a.message.startsWith(`Robot alert ${a.code}:`) ? a.message : `${a.message} (fault ${a.code})`;
65044
- return { code: a.code.slice(0, 60), message: words.slice(0, 200) };
65386
+ return { code: "error", message: a.message.slice(0, 300) };
65387
+ const words = alertWords(a.code) ?? a.vendorText ?? a.message;
65388
+ return { code: a.code.slice(0, 60), message: `${a.code} \xB7 ${words}`.slice(0, 300) };
65045
65389
  }
65046
65390
  function ounitechMapShare(map, x, y, clamp4 = false) {
65047
65391
  const w = map?.width, h = map?.height, r = map?.resolution;
@@ -65189,22 +65533,8 @@ function ounitechTaskRecord(t, maps = [], tz = 480) {
65189
65533
  floorNo: Number.isInteger(t.floorNo) ? t.floorNo : map?.floorNo ?? null
65190
65534
  };
65191
65535
  }
65192
- var ALERT_WORDS = {
65193
- "205": "Side brush driver offline",
65194
- "206": "Button panel offline",
65195
- // 306 "接收定点指定区域故障": seen live 30 Sep right after start-area-cleaning on an area with no centre point.
65196
- "306": "Robot couldn't receive the chosen cleaning area",
65197
- "540": "Floor check: cliff sensor switched on",
65198
- "563": "Robot is in a fault state",
65199
- "588": "Switched off with the power button",
65200
- "589": "Switched off by the timed power schedule",
65201
- "593": "Still finding its position - try again shortly",
65202
- "601": "Left wheel driver reset",
65203
- "620": "Right wheel driver offline",
65204
- "621": "Right wheel driver reset",
65205
- "734": "IMU zero offset too large",
65206
- "785": "Localisation ready"
65207
- };
65536
+ var VENDOR_TEXT_EN = { "734": "IMU zero offset too large" };
65537
+ var alertWords = (code) => code ? ounitechCodeWords(code) ?? VENDOR_TEXT_EN[code] ?? null : null;
65208
65538
  var ALERT_LEVEL_WORDS = { \u63D0\u793A: "Notice", \u8B66\u544A: "Warning", \u6545\u969C: "Fault", \u9519\u8BEF: "Error", \u4E25\u91CD: "Critical" };
65209
65539
  function ounitechAlertRecord(a, tz = 480) {
65210
65540
  const raw = String(a.errMsg ?? a.errCode ?? "").trim();
@@ -65220,7 +65550,7 @@ function ounitechAlertRecord(a, tz = 480) {
65220
65550
  code,
65221
65551
  level: lv ? ALERT_LEVEL_WORDS[lv[1].trim()] ?? lv[1].trim() : null,
65222
65552
  // An untranslated Chinese text keeps its code up front, so the row is never only Chinese.
65223
- message: ALERT_WORDS[code] ?? (/[㐀-鿿]/.test(rest || raw) && code ? `Robot alert ${code}: ${rest || raw}` : rest || raw),
65553
+ message: alertWords(code) ?? (/[㐀-鿿]/.test(rest || raw) && code ? `Robot alert ${code}: ${rest || raw}` : rest || raw),
65224
65554
  vendorText: rest || raw,
65225
65555
  resolved,
65226
65556
  at: ounitechTime(a.createTime, tz)
@@ -65598,7 +65928,7 @@ function createOunitechAdapter(credentials, options) {
65598
65928
  const blind = command.areas.filter((n) => centre.has(n) && !centre.get(n));
65599
65929
  if (blind.length) {
65600
65930
  throw new BadRequestError137(
65601
- `${blind.map((n) => `"${n}"`).join(", ")} has no centre point on the robot's map, so the robot can't be sent there (it stops with fault 306). Use Clean all, or redraw the area in the Ounitech app.`
65931
+ `${blind.map((n) => `"${n}"`).join(", ")} has no centre point on the robot's map, so the robot can't be sent there (it stops with fault 306). ` + (blind.includes("default") ? `"default" is the whole map before it is divided into areas. Use Clean all, or divide the map in the Ounitech app (Control -> Divide areas).` : "Use Clean all, or redraw the area in the Ounitech app.")
65602
65932
  );
65603
65933
  }
65604
65934
  await cloud.startAreaCleaning(robotSn, command.areas, options2);
@@ -65861,7 +66191,17 @@ var OUNITECH_BRAND_DEFINITION = {
65861
66191
  { name: "Q3-G", codes: ["Q3G", "Q3-G"] },
65862
66192
  { name: "Q3-W", codes: ["Q3W", "Q3-W"] }
65863
66193
  ],
65864
- info: { makerApp: "the Ounitech app", makerAppOnly: ["pausing or stopping a job", "cleaning across floors", "changing maps, areas and schedules"] },
66194
+ info: {
66195
+ makerApp: "the Ounitech app",
66196
+ makerAppOnly: ["pausing or stopping a job", "cleaning across floors", "changing maps, areas and schedules"],
66197
+ // Q3-G User Manual / Troubleshooting Checklist (2026-09-30). The API has no documented way to clear an Error.
66198
+ clearFault: [
66199
+ 'Press the red emergency stop on top of the robot, then turn it clockwise to release it. It says "emergency stop button bounced" and the fault clears.',
66200
+ "If it has lost its position: lift the handle, push it onto its charging dock until the metal contacts touch, then press and release the emergency stop. It relocates by itself.",
66201
+ "Still stuck: restart it with the power switch at the back. It is ready when only the child-lock light stays on.",
66202
+ "Handle buttons not responding: press Child lock 3 times to unlock them."
66203
+ ]
66204
+ },
65865
66205
  create: (credentials) => createOunitechAdapter(credentials)
65866
66206
  };
65867
66207
  function registerOunitechBrand() {
@@ -71153,7 +71493,7 @@ function toBuildingYaml(name, rmf) {
71153
71493
  }
71154
71494
  const lifts = {};
71155
71495
  for (const l of rmf.lifts) {
71156
- const range = liftFloorRange(l, rmf.levels);
71496
+ const range2 = liftFloorRange(l, rmf.levels);
71157
71497
  lifts[l.name] = {
71158
71498
  x: l.x,
71159
71499
  y: l.y,
@@ -71161,9 +71501,9 @@ function toBuildingYaml(name, rmf) {
71161
71501
  width: l.widthM,
71162
71502
  depth: l.depthM,
71163
71503
  reference_floor_name: l.referenceFloor,
71164
- initial_floor_name: range.initial,
71165
- lowest_floor: range.lowest,
71166
- highest_floor: range.highest,
71504
+ initial_floor_name: range2.initial,
71505
+ lowest_floor: range2.lowest,
71506
+ highest_floor: range2.highest,
71167
71507
  doors: Object.fromEntries(
71168
71508
  l.doors.map((d) => [d.name, { door_type: LIFT_DOOR_TYPE_INT[d.type] ?? 2, x: d.x, y: d.y, width: d.widthM, motion_axis_orientation: d.axisYawRad }])
71169
71509
  ),
@@ -71213,13 +71553,13 @@ function toNavGraphs(name, rmf) {
71213
71553
  const out = {};
71214
71554
  const elevation = new Map(rmf.levels.map((l) => [l.name, l.elevationM]));
71215
71555
  const shafts = rmf.lifts.map((l) => {
71216
- const range = liftFloorRange(l, rmf.levels);
71556
+ const range2 = liftFloorRange(l, rmf.levels);
71217
71557
  return {
71218
71558
  lift: l,
71219
71559
  verts: liftShaft(l),
71220
71560
  // lift.py: no lowest / highest floor = no bound; a lift with no level_doors marks nothing.
71221
- lo: elevation.get(range.lowest) ?? -Infinity,
71222
- hi: elevation.get(range.highest) ?? Infinity,
71561
+ lo: elevation.get(range2.lowest) ?? -Infinity,
71562
+ hi: elevation.get(range2.highest) ?? Infinity,
71223
71563
  hasDoors: l.levelDoors.length > 0
71224
71564
  };
71225
71565
  });