@@ -1,4 +1,13 @@
import { ref , reactive , computed } from 'vue'
import { ref , computed } from 'vue'
// ── Shared State (singleton: same state regardless of how many times useDashboardData() is called) ──
const sessionStart = Date . now ( )
// Intervals registry for cleanup
const intervals : ReturnType < typeof setInterval > [ ] = [ ]
let cleanupRegistered = false
// ── Interfaces (exported for components) ──
export interface AgentNodeData {
id : string
@@ -49,242 +58,315 @@ export interface QueueItem {
waitTime : string
}
const now = Date . now ( )
// ── API Response Interfaces ──
export function useDashboardData() {
const sessionStart = Date . now ( )
// Runtime counter
const runtimeSeconds = ref ( 0 )
let runtimeInterval : ReturnType < typeof setInterval > | 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 )
interface DashboardStatusResponse {
gatewayOk : boolean
irisStatus : string
activeAgents : number
pendingTasks : number
}
function stopRuntime() {
if ( runtimeInterval ) {
clearInterval ( runtimeInterval )
runtimeInterval = null
}
interface DashboardAgentInfo {
id : string
name : string
role : string
model : string
isActive : boolean
currentTask : string
}
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' ) } `
interface DashboardOperationEntry {
agent : string
action : string
timestamp : string
time : string
}
const irisRuntime = computed ( ( ) = > formatRuntime ( runtimeSeconds . value ) )
// Agent runtimes (simulated)
const agentStartTimes = reactive < Record < string , number > > ( {
iris : now - 28800000 ,
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' ) } `
interface DashboardChatMessage {
role : 'user' | 'assistant'
content : string
timestamp : string
}
// Agents
const agents = ref < AgentNodeData [ ] > ( [
{
id : 'iris' ,
name : 'Iris' ,
role : 'Chief of Staff' ,
interface DashboardSendResponse {
ok : boolean
reply? : string
error? : string
}
interface DashboardQueueItem {
id : string
name : string
status : string
}
// ── Agent Catalog (static enrichment) ──
const AGENT_CATALOG : Record < string , Partial < AgentNodeData > > = {
iris : {
description : 'Koordiniert, delegiert, hält das Team tight. Die erste Anlaufstelle zwischen Boss und Maschine.' ,
tags : [ 'Orchestration' , 'Delegation' , 'Approval' ] ,
color : '#8b7cf6' ,
icon : 'bot' ,
hero : true ,
currentTask : 'Orchestrating Nexus Dashboard redesign' ,
goal : 'Complete Mission Control v3' ,
progress : 85 ,
workload : 55 ,
active : true ,
runtimeSeconds : 28800 ,
model : 'GPT-5.4' ,
workingFeed : [
{ time : '22:38' , text : 'Analyzed user feedback on Dashboard' } ,
{ time : '22:36' , text : 'Delegated card redesign to Developer' } ,
{ time : '22:34' , text : 'Verifying full-width layout deployment' } ,
{ time : '22:32' , text : 'Reviewing AgentModal integration' } ,
] ,
thinkingStream : [
{ time : '22:24' , text : 'Analysing constraint: full-width layout' } ,
{ time : '22:25' , text : 'Removing max-width from global CSS' } ,
{ time : '22:26' , text : 'Verifying Dashboard grid reflow' } ,
] ,
workingFeed : [ ] ,
thinkingStream : [ ] ,
} ,
{
id : 'developer' ,
name : 'Developer' ,
role : 'Backend & Frontend' ,
developer : {
description : 'Implements features across the stack with TypeScript, C#, and Vue.' ,
tags : [ 'Coding' , 'Development' , 'Builds' ] ,
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 : [
{ time : '22:30' , text : 'Created DungeonController' } ,
{ time : '22:28' , text : 'Defined dungeon schema' } ,
{ time : '22:26' , text : 'Implementing room generation algorithm' } ,
{ time : '22:24' , text : 'Writing unit tests for RoomFactory' } ,
] ,
thinkingStream : [
{ time : '22:22' , text : 'Parsing dungeon spec from Iris' } ,
{ time : '22:23' , text : 'Designing RoomFactory interface' } ,
{ time : '22:24' , text : 'Implementing corridor connection logic' } ,
] ,
model : 'DeepSeek V4 Flash' ,
workingFeed : [ ] ,
thinkingStream : [ ] ,
} ,
{
id : 'devops' ,
name : 'DevOps' ,
role : 'Infrastructure & CI/CD' ,
devops : {
description : 'Manages Docker, deployment pipelines, and system reliability.' ,
tags : [ 'Deployment' , 'Docker' , 'CI/CD' ] ,
color : '#eab308' ,
icon : 'server' ,
currentTask : 'Optimizing Docker Compose caching' ,
goal : 'Reduce build times by 40%' ,
progress : 45 ,
workload : 40 ,
active : false ,
runtimeSeconds : 1800 ,
workingFeed : [
{ time : '22:20' , text : 'Analyzed Docker layer cache' } ,
{ time : '22:18' , text : 'Optimized COPY order in Dockerfile' } ,
{ time : '22:16' , text : 'Added .dockerignore for node_modules' } ,
{ time : '22:14' , text : 'Testing incremental builds' } ,
] ,
thinkingStream : [
{ time : '22:20' , text : 'Checking build cache hit rates' } ,
{ time : '22:21' , text : 'Benchmarking multi-stage vs single-stage' } ,
{ time : '22:22' , text : 'Calculating potential speedup from caching' } ,
] ,
model : 'DeepSeek V4 Pro' ,
workingFeed : [ ] ,
thinkingStream : [ ] ,
} ,
{
id : 'researcher' ,
name : 'Researcher' ,
role : 'Analysis & Documentation' ,
researcher : {
description : 'Researches APIs, patterns, and best practices. Maintains docs.' ,
tags : [ 'Research' , 'Analysis' , 'Docs' ] ,
color : '#22c55e' ,
icon : 'search' ,
currentTask : 'Analyzing WebSocket alternatives' ,
goal : 'Recommend real-time communication strategy' ,
progress : 30 ,
workload : 25 ,
active : true ,
runtimeSeconds : 2700 ,
workingFeed : [
{ time : '22:18' , text : 'Evaluated WebSocket vs SSE vs WebRTC' } ,
{ time : '22:17' , text : 'Documented SignalR limitations' } ,
{ time : '22:16' , text : 'Prototyping WebSocket fallback' } ,
] ,
thinkingStream : [
{ time : '22:18' , text : 'Cross-referencing WebSocket latency benchmarks' } ,
{ time : '22:19' , text : 'Checking SSE browser support matrix' } ,
{ time : '22:20' , text : 'Drafting recommendation summary' } ,
] ,
model : 'DeepSeek V4 Pro' ,
workingFeed : [ ] ,
thinkingStream : [ ] ,
} ,
{
id : 'reviewer' ,
name : 'Reviewer' ,
role : 'Code Quality & Testing' ,
reviewer : {
description : 'Reviews pull requests, enforces standards, runs test suites.' ,
tags : [ 'Code Review' , 'Testing' , 'Quality' ] ,
color : '#a855f7' ,
icon : 'shield' ,
currentTask : 'Reviewing Dungeon System PR' ,
goal : 'Zero critical findings before merge' ,
progress : 80 ,
workload : 50 ,
active : false ,
runtimeSeconds : 900 ,
workingFeed : [
{ time : '22:15' , text : 'Reviewed DungeonController.cs' } ,
{ time : '22:14' , text : 'Found 3 minor style issues' } ,
{ time : '22:13' , text : 'Approved RoomValidator' } ,
{ time : '22:12' , text : 'Running integration tests' } ,
] ,
thinkingStream : [
{ time : '22:15' , text : 'Analyzing DungeonController PR diff' } ,
{ time : '22:16' , text : 'Checking RoomValidator edge cases' } ,
{ time : '22:17' , text : 'Verifying integration test coverage' } ,
] ,
model : 'DeepSeek V4 Pro' ,
workingFeed : [ ] ,
thinkingStream : [ ] ,
} ,
] )
}
// Open Tasks
function enrichAgent ( api : DashboardAgentInfo ) : AgentNodeData {
const catalog = AGENT_CATALOG [ api . id ] ? ? AGENT_CATALOG [ 'developer' ]
return {
id : api.id ,
name : api.name ,
role : api.role ,
model : api.model ,
currentTask : api.currentTask ? ? 'Idle' ,
active : api.isActive ,
description : catalog.description ? ? '' ,
tags : catalog.tags ? ? [ ] ,
color : catalog.color ? ? '#6b7385' ,
icon : catalog.icon ? ? 'bot' ,
hero : catalog.hero ? ? false ,
goal : catalog.goal ? ? 'No goal set' ,
progress : catalog.progress ? ? 0 ,
workload : catalog.workload ? ? 0 ,
runtimeSeconds : 0 ,
workingFeed : catalog.workingFeed ? ? [ ] ,
thinkingStream : catalog.thinkingStream ? ? [ ] ,
}
}
// ── Helper: API Fetch with auth ──
async function apiFetch ( path : string , init : RequestInit = { } ) : Promise < Response > {
const base = '' // same-origin proxy
return fetch ( ` ${ base } ${ path } ` , {
. . . init ,
credentials : 'include' ,
headers : {
'Content-Type' : 'application/json' ,
. . . ( init . headers as Record < string , string > ? ? { } ) ,
} ,
} )
}
// ── State ──
// Status
const gatewayOk = ref ( true )
const irisStatus = ref ( 'Active' )
const activeAgents = ref ( 0 )
const pendingTasks = ref ( 0 )
// Agents
const agents = ref < AgentNodeData [ ] > ( [ ] )
// Chat
const chatMessages = ref < ChatMessage [ ] > ( [ ] )
const irisBusy = ref ( false )
const irisFocus = ref ( '' )
// Operations Feed
const feedEntries = ref < FeedEntry [ ] > ( [ ] )
// Open Tasks (mock only – no API endpoint)
const openTasks = ref < OpenTask [ ] > ( [
{ id : 't1' , title : 'Agent Thinking Panel visualisieren' , detail : 'Live-Animation der Denkprozesse im AgentModal' , source : 'iris' , createdAt : '22:30' } ,
{ id : 't2' , title : 'CI/CD Pipeline Monitoring Dashboard' , detail : 'Echtzeit-Status der Gitea Actions im Dashboard' , source : 'iris' , createdAt : '21:15' } ,
{ id : 't3' , title : 'Dungeon System Dokumentation' , detail : 'API-Doku für Room-Generation-Endpunkte schreiben' , source : 'bao' , createdAt : '20:00' } ,
] )
// Feed
const ts = ( offset : number ) = > new Date ( now + offset ) . toISOString ( )
const feedEntries = ref < FeedEntry [ ] > ( [
{ time : '22:50' , agent : 'Developer' , action : 'Created DungeonController endpoints' , timestamp : ts ( - 120000 ) } ,
{ time : '22:46' , agent : 'DevOps' , action : 'Optimized Docker COPY order for layer caching' , timestamp : ts ( - 360000 ) } ,
{ time : '22:42' , agent : 'Iris' , action : 'Delegated room generation to Developer with spec' , timestamp : ts ( - 600000 ) } ,
{ time : '22:35' , agent : 'Researcher' , action : 'Documented WebSocket vs SSE analysis results' , timestamp : ts ( - 960000 ) } ,
{ time : '22:28' , agent : 'Reviewer' , action : 'Approved RoomValidator PR with minor fixes' , timestamp : ts ( - 1200000 ) } ,
{ time : '22:18' , agent : 'DevOps' , action : 'Added .dockerignore for node_modules and build artifacts' , timestamp : ts ( - 1500000 ) } ,
{ time : '22:08' , agent : 'Iris' , action : 'Broke down Dungeon System tasks into sub-tasks' , timestamp : ts ( - 1800000 ) } ,
{ time : '21:55' , agent : 'Developer' , action : 'Defined dungeon schema models with validation' , timestamp : ts ( - 2400000 ) } ,
] )
// Chat
const chatMessages = ref < ChatMessage [ ] > ( [
{ 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 < QueueItem [ ] > ( [
{ 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' } ,
] )
const queue = ref < QueueItem [ ] > ( [ ] )
function sendChat ( text : string ) : void {
// Runtime
const runtimeSeconds = ref ( 0 )
let runtimeInterval : ReturnType < typeof setInterval > | null = null
// ── Fetch Functions ──
async function fetchStatus ( ) : Promise < void > {
try {
const res = await apiFetch ( '/api/dashboard/status' )
if ( ! res . ok ) return
const data : DashboardStatusResponse = await res . json ( )
gatewayOk . value = data . gatewayOk
irisStatus . value = data . irisStatus
activeAgents . value = data . activeAgents
pendingTasks . value = data . pendingTasks
} catch {
// API unreachable – keep current values
}
}
async function fetchAgents ( ) : Promise < void > {
try {
const res = await apiFetch ( '/api/dashboard/agents' )
if ( ! res . ok ) return
const data : DashboardAgentInfo [ ] = await res . json ( )
agents . value = data . map ( enrichAgent )
} catch {
// API unreachable – keep current values
}
}
async function fetchOperations ( ) : Promise < void > {
try {
const res = await apiFetch ( '/api/dashboard/operations?limit=20' )
if ( ! res . ok ) return
const data : DashboardOperationEntry [ ] = await res . json ( )
feedEntries . value = data . map ( ( entry ) = > ( {
time : entry.time ,
agent : entry.agent ,
action : entry.action ,
timestamp : entry.timestamp ,
} ) )
} catch {
// API unreachable – keep current values
}
}
async function fetchChatMessages ( ) : Promise < void > {
try {
const res = await apiFetch ( '/api/dashboard/chat/messages?limit=50' )
if ( ! res . ok ) return
const data : DashboardChatMessage [ ] = await res . json ( )
// Merge instead of replace — only add messages not already present
const existingTexts = new Set ( chatMessages . value . map ( m = > m . text ) )
const existingTimestamps = new Set ( chatMessages . value . map ( m = > m . timestamp ) )
for ( const msg of data ) {
const msgTime = new Date ( msg . timestamp ) . getTime ( )
if ( existingTexts . has ( msg . content ) && existingTimestamps . has ( msgTime ) ) continue
chatMessages . value . push ( {
id : ` msg- ${ msgTime } - ${ msg . role } ` ,
sender : msg.role === 'assistant' ? 'iris' : 'user' ,
text : msg.content ,
timestamp : msgTime ,
} )
}
} catch {
// API unreachable – keep current values
}
}
async function fetchQueue ( ) : Promise < void > {
try {
const res = await apiFetch ( '/api/dashboard/queue' )
if ( ! res . ok ) return
const data : DashboardQueueItem [ ] = await res . json ( )
queue . value = data . map ( ( item ) = > ( {
id : item.id ,
text : item.name ,
priority : ( item . status === 'high' || item . status === 'medium' || item . status === 'low' )
? item . status as 'high' | 'medium' | 'low'
: 'medium' ,
waitTime : '--' ,
} ) )
} catch {
// API unreachable – keep current values
}
}
// ── Chat Send ──
async function sendChatMessage ( text : string ) : Promise < void > {
if ( ! text . trim ( ) ) return
// Optimistic add
chatMessages . value . push ( {
id : ` user- ${ Date . now ( ) } ` ,
sender : 'user' ,
text : text.trim ( ) ,
timestamp : Date.now ( ) ,
} )
irisBusy . value = true
try {
const res = await apiFetch ( '/api/dashboard/chat/send' , {
method : 'POST' ,
body : JSON.stringify ( { message : text.trim ( ) } ) ,
} )
const data : DashboardSendResponse = await res . json ( )
if ( data . ok && data . reply ) {
chatMessages . value . push ( {
id : ` iris- ${ Date . now ( ) } ` ,
sender : 'iris' ,
text : data.reply ,
timestamp : Date.now ( ) ,
} )
} else if ( data . error ) {
chatMessages . value . push ( {
id : ` error- ${ Date . now ( ) } ` ,
sender : 'iris' ,
text : ` ⚠️ ${ data . error } ` ,
timestamp : Date.now ( ) ,
} )
}
} catch {
chatMessages . value . push ( {
id : ` error- ${ Date . now ( ) } ` ,
sender : 'iris' ,
text : '⚠️ Connection error. Please try again.' ,
timestamp : Date.now ( ) ,
} )
} finally {
irisBusy . value = false
irisFocus . value = text . trim ( )
}
}
// ── Queue Operations ──
function removeQueueItem ( id : string ) : void {
const idx = queue . value . findIndex ( q = > q . id === id )
@@ -302,7 +384,73 @@ export function useDashboardData() {
if ( item ) item . priority = priority
}
// ── Runtime ──
function startRuntime ( ) : void {
const startTs = sessionStart
runtimeSeconds . value = Math . floor ( ( Date . now ( ) - startTs ) / 1000 )
runtimeInterval = setInterval ( ( ) = > {
runtimeSeconds . value = Math . floor ( ( Date . now ( ) - startTs ) / 1000 )
} , 1000 )
}
function stopRuntime ( ) : void {
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 ) )
const getAgentRuntime = ( _id : string ) : string = > {
// Could be extended to track per-agent runtimes from API
return formatRuntime ( runtimeSeconds . value )
}
// ── Polling starten (nur einmal) ──
function startPolling ( ) : void {
if ( cleanupRegistered ) return
cleanupRegistered = true
// Initial fetches
fetchStatus ( )
fetchAgents ( )
fetchOperations ( )
fetchChatMessages ( )
fetchQueue ( )
// Polling intervals
intervals . push ( setInterval ( fetchStatus , 5000 ) )
intervals . push ( setInterval ( fetchAgents , 10000 ) )
intervals . push ( setInterval ( fetchOperations , 10000 ) )
intervals . push ( setInterval ( fetchChatMessages , 3000 ) )
intervals . push ( setInterval ( fetchQueue , 10000 ) )
}
function stopPolling ( ) : void {
for ( const interval of intervals ) {
clearInterval ( interval )
}
intervals . length = 0
cleanupRegistered = false
}
// ── Composable Export ──
export function useDashboardData() {
// Start polling on first call
startPolling ( )
return {
// State
agents ,
openTasks ,
feedEntries ,
@@ -311,14 +459,29 @@ export function useDashboardData() {
irisFocus ,
irisRuntime ,
queue ,
gatewayOk ,
irisStatus ,
pendingTasks ,
activeAgents ,
// Runtime
runtimeSeconds ,
getAgentRuntime ,
startRuntime ,
stopRuntime ,
formatRuntime ,
sendChat ,
// Actions
sendChatMessage ,
removeQueueItem ,
moveQueueItem ,
changeQueuePriority ,
// Fetch (for manual refresh)
fetchStatus ,
fetchAgents ,
fetchOperations ,
fetchChatMessages ,
fetchQueue ,
}
}