@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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +439 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +439 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/service-provider-invite-email-new-company.e2e.test.mjs +105 -1
- package/test/member-working-membership.test.mjs +28 -0
- package/test/ounitech-api-2025-10.test.mjs +51 -8
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
|
|
3382
|
+
var idOf2 = (value) => value === null || value === void 0 ? "" : String(value?.toString?.() ?? value);
|
|
3336
3383
|
function isCrossSiteRole(role, memberSite) {
|
|
3337
|
-
const roleSite =
|
|
3338
|
-
return !!roleSite && roleSite !==
|
|
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,
|
|
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=${
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
24636
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
65043
|
-
const words =
|
|
65044
|
-
return { code: a.code.slice(0, 60), message: words
|
|
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
|
|
65193
|
-
|
|
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:
|
|
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: {
|
|
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
|
|
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:
|
|
71165
|
-
lowest_floor:
|
|
71166
|
-
highest_floor:
|
|
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
|
|
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(
|
|
71222
|
-
hi: elevation.get(
|
|
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
|
});
|