@7365admin1/layer-common 4.130.0 → 4.131.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,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 4.131.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9c314c1: Robots shared space: status line on the site robots page ("One robot moves at a time" / "HERO Fleet Core is coordinating both robots" / on but not coordinating, with the reason) and the Super Admin "Shared space (HERO Fleet Core coordinates)" switch on the Setup page (site feature setting `robotSharedSpace`, default off).
8
+
9
+ ## 4.130.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 3c80d85: Robots setup checklist: OneWay's "not online" reason and its before-adding prerequisite, in the same words as Ounitech and PadBot.
14
+
3
15
  ## 4.130.0
4
16
 
5
17
  ### Minor Changes
@@ -3328,11 +3340,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3328
3340
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3329
3341
  own rule from two endpoints the console already calls, unprojected:
3330
3342
 
3331
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3332
- GET /api/roles/id/:role that membership's role document
3343
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3344
+ GET /api/roles/id/:role that membership's role document
3333
3345
 
3334
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3335
- staff = member.type === "admin" && role.type === "admin"
3346
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3347
+ staff = member.type === "admin" && role.type === "admin"
3336
3348
 
3337
3349
  `role.default` is the marker because it is the only property of a platform
3338
3350
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -55,6 +55,7 @@
55
55
  <RobotSiteFloors :site="site" :robots="all" :states="states as Record<string, Record<string, any>>" :map-floors="mapFloors" @saved="floorsSaved" />
56
56
  </div>
57
57
  <RobotSiteFleetCore :site="site" />
58
+ <RobotSiteSharedSpace :site="site" editable />
58
59
  </template>
59
60
 
60
61
  <script setup lang="ts">
@@ -62,6 +63,7 @@ import { computed, ref, toRef, watch } from "vue";
62
63
  import RobotFleetCoreButton from "./FleetCoreButton.vue";
63
64
  import RobotSiteFloors from "./SiteFloors.vue";
64
65
  import RobotSiteFleetCore from "./SiteFleetCore.vue";
66
+ import RobotSiteSharedSpace from "./SiteSharedSpace.vue";
65
67
  import useRobotBuilding from "../../composables/useRobotBuilding";
66
68
  import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
67
69
  import { robotSetupSteps, setupProgress, STATUS_TEXT, type TSetupStatus, type TSetupTarget } from "../../utils/robot-setup";
@@ -15,6 +15,7 @@
15
15
  </div>
16
16
 
17
17
  <RobotSummaryStrip :key="`${site}:${type}`" class="hero-summary--six" :cells="cells" label="Site summary" />
18
+ <RobotSiteSharedSpace :key="`ss:${site}`" :site="site" />
18
19
 
19
20
  <div class="hero-split">
20
21
  <div class="hero-split__main hero-split__main--fleet">
@@ -85,6 +86,7 @@
85
86
  <script setup lang="ts">
86
87
  import { computed, onBeforeUnmount, ref, watch } from "vue";
87
88
  import RobotSummaryStrip from "./SummaryStrip.vue";
89
+ import RobotSiteSharedSpace from "./SiteSharedSpace.vue";
88
90
  import RobotTable from "./Table.vue";
89
91
  import RobotStatusChip from "./StatusChip.vue";
90
92
  import RobotBattery from "./Battery.vue";
