@7365admin1/layer-common 4.97.1-staging.606 → 4.97.1-staging.608

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.
@@ -83,7 +83,7 @@
83
83
  </div>
84
84
  <div class="hero-bmap__seg" role="group" aria-label="Floor">
85
85
  <button type="button" :aria-pressed="focus === null" @click="focus = null">All</button>
86
- <button v-for="f in floors" :key="f" type="button" :aria-pressed="focus === f" @click="focus = f">{{ floorName(f) }}</button>
86
+ <button v-for="f in floors" :key="f" type="button" :aria-pressed="focus === f" @click="focus = f">{{ levelName(f) }}</button>
87
87
  </div>
88
88
  <span class="hero-bmap__readout">{{ cameraReadout(cam) }}</span>
89
89
  <span class="hero-bmap__readout">Drag to rotate · shift+drag or right-drag to pan · scroll to zoom</span>
@@ -184,7 +184,7 @@
184
184
  </button>
185
185
  </div>
186
186
 
187
- <div class="hero-bmap__floorlabel" :style="{ transform: `translateZ(6px) ${unskew}` }">{{ floorName(f.floorNo) }}</div>
187
+ <div class="hero-bmap__floorlabel" :style="{ transform: `translateZ(6px) ${unskew}` }">{{ levelName(f.floorNo) }}</div>
188
188
  </div>
189
189
 
190
190
  <div
@@ -212,7 +212,7 @@
212
212
  <aside class="hero-bmap__aside">
213
213
  <div class="hero-aside__title hero-bmap__asidetitle">Floors · top down</div>
214
214
  <div v-for="f in legend" :key="f.floorNo" class="hero-bmap__legendrow">
215
- <b>{{ floorName(f.floorNo) }}</b>
215
+ <b>{{ levelName(f.floorNo) }}</b>
216
216
  <div v-if="f.robotMaps.length" class="hero-mapsel__hint">Robot map: {{ f.robotMaps.map((m) => m.label + (m.current ? " (robot is here)" : "")).join(", ") }}</div>
217
217
  <div class="hero-bmap__chips">
218
218
  <button v-for="r in f.robots" :key="r.id" type="button" class="hero-bmap__chip" @click="emit('open', r.id)"><span :class="`hero-tone--${tone(r)}`">{{ r.name }}</span></button>
