@7365admin1/layer-common 4.128.0 → 4.129.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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.129.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f06eeb9: Robots: OneWay brand in Add / Edit robot (API key + server address, OneWay's cloud prefilled, Find robots lists the company's robots) and a Release emergency stop button for brands whose emergency stop is a software one (OneWay).
|
|
8
|
+
|
|
3
9
|
## 4.128.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -3316,11 +3322,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
|
|
|
3316
3322
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
3317
3323
|
own rule from two endpoints the console already calls, unprojected:
|
|
3318
3324
|
|
|
3319
|
-
|
|
3320
|
-
|
|
3325
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
3326
|
+
GET /api/roles/id/:role that membership's role document
|
|
3321
3327
|
|
|
3322
|
-
|
|
3323
|
-
|
|
3328
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
3329
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
3324
3330
|
|
|
3325
3331
|
`role.default` is the marker because it is the only property of a platform
|
|
3326
3332
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -30,8 +30,14 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
<!-- Emergency stop: anyone allowed to command this robot, one tap, no confirm (PadBot
|
|
34
|
-
<RobotEmergencyStop
|
|
33
|
+
<!-- Emergency stop: anyone allowed to command this robot, one tap, no confirm (PadBot, OneWay). OneWay's is a software stop: Release lifts it. -->
|
|
34
|
+
<RobotEmergencyStop
|
|
35
|
+
v-if="cmdOk && caps?.emergencyStop === true"
|
|
36
|
+
:robot-id="robotId"
|
|
37
|
+
:name="record?.name"
|
|
38
|
+
:releasable="caps?.emergencyRelease === true && fault?.code === 'E-STOP'"
|
|
39
|
+
@stopped="fastUntil = Date.now() + COMMAND_FOLLOW_MS"
|
|
40
|
+
/>
|
|
35
41
|
|
|
36
42
|
<div v-if="error" class="hero-notice" role="alert">{{ error }}</div>
|
|
37
43
|
<div v-else-if="live && !live.ok && live.reason" class="hero-notice" role="status">Can't read this robot right now: {{ live.reason }}</div>
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
command this robot. One tap, NO confirm - a stop must never wait on a
|
|
5
5
|
dialog or on a state read. The server sends PadBot's "full machine stop"
|
|
6
6
|
over the live link AND its stop command, and says which reached the robot.
|
|
7
|
+
OneWay's stop is a software one that stays on: while the robot reports it
|
|
8
|
+
(`releasable`), Release lifts it - after a confirm, since the robot may move again.
|
|
7
9
|
-->
|
|
8
10
|
<div class="hero-estop" role="group" aria-label="Emergency stop">
|
|
9
11
|
<button type="button" class="hero-estop__btn" :disabled="busy" @click="stop">
|
|
10
12
|
<span class="hero-estop__icon" aria-hidden="true">■</span>
|
|
11
13
|
{{ busy ? "Stopping…" : "Emergency stop" }}
|
|
12
14
|
</button>
|
|
15
|
+
<v-btn v-if="releasable" variant="outlined" :loading="releasing" :disabled="busy" @click="release">Release emergency stop</v-btn>
|
|
13
16
|
<div v-if="result" class="hero-estop__msg" :class="{ 'hero-estop__msg--error': failed }" role="status" aria-live="assertive">{{ result }}</div>
|
|
14
17
|
</div>
|
|
15
18
|
</template>
|
|
@@ -18,11 +21,12 @@
|
|
|
18
21
|
import { ref } from "vue";
|
|
19
22
|
import { estopResultText } from "../../utils/robot-config";
|
|
20
23
|
|
|
21
|
-
const props = defineProps<{ robotId: string; name?: string }>();
|
|
24
|
+
const props = defineProps<{ robotId: string; name?: string; releasable?: boolean }>();
|
|
22
25
|
const emit = defineEmits<{ stopped: [] }>();
|
|
23
|
-
const { emergencyStopRobot } = useRobots();
|
|
26
|
+
const { emergencyStopRobot, releaseEmergencyStopRobot } = useRobots();
|
|
24
27
|
|
|
25
28
|
const busy = ref(false);
|
|
29
|
+
const releasing = ref(false);
|
|
26
30
|
const result = ref("");
|
|
27
31
|
const failed = ref(false);
|
|
28
32
|
|
|
@@ -42,6 +46,23 @@ async function stop() {
|
|
|
42
46
|
busy.value = false;
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
|
|
50
|
+
async function release() {
|
|
51
|
+
if (!window.confirm(`Release the emergency stop on ${props.name || "this robot"}? It can take commands and move again.`)) return;
|
|
52
|
+
releasing.value = true;
|
|
53
|
+
failed.value = false;
|
|
54
|
+
result.value = "";
|
|
55
|
+
try {
|
|
56
|
+
const r = await releaseEmergencyStopRobot(props.robotId);
|
|
57
|
+
result.value = r?.message || "Emergency stop released.";
|
|
58
|
+
emit("stopped");
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
failed.value = true;
|
|
61
|
+
result.value = e?.data?.message || e?.message || "The release did not reach the robot. The emergency stop is still on.";
|
|
62
|
+
} finally {
|
|
63
|
+
releasing.value = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
</script>
|
|
46
67
|
|
|
47
68
|
<style scoped>
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
:id="`${uid}-token`"
|
|
68
68
|
v-model="appToken"
|
|
69
69
|
class="hero-input"
|
|
70
|
-
type="password"
|
|
70
|
+
:type="tokenSecret ? 'password' : 'text'"
|
|
71
71
|
autocomplete="new-password"
|
|
72
72
|
spellcheck="false"
|
|
73
73
|
:placeholder="credentialPlaceholder(editing)"
|
|
@@ -108,7 +108,14 @@ import {
|
|
|
108
108
|
type TRobotType,
|
|
109
109
|
} from "../../utils/robot-display";
|
|
110
110
|
import { credentialsNote } from "../../utils/robot-floorplan";
|
|
111
|
-
import {
|
|
111
|
+
import {
|
|
112
|
+
credentialDiscoverable,
|
|
113
|
+
credentialLabels,
|
|
114
|
+
credentialMissingMessage,
|
|
115
|
+
credentialTokenDefault,
|
|
116
|
+
credentialTokenIsSecret,
|
|
117
|
+
discoveredRobotOptions,
|
|
118
|
+
} from "../../utils/robot-credential-form";
|
|
112
119
|
|
|
113
120
|
const props = withDefaults(
|
|
114
121
|
defineProps<{
|
|
@@ -154,9 +161,17 @@ const robotSn = ref(savedSn);
|
|
|
154
161
|
const found = ref<Array<{ value: string; label: string; disabled: boolean }>>([]);
|
|
155
162
|
const finding = ref(false);
|
|
156
163
|
const labels = computed(() => credentialLabels(brand.value));
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
164
|
+
// A prefilled server address with no key typed is not a typed pair (Find robots then uses the client's saved one).
|
|
165
|
+
const tokenValue = computed(() => (!appKey.value.trim() && appToken.value.trim() === credentialTokenDefault(brand.value) ? "" : appToken.value.trim()));
|
|
166
|
+
const typed = computed(() => !!appKey.value.trim() && !!tokenValue.value);
|
|
167
|
+
// Ounitech / PadBot / OneWay: one company credential lists its robots, so the robot is picked from that list.
|
|
168
|
+
const discoverable = computed(() => credentialDiscoverable(brand.value));
|
|
169
|
+
// OneWay's second field is its server's address: plain text, prefilled with OneWay's cloud when adding.
|
|
170
|
+
const tokenSecret = computed(() => credentialTokenIsSecret(brand.value));
|
|
171
|
+
watch(brand, (b, was) => {
|
|
172
|
+
if (editing.value) return;
|
|
173
|
+
if (!appToken.value.trim() || appToken.value === credentialTokenDefault(was)) appToken.value = credentialTokenDefault(b);
|
|
174
|
+
});
|
|
160
175
|
const findHint = computed(() =>
|
|
161
176
|
editing.value || typed.value
|
|
162
177
|
? "Press Find robots"
|
|
@@ -219,12 +234,12 @@ onMounted(async () => {
|
|
|
219
234
|
|
|
220
235
|
async function find() {
|
|
221
236
|
error.value = "";
|
|
222
|
-
if (!!appKey.value.trim() !== !!
|
|
237
|
+
if (!!appKey.value.trim() !== !!tokenValue.value) {
|
|
223
238
|
return void (error.value = `Enter both the ${labels.value.key} and the ${labels.value.token}.`);
|
|
224
239
|
}
|
|
225
240
|
finding.value = true;
|
|
226
241
|
try {
|
|
227
|
-
const pair = typed.value ? { appKey: appKey.value.trim(), appToken:
|
|
242
|
+
const pair = typed.value ? { appKey: appKey.value.trim(), appToken: tokenValue.value } : null;
|
|
228
243
|
const body = pair
|
|
229
244
|
? { site: site.value, brand: brand.value, ...pair }
|
|
230
245
|
: editing.value
|
|
@@ -247,11 +262,11 @@ async function submit() {
|
|
|
247
262
|
error.value = "";
|
|
248
263
|
if (!name.value.trim()) return void (error.value = "Give the robot a name.");
|
|
249
264
|
// 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() && !
|
|
265
|
+
const useSaved = !editing.value && discoverable.value && !appKey.value.trim() && !tokenValue.value;
|
|
251
266
|
if (useSaved && !robotSn.value) {
|
|
252
267
|
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
268
|
}
|
|
254
|
-
const creds = useSaved ? ({ ok: true, body: {} } as const) : credentialPatch(appKey.value,
|
|
269
|
+
const creds = useSaved ? ({ ok: true, body: {} } as const) : credentialPatch(appKey.value, tokenValue.value, editing.value);
|
|
255
270
|
if (!creds.ok) return void (error.value = credentialMissingMessage(labels.value, editing.value));
|
|
256
271
|
const serial = robotSn.value && robotSn.value !== savedSn ? { robotSn: robotSn.value } : {};
|
|
257
272
|
saving.value = true;
|
package/composables/useRobots.ts
CHANGED
|
@@ -358,6 +358,15 @@ export default function useRobots() {
|
|
|
358
358
|
);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/** `emergencyRelease` (OneWay): lift a software emergency stop so the robot takes commands again. */
|
|
362
|
+
function releaseEmergencyStopRobot(id: string) {
|
|
363
|
+
return useNuxtApp().$api<{ ok: boolean; message: string }>(`/api/robots/${id}/emergency-stop`, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
query: { appSlug },
|
|
366
|
+
body: { release: true },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
361
370
|
/** Super Admin setup: what can be changed now, read fresh from the maker's cloud (`mapId` picks the map). */
|
|
362
371
|
function getRobotSetup(id: string, mapId?: string | null) {
|
|
363
372
|
return useNuxtApp().$api<Record<string, any>>(`/api/robots/${id}/setup`, {
|
|
@@ -656,6 +665,7 @@ export default function useRobots() {
|
|
|
656
665
|
saveSiteFleetCore,
|
|
657
666
|
createSiteRelayKey,
|
|
658
667
|
emergencyStopRobot,
|
|
668
|
+
releaseEmergencyStopRobot,
|
|
659
669
|
getRobotSetup,
|
|
660
670
|
applyRobotSetup,
|
|
661
671
|
listBuildingMaps,
|
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.129.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.",
|
|
@@ -14,9 +14,29 @@ export function credentialLabels(brand: unknown): { key: string; token: string }
|
|
|
14
14
|
// PadBot issues an AccessKey ID + AccessKey Secret per account (API v4.0 §2.1).
|
|
15
15
|
if (brand === "padbot") return { key: "AccessKey ID", token: "AccessKey Secret" };
|
|
16
16
|
if (brand === "demo") return { key: "App key (any value)", token: "Token (any value)" };
|
|
17
|
+
// OneWay (Anbot RMS): one API key per company; the second field is the RMS server's address.
|
|
18
|
+
if (brand === "oneway") return { key: "API key", token: "Server address" };
|
|
17
19
|
return { key: "App key", token: "Token" };
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/** OneWay's cloud, prefilled in "Server address" when adding (change it only for a company's own RMS). */
|
|
23
|
+
export const ONEWAY_DEFAULT_SERVER = "https://www.anbotcloud.cn";
|
|
24
|
+
|
|
25
|
+
/** What the second field starts with when adding a robot of this brand ("" = empty). */
|
|
26
|
+
export function credentialTokenDefault(brand: unknown): string {
|
|
27
|
+
return brand === "oneway" ? ONEWAY_DEFAULT_SERVER : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** False when the second field is not a secret (OneWay's server address): shown as plain text. */
|
|
31
|
+
export function credentialTokenIsSecret(brand: unknown): boolean {
|
|
32
|
+
return brand !== "oneway";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Brands whose one company credential lists its robots, so Add robot picks the serial (Find robots). */
|
|
36
|
+
export function credentialDiscoverable(brand: unknown): boolean {
|
|
37
|
+
return brand === "ounitech" || brand === "padbot" || brand === "oneway";
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
/** "Enter the AppID and the Secret ..." - `credentialPatch`'s refusal, in the brand's words. */
|
|
21
41
|
export function credentialMissingMessage(labels: { key: string; token: string }, editing: boolean): string {
|
|
22
42
|
return editing
|