<script setup>
// Каркас приложения — GnNavigationShell из gnexus-ui-kit: topbar + drawer + footer.
// Страницы входа/отсечения идут без каркаса (meta.bare в роутере).
//
// Футер дровер — карточка профиля (GnUserCard) с кнопкой выхода; здесь же живёт
// сессия: /api/v1/admin/me каждые 45 c (single sign-out: вебхук отзывает токены,
// heartbeat ловит 401 и уводит на /login).
//
// :key="route.path" — обход ограничения vue-адаптера кита: GnNavList читает
// route.path ($route) один раз при первом рендере, а vue-router отдаёт $route
// как unref(currentRoute) — без реактивности, computed с isActive закешируется
// навсегда. Перезмонтирование шела на каждую смену пути даёт корректную
// подсветку активного пункта навигации.
import { computed, onUnmounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { GnNavigationShell, GnUserCard } from "gnexus-ui-kit/vue";
import { adminMe, clearToken, logout } from "./api.js";
const route = useRoute();
const router = useRouter();
// Пока в Synapse одна секция — «Обзор»; новые секции добавляются сюда и в router.js.
const items = [
{ to: "/dashboard", label: "Обзор", icon: "ph-pulse" },
];
const currents = {
"/dashboard": "Обзор",
};
const current = computed(() => currents[route.path] || "");
const bare = computed(() => Boolean(route.meta.bare));
const me = ref(null);
let heartbeatTimer = null;
const initials = computed(() => {
const src = me.value?.email || me.value?.sub || "";
return src
.split(/[.\s@_]+/)
.filter(Boolean)
.slice(0, 2)
.map((w) => w[0].toUpperCase())
.join("");
});
function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
function authFailure(ex) {
stopHeartbeat();
if (ex?.status === 403) router.push("/denied");
else if (ex?.status === 401) {
clearToken();
router.push("/login");
}
}
async function loadProfile() {
try {
me.value = await adminMe();
} catch (ex) {
authFailure(ex);
}
}
function startHeartbeat() {
stopHeartbeat();
heartbeatTimer = setInterval(async () => {
try {
me.value = await adminMe();
} catch (ex) {
authFailure(ex);
}
}, 45000);
}
watch(
bare,
(isBare) => {
if (isBare) {
me.value = null;
stopHeartbeat();
} else {
loadProfile();
startHeartbeat();
}
},
{ immediate: true },
);
onUnmounted(stopHeartbeat);
async function signOut() {
stopHeartbeat();
me.value = null;
await logout();
router.push("/login");
}
</script>
<template>
<GnNavigationShell
v-if="!bare"
:key="route.path"
:items="items"
brand="Synapse"
title="Synapse"
subtitle="Хаб уведомлений Gnexus"
:current="current"
>
<template #footer>
<GnUserCard
v-if="me"
compact
:name="me.email"
:avatar="{ initials }"
:actions="[{ label: 'Выйти', icon: 'ph-sign-out', onClick: signOut }]"
/>
</template>
<template #content>
<RouterView />
</template>
</GnNavigationShell>
<RouterView v-else />
</template>