feat: unified rail shell, robust live sync, task board performance
CI - Build & Test / Backend (.NET) (push) Successful in 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 17s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Has been skipped

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:
2026-07-10 13:25:26 +02:00
parent 86ceb2bcce
commit f564ecfbc7
19 changed files with 540 additions and 2057 deletions
+113 -27
View File
@@ -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>