@7365admin1/layer-common 4.51.0 → 4.52.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 +16 -4
- package/components/Robot/Form.vue +74 -9
- package/composables/useRobots.ts +18 -2
- package/package.json +1 -1
- package/utils/rmf-editor.ts +2 -2
- package/utils/robot-credential-form.ts +40 -0
- package/utils/theme-aa-ledger.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.52.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a983a42: Add / Edit robot: Ounitech credential fields read "AppID" / "Secret" (demo: "any value"); a Robot picker lists the robots the company credential covers (Find robots) so the serial is picked, and adding with no pair typed reuses the client's saved Ounitech credentials.
|
|
8
|
+
|
|
9
|
+
## 4.51.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 39cbe08: Fix the robot pages crashing in the browser on 4.50.0/4.51.0 ("process is not defined" / "Couldn't resolve component default"). A parameter named `measure` in `utils/rmf-editor.ts` was picked up by Nuxt auto-imports as the `measure` export of `utils/theme-aa-ledger.ts`, whose top-level command-line check read `process.argv`. The parameter is renamed and the ledger's check is guarded; a new test evaluates every util in a browser-like scope with the auto-imports injected.
|
|
14
|
+
|
|
3
15
|
## 4.51.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1545,11 +1557,11 @@
|
|
|
1545
1557
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1546
1558
|
own rule from two endpoints the console already calls, unprojected:
|
|
1547
1559
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1560
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1561
|
+
GET /api/roles/id/:role that membership's role document
|
|
1550
1562
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1563
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1564
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1553
1565
|
|
|
1554
1566
|
`role.default` is the marker because it is the only property of a platform
|
|
1555
1567
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<!--
|
|
3
3
|
Add / edit robot (HERO README §8): a 520px dialog - Name · Type (only when
|
|
4
4
|
the account presents both types) · Brand (filtered by type; each option
|
|
5
|
-
shows its capability) · Site · App key · Token (password fields
|
|
5
|
+
shows its capability) · Site · App key · Token (password fields; named in
|
|
6
|
+
the brand's own words - Ounitech: AppID · Secret), Robot (Ounitech: pick the
|
|
7
|
+
serial from the company account; with no pair typed, the client's saved
|
|
8
|
+
pair is reused server-side), the
|
|
6
9
|
credentials note with the brand's capability, Cancel / Save robot.
|
|
7
10
|
|
|
8
11
|
Credentials are write-only: the stored pair never reaches the browser, so
|
|
@@ -46,7 +49,7 @@
|
|
|
46
49
|
</select>
|
|
47
50
|
</div>
|
|
48
51
|
<div class="hero-field">
|
|
49
|
-
<label :for="`${uid}-key`">
|
|
52
|
+
<label :for="`${uid}-key`">{{ labels.key }}</label>
|
|
50
53
|
<input
|
|
51
54
|
:id="`${uid}-key`"
|
|
52
55
|
v-model="appKey"
|
|
@@ -59,7 +62,7 @@
|
|
|
59
62
|
/>
|
|
60
63
|
</div>
|
|
61
64
|
<div class="hero-field">
|
|
62
|
-
<label :for="`${uid}-token`">
|
|
65
|
+
<label :for="`${uid}-token`">{{ labels.token }}</label>
|
|
63
66
|
<input
|
|
64
67
|
:id="`${uid}-token`"
|
|
65
68
|
v-model="appToken"
|
|
@@ -71,6 +74,18 @@
|
|
|
71
74
|
:disabled="saving"
|
|
72
75
|
/>
|
|
73
76
|
</div>
|
|
77
|
+
<!-- One company credential covers many robots: pick the machine by serial. -->
|
|
78
|
+
<div v-if="discoverable" class="hero-field hero-form-grid__full">
|
|
79
|
+
<label :for="`${uid}-serial`">Robot</label>
|
|
80
|
+
<div class="d-flex ga-2 align-center">
|
|
81
|
+
<select :id="`${uid}-serial`" v-model="robotSn" class="hero-input" :disabled="saving || !found.length">
|
|
82
|
+
<option value="">{{ found.length ? "Choose the robot" : robotSn || findHint }}</option>
|
|
83
|
+
<option v-if="robotSn && !found.some((o) => o.value === robotSn)" :value="robotSn">{{ robotSn }}</option>
|
|
84
|
+
<option v-for="o in found" :key="o.value" :value="o.value" :disabled="o.disabled">{{ o.label }}</option>
|
|
85
|
+
</select>
|
|
86
|
+
<v-btn variant="outlined" :loading="finding" :disabled="saving || (!editing && !site)" @click="find">Find robots</v-btn>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
74
89
|
</div>
|
|
75
90
|
<div class="hero-dialog__note">{{ credentialsNote(credentialsEncrypted) }} Capability: {{ note }}</div>
|
|
76
91
|
<div v-if="error" class="hero-notice hero-notice--error" role="alert">{{ error }}</div>
|
|
@@ -93,6 +108,7 @@ import {
|
|
|
93
108
|
type TRobotType,
|
|
94
109
|
} from "../../utils/robot-display";
|
|
95
110
|
import { credentialsNote } from "../../utils/robot-floorplan";
|
|
111
|
+
import { credentialLabels, credentialMissingMessage, discoveredRobotOptions } from "../../utils/robot-credential-form";
|
|
96
112
|
|
|
97
113
|
const props = withDefaults(
|
|
98
114
|
defineProps<{
|
|
@@ -119,7 +135,7 @@ const emit = defineEmits<{
|
|
|
119
135
|
"update:client": [org: string];
|
|
120
136
|
}>();
|
|
121
137
|
|
|
122
|
-
const { createRobot, updateRobot, getRobotBrands } = useRobots();
|
|
138
|
+
const { createRobot, updateRobot, getRobotBrands, discoverRobots } = useRobots();
|
|
123
139
|
const uid = useId();
|
|
124
140
|
const titleId = `${uid}-title`;
|
|
125
141
|
const editing = computed(() => !!props.robot?._id);
|
|
@@ -133,6 +149,23 @@ const org = ref(props.organization);
|
|
|
133
149
|
const site = ref(String(props.robot?.site?._id ?? props.site ?? ""));
|
|
134
150
|
const appKey = ref("");
|
|
135
151
|
const appToken = ref("");
|
|
152
|
+
const savedSn = String(props.robot?.robotSn ?? "");
|
|
153
|
+
const robotSn = ref(savedSn);
|
|
154
|
+
const found = ref<Array<{ value: string; label: string; disabled: boolean }>>([]);
|
|
155
|
+
const finding = ref(false);
|
|
156
|
+
const labels = computed(() => credentialLabels(brand.value));
|
|
157
|
+
const typed = computed(() => !!appKey.value.trim() && !!appToken.value.trim());
|
|
158
|
+
// Ounitech: one AppID/Secret per company, so the robot is picked from its list.
|
|
159
|
+
const discoverable = computed(() => brand.value === "ounitech");
|
|
160
|
+
const findHint = computed(() =>
|
|
161
|
+
editing.value || typed.value
|
|
162
|
+
? "Press Find robots"
|
|
163
|
+
: `Press Find robots to use this client's saved ${labels.value.key} and ${labels.value.token}, or enter them first`,
|
|
164
|
+
);
|
|
165
|
+
watch([brand, site], () => {
|
|
166
|
+
found.value = [];
|
|
167
|
+
if (!editing.value) robotSn.value = "";
|
|
168
|
+
});
|
|
136
169
|
const saving = ref(false);
|
|
137
170
|
const error = ref("");
|
|
138
171
|
|
|
@@ -184,15 +217,47 @@ onMounted(async () => {
|
|
|
184
217
|
}
|
|
185
218
|
});
|
|
186
219
|
|
|
220
|
+
async function find() {
|
|
221
|
+
error.value = "";
|
|
222
|
+
if (!!appKey.value.trim() !== !!appToken.value.trim()) {
|
|
223
|
+
return void (error.value = `Enter both the ${labels.value.key} and the ${labels.value.token}.`);
|
|
224
|
+
}
|
|
225
|
+
finding.value = true;
|
|
226
|
+
try {
|
|
227
|
+
const pair = typed.value ? { appKey: appKey.value.trim(), appToken: appToken.value.trim() } : null;
|
|
228
|
+
const body = pair
|
|
229
|
+
? { site: site.value, brand: brand.value, ...pair }
|
|
230
|
+
: editing.value
|
|
231
|
+
? { robot: String(props.robot!._id) }
|
|
232
|
+
: { site: site.value, brand: brand.value };
|
|
233
|
+
const res = await discoverRobots(body);
|
|
234
|
+
found.value = discoveredRobotOptions(res?.items);
|
|
235
|
+
const free = found.value.filter((o) => !o.disabled);
|
|
236
|
+
if (!found.value.length) error.value = "No robots are on this account.";
|
|
237
|
+
else if (!robotSn.value && free.length === 1) robotSn.value = free[0]!.value;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
found.value = [];
|
|
240
|
+
error.value = robotErrorMessage(e, "Could not list the robots.");
|
|
241
|
+
} finally {
|
|
242
|
+
finding.value = false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
187
246
|
async function submit() {
|
|
188
247
|
error.value = "";
|
|
189
248
|
if (!name.value.trim()) return void (error.value = "Give the robot a name.");
|
|
190
|
-
|
|
191
|
-
|
|
249
|
+
// Adding with no pair typed = reuse this client's saved one; needs the picked robot.
|
|
250
|
+
const useSaved = !editing.value && discoverable.value && !appKey.value.trim() && !appToken.value.trim();
|
|
251
|
+
if (useSaved && !robotSn.value) {
|
|
252
|
+
return void (error.value = `Enter the ${labels.value.key} and ${labels.value.token}, or press Find robots to use this client's saved ones and pick the robot.`);
|
|
253
|
+
}
|
|
254
|
+
const creds = useSaved ? ({ ok: true, body: {} } as const) : credentialPatch(appKey.value, appToken.value, editing.value);
|
|
255
|
+
if (!creds.ok) return void (error.value = credentialMissingMessage(labels.value, editing.value));
|
|
256
|
+
const serial = robotSn.value && robotSn.value !== savedSn ? { robotSn: robotSn.value } : {};
|
|
192
257
|
saving.value = true;
|
|
193
258
|
try {
|
|
194
259
|
if (editing.value) {
|
|
195
|
-
await updateRobot(String(props.robot!._id), { name: name.value.trim(), ...creds.body });
|
|
260
|
+
await updateRobot(String(props.robot!._id), { name: name.value.trim(), ...creds.body, ...serial });
|
|
196
261
|
emit("saved", name.value.trim());
|
|
197
262
|
} else {
|
|
198
263
|
if (!brand.value) throw new Error("Choose the robot's brand.");
|
|
@@ -201,8 +266,8 @@ async function submit() {
|
|
|
201
266
|
name: name.value.trim(),
|
|
202
267
|
brand: brand.value,
|
|
203
268
|
type: type.value,
|
|
204
|
-
appKey: creds.body.appKey!,
|
|
205
|
-
|
|
269
|
+
...(useSaved ? { useClientCredentials: true } : { appKey: creds.body.appKey!, appToken: creds.body.appToken! }),
|
|
270
|
+
...serial,
|
|
206
271
|
metadata: { orgId: org.value, siteId: site.value },
|
|
207
272
|
});
|
|
208
273
|
emit("created", { name: name.value.trim(), site: site.value });
|
package/composables/useRobots.ts
CHANGED
|
@@ -86,8 +86,10 @@ export default function useRobots() {
|
|
|
86
86
|
function createRobot(payload: {
|
|
87
87
|
name: string;
|
|
88
88
|
brand?: string;
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
/** Omitted with `useClientCredentials` - the server copies the client's saved pair. */
|
|
90
|
+
appKey?: string;
|
|
91
|
+
appToken?: string;
|
|
92
|
+
useClientCredentials?: boolean;
|
|
91
93
|
type?: "cleaning" | "security";
|
|
92
94
|
robotSn?: string;
|
|
93
95
|
metadata: { orgId: string; siteId: string; administratorId?: string };
|
|
@@ -111,6 +113,19 @@ export default function useRobots() {
|
|
|
111
113
|
});
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The robots a company credential covers (Ounitech: one AppID/Secret per
|
|
118
|
+
* company), for the serial picker. Pass a typed pair, a saved robot's id
|
|
119
|
+
* (`robot`, when editing), or just the site to use the client's saved pair.
|
|
120
|
+
* POST so a typed secret never goes in a URL; the answer holds no credential.
|
|
121
|
+
*/
|
|
122
|
+
function discoverRobots(body: { site?: string; brand?: string; robot?: string; appKey?: string; appToken?: string }) {
|
|
123
|
+
return useNuxtApp().$api<{ items: Array<{ robotSn: string; name: string; online: boolean; statusName: string; added: boolean }> }>(
|
|
124
|
+
"/api/robots/discover",
|
|
125
|
+
{ method: "POST", body },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
/** Remove a robot. It stops listing and answering; the record is kept. */
|
|
115
130
|
function deleteRobot(id: string) {
|
|
116
131
|
return useNuxtApp().$api<Record<string, any>>(`/api/robots/${id}`, {
|
|
@@ -498,6 +513,7 @@ export default function useRobots() {
|
|
|
498
513
|
createRobot,
|
|
499
514
|
updateRobot,
|
|
500
515
|
deleteRobot,
|
|
516
|
+
discoverRobots,
|
|
501
517
|
getRobotState,
|
|
502
518
|
getRobotMaps,
|
|
503
519
|
getRobotMapImages,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.52.0",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/utils/rmf-editor.ts
CHANGED
|
@@ -249,9 +249,9 @@ export function alignFloor(shapes: TEditorShape[], f: TFrame, floorNo: number, r
|
|
|
249
249
|
* about the plan's top-left corner - so the plan and the drawing stay
|
|
250
250
|
* together. Null factor with a reason when it cannot be done.
|
|
251
251
|
*/
|
|
252
|
-
export function measureScale(f: TFrame,
|
|
252
|
+
export function measureScale(f: TFrame, line: TEditorShape, realM: number, placement: TPlanPlacement | null) {
|
|
253
253
|
if (!(realM > 0) || !Number.isFinite(realM)) return { error: "Enter the real length in metres (more than 0)." } as const;
|
|
254
|
-
const [a, b] =
|
|
254
|
+
const [a, b] = line.points.map((p) => shareToWorld(f, p));
|
|
255
255
|
const drawn = Math.hypot(b!.x - a!.x, b!.y - a!.y);
|
|
256
256
|
if (drawn < 0.05) return { error: "The two ends are too close together - measure a longer length." } as const;
|
|
257
257
|
const k = realM / drawn;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add / Edit robot: the credential fields in each brand's own words, and the
|
|
3
|
+
* company-account robot picker. Kept out of `robot-display.ts` because that file
|
|
4
|
+
* is vendored into the mobile apps, which have no Add robot.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ounitech issues an "AppID" and a "Secret" (one pair per company - vendor
|
|
9
|
+
* answer of 2026-09-26); the stored fields are still `appKey` / `appToken`.
|
|
10
|
+
* The demo brand accepts anything.
|
|
11
|
+
*/
|
|
12
|
+
export function credentialLabels(brand: unknown): { key: string; token: string } {
|
|
13
|
+
if (brand === "ounitech") return { key: "AppID", token: "Secret" };
|
|
14
|
+
if (brand === "demo") return { key: "App key (any value)", token: "Token (any value)" };
|
|
15
|
+
return { key: "App key", token: "Token" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** "Enter the AppID and the Secret ..." - `credentialPatch`'s refusal, in the brand's words. */
|
|
19
|
+
export function credentialMissingMessage(labels: { key: string; token: string }, editing: boolean): string {
|
|
20
|
+
return editing
|
|
21
|
+
? `Enter both the ${labels.key} and the ${labels.token} to replace them, or leave both empty.`
|
|
22
|
+
: `Enter the ${labels.key} and the ${labels.token} from the brand portal.`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Discovered robots as picker options: "Lobby Q3 · OT-SN-A", marking ones this
|
|
27
|
+
* client already added (disabled - one serial, one record) and offline ones.
|
|
28
|
+
*/
|
|
29
|
+
export function discoveredRobotOptions(
|
|
30
|
+
items: Array<{ robotSn?: unknown; name?: unknown; online?: unknown; added?: unknown }> | null | undefined,
|
|
31
|
+
): Array<{ value: string; label: string; disabled: boolean }> {
|
|
32
|
+
return (items ?? [])
|
|
33
|
+
.filter((r) => typeof r?.robotSn === "string" && r.robotSn)
|
|
34
|
+
.map((r) => {
|
|
35
|
+
const sn = String(r.robotSn);
|
|
36
|
+
const name = typeof r.name === "string" && r.name.trim() ? `${r.name.trim()} · ${sn}` : sn;
|
|
37
|
+
const tags = [r.added === true ? "already added" : "", r.online === false ? "offline" : ""].filter(Boolean);
|
|
38
|
+
return { value: sn, label: tags.length ? `${name} (${tags.join(", ")})` : name, disabled: r.added === true };
|
|
39
|
+
});
|
|
40
|
+
}
|
package/utils/theme-aa-ledger.ts
CHANGED
|
@@ -134,7 +134,8 @@ export const DARK_PAIRS = () => {
|
|
|
134
134
|
].map((p) => ({ ...p, ratio: Math.round(contrast(p.fg, p.bg) * 100) / 100, passes: contrast(p.fg, p.bg) >= p.need }));
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
// Guarded: this file is auto-importable, and a browser has no `process` (an unguarded check crashed the robot pages in 4.50.0).
|
|
138
|
+
if (typeof process !== "undefined" && process.argv?.[1]?.endsWith("theme-aa-ledger.ts")) {
|
|
138
139
|
const row = (r: { what: string; fg: string; bg: string; ratio: number; need: number; passes: boolean }) =>
|
|
139
140
|
`${r.passes ? "PASS" : "FAIL"} ${String(r.ratio).padStart(5)}:1 / ${r.need} ${r.fg} on ${r.bg} ${r.what}`;
|
|
140
141
|
console.log("LIGHT");
|