@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
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3320
- GET /api/roles/id/:role that membership's role document
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
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3323
- staff = member.type === "admin" && role.type === "admin"
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 today). -->
34
- <RobotEmergencyStop v-if="cmdOk && caps?.emergencyStop === true" :robot-id="robotId" :name="record?.name" @stopped="fastUntil = Date.now() + COMMAND_FOLLOW_MS" />
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 { credentialLabels, credentialMissingMessage, discoveredRobotOptions } from "../../utils/robot-credential-form";
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
- const typed = computed(() => !!appKey.value.trim() && !!appToken.value.trim());
158
- // Ounitech / PadBot: one credential pair per company account, so the robot is picked from its list.
159
- const discoverable = computed(() => brand.value === "ounitech" || brand.value === "padbot");
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() !== !!appToken.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: appToken.value.trim() } : null;
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() && !appToken.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, appToken.value, editing.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;
@@ -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.128.0",
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