Files
ams-frontend/src/layouts/AppLayout.vue
2026-02-19 14:40:46 +00:00

303 lines
7.6 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useWebSocketStore } from '../stores/websocket'
import Popover from 'primevue/popover'
import WsStatsPanel from '../components/WsStatsPanel.vue'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const wsStore = useWebSocketStore()
const wsPopover = ref()
const sidebarOpen = ref(false)
// Close sidebar on route change (mobile)
watch(() => route.path, () => {
sidebarOpen.value = false
})
const feVersion = import.meta.env.VITE_APP_VERSION || 'dev'
const beVersion = ref('...')
onMounted(async () => {
try {
const apiBase = import.meta.env.VITE_API_URL || 'http://localhost:8000/api'
const res = await fetch(`${apiBase}/version`)
const data = await res.json()
beVersion.value = data.version || 'unknown'
} catch {
beVersion.value = '?'
}
wsStore.connect()
})
onUnmounted(() => {
wsStore.disconnect()
})
function handleLogout() {
wsStore.disconnect()
authStore.logout()
router.push('/login')
}
function toggleWsStats(event: Event) {
wsPopover.value?.toggle(event)
}
</script>
<template>
<div class="app-layout">
<!-- Mobile Hamburger -->
<button class="hamburger" @click="sidebarOpen = !sidebarOpen" :class="{ open: sidebarOpen }">
<i :class="sidebarOpen ? 'pi pi-times' : 'pi pi-bars'"></i>
</button>
<!-- Overlay -->
<div v-if="sidebarOpen" class="sidebar-overlay" @click="sidebarOpen = false"></div>
<aside class="sidebar" :class="{ open: sidebarOpen }">
<div class="sidebar-header">
<h2>🤖 AMS</h2>
</div>
<nav class="sidebar-nav-container">
<ul class="sidebar-nav">
<li>
<router-link to="/">
<i class="pi pi-home"></i>
Dashboard
</router-link>
</li>
<li>
<router-link to="/agents">
<i class="pi pi-users"></i>
Agents
</router-link>
</li>
<li>
<router-link to="/projects">
<i class="pi pi-folder"></i>
Projekte
</router-link>
</li>
<li>
<router-link to="/tasks">
<i class="pi pi-list-check"></i>
Tasks
</router-link>
</li>
<li>
<router-link to="/logs">
<i class="pi pi-file"></i>
Logs
</router-link>
</li>
<li>
<router-link to="/labels">
<i class="pi pi-tags"></i>
Labels
</router-link>
</li>
<li>
<router-link to="/gitlab">
<i class="pi pi-github"></i>
GitLab
</router-link>
</li>
<li>
<router-link to="/agent-task">
<i class="pi pi-send"></i>
Aufgabe stellen
</router-link>
</li>
<li>
<router-link to="/agent-tasks-overview">
<i class="pi pi-list-check"></i>
Agent-Aufträge
</router-link>
</li>
<li>
<router-link to="/backup">
<i class="pi pi-download"></i>
Backup
</router-link>
</li>
<li>
<router-link to="/cronjobs">
<i class="pi pi-clock"></i>
CronJobs
</router-link>
</li>
<li>
<router-link to="/workspace">
<i class="pi pi-folder-open"></i>
Agent-Dateien
</router-link>
</li>
<li>
<router-link to="/secrets">
<i class="pi pi-lock"></i>
Secrets
</router-link>
</li>
<li>
<router-link to="/token-analytics">
<i class="pi pi-chart-line"></i>
Token Analytics
</router-link>
</li>
<li v-if="authStore.user?.role === 'admin'">
<router-link to="/containers">
<i class="pi pi-box"></i>
Container
</router-link>
</li>
<li>
<router-link to="/settings">
<i class="pi pi-cog"></i>
Einstellungen
</router-link>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<div style="color: rgba(255,255,255,0.5); font-size: 0.875rem; margin-bottom: 0.5rem;">
{{ authStore.user?.username }}
</div>
<button
@click="handleLogout"
style="background: none; border: 1px solid rgba(255,255,255,0.2); color: rgba(255,255,255,0.7); padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; width: 100%;"
>
<i class="pi pi-sign-out" style="margin-right: 0.5rem;"></i>
Abmelden
</button>
<div class="sidebar-version">
<span class="ws-indicator" :class="{ online: wsStore.isConnected }" :title="wsStore.isConnected ? 'WebSocket verbunden' : 'WebSocket getrennt'" @click="toggleWsStats" style="cursor: pointer;"></span>
FE {{ feVersion }} | BE {{ beVersion }}
</div>
<Popover ref="wsPopover">
<WsStatsPanel />
</Popover>
</div>
</aside>
<main class="main-content">
<router-view />
</main>
</div>
</template>
<style scoped>
.hamburger {
display: none;
position: fixed;
top: 0.75rem;
left: 0.75rem;
z-index: 1100;
width: 40px;
height: 40px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(26, 26, 46, 0.95);
color: rgba(255, 255, 255, 0.8);
font-size: 1.1rem;
cursor: pointer;
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
transition: all 0.2s;
}
.hamburger:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.sidebar-overlay {
display: none;
}
.sidebar {
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
@media (max-width: 768px) {
.hamburger {
display: flex;
}
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
backdrop-filter: blur(2px);
}
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: 1000;
transform: translateX(-100%);
width: 220px;
background: #0f0f1e;
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0 !important;
padding-top: 3.5rem !important;
}
}
.sidebar-version {
text-align: center;
margin-top: 0.75rem;
color: rgba(255, 255, 255, 0.25);
font-size: 0.65rem;
letter-spacing: 0.025em;
}
.ws-indicator {
color: rgba(239, 68, 68, 0.6);
font-size: 0.5rem;
vertical-align: middle;
margin-right: 0.2rem;
}
.ws-indicator.online {
color: rgba(34, 197, 94, 0.8);
}
/* Popover Dark Theme */
:deep(.p-popover) {
background: #1a1a2e !important;
border: 1px solid rgba(255, 255, 255, 0.15) !important;
color: #fff !important;
}
:deep(.p-popover-content) {
background: #1a1a2e !important;
color: #fff !important;
}
:deep(.p-popover)::before,
:deep(.p-popover)::after {
border-color: transparent !important;
border-bottom-color: #1a1a2e !important;
}
</style>