@@ -0,0 +1,89 @@
1
+ <template>
2
+ <!--
3
+ ROBOTS "SHARED SPACE" (owner 2026-10-19): one status line on the site robots page for everyone at the site,
4
+ plus the Super Admin switch on the Setup page (`editable`). Off (default) = one robot moves at a time.
5
+ On = both robots may move while HERO Fleet Core says it is coordinating them (it stops the security robot
6
+ near the cleaning robot); without that word the site falls back to one robot at a time.
7
+ Server: GET /api/robots/sites/:site/coordination; switch = site feature setting `robotSharedSpace`.
8
+ -->
9
+ <section class="ssp" :class="{ 'ssp--panel': editable }" aria-label="Robot coordination">
10
+ <h3 v-if="editable" class="hero-title ssp__title">Shared space (HERO Fleet Core coordinates)</h3>
11
+ <div class="ssp__line" :class="`hero-tone--${TONE[line.tone]}`" role="status" data-test="shared-space-line">{{ line.text }}</div>
12
+ <template v-if="editable && loaded">
13
+ <v-switch v-model="enabled" color="primary" density="compact" hide-details inset label="Let both robots work at the same time" />
14
+ <p class="hero-muted ssp__note">
15
+ HERO Fleet Core stops the security robot when it comes within about 1.2 m of the cleaning robot and restarts it
16
+ when clear. It cannot stop the cleaning robot: that robot's own sensors and no-go zones still keep it away.
17
+ Switch on only after the on-site calibration and test. Stop, return and emergency stop always work.
18
+ </p>
19
+ <div class="ssp__act">
20
+ <v-btn size="small" color="primary" variant="flat" :loading="saving" :disabled="!loaded || saving || enabled === saved" @click="save">Save</v-btn>
21
+ </div>
22
+ <div v-if="message" class="hero-muted" role="status">{{ message }}</div>
23
+ <div v-if="error" class="hero-notice hero-notice--error" role="alert">{{ error }}</div>
24
+ </template>
25
+ </section>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed, onBeforeUnmount, ref, watch } from "vue";
30
+ import { robotErrorMessage } from "../../utils/robot-display";
31
+ import { sharedSpaceLine, type TCoordination } from "../../utils/robot-shared-space";
32
+ import useSiteFeatureSettings from "../../composables/useSiteFeatureSettings";
33
+
34
+ const props = withDefaults(defineProps<{ site: string; editable?: boolean }>(), { editable: false });
35
+ const { getSiteCoordination } = useRobots();
36
+ const { saveSiteFeature } = useSiteFeatureSettings();
37
+ const TONE = { neutral: "neutral", ok: "success", warn: "warning" } as const;
38
+
39
+ const status = ref<TCoordination | null>(null);
40
+ const loaded = ref(false);
41
+ const saved = ref(false);
42
+ const enabled = ref(false);
43
+ const saving = ref(false);
44
+ const message = ref("");
45
+ const error = ref("");
46
+ const line = computed(() => sharedSpaceLine(status.value));
47
+
48
+ async function load() {
49
+ try {
50
+ const res = await getSiteCoordination(props.site);
51
+ status.value = res;
52
+ if (!loaded.value || saved.value !== res.sharedSpace) {
53
+ saved.value = res.sharedSpace;
54
+ if (!saving.value) enabled.value = res.sharedSpace;
55
+ }
56
+ loaded.value = true;
57
+ } catch {
58
+ // An older server has no coordination route: say nothing rather than a wrong line.
59
+ status.value = { sharedSpace: false, coordinating: false };
60
+ }
61
+ }
62
+
63
+ async function save() {
64
+ error.value = message.value = "";
65
+ saving.value = true;
66
+ try {
67
+ await saveSiteFeature(props.site, "robotSharedSpace", { enabled: enabled.value });
68
+ message.value = enabled.value ? "Saved. Both robots may work together while HERO Fleet Core is coordinating." : "Saved. One robot moves at a time.";
69
+ await load();
70
+ } catch (e) {
71
+ error.value = robotErrorMessage(e, "Could not save the shared space setting.");
72
+ } finally {
73
+ saving.value = false;
74
+ }
75
+ }
76
+
77
+ const timer = setInterval(load, 10_000);
78
+ onBeforeUnmount(() => clearInterval(timer));
79
+ watch(() => props.site, () => { loaded.value = false; load(); }, { immediate: true });
80
+ </script>
81
+
82
+ <style scoped>
83
+ .ssp { margin: 8px 0; display: grid; gap: 6px; }
84
+ .ssp--panel { margin-top: 16px; padding: 12px; background: var(--hero-surface); box-shadow: var(--hero-shadow-sm); }
85
+ .ssp__title { font-size: 16px; }
86
+ .ssp__line { font-size: 13px; padding: 6px 10px; border-radius: 6px; overflow-wrap: anywhere; }
87
+ .ssp__note { font-size: 12px; margin: 0; }
88
+ .ssp__act { display: flex; gap: 6px; }
89
+ </style>
@@ -652,6 +652,13 @@ export default function useRobots() {
652
652
  function undoSiteFloors(site: string, version: number) {
653
653
  return useNuxtApp().$api<Record<string, any>>(`${sitePath(site)}/floors/undo`, { method: "POST", query: { appSlug }, body: { version } });
654
654
  }
655
+ /** Shared space status line: the site switch + whether HERO Fleet Core coordinates the robots now (anyone at the site). */
656
+ function getSiteCoordination(site: string) {
657
+ return useNuxtApp().$api<{ sharedSpace: boolean; coordinating: boolean; reason: string; separationM: number | null; held: boolean; at: string | null }>(
658
+ `${sitePath(site)}/coordination`,
659
+ { query: { appSlug } },
660
+ );
661
+ }
655
662
  function getSiteFleetCore(site: string) {
656
663
  return useNuxtApp().$api<Record<string, any>>(`${sitePath(site)}/fleet-core`, { query: { appSlug } });
657
664
  }
@@ -666,6 +673,7 @@ export default function useRobots() {
666
673
  getSiteFloors,
667
674
  saveSiteFloors,
668
675
  undoSiteFloors,
676
+ getSiteCoordination,
669
677
  getSiteFleetCore,
670
678
  saveSiteFleetCore,
671
679
  createSiteRelayKey,
@@ -16,6 +16,7 @@ export const SITE_FEATURE_KEYS = [
16
16
  "dataRetention",
17
17
  "maStaffRegistration",
18
18
  "visitorPassSms",
19
+ "robotSharedSpace",
19
20
  ] as const;
20
21
 
21
22
  export type TSiteFeatureKey = (typeof SITE_FEATURE_KEYS)[number];
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.130.0",
5
+ "version": "4.131.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.",
@@ -88,6 +88,7 @@ export function offlineReason(robot: Record<string, any>, state: Record<string,
88
88
  const why = state.reason ? ` (${state.reason})` : "";
89
89
  if (robot?.brand === "ounitech") return `${name}: the Ounitech cloud did not answer${why}. Ask Ounitech to link this robot's serial to our account.`;
90
90
  if (robot?.brand === "padbot") return `${name}: the PadBot cloud did not answer${why}. Check the robot's key and token.`;
91
+ if (robot?.brand === "oneway") return `${name}: OneWay's RMS did not answer${why}. Check the robot's API key and server address, and ask OneWay to put this robot under our company key.`;
91
92
  return `${name}: its cloud did not answer${why}.`;
92
93
  }
93
94
  return `${name}: its cloud says it is offline - switched off, charging dock unplugged or out of Wi-Fi.`;
@@ -129,7 +130,7 @@ export function robotSetupSteps(input: TSetupInput): TSetupStep[] {
129
130
  }
130
131
 
131
132
  // 2. Robots added.
132
- const prereq = "Before adding: Ounitech must link each robot's serial to our Seven365 account; for PadBot, save the robot's points (charger, stops) in the PadBot app first.";
133
+ const prereq = "Before adding: Ounitech must link each robot's serial to our Seven365 account; for PadBot, save the robot's points (charger, stops) in the PadBot app first; for OneWay, get our company API key from OneWay and have the robot's routes and charging point set in OneWay's RMS.";
133
134
  if (!robots) {
134
135
  steps.push({ key: "robots", title: "Robots added", status: "check", reason: "Could not read this site's robots.", button: { label: "Open fleet", target: "fleet" } });
135
136
  } else {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Robots "Shared space" status line (core `GET /api/robots/sites/:site/coordination`, 2026-10-19).
3
+ * Off = one robot moves at a time (the 15 Oct rule). On = both may move while HERO Fleet Core
4
+ * says it is coordinating them; otherwise the site falls back to one robot at a time.
5
+ */
6
+ export type TCoordination = {
7
+ sharedSpace: boolean;
8
+ coordinating: boolean;
9
+ reason?: string;
10
+ separationM?: number | null;
11
+ held?: boolean;
12
+ };
13
+
14
+ export function sharedSpaceLine(c: TCoordination | null | undefined): { text: string; tone: "neutral" | "ok" | "warn" } {
15
+ if (!c) return { text: "Robot coordination: checking…", tone: "neutral" };
16
+ if (!c.sharedSpace) return { text: "One robot moves at a time at this site.", tone: "neutral" };
17
+ if (!c.coordinating) {
18
+ return {
19
+ text: `Shared space is on, but HERO Fleet Core is not coordinating now${c.reason ? ` (${c.reason})` : ""} - one robot moves at a time.`,
20
+ tone: "warn",
21
+ };
22
+ }
23
+ const apart = typeof c.separationM === "number" ? ` - robots ${c.separationM.toFixed(1)} m apart` : "";
24
+ const held = c.held ? " · security robot held until the cleaning robot is clear" : "";
25
+ return { text: `Shared space: HERO Fleet Core is coordinating both robots${apart}${held}.`, tone: "ok" };
26
+ }