@7365admin1/layer-common 4.111.0 → 4.112.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.112.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9350b17: Robots: "Open HERO Fleet Core" button (`RobotFleetCoreButton`, in the site robots header) for Super Admin and Property Management. Shown only when `GET /api/robots/fleet-core/session?check=1` answers 200; on click it opens a new tab, fetches the one-hour sign-in pass and points the tab at the console (`https://…/#token=…` only). Web only.
8
+
3
9
  ## 4.111.0
4
10
 
5
11
  ### Minor Changes
@@ -3080,11 +3086,11 @@ Monthly report: a month that had patrols opens the list of that month's patrol r
3080
3086
  `utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
3081
3087
  own rule from two endpoints the console already calls, unprojected:
3082
3088
 
3083
- GET /api/members/user/:user/app/admin the Seven365 staff membership
3084
- GET /api/roles/id/:role that membership's role document
3089
+ GET /api/members/user/:user/app/admin the Seven365 staff membership
3090
+ GET /api/roles/id/:role that membership's role document
3085
3091
 
3086
- owner = member.type === "admin" && role.type === "admin" && role.default === true
3087
- staff = member.type === "admin" && role.type === "admin"
3092
+ owner = member.type === "admin" && role.type === "admin" && role.default === true
3093
+ staff = member.type === "admin" && role.type === "admin"
3088
3094
 
3089
3095
  `role.default` is the marker because it is the only property of a platform
3090
3096
  staff role no API caller can set - `role.controller.ts` validates create and
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <!--
3
+ "Open HERO Fleet Core" (web only, owner 2026-10-09: Super Admin + Property
4
+ Management). Shown only when the server says this account may open the
5
+ console (`?check=1` answered 200) - 404 where Fleet Core is not set up and
6
+ 403 for everyone else keep it hidden. The pass is fetched on click only.
7
+ -->
8
+ <v-btn
9
+ v-if="available"
10
+ size="small"
11
+ variant="tonal"
12
+ color="primary"
13
+ prepend-icon="mdi-open-in-new"
14
+ :loading="opening"
15
+ @click="open"
16
+ >
17
+ Open HERO Fleet Core
18
+ </v-btn>
19
+ <span v-if="failed" class="hero-tag hero-tag--neutral" role="status">Could not open HERO Fleet Core. Try again.</span>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { onMounted, ref } from "vue";
24
+ import { openFleetCoreConsole } from "../../utils/robot-fleet-core";
25
+
26
+ const { getFleetCoreSession } = useRobots();
27
+ const available = ref(false);
28
+ const opening = ref(false);
29
+ const failed = ref(false);
30
+
31
+ onMounted(async () => {
32
+ try {
33
+ available.value = (await getFleetCoreSession({ check: true }))?.available === true;
34
+ } catch {
35
+ available.value = false;
36
+ }
37
+ });
38
+
39
+ async function open() {
40
+ opening.value = true;
41
+ failed.value = false;
42
+ failed.value = !(await openFleetCoreConsole(
43
+ () => getFleetCoreSession(),
44
+ () => window.open("", "_blank") as any,
45
+ ));
46
+ opening.value = false;
47
+ }
48
+ </script>
@@ -35,6 +35,7 @@
35
35
  </div>
36
36
  <span class="hero-head__spacer" />
37
37
  <div class="hero-header__right">
38
+ <RobotFleetCoreButton />
38
39
  <RobotLiveIndicator :connected="connected" />
39
40
  <span class="hero-tag hero-tag--neutral">{{ role }}</span>
40
41
  </div>
@@ -45,6 +46,7 @@
45
46
  <script setup lang="ts">
46
47
  import RobotMark from "./Mark.vue";
47
48
  import RobotLiveIndicator from "./LiveIndicator.vue";
49
+ import RobotFleetCoreButton from "./FleetCoreButton.vue";
48
50
  import { typeLabel, type TRobotType } from "../../utils/robot-display";
49
51
 
50
52
  withDefaults(
@@ -400,6 +400,18 @@ export default function useRobots() {
400
400
  });
401
401
  }
402
402
 
403
+ /**
404
+ * HERO Fleet Core sign-in pass (web only): `{ url: "<console>/#token=...", expiresAt }`.
405
+ * `check: true` only asks whether this account may open the console (mints nothing).
406
+ * 404 where Fleet Core is not set up, 403 for anyone but Super Admin / Property Management.
407
+ */
408
+ function getFleetCoreSession({ check = false }: { check?: boolean } = {}) {
409
+ return useNuxtApp().$api<{ url?: string; expiresAt?: string; available?: boolean }>("/api/robots/fleet-core/session", {
410
+ method: "GET",
411
+ query: check ? { check: "1" } : {},
412
+ });
413
+ }
414
+
403
415
  /* ------------------------------------------ alerts + activity log */
404
416
 
405
417
  /**
@@ -566,6 +578,7 @@ export default function useRobots() {
566
578
  listBuildingMaps,
567
579
  listSiteBuildingMaps,
568
580
  getFleetCoreBuilding,
581
+ getFleetCoreSession,
569
582
  getSiteFaults,
570
583
  getSiteActivity,
571
584
  getNetworkActivity,
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.111.0",
5
+ "version": "4.112.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.",
@@ -55,3 +55,41 @@ export function fleetLiftItems(map: Record<string, any> | null | undefined, rows
55
55
  };
56
56
  });
57
57
  }
58
+
59
+ /* ---- "Open HERO Fleet Core" (web only, owner 2026-10-09) ---------------- */
60
+
61
+ /**
62
+ * Is this a console URL we may open? https, and the pass in the fragment
63
+ * (`#token=`) - the only form the console reads, and one no server ever logs.
64
+ */
65
+ export function isFleetCoreConsoleUrl(url: unknown): url is string {
66
+ if (typeof url !== "string") return false;
67
+ try {
68
+ const u = new URL(url);
69
+ return u.protocol === "https:" && /^#token=[\w-]+\.[\w-]+\.[\w-]+$/.test(u.hash);
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Open the console in a new tab. The tab is opened BEFORE the pass is fetched
77
+ * (inside the click), so popup blockers allow it; it is then pointed at the
78
+ * console, or closed if no pass came back. Returns false when it failed.
79
+ */
80
+ export async function openFleetCoreConsole(
81
+ getSession: () => Promise<{ url?: unknown } | null | undefined>,
82
+ openTab: () => { location: { href: string }; opener: unknown; close: () => void } | null,
83
+ ): Promise<boolean> {
84
+ const tab = openTab();
85
+ if (tab) tab.opener = null; // the console never gets a handle on this app
86
+ try {
87
+ const session = await getSession();
88
+ if (!tab || !isFleetCoreConsoleUrl(session?.url)) throw new Error("no pass");
89
+ tab.location.href = session!.url as string;
90
+ return true;
91
+ } catch {
92
+ tab?.close();
93
+ return false;
94
+ }
95
+ }