@7365admin1/layer-common 4.130.1 → 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,11 @@
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
+
3
9
  ## 4.130.1
4
10
 
5
11
  ### Patch Changes
@@ -3334,11 +3340,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3334
3340
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3335
3341
  own rule from two endpoints the console already calls, unprojected:
3336
3342
 
3337
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3338
- 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
3339
3345
 
3340
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3341
- 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"
3342
3348
 
3343
3349
  `role.default` is the marker because it is the only property of a platform
3344
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.1",
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.",
@@ -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
+ }