@@ -242,11 +242,12 @@ import { floorAspect, floorDoors, placementInWorld, planBoxIn, routeOverlay, vie
242
242
  import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
243
243
  import { openDoorNames } from "../../utils/robot-fleet-core";
244
244
  import { robotMapViews, robotMapsByFloor, type TRobotMapView } from "../../utils/robot-maps";
245
+ import { siteFloorKey, siteFloorLabel, type TSiteFloor } from "../../utils/robot-floors";
245
246
  import {
246
247
  BUILDING_TAG,
247
248
  nameTagSide,
248
249
  brandName,
249
- floorName,
250
+ siteFloorOf,
250
251
  mapFloors,
251
252
  mapRobots,
252
253
  pickFloor,
@@ -319,7 +320,19 @@ const siteLabel = computed(() => props.siteName || names.value.siteName || "This
319
320
  const ids = computed(() => rows.value.map((r) => String(r._id)));
320
321
  const { states, connected } = useRobotLive(ids, siteStatesFeed());
321
322
  const routes = ref<Record<string, TPatrolRouteView[]>>({});
322
- const { getPatrolRoutes, getRobotBrands, getRobotMaps } = useRobots();
323
+ const { getPatrolRoutes, getRobotBrands, getRobotMaps, getSiteFloors } = useRobots();
324
+ /** The site's floor names: the robots' own maps and routes go on the site floor (the PadBot's 3 = 34). */
325
+ const siteFloors = ref<TSiteFloor[]>([]);
326
+ watch(
327
+ () => props.site,
328
+ async (site) => {
329
+ siteFloors.value = [];
330
+ const res = await getSiteFloors(site).catch(() => null);
331
+ if (site === props.site) siteFloors.value = res?.floors ?? [];
332
+ },
333
+ { immediate: true },
334
+ );
335
+ const levelName = (f: number) => siteFloorLabel(siteFloors.value, f, states.value);
323
336
  /** Each security robot's own maps (PadBot), read once per robot: drawn on their floor when the floor has no saved plan. */
324
337
  const robotMaps = ref<Record<string, Array<Record<string, any>>>>({});
325
338
  watch(ids, async (list) => {
@@ -346,7 +359,8 @@ const shapes = computed<TEditorShape[]>(() => shapesFromMap(map.value));
346
359
  const robotFloorMaps = computed(() => {
347
360
  const all: TRobotMapView[] = [];
348
361
  for (const id of ids.value) {
349
- for (const m of robotMapViews(robotMaps.value[id], states.value[id]?.mapId ?? null, routes.value[id] ?? [])) {
362
+ const site = { key: (n: number) => siteFloorKey(siteFloors.value, id, n), name: levelName };
363
+ for (const m of robotMapViews(robotMaps.value[id], states.value[id]?.mapId ?? null, routes.value[id] ?? [], site)) {
350
364
  const i = all.findIndex((x) => x.mapId === m.mapId);
351
365
  if (i < 0) all.push(m);
352
366
  else if (m.current) all[i] = { ...all[i]!, current: true };
@@ -364,7 +378,7 @@ const floorRoutes = (floorNo: number) => {
364
378
  for (const id of ids.value) {
365
379
  const st = states.value[id];
366
380
  for (const r of routes.value[id] ?? []) {
367
- const f = r.floorNo ?? (st?.floorKnown ? st.floorNo : null);
381
+ const f = r.floorNo !== null && r.floorNo !== undefined ? siteFloorKey(siteFloors.value, id, r.floorNo) : siteFloorOf(st);
368
382
  if (f === floorNo && !seen.has(r.id)) seen.set(r.id, r);
369
383
  }
370
384
  }
@@ -336,6 +336,7 @@ import RobotEmergencyStop from "./EmergencyStop.vue";
336
336
  import RobotSetupPanel from "./SetupPanel.vue";
337
337
  import { faultPhotos } from "../../utils/robot-control";
338
338
  import { cleanAreaChoices, cleanAreaCommand, mapLayerLegend, robotMapDefault, robotMapViews } from "../../utils/robot-maps";
339
+ import { siteFloorKey, siteFloorLabel, type TSiteFloor } from "../../utils/robot-floors";
339
340
  import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
340
341
  import {
341
342
  attentionText,
@@ -435,6 +436,7 @@ const {
435
436
  deleteRobot,
436
437
  getRobotMaps,
437
438
  getRobotMapImages,
439
+ getSiteFloors,
438
440
  resolveRobotAlerts,
439
441
  getPatrolHistory,
440
442
  getSiteFaults,
@@ -626,7 +628,24 @@ async function loadSchedule() {
626
628
 
627
629
  /* ---- the robot's own maps (PadBot: one per floor / area): a selector, opening where the robot is ---- */
628
630
  const mapItems = ref<Array<Record<string, any>>>([]);
629
- const mapViews = computed(() => robotMapViews(mapItems.value, live.value?.mapId ?? null, patrol.value?.routes ?? []));
631
+ /** The site's floor names: this robot's map floors are shown as the site calls them (the PadBot's 3 = "34"). */
632
+ const siteFloors = ref<TSiteFloor[]>([]);
633
+ watch(
634
+ () => String(record.value?.site?._id ?? ""),
635
+ async (site) => {
636
+ siteFloors.value = [];
637
+ if (!site) return;
638
+ const res = await getSiteFloors(site).catch(() => null);
639
+ if (site === String(record.value?.site?._id ?? "")) siteFloors.value = res?.floors ?? [];
640
+ },
641
+ { immediate: true },
642
+ );
643
+ const mapViews = computed(() =>
644
+ robotMapViews(mapItems.value, live.value?.mapId ?? null, patrol.value?.routes ?? [], {
645
+ key: (n) => siteFloorKey(siteFloors.value, props.robotId, n),
646
+ name: (key) => siteFloorLabel(siteFloors.value, key, live.value ? { [props.robotId]: live.value } : {}),
647
+ }),
648
+ );
630
649
  const selectedMapId = ref<string | null>(null);
631
650
  const selectedMap = computed(() => mapViews.value.find((m) => m.mapId === selectedMapId.value) ?? null);
632
651
  const mapLegend = computed(() => mapLayerLegend(selectedMap.value));
@@ -70,7 +70,7 @@
70
70
  :type="tokenSecret ? 'password' : 'text'"
71
71
  autocomplete="new-password"
72
72
  spellcheck="false"
73
- :placeholder="credentialPlaceholder(editing)"
73
+ :placeholder="credentialTokenPlaceholder(brand, editing) ?? credentialPlaceholder(editing)"
74
74
  :disabled="saving"
75
75
  />
76
76
  </div>
@@ -111,6 +111,7 @@ import { credentialsNote } from "../../utils/robot-floorplan";
111
111
  import {
112
112
  credentialDiscoverable,
113
113
  credentialLabels,
114
+ credentialTokenPlaceholder,
114
115
  credentialMissingMessage,
115
116
  credentialTokenDefault,
116
117
  credentialTokenIsSecret,
@@ -63,12 +63,12 @@
63
63
  import { computed, ref, watch } from "vue";
64
64
  import { routeOverlay } from "../../utils/rmf-editor";
65
65
  import { robotMapViews, type TRobotMapView } from "../../utils/robot-maps";
66
+ import { siteFloorKey, siteFloorLabel, type TSiteFloor } from "../../utils/robot-floors";
66
67
  import RobotFloorPlan from "./FloorPlan.vue";
67
68
  import RobotPatrolRoutes from "./PatrolRoutes.vue";
68
69
  import RobotReconnecting from "./Reconnecting.vue";
69
70
  import {
70
71
  brandName,
71
- floorLabel,
72
72
  mapFloors,
73
73
  mapRobots,
74
74
  nameTagSide,
@@ -76,6 +76,7 @@ import {
76
76
  positionGap,
77
77
  positionGapText,
78
78
  robotStatus,
79
+ siteFloorOf,
79
80
  unplacedRobots,
80
81
  unplacedText,
81
82
  type TLiveRobotState,
@@ -100,8 +101,10 @@ const props = withDefaults(
100
101
  map?: Record<string, any> | null;
101
102
  /** Each robot's own maps (`/robots/:id/maps` items) by robot id - PadBot's map images and a map selector. */
102
103
  robotMaps?: Record<string, Array<Record<string, any>>>;
104
+ /** The site's floor names (`/robots/sites/:site/floors`): robot maps and routes are put on the SITE floor (the PadBot's 3 = 34). */
105
+ siteFloors?: TSiteFloor[];
103
106
  }>(),
104
- { type: "cleaning", routes: () => ({}), connected: true, brands: () => [], tickMs: 4000, map: null, robotMaps: () => ({}) },
107
+ { type: "cleaning", routes: () => ({}), connected: true, brands: () => [], tickMs: 4000, map: null, robotMaps: () => ({}), siteFloors: () => [] },
105
108
  );
106
109
  const emit = defineEmits<{ open: [id: string] }>();
107
110
 
@@ -111,7 +114,8 @@ const ids = computed(() => props.rows.map((r) => String(r._id)));
111
114
  const allMaps = computed(() => {
112
115
  const seen = new Map<string, TRobotMapView>();
113
116
  for (const id of ids.value) {
114
- for (const m of robotMapViews(props.robotMaps[id], props.states[id]?.mapId ?? null, props.routes[id] ?? [])) {
117
+ const site = { key: (n: number) => siteFloorKey(props.siteFloors, id, n), name: floorName };
118
+ for (const m of robotMapViews(props.robotMaps[id], props.states[id]?.mapId ?? null, props.routes[id] ?? [], site)) {
115
119
  const had = seen.get(m.mapId);
116
120
  seen.set(m.mapId, had ? { ...had, current: had.current || m.current } : m);
117
121
  }
@@ -142,7 +146,9 @@ const robots = computed(() =>
142
146
  const overlay = computed(() => (floor.value === null ? { lanes: [], waypoints: [], fiducials: [] } : routeOverlay(props.map, floor.value)));
143
147
  const hasRoutes = computed(() => !!props.map && (props.map.rmf?.levels ?? props.map.levels ?? []).some((l: any) => (l.lanes ?? []).length));
144
148
  const showRoutes = ref(true);
145
- const floorName = (f: number) => floorLabel({ floorNo: f, floorKnown: true });
149
+ function floorName(f: number) {
150
+ return siteFloorLabel(props.siteFloors, f, props.states);
151
+ }
146
152
 
147
153
  /** The routes walked on this floor, once each. A route with no floor is drawn on its robot's floor. */
148
154
  const floorRoutes = computed(() => {
@@ -150,7 +156,7 @@ const floorRoutes = computed(() => {
150
156
  for (const id of ids.value) {
151
157
  const st = props.states[id];
152
158
  for (const r of props.routes[id] ?? []) {
153
- const f = r.floorNo ?? (st?.floorKnown ? st.floorNo : null);
159
+ const f = r.floorNo !== null && r.floorNo !== undefined ? siteFloorKey(props.siteFloors, id, r.floorNo) : siteFloorOf(st);
154
160
  const onMap = !shownMap.value || !r.mapId || r.mapId === shownMap.value.mapId;
155
161
  if ((r.mapId && shownMap.value ? onMap : f === floor.value) && !seen.has(r.id)) seen.set(r.id, r);
156
162
  }
@@ -81,6 +81,7 @@
81
81
  :connected="connected"
82
82
  :brands="brands"
83
83
  :map="routeMap"
84
+ :site-floors="siteFloors"
84
85
  @open="onMapOpen"
85
86
  />
86
87
  </aside>
@@ -101,6 +102,7 @@ import RobotBattery from "./Battery.vue";
101
102
  import RobotFloorLabel from "./FloorLabel.vue";
102
103
  import RobotFaultBadge from "./FaultBadge.vue";
103
104
  import RobotMiniMap from "./MiniMap.vue";
105
+ import type { TSiteFloor } from "../../utils/robot-floors";
104
106
  import useRobotLive, { siteStatesFeed } from "../../composables/useRobotLive";
105
107
  import {
106
108
  agoText,
@@ -144,7 +146,18 @@ const emit = defineEmits<{
144
146
  status: [status: { connected: boolean; faults: number; siteName: string; clientName: string }];
145
147
  }>();
146
148
 
147
- const { getSiteFleet, getSiteSummary, getRobotBrands, getPatrolRoutes, getRobotMaps, listSiteBuildingMaps, getBuildingMap, getFleetCoreBuilding } = useRobots();
149
+ const { getSiteFleet, getSiteSummary, getRobotBrands, getPatrolRoutes, getRobotMaps, listSiteBuildingMaps, getBuildingMap, getFleetCoreBuilding, getSiteFloors } = useRobots();
150
+ /** The site's floor names: the robots' own maps and routes go on the site floor (the PadBot's 3 = 34). None saved / not readable = robot numbers as before. */
151
+ const siteFloors = ref<TSiteFloor[]>([]);
152
+ watch(
153
+ () => props.site,
154
+ async (site) => {
155
+ siteFloors.value = [];
156
+ const res = await getSiteFloors(site).catch(() => null);
157
+ if (site === props.site) siteFloors.value = res?.floors ?? [];
158
+ },
159
+ { immediate: true },
160
+ );
148
161
 
149
162
  /**
150
163
  * The site's building for the mini-map's robot lanes: the live HERO Fleet
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.97.1-staging.606",
5
+ "version": "4.97.1-staging.608",
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.",
@@ -34,6 +34,15 @@ export function credentialTokenIsSecret(brand: unknown): boolean {
34
34
  return brand !== "oneway" && brand !== "viggo";
35
35
  }
36
36
 
37
+ /**
38
+ * The second field's hint when it is not a credential to paste (VIGGO: the
39
+ * robots' VINs); null = the usual write-only hint. Once saved it is never shown
40
+ * back, so editing keeps the usual "Saved · hidden" hint.
41
+ */
42
+ export function credentialTokenPlaceholder(brand: unknown, saved: unknown): string | null {
43
+ return brand === "viggo" && saved !== true ? "Type each robot's VIN, separated by commas" : null;
44
+ }
45
+
37
46
  /** Brands whose one company credential lists its robots, so Add robot picks the serial (Find robots). */
38
47
  export function credentialDiscoverable(brand: unknown): boolean {
39
48
  return brand === "ounitech" || brand === "padbot" || brand === "oneway" || brand === "viggo";
@@ -637,6 +637,10 @@ export type TLiveRobotState = {
637
637
  batteryPercent?: number | null;
638
638
  floorNo?: number | null;
639
639
  floorKnown?: boolean;
640
+ /** The SITE's floor for `floorNo` (server, from the site's floor names): the PadBot's 3 is the 34th floor. */
641
+ siteFloorNo?: number | null;
642
+ /** The site's name for that floor (what the lift car display shows). */
643
+ floorName?: string | null;
640
644
  /** The brand's id of the map the robot is on now (PadBot); absent / null when unknown. */
641
645
  mapId?: string | null;
642
646
  position?: { x: number; y: number; theta: number } | null;
@@ -852,6 +856,13 @@ export function headingDeg(state: { position?: { theta?: unknown } | null } | nu
852
856
  return Math.round(((((-t * 180) / Math.PI) % 360) + 360) % 360);
853
857
  }
854
858
 
859
+ /** The site floor a robot is on: the server's site floor, else its own number (no floor names saved). */
860
+ export function siteFloorOf(live: Partial<TLiveRobotState> | null | undefined): number | null {
861
+ if (!live || live.floorKnown !== true) return null;
862
+ if (finite(live.siteFloorNo) && Number.isInteger(live.siteFloorNo)) return live.siteFloorNo;
863
+ return finite(live.floorNo) ? live.floorNo : null;
864
+ }
865
+
855
866
  /** Is the robot live on a known floor with a plan position - i.e. may it be drawn? */
856
867
  function placeable(live: TLiveRobotState | null | undefined) {
857
868
  return !!live?.ok && live.online !== false && live.floorKnown === true && finite(live.floorNo) && !!planPoint(live as any);
@@ -862,7 +873,8 @@ export function mapFloors(ids: string[], states: Record<string, TLiveRobotState>
862
873
  const floors = new Set<number>();
863
874
  for (const id of ids) {
864
875
  const s = states[id];
865
- if (s?.ok && s.floorKnown === true && finite(s.floorNo)) floors.add(s.floorNo);
876
+ const f = s?.ok ? siteFloorOf(s) : null;
877
+ if (f !== null) floors.add(f);
866
878
  }
867
879
  return [...floors].sort((a, b) => a - b);
868
880
  }
@@ -877,7 +889,7 @@ export function pickFloor(floors: number[], chosen: unknown, ids: string[], stat
877
889
  let best = floors[0]!;
878
890
  let most = -1;
879
891
  for (const f of floors) {
880
- const n = ids.filter((id) => placeable(states[id]) && states[id]!.floorNo === f).length;
892
+ const n = ids.filter((id) => placeable(states[id]) && siteFloorOf(states[id]) === f).length;
881
893
  if (n > most) {
882
894
  most = n;
883
895
  best = f;
@@ -896,7 +908,7 @@ export function mapRobots(
896
908
  return rows.flatMap((r) => {
897
909
  const id = String(r?._id ?? "");
898
910
  const live = states[id];
899
- if (!placeable(live) || live!.floorNo !== floor) return [];
911
+ if (!placeable(live) || siteFloorOf(live) !== floor) return [];
900
912
  const at = planPoint(live as any)!;
901
913
  // A kept (stale) reading is drawn faded with "last seen N s ago" - never as live.
902
914
  return [{ id, name: String(r.name ?? ""), mode: live!.mode, type: r.type as string, ...at, rot: `rotate(${headingDeg(live)}deg)`, stale: live!.stale === true, seen: staleText(live) }];
@@ -8,6 +8,8 @@
8
8
  * arithmetic only.
9
9
  */
10
10
 
11
+ import { siteFloorOf, type TLiveRobotState } from "./robot-display.ts";
12
+
11
13
  export type TFloorLink = { robotId: string; floorNo: number };
12
14
  export type TSiteFloor = { key: number; name: string; vendor?: TFloorLink[]; fleetCoreLevel?: string };
13
15
 
@@ -93,3 +95,23 @@ export function floorsBody(floors: TSiteFloor[]): TSiteFloor[] {
93
95
  export function floorNameByKey(floors: TSiteFloor[] | null | undefined, key: number): string {
94
96
  return floors?.find((f) => f.key === key)?.name ?? defaultFloorName(key);
95
97
  }
98
+
99
+ /**
100
+ * A robot's own floor number as the site floor (same rule as core
101
+ * `siteFloorOf`): its link, else the floor with that number, else its own
102
+ * number unchanged. Used for a robot's maps and routes, which carry the
103
+ * robot's number (the PadBot's map says 3 on the 34th floor).
104
+ */
105
+ export function siteFloorKey(floors: TSiteFloor[] | null | undefined, robotId: string, floorNo: number): number {
106
+ return floorKeyFor(floors ?? [], robotId, floorNo) ?? floorNo;
107
+ }
108
+
109
+ /**
110
+ * A site floor's name for the maps: the saved list's name, else the name the
111
+ * server sent with a robot on that floor, else the old label (34 -> "L34").
112
+ */
113
+ export function siteFloorLabel(floors: TSiteFloor[] | null | undefined, key: number, states: Record<string, TLiveRobotState> = {}): string {
114
+ if (floors?.some((f) => f.key === key)) return floorNameByKey(floors, key);
115
+ const named = Object.values(states).find((s) => s?.ok && siteFloorOf(s) === key && typeof s.floorName === "string" && s.floorName.trim());
116
+ return named ? String(named.floorName).trim() : defaultFloorName(key);
117
+ }
@@ -62,7 +62,7 @@ export function mapLayerLegend(view: Pick<TRobotMapView, "zones" | "taughtRoute"
62
62
  }
63
63
 
64
64
  const isNum = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
65
- const levelName = (n: number) => (n > 0 ? `L${n}` : n < 0 ? `B${-n}` : "G");
65
+ const defaultLevelName = (n: number) => (n > 0 ? `L${n}` : n < 0 ? `B${-n}` : "G");
66
66
  const TYPE_WORDS: Readonly<Record<number, string>> = { 0: "Point", 3: "Lift", 4: "Loading", 6: "Access door", 10: "Charging" };
67
67
 
68
68
  /**
@@ -91,10 +91,13 @@ export function robotMapViews(
91
91
  items: Array<Record<string, any>> | null | undefined,
92
92
  currentMapId: string | null | undefined,
93
93
  routes: Array<{ name?: string; mapId?: string }> | null | undefined = [],
94
+ /** The site's floors for this robot: its map floor numbers become site floors, named as the lift shows them. */
95
+ site?: { key: (floorNo: number) => number; name: (key: number) => string },
94
96
  ): TRobotMapView[] {
95
97
  const list = (Array.isArray(items) ? items : []).filter((m) => typeof m?.mapId === "string" && m.mapId && typeof m?.imageUrl === "string");
96
98
  return list.map((m, i) => {
97
- const floorNo = Number.isInteger(m.floor) ? (m.floor as number) : null;
99
+ const floorNo = Number.isInteger(m.floor) ? (site ? site.key(m.floor) : (m.floor as number)) : null;
100
+ const levelName = site ? site.name : defaultLevelName;
98
101
  const routeNames = (routes ?? []).filter((r) => r?.mapId === m.mapId && r.name).map((r) => String(r.name));
99
102
  const shown = typeof m.mapDisplayName === "string" ? m.mapDisplayName.trim() : "";
100
103
  const hint = floorNo !== null ? levelName(floorNo) : routeNames[0]?.replace(/ \(default\)$/, "") ?? "";