feat: board-first orchestration with Gateway Bridge, live-update, and flow-board
CI - Build & Test / Backend (.NET) (push) Successful in 1m19s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 18s
CI - Build & Test / Security Check (push) Successful in 3s

- GatewayBridgeController: MCP-artiger Kommando-Adapter für Agent-zu-Backend
- TaskBridgeService + LiveUpdateService: SSE Live-Sync + Bridge-Kommandos
- FlowBoard.vue: Board-first orchestration dashboard panel
- live-sync.ts store + live.ts service: SSE-basierte Live-Updates
- Nullability-Warnung in HealthController.cs gefixt
- nginx.conf: SSE-Proxy + CORS für Bridge-Endpunkte
- .gitignore: pnpm/corepack local caches ausgeschlossen
- docs: architecture-board-first-orchestration.md hinzugefügt
- README: Backend Bridge API dokumentiert
This commit is contained in:
2026-06-22 19:56:45 +02:00
parent de1fc198cb
commit df94ed3cd4
34 changed files with 2115 additions and 118 deletions
+35 -2
View File
@@ -17,6 +17,7 @@ import { Plus, X, CalendarDays, Clock3, ExternalLink, Link2, ListChecks, Save, A
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useTaskStore } from '../stores/tasks'
import { useLiveSyncStore } from '../stores/live-sync'
type BoardTask = ReturnType<typeof flattenBoard>[number]
@@ -32,6 +33,7 @@ type TaskFormState = {
const authStore = useAuthStore()
const taskStore = useTaskStore()
const router = useRouter()
const liveSyncStore = useLiveSyncStore()
const showCreateModal = ref(false)
const showDetailPanel = ref(false)
const showIrisPanel = ref(false)
@@ -215,6 +217,8 @@ function hydrateDetailForm(task: BoardTask | null) {
const staleCount = computed(() => taskStore.staleTasksList.length)
const waitingForIrisCount = computed(() => taskStore.waitingForIrisTasks.length)
const waitingForBaoCount = computed(() => taskStore.waitingForBaoTasks.length)
const liveModeLabel = computed(() => liveSyncStore.liveIndicatorLabel)
const liveModeClass = computed(() => `live-pill-${liveSyncStore.connectionHealth}`)
function expectedFromLabel(expected: string | null | undefined): string {
if (!expected) return ''
@@ -277,6 +281,14 @@ function hasChildTasks(taskId: string): boolean {
return allBoardTasks.value.some(task => task.parentTaskId === taskId)
}
function delegationBadge(task: BoardTask): string | null {
if (task.childTaskCount && task.openChildTaskCount) return `${task.openChildTaskCount}/${task.childTaskCount} aktiv`
if (task.childTaskCount) return `${task.childTaskCount} Child-Tasks`
if (task.parentTaskId) return 'Child-Task'
if (task.isAgentTask || task.hasVisibleDelegation) return 'delegiert'
return null
}
function assigneeLabel(assignedTo: string | null | undefined): string {
return expectedFromLabel(assignedTo)
}
@@ -382,12 +394,14 @@ watch(showDetailPanel, (open) => {
})
/* ── Lifecycle ────────────────────────────────────── */
let agentOverviewInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
taskStore.startBoardPolling()
taskStore.fetchAgentOverview()
liveSyncStore.connect()
window.addEventListener('keydown', onGlobalKeydown)
// Refresh agent overview on the same interval
setInterval(() => taskStore.fetchAgentOverview(), 30000)
agentOverviewInterval = setInterval(() => taskStore.fetchAgentOverview(), 30000)
})
onBeforeUnmount(() => {
@@ -396,6 +410,8 @@ onBeforeUnmount(() => {
onUnmounted(() => {
taskStore.stopBoardPolling()
liveSyncStore.disconnect()
if (agentOverviewInterval) clearInterval(agentOverviewInterval)
window.removeEventListener('keydown', onGlobalKeydown)
})
</script>
@@ -406,6 +422,10 @@ onUnmounted(() => {
<div>
<h1><span class="grad-text">Aufgaben</span></h1>
<p class="board-subtitle">Task Board Übersicht aller Arbeitspakete</p>
<div class="board-live-row">
<span class="live-pill" :class="liveModeClass">{{ liveModeLabel }}</span>
<span v-if="liveSyncStore.lastEventAt" class="live-meta">Letztes Event {{ relativeTime(liveSyncStore.lastEventAt) }}</span>
</div>
</div>
<div class="board-header-actions">
<button
@@ -546,6 +566,9 @@ onUnmounted(() => {
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
{{ task.expectedFrom }}
</span>
<span v-if="delegationBadge(task)" class="expected-badge expected-badge--delegation" title="Sichtbare Delegation im Board">
{{ delegationBadge(task) }}
</span>
<span
v-if="task.assignedTo"
class="assignee"
@@ -595,6 +618,9 @@ onUnmounted(() => {
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
{{ task.expectedFrom }}
</span>
<span v-if="delegationBadge(task)" class="expected-badge expected-badge--delegation" title="Sichtbare Delegation im Board">
{{ delegationBadge(task) }}
</span>
<span
v-if="task.assignedTo"
class="assignee"
@@ -644,6 +670,9 @@ onUnmounted(() => {
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
{{ task.expectedFrom }}
</span>
<span v-if="delegationBadge(task)" class="expected-badge expected-badge--delegation" title="Sichtbare Delegation im Board">
{{ delegationBadge(task) }}
</span>
<span
v-if="task.assignedTo"
class="assignee"
@@ -737,6 +766,9 @@ onUnmounted(() => {
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
{{ task.expectedFrom }}
</span>
<span v-if="delegationBadge(task)" class="expected-badge expected-badge--delegation" title="Sichtbare Delegation im Board">
{{ delegationBadge(task) }}
</span>
<span
v-if="task.assignedTo"
class="assignee"
@@ -837,6 +869,7 @@ onUnmounted(() => {
<span v-if="selectedTask.isAgentTask" class="meta-agent-tag">🤖 Agent-Task</span>
<span v-if="selectedTask.expectedFrom" class="meta-expected"> Erwartet: {{ selectedTask.expectedFrom }}</span>
<span v-if="selectedTask.parentTaskId" class="meta-expected"> Child-Task</span>
<span v-if="delegationBadge(selectedTask)" class="meta-expected"> {{ delegationBadge(selectedTask) }}</span>
<span><Clock3 :size="13" /> Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }}</span>
<span><CalendarDays :size="13" /> Erstellt {{ formatDate(selectedTask.createdAt) }}</span>
<span v-if="selectedTask.isAgentTask"><MessageSquareText :size="13" /> Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }}</span>