From 3c952811194760769ab079ac3f58c342b2071115 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 9 Jun 2026 20:59:45 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Mission=20Control=20Dashboard=20?= =?UTF-8?q?=E2=80=93=20AI=20Team=20Network,=20Chat,=20Queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 3-Spalten-Layout: Missions/Feed | Team Network | Chat/Queue - AI Team Network (Herzstück): Iris + 4 Agenten mit SVG-Linien - AgentNode: Workload-Ring, Pulse-Animation, Farbcodierung - AgentModal: Task, Goal, Progress, Working Feed - MissionCard: Glass-Morphism, Progress-Bar, Status-Badges - ChatPanel: Iris Chat mit Focus-Banner, Auto-Scroll - QueuePanel: Drag&Drop, Prioritäten, Force-Execute - Composables: useTimer, useDashboardData --- .../src/components/dashboard/AgentModal.vue | 286 +++++++++++++++ .../src/components/dashboard/AgentNode.vue | 208 +++++++++++ .../src/components/dashboard/ChatPanel.vue | 296 +++++++++++++++ .../src/components/dashboard/MissionCard.vue | 201 ++++++++++ .../components/dashboard/OperationsFeed.vue | 289 ++++++--------- .../src/components/dashboard/QueuePanel.vue | 344 +++++++++++++++++ .../src/components/dashboard/TeamNetwork.vue | 345 ++++++++++++++++++ frontend/src/composables/useDashboardData.ts | 297 +++++++++++++++ frontend/src/composables/useTimer.ts | 37 ++ frontend/src/views/DashboardView.vue | 167 ++++----- 10 files changed, 2187 insertions(+), 283 deletions(-) create mode 100644 frontend/src/components/dashboard/AgentModal.vue create mode 100644 frontend/src/components/dashboard/AgentNode.vue create mode 100644 frontend/src/components/dashboard/ChatPanel.vue create mode 100644 frontend/src/components/dashboard/MissionCard.vue create mode 100644 frontend/src/components/dashboard/QueuePanel.vue create mode 100644 frontend/src/components/dashboard/TeamNetwork.vue create mode 100644 frontend/src/composables/useDashboardData.ts create mode 100644 frontend/src/composables/useTimer.ts diff --git a/frontend/src/components/dashboard/AgentModal.vue b/frontend/src/components/dashboard/AgentModal.vue new file mode 100644 index 0000000..c1a7a7b --- /dev/null +++ b/frontend/src/components/dashboard/AgentModal.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/frontend/src/components/dashboard/AgentNode.vue b/frontend/src/components/dashboard/AgentNode.vue new file mode 100644 index 0000000..dd44110 --- /dev/null +++ b/frontend/src/components/dashboard/AgentNode.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/frontend/src/components/dashboard/ChatPanel.vue b/frontend/src/components/dashboard/ChatPanel.vue new file mode 100644 index 0000000..157e7ef --- /dev/null +++ b/frontend/src/components/dashboard/ChatPanel.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/frontend/src/components/dashboard/MissionCard.vue b/frontend/src/components/dashboard/MissionCard.vue new file mode 100644 index 0000000..f74b45b --- /dev/null +++ b/frontend/src/components/dashboard/MissionCard.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/frontend/src/components/dashboard/OperationsFeed.vue b/frontend/src/components/dashboard/OperationsFeed.vue index 8d48d3a..ba1c878 100644 --- a/frontend/src/components/dashboard/OperationsFeed.vue +++ b/frontend/src/components/dashboard/OperationsFeed.vue @@ -1,98 +1,38 @@ @@ -101,134 +41,115 @@ const statusColor = (s: FeedStatus): string => { .feed-panel { display: flex; flex-direction: column; - gap: 8px; - min-height: 420px; - padding: 18px; - background: rgba(22, 27, 34, 0.8); - border: 1px solid rgba(139, 124, 246, 0.12); - border-radius: 16px; - box-shadow: 0 4px 24px rgba(0,0,0,0.3); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - transition: all 0.2s ease; + gap: 10px; + padding: 14px; + background: rgba(22, 27, 34, 0.65); + border: 1px solid rgba(139, 124, 246, 0.08); + border-radius: 14px; + transition: border-color 0.2s ease; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); } .feed-panel:hover { - border-color: rgba(139, 124, 246, 0.18); + border-color: rgba(139, 124, 246, 0.15); } -.feed-title { - font-size: 14px; - font-weight: 600; + +.feed-header { + display: flex; + align-items: center; + gap: 6px; +} +.feed-icon { + color: #a78bfa; +} +.feed-header h2 { margin: 0; + font-size: 11px; + font-weight: 600; color: #e8eaf0; } -/* Filter pills */ -.filter-pills { - display: flex; - gap: 4px; - overflow-x: auto; - scrollbar-width: none; - -ms-overflow-style: none; - padding-bottom: 2px; -} -.filter-pills::-webkit-scrollbar { - display: none; -} -.filter-pills button { - flex-shrink: 0; - padding: 4px 10px; - border: 1px solid rgba(139, 124, 246, 0.08); - border-radius: 20px; - background: transparent; - color: #6b7385; - font-size: 9px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} -.filter-pills button:hover { - border-color: rgba(139, 124, 246, 0.25); - color: #7e8799; -} -.filter-pills button.active { - background: rgba(139, 124, 246, 0.12); - border-color: rgba(139, 124, 246, 0.25); - color: #a78bfa; -} - -/* Feed list */ .feed-list { display: flex; flex-direction: column; gap: 2px; - overflow-y: auto; - flex: 1; + position: relative; } -.feed-group-items { - display: contents; -} -.feed-date-heading { - font-size: 9px; - font-weight: 700; - color: #6b7385; - text-transform: uppercase; - letter-spacing: 0.08em; - padding: 8px 0 4px; -} -.feed-item { + +.feed-entry { display: flex; align-items: center; - gap: 8px; + gap: 5px; padding: 5px 6px; border-radius: 6px; + font-size: 9.5px; + line-height: 1.3; transition: background 0.15s; } -.feed-item:hover { - background: rgba(139, 124, 246, 0.04); +.feed-entry:hover { + background: rgba(255, 255, 255, 0.03); } + .feed-time { - font-size: 9px; color: #6b7385; flex-shrink: 0; - width: 36px; font-variant-numeric: tabular-nums; + width: 32px; } -.feed-dot { - width: 7px; - height: 7px; - border-radius: 50%; +.feed-bullet { + color: #6b7385; flex-shrink: 0; - box-shadow: 0 0 4px currentColor; } -.feed-text { - font-size: 10.5px; - line-height: 1.3; +.feed-agent { + font-weight: 600; + flex-shrink: 0; +} +.agent-iris { + color: #a78bfa; +} +.agent-developer { + color: #3b82f6; +} +.agent-devops { + color: #eab308; +} +.agent-researcher { + color: #22c55e; +} +.agent-reviewer { + color: #a855f7; +} +.feed-action { color: #7e8799; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -/* TransitionGroup animations */ -.feed-item-enter-active { - transition: all 0.3s ease; -} -.feed-item-leave-active { - transition: all 0.3s ease; -} -.feed-item-enter-from { - opacity: 0; - transform: translateX(-12px); -} -.feed-item-leave-to { - opacity: 0; - transform: translateX(12px); -} -.feed-item-move { - transition: all 0.3s ease; +.feed-empty { + text-align: center; + padding: 12px 0; + font-size: 10px; + color: #6b7385; } -@media (max-width: 900px) { - .feed-panel { - order: 2; - } +/* TransitionGroup */ +.feed-enter-active { + transition: all 0.3s ease; +} +.feed-leave-active { + transition: all 0.3s ease; + position: absolute; +} +.feed-enter-from { + opacity: 0; + transform: translateX(-10px); +} +.feed-leave-to { + opacity: 0; + transform: translateX(10px); +} +.feed-move { + transition: transform 0.3s ease; } diff --git a/frontend/src/components/dashboard/QueuePanel.vue b/frontend/src/components/dashboard/QueuePanel.vue new file mode 100644 index 0000000..d5dba3e --- /dev/null +++ b/frontend/src/components/dashboard/QueuePanel.vue @@ -0,0 +1,344 @@ + + + + + diff --git a/frontend/src/components/dashboard/TeamNetwork.vue b/frontend/src/components/dashboard/TeamNetwork.vue new file mode 100644 index 0000000..adfdd2a --- /dev/null +++ b/frontend/src/components/dashboard/TeamNetwork.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/frontend/src/composables/useDashboardData.ts b/frontend/src/composables/useDashboardData.ts new file mode 100644 index 0000000..49095d1 --- /dev/null +++ b/frontend/src/composables/useDashboardData.ts @@ -0,0 +1,297 @@ +import { ref, reactive, computed } from 'vue' + +export interface AgentNodeData { + id: string + name: string + role: string + description: string + color: string + icon: string + currentTask: string + goal: string + progress: number + workload: number // 0-100 + active: boolean + runtimeSeconds: number + workingFeed: string[] +} + +export interface MissionData { + id: string + name: string + progress: number + currentTask: string + lastActivity: string + remainingTasks: number + status: 'healthy' | 'attention' | 'blocked' | 'paused' +} + +export interface FeedEntry { + time: string + agent: string + action: string + timestamp: number +} + +export interface ChatMessage { + id: string + sender: 'user' | 'iris' + text: string + timestamp: number +} + +export interface QueueItem { + id: string + text: string + priority: 'high' | 'medium' | 'low' + waitTime: string +} + +const now = Date.now() + +export function useDashboardData() { + const sessionStart = Date.now() + + // Runtime counter + const runtimeSeconds = ref(0) + let runtimeInterval: ReturnType | null = null + + function startRuntime() { + const startTs = sessionStart + runtimeSeconds.value = Math.floor((Date.now() - startTs) / 1000) + runtimeInterval = setInterval(() => { + runtimeSeconds.value = Math.floor((Date.now() - startTs) / 1000) + }, 1000) + } + + function stopRuntime() { + if (runtimeInterval) { + clearInterval(runtimeInterval) + runtimeInterval = null + } + } + + const formatRuntime = (seconds: number): string => { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` + } + + const irisRuntime = computed(() => formatRuntime(runtimeSeconds.value)) + + // Agent runtimes (simulated) + const agentStartTimes = reactive>({ + developer: now - 3600000, + devops: now - 1800000, + researcher: now - 2700000, + reviewer: now - 900000, + }) + + const getAgentRuntime = (id: string): string => { + const start = agentStartTimes[id] + if (!start) return '00:00' + const secs = Math.floor((now - start) / 1000) + const m = Math.floor(secs / 60) + const s = secs % 60 + return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` + } + + // Agents + const agents = ref([ + { + id: 'developer', + name: 'Developer', + role: 'Backend & Frontend', + description: 'Implements features across the stack with TypeScript, C#, and Vue.', + color: '#3b82f6', + icon: 'code', + currentTask: 'Building Dungeon System API endpoints', + goal: 'Complete Dungeon CRUD + room generation', + progress: 62, + workload: 65, + active: true, + runtimeSeconds: 3600, + workingFeed: [ + 'Created DungeonController', + 'Defined dungeon schema', + 'Implementing room generation algorithm', + 'Writing unit tests for RoomFactory', + ], + }, + { + id: 'devops', + name: 'DevOps', + role: 'Infrastructure & CI/CD', + description: 'Manages Docker, deployment pipelines, and system reliability.', + color: '#eab308', + icon: 'server', + currentTask: 'Optimizing Docker Compose caching', + goal: 'Reduce build times by 40%', + progress: 45, + workload: 40, + active: false, + runtimeSeconds: 1800, + workingFeed: [ + 'Analyzed Docker layer cache', + 'Optimized COPY order in Dockerfile', + 'Added .dockerignore for node_modules', + 'Testing incremental builds', + ], + }, + { + id: 'researcher', + name: 'Researcher', + role: 'Analysis & Documentation', + description: 'Researches APIs, patterns, and best practices. Maintains docs.', + color: '#22c55e', + icon: 'search', + currentTask: 'Analyzing WebSocket alternatives', + goal: 'Recommend real-time communication strategy', + progress: 30, + workload: 25, + active: true, + runtimeSeconds: 2700, + workingFeed: [ + 'Evaluated WebSocket vs SSE vs WebRTC', + 'Documented SignalR limitations', + 'Prototyping WebSocket fallback', + ], + }, + { + id: 'reviewer', + name: 'Reviewer', + role: 'Code Quality & Testing', + description: 'Reviews pull requests, enforces standards, runs test suites.', + color: '#a855f7', + icon: 'shield', + currentTask: 'Reviewing Dungeon System PR', + goal: 'Zero critical findings before merge', + progress: 80, + workload: 50, + active: false, + runtimeSeconds: 900, + workingFeed: [ + 'Reviewed DungeonController.cs', + 'Found 3 minor style issues', + 'Approved RoomValidator', + 'Running integration tests', + ], + }, + ]) + + // Missions + const missions = ref([ + { + id: 'dungeon-system', + name: 'Dungeon System', + progress: 62, + currentTask: 'Implement room generation', + lastActivity: '3 min ago', + remainingTasks: 8, + status: 'healthy', + }, + { + id: 'dashboard-redesign', + name: 'Dashboard Redesign', + progress: 45, + currentTask: 'AI Team Network layout', + lastActivity: 'Just now', + remainingTasks: 6, + status: 'healthy', + }, + { + id: 'infra-optimization', + name: 'Infra Optimization', + progress: 30, + currentTask: 'Optimize build caching', + lastActivity: '12 min ago', + remainingTasks: 4, + status: 'attention', + }, + { + id: 'auth-system', + name: 'Auth System', + progress: 88, + currentTask: 'Finalize refresh token flow', + lastActivity: '45 min ago', + remainingTasks: 2, + status: 'healthy', + }, + ]) + + // Feed + const feedEntries = ref([ + { time: '20:42', agent: 'Developer', action: 'Created DungeonController endpoints', timestamp: now - 60000 }, + { time: '20:38', agent: 'DevOps', action: 'Optimized Docker COPY order', timestamp: now - 300000 }, + { time: '20:35', agent: 'Iris', action: 'Delegated room generation to Developer', timestamp: now - 540000 }, + { time: '20:28', agent: 'Researcher', action: 'Documented WebSocket vs SSE analysis', timestamp: now - 780000 }, + { time: '20:22', agent: 'Reviewer', action: 'Approved RoomValidator PR', timestamp: now - 900000 }, + { time: '20:15', agent: 'DevOps', action: 'Added .dockerignore for node_modules', timestamp: now - 1200000 }, + { time: '20:08', agent: 'Iris', action: 'Broke down Dungeon System tasks', timestamp: now - 1500000 }, + { time: '19:55', agent: 'Developer', action: 'Defined dungeon schema models', timestamp: now - 1800000 }, + ]) + + // Chat + const chatMessages = ref([ + { id: 'm1', sender: 'iris', text: 'Guten Abend, Bao. Ready to continue the Dungeon System?', timestamp: now - 600000 }, + { id: 'm2', sender: 'user', text: "Yes, what's the status?", timestamp: now - 540000 }, + { id: 'm3', sender: 'iris', text: "Developer is at 62% on room generation. Reviewer approved the schema. I'd recommend focusing on the room connection logic next.", timestamp: now - 480000 }, + ]) + + const irisBusy = ref(true) + const irisFocus = ref('Breaking down Dungeon System for DevOps and Developer') + + // Queue + const queue = ref([ + { id: 'q1', text: 'Deploy latest dashboard build to preview', priority: 'high', waitTime: '2 min' }, + { id: 'q2', text: 'Review infrastructure cost analysis', priority: 'medium', waitTime: '8 min' }, + { id: 'q3', text: 'Schedule B2 German lesson review', priority: 'low', waitTime: '15 min' }, + { id: 'q4', text: 'Update project roadmap document', priority: 'medium', waitTime: '12 min' }, + ]) + + function sendChat(text: string): void { + if (!text.trim()) return + chatMessages.value.push({ + id: `user-${Date.now()}`, + sender: 'user', + text: text.trim(), + timestamp: Date.now(), + }) + } + + function removeQueueItem(id: string): void { + const idx = queue.value.findIndex(q => q.id === id) + if (idx !== -1) queue.value.splice(idx, 1) + } + + function moveQueueItem(fromIdx: number, toIdx: number): void { + if (toIdx < 0 || toIdx >= queue.value.length) return + const [item] = queue.value.splice(fromIdx, 1) + queue.value.splice(toIdx, 0, item) + } + + function changeQueuePriority(id: string, priority: QueueItem['priority']): void { + const item = queue.value.find(q => q.id === id) + if (item) item.priority = priority + } + + return { + agents, + missions, + feedEntries, + chatMessages, + irisBusy, + irisFocus, + irisRuntime, + queue, + runtimeSeconds, + getAgentRuntime, + startRuntime, + stopRuntime, + formatRuntime, + sendChat, + removeQueueItem, + moveQueueItem, + changeQueuePriority, + } +} diff --git a/frontend/src/composables/useTimer.ts b/frontend/src/composables/useTimer.ts new file mode 100644 index 0000000..f0c1da0 --- /dev/null +++ b/frontend/src/composables/useTimer.ts @@ -0,0 +1,37 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useTimer() { + const elapsed = ref(0) + let interval: ReturnType | null = null + + function start() { + if (interval !== null) return + const startTime = Date.now() + elapsed.value = 0 + interval = setInterval(() => { + elapsed.value = Math.floor((Date.now() - startTime) / 1000) + }, 1000) + } + + function stop() { + if (interval !== null) { + clearInterval(interval) + interval = null + } + } + + function format(minutes: number, seconds: number): string { + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` + } + + const formatted = (): string => { + const m = Math.floor(elapsed.value / 60) + const s = elapsed.value % 60 + return format(m, s) + } + + onMounted(start) + onUnmounted(stop) + + return { elapsed, formatted, start, stop } +} diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue index 933681f..ef62ecd 100644 --- a/frontend/src/views/DashboardView.vue +++ b/frontend/src/views/DashboardView.vue @@ -1,116 +1,85 @@