<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import {
GnAvatar, GnButton, GnIconButton, GnIdentity, GnLoader,
GnNavList, GnNavigationShell, GnToastProvider,
} from 'gnexus-ui-kit/vue'
import { api } from './api.js'
import { router } from './router.js'
// GnNavList захватывает $route одним снапшотом в setup, и с vue-router 4
// (объект маршрута заменяется при навигации) класс active «застывает».
// Поэтому активность считаем сами через реактивный useRoute и отдаём киту
// флагом item.active: без item.to кит рендерит <a> и берёт именно наш флаг,
// а переходы делает наш onSelect (preventDefault + router.push).
const route = useRoute()
const navItems = computed(() => [
{
label: 'Dashboard', icon: 'ph-squares-four', href: '/',
active: route.path === '/' || route.path.startsWith('/servers/'),
onSelect: (item, event) => { event.preventDefault(); router.push(item.href) },
},
{
label: 'Storage', icon: 'ph-hard-drives', href: '/storage',
active: route.path.startsWith('/storage'),
onSelect: (item, event) => { event.preventDefault(); router.push(item.href) },
},
])
// --- Шлюз входа (как в gnexus-creds): splash → /me → панель или экран входа.
// OAuth стартует по клику пользователя — без тихого авто-редиректа на page load
// (иначе gnexus-auth молча подхватит ещё живую сессию вместо явного входа).
const authState = ref('checking') // checking | denied | ready
const me = ref(null)
const userInitials = ref('')
function initials(name, email) {
const base = (name || email || '?').trim()
return base.slice(0, 2).toUpperCase()
}
async function loadMe() {
authState.value = 'checking'
try {
const data = await api.me()
me.value = data.user
if (!data.auth_enabled || !data.user) {
// панель открыта вовсе (gnexus-auth не настроен)
authState.value = 'ready'
} else {
userInitials.value = initials(data.user.display_name, data.user.email)
authState.value = 'ready'
}
} catch (err) {
if (err?.status === 401) {
authState.value = 'denied'
} else {
// сеть/бэкенд упал — продолжаем с экраном входа, /auth/login вернёт обратно
authState.value = 'denied'
}
}
}
function loginNow() {
// клик начинает OAuth; return_to — текущий маршрут, чтобы вернуться сюда
const to = window.location.pathname + window.location.search
window.location.href = '/auth/login?return_to=' + encodeURIComponent(to)
}
async function logoutNow() {
await api.logout()
me.value = null
authState.value = 'denied'
}
const onUnauthenticated = () => {
me.value = null
authState.value = 'denied'
}
onMounted(() => {
loadMe()
window.addEventListener('ghard:unauthenticated', onUnauthenticated)
})
onBeforeUnmount(() => {
window.removeEventListener('ghard:unauthenticated', onUnauthenticated)
})
</script>
<template>
<GnToastProvider :lifetime="4000">
<!-- splash, пока /me не ответил: лого + спиннер, чтобы shell не мигал -->
<div v-if="authState === 'checking'" class="login-screen" aria-hidden="true">
<article class="card login-screen-card">
<img src="/logo.svg" alt="" class="login-screen-logo" />
<GnLoader circle class="login-screen-loader" />
</article>
</div>
<!-- экран входа: OAuth начинается с клика -->
<div v-else-if="authState === 'denied'" class="login-screen">
<article class="card login-screen-card">
<img src="/logo.svg" alt="" class="login-screen-logo" />
<h1 class="login-screen-title">Sign in</h1>
<p class="login-screen-subtitle">GHard Monitor is protected by gnexus-auth</p>
<GnButton variant="primary" icon="ph ph-sign-in" @click="loginNow">Sign in</GnButton>
</article>
</div>
<GnNavigationShell
v-else
brand="GHard Monitor"
logo-src="/logo.svg"
current="Gnexus Hardware Monitor"
title="Monitoring"
subtitle="GHard"
>
<template #default="{ close }">
<GnNavList :items="navItems" @select="close" />
</template>
<template #footer>
<span v-if="me" class="profile-identity">
<GnIdentity
:title="me.display_name || me.email"
:meta="me.email"
:avatar="{ src: me.avatar_url || '', initials: userInitials, size: 'sm', variant: 'primary' }"
/>
</span>
<GnIconButton
v-if="me"
icon="ph ph-sign-out"
label="Sign out"
@click="logoutNow"
/>
</template>
<template #content>
<div class="app-page">
<router-view />
</div>
</template>
</GnNavigationShell>
</GnToastProvider>
</template>
<style>
/* Шлюз входа: полноэкранная карточка с одной кнопкой; OAuth начинается
с клика, не с загрузки страницы (как в gnexus-creds) */
.login-screen {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.login-screen-card {
width: 100%;
max-width: 380px;
padding: 40px 32px;
text-align: center;
min-width: 0;
}
.login-screen-logo {
width: 56px;
height: 56px;
margin: 0 auto 18px;
display: block;
}
.login-screen-title {
margin-bottom: 6px;
}
.login-screen-subtitle {
margin-bottom: 22px;
}
/* splash, пока /me не ответил: лого + спиннер, ничего лишнего */
.login-screen-loader {
margin: 0 auto;
justify-content: center;
}
/* identity текущего пользователя в футере дровера */
.profile-identity {
display: block;
color: inherit;
min-width: 0;
}
/* Служебный layout-контейнер страницы (стили кита не дублируем) */
.app-page {
max-width: 1200px;
margin: 0 auto;
padding: 24px 24px 64px;
width: 100%;
box-sizing: border-box;
}
/* .tabs у кита — flex-wrap: узкая панель помещается рядом со списком табов.
Заставляем панели всегда занимать собственную строку на всю ширину. */
.tabs .tabs-panels {
flex-basis: 100%;
}
</style>