feat: unified rail shell, robust live sync, task board performance
Shell: - One shared NexusLayout for ALL routes (dashboard + pages): compact 68px icon rail with hover-expand overlay, replaces both old sidebars + topbars - Single flat nav source (railNav) — same menu everywhere incl. Settings - Removed dead shell components (AppSidebar, AppHeader, Topbar, NavGroup, NavItem, ModuleView) and dead nav routes; App.vue is now just RouterView Live updates: - Fix SSE loop in DashboardController: PeriodicTimer.WaitForNextTickAsync and ChannelReader.ReadAsync were re-invoked while pending — every published update threw InvalidOperationException and killed ALL live streams - liveSync store: treat graceful stream close as disconnect (was stuck connected=true with polling stopped -> page frozen until manual reload), fast first retry, heartbeat watchdog (65s), reconnect on online/visibility - One app-wide SSE connection owned by the layout instead of per-view connect/disconnect churn; removed duplicate live-sync.ts store Performance: - Board endpoint: drop nested childTasks duplication (counts stay) — payload 104KB -> 65KB; SSE snapshots shrink equally - nginx: gzip for JSON/JS/CSS (board 18KB, bundle 95KB over the wire); text/event-stream excluded to keep SSE unbuffered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,49 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* NexusLayout — V2 Dashboard Shell
|
||||
* Flex row, 100vh, overflow hidden.
|
||||
* Sidebar (248px) + Main (flex:1, flex-column)
|
||||
* Mobile: Sidebar als Overlay mit Hamburger-Toggle
|
||||
* NexusLayout — gemeinsame Shell für ALLE Seiten
|
||||
*
|
||||
* Icon-Rail links (68px, Hover-Expand als Overlay), keine Topbar.
|
||||
* Content bekommt die volle restliche Fläche:
|
||||
* - Routen mit meta.fullBleed (Dashboard): overflow hidden, eigene Höhenlogik
|
||||
* - alle anderen: scrollbarer Container mit Seiten-Padding
|
||||
*
|
||||
* Die Live-Verbindung (SSE) gehört der Shell — EINE Verbindung für die
|
||||
* ganze App statt connect/disconnect bei jedem Seitenwechsel.
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { useDashboardStore } from '../stores/dashboard'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useLiveSyncStore } from '../stores/liveSync'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
import GalaxyBackground from '../components/background/GalaxyBackground.vue'
|
||||
import Sidebar from '../components/layout/Sidebar.vue'
|
||||
import Topbar from '../components/layout/Topbar.vue'
|
||||
import { svg } from '../composables/icons'
|
||||
|
||||
const dashboardStore = useDashboardStore()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const liveSync = useLiveSyncStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
/* ── Mobile Sidebar State ───────────────────────── */
|
||||
const isFullBleed = computed(() => Boolean(route.meta.fullBleed))
|
||||
|
||||
/* ── Mobile Drawer ─────────────────────────────── */
|
||||
const mobileMenuOpen = ref(false)
|
||||
|
||||
function closeMobileMenu() {
|
||||
mobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
/* ── Live-Verbindung (app-weit, genau eine) ─────── */
|
||||
const liveUser = computed(() => (auth.isIris ? 'iris' : 'bao'))
|
||||
|
||||
function onVisibilityChange() {
|
||||
if (document.visibilityState === 'visible' && !liveSync.connected && !liveSync.connecting) {
|
||||
liveSync.connect(liveUser.value)
|
||||
}
|
||||
}
|
||||
|
||||
function onOnline() {
|
||||
liveSync.reconnectNow()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
liveSync.connect(liveUser.value)
|
||||
notificationStore.startPolling()
|
||||
document.addEventListener('visibilitychange', onVisibilityChange)
|
||||
window.addEventListener('online', onOnline)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
liveSync.disconnect()
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange)
|
||||
window.removeEventListener('online', onOnline)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="nexus-layout">
|
||||
<GalaxyBackground />
|
||||
<Sidebar
|
||||
:mobile-open="mobileMenuOpen"
|
||||
@close="closeMobileMenu"
|
||||
/>
|
||||
|
||||
<!-- Mobile Backdrop -->
|
||||
<div
|
||||
v-if="mobileMenuOpen"
|
||||
class="mobile-backdrop"
|
||||
@click="closeMobileMenu"
|
||||
></div>
|
||||
<div class="rail-slot">
|
||||
<Sidebar :mobile-open="mobileMenuOpen" @close="closeMobileMenu" />
|
||||
</div>
|
||||
|
||||
<!-- Mobile: Hamburger + Backdrop -->
|
||||
<button class="mobile-toggle" @click="mobileMenuOpen = !mobileMenuOpen" v-html="svg('list')"></button>
|
||||
<div v-if="mobileMenuOpen" class="mobile-backdrop" @click="closeMobileMenu"></div>
|
||||
|
||||
<main class="nexus-main">
|
||||
<Topbar
|
||||
:connected="dashboardStore.isGatewayConnected"
|
||||
:status-label="dashboardStore.irisStatusLabel"
|
||||
@toggle-sidebar="mobileMenuOpen = !mobileMenuOpen"
|
||||
/>
|
||||
<div class="nexus-content">
|
||||
<div :class="['nexus-content', isFullBleed ? 'full-bleed' : 'page-scroll v2-scroll']">
|
||||
<RouterView />
|
||||
</div>
|
||||
</main>
|
||||
@@ -59,6 +89,15 @@ function closeMobileMenu() {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Platzhalter in der Flex-Reihe — die Rail selbst liegt absolut darüber
|
||||
und kann expandieren, ohne den Content zu verschieben. */
|
||||
.rail-slot {
|
||||
width: 68px;
|
||||
flex: 0 0 68px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.nexus-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -70,19 +109,62 @@ function closeMobileMenu() {
|
||||
|
||||
.nexus-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.nexus-content.full-bleed {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nexus-content.full-bleed > :deep(*) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.nexus-content.page-scroll {
|
||||
overflow-y: auto;
|
||||
padding: 24px 28px 64px;
|
||||
}
|
||||
|
||||
.mobile-toggle,
|
||||
.mobile-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.rail-slot {
|
||||
width: 0;
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.nexus-main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 90;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--line-2);
|
||||
border-radius: 12px;
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(12px);
|
||||
color: var(--tx-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-toggle :deep(svg) {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.mobile-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
@@ -90,5 +172,9 @@ function closeMobileMenu() {
|
||||
z-index: 99;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.nexus-content.page-scroll {
|
||||
padding: 60px 16px 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user