Files
nexus/frontend/src/views/Dashboard/FlowBoard.vue
T
devops f564ecfbc7
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
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>
2026-07-10 13:25:26 +02:00

179 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* FlowBoard Das neue V2 Dashboard
*
* Layout:
* Stage (AlertBar + FlowCanvas) + Rail (IrisChat) + TaskStrip (unten)
*
* Datenquellen:
* - AgentStore: agents, models, AlertBar-Metriken, Modal-Status
* - ChatStore: messages, isThinking, sendMessage()
* - TaskStore: tasks
*
* Polling startet bei Mount, stoppt bei Unmount.
*/
import { computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAgentStore } from '../../stores/agents'
import { useChatStore } from '../../stores/chat'
import { useDashboardStore } from '../../stores/dashboard'
import { useTaskStore } from '../../stores/tasks'
import AlertBar from '../../components/dashboard/v2/AlertBar.vue'
import FlowCanvas from '../../components/dashboard/v2/FlowCanvas.vue'
import IrisChat from '../../components/dashboard/v2/IrisChat.vue'
import TaskStrip from '../../components/dashboard/v2/TaskStrip.vue'
import AgentDetailModal from '../../components/dashboard/v2/AgentDetailModal.vue'
import { useFlowBoardState } from '../../composables/useFlowBoardState'
/* ── Stores ──────────────────────────────────────── */
const agentStore = useAgentStore()
const chatStore = useChatStore()
const dashboardStore = useDashboardStore()
const taskStore = useTaskStore()
const router = useRouter()
const {
addAgent,
agentPositions,
changeModel,
closeAgent,
enteringIds,
resetLayout,
selectAgent,
sendChatMessage,
updatePositions,
} = useFlowBoardState(agentStore, chatStore)
const blockedTasks = computed(() => taskStore.taskList.filter(task => task.status === 'blocked'))
function handleBlockerClick() {
if (!blockedTasks.value.length) return
router.push('/tasks')
}
function blockerLabel() {
const blockedTask = blockedTasks.value[0]
if (!blockedTask) return undefined
return `${blockedTasks.value.length} Blocker — ${blockedTask.title}`
}
function blockerCount() {
return blockedTasks.value.length
}
/* ── Lifecycle ────────────────────────────────────── */
onMounted(() => {
agentStore.startPolling()
chatStore.startPolling()
dashboardStore.startPolling()
taskStore.startPolling()
taskStore.startBoardPolling()
})
onUnmounted(() => {
agentStore.stopPolling()
chatStore.stopPolling()
dashboardStore.stopPolling()
taskStore.stopPolling()
taskStore.stopBoardPolling()
})
</script>
<template>
<div class="flow-board">
<!-- Stage + Rail row -->
<div class="board-body">
<!-- Stage: AlertBar + FlowCanvas + TaskStrip -->
<div class="stage">
<AlertBar
:active-count="agentStore.activeCount"
:think-count="agentStore.thinkCount"
:idle-count="agentStore.idleCount"
:blocker-count="blockerCount()"
:today-cost="agentStore.todayCost"
:today-tokens="agentStore.todayTokens"
:blocker-label="blockerLabel()"
@blocker-click="handleBlockerClick"
/>
<FlowCanvas
:agents="agentStore.agentList"
:positions="agentPositions"
:entering-ids="enteringIds"
@select="selectAgent"
@add="addAgent"
@reset-layout="resetLayout"
@update-positions="updatePositions"
/>
<TaskStrip :tasks="taskStore.taskList" :loading="taskStore.loading" :error="taskStore.error" />
</div>
<!-- Rail: IrisChat -->
<IrisChat
:messages="chatStore.messageList"
:is-thinking="chatStore.isThinking"
:error="chatStore.error"
@send="sendChatMessage"
/>
</div>
<!-- Agent Detail Modal -->
<AgentDetailModal
v-if="agentStore.modalOpen && agentStore.selectedAgent"
:agent="agentStore.selectedAgent"
:agent-order="agentStore.agentOrder"
@close="closeAgent"
@select="selectAgent"
@change-model="changeModel"
/>
</div>
</template>
<style scoped>
.flow-board {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0;
animation: fade-in 0.35s ease-out;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.board-body {
flex: 1;
display: flex;
flex-direction: row;
gap: 18px;
padding: 18px 20px;
overflow: hidden;
min-height: 0;
}
.stage {
flex: 1;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
min-width: 0;
overflow: hidden;
}
@media (max-width: 767px) {
.board-body {
flex-direction: column;
padding: 8px;
gap: 10px;
}
.stage {
flex: 1;
}
}
</style>