feat: wire dashboard v2 to backend data
This commit is contained in:
@@ -160,6 +160,16 @@ const statusColors: Record<string, string> = {
|
||||
<div class="m-think">{{ thinkDisplay }}<span class="caret"></span></div>
|
||||
</div>
|
||||
|
||||
<div v-if="agent.activity.length" class="m-sec">
|
||||
<h4>Agent Activity</h4>
|
||||
<div class="m-activity">
|
||||
<div v-for="(entry, index) in agent.activity" :key="index" class="m-activity-row">
|
||||
<span class="m-activity-time">{{ entry.time }}</span>
|
||||
<span class="m-activity-text">{{ entry.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modell wählen -->
|
||||
<div class="m-sec">
|
||||
<h4>Modell wählen</h4>
|
||||
@@ -519,6 +529,35 @@ const statusColors: Record<string, string> = {
|
||||
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
|
||||
.m-activity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.m-activity-row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(124,108,255,.06);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.m-activity-time {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
color: var(--tx-3);
|
||||
}
|
||||
|
||||
.m-activity-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--tx-2);
|
||||
}
|
||||
|
||||
/* ── Models ──────────────────────────────────── */
|
||||
.m-models {
|
||||
display: flex;
|
||||
|
||||
@@ -19,6 +19,7 @@ defineProps<{
|
||||
blockerCount: number
|
||||
todayCost: string
|
||||
todayTokens: string
|
||||
blockerLabel?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
@@ -62,7 +63,7 @@ defineEmits<{
|
||||
@click="$emit('blockerClick')"
|
||||
>
|
||||
<span class="dot block"></span>
|
||||
{{ blockerCount }} Blocker
|
||||
{{ blockerLabel || `${blockerCount} Blocker` }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface TaskItem {
|
||||
priority: 'high' | 'medium' | 'low'
|
||||
status: 'active' | 'pending' | 'blocked'
|
||||
progress: number // 0–100
|
||||
detail?: string | null
|
||||
source?: string
|
||||
}
|
||||
|
||||
/* ── Agent Detail Modal Types ─────────────────── */
|
||||
@@ -26,6 +28,11 @@ export interface ThinkingItem {
|
||||
ts: string
|
||||
}
|
||||
|
||||
export interface AgentActivityItem {
|
||||
time: string
|
||||
text: string
|
||||
}
|
||||
|
||||
/** Dashboard view-model for an agent detail modal */
|
||||
export interface AgentDetailData {
|
||||
id: string
|
||||
@@ -51,5 +58,6 @@ export interface AgentDetailData {
|
||||
lastActive: string
|
||||
activeTaskCount: number
|
||||
thinking: ThinkingItem[]
|
||||
activity: AgentActivityItem[]
|
||||
availableModels: { id: string; alias: string }[]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { icons } from '../../composables/icons'
|
||||
|
||||
defineProps<{
|
||||
connected?: boolean
|
||||
statusLabel?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
@@ -27,7 +28,7 @@ defineEmits<{
|
||||
<!-- Status Pill -->
|
||||
<span :class="['pill', connected ? 'live' : 'preview']">
|
||||
<span class="status-dot" :class="connected ? 'on' : 'off'"></span>
|
||||
{{ connected ? 'OpenClaw verbunden' : 'Preview' }}
|
||||
{{ connected ? (statusLabel || 'OpenClaw verbunden') : 'Preview' }}
|
||||
</span>
|
||||
|
||||
<!-- Ask Iris Button -->
|
||||
|
||||
@@ -13,8 +13,20 @@ interface FlowBoardChatStore {
|
||||
sendMessage: (text: string) => void
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'nexus-flow-positions'
|
||||
|
||||
function readStoredPositions() {
|
||||
if (typeof window === 'undefined') return {}
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
return raw ? JSON.parse(raw) as Record<string, { x: number; y: number }> : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function useFlowBoardState(agentStore: FlowBoardAgentStore, chatStore: FlowBoardChatStore) {
|
||||
const agentPositions = ref<Record<string, { x: number; y: number }>>({})
|
||||
const agentPositions = ref<Record<string, { x: number; y: number }>>(readStoredPositions())
|
||||
const enteringIds = ref<string[]>([])
|
||||
const localAgentPool = ref<AgentNodeData[]>([...extraAgentPool])
|
||||
|
||||
@@ -46,10 +58,14 @@ export function useFlowBoardState(agentStore: FlowBoardAgentStore, chatStore: Fl
|
||||
|
||||
function resetLayout() {
|
||||
agentPositions.value = {}
|
||||
if (typeof window !== 'undefined') window.localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
function updatePositions(positions: Record<string, { x: number; y: number }>) {
|
||||
agentPositions.value = { ...positions }
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(agentPositions.value))
|
||||
}
|
||||
}
|
||||
|
||||
function sendChatMessage(text: string) {
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { useAgentStore } from '../stores/agents'
|
||||
import { useDashboardStore } from '../stores/dashboard'
|
||||
import GalaxyBackground from '../components/background/GalaxyBackground.vue'
|
||||
import Sidebar from '../components/layout/Sidebar.vue'
|
||||
import Topbar from '../components/layout/Topbar.vue'
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const dashboardStore = useDashboardStore()
|
||||
|
||||
/* ── Mobile Sidebar State ───────────────────────── */
|
||||
const mobileMenuOpen = ref(false)
|
||||
@@ -39,7 +39,8 @@ function closeMobileMenu() {
|
||||
|
||||
<main class="nexus-main">
|
||||
<Topbar
|
||||
:connected="agentStore.isConnected"
|
||||
:connected="dashboardStore.isGatewayConnected"
|
||||
:status-label="dashboardStore.irisStatusLabel"
|
||||
@toggle-sidebar="mobileMenuOpen = !mobileMenuOpen"
|
||||
/>
|
||||
<div class="nexus-content">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { apiFetch } from '../services/api'
|
||||
import type { AgentNodeData } from '../composables/useFlowLayout'
|
||||
import type { AgentDetailData, ThinkingItem } from '../components/dashboard/v2/types'
|
||||
import type { AgentActivityItem, AgentDetailData, ThinkingItem } from '../components/dashboard/v2/types'
|
||||
|
||||
/* ── API Response Shapes ──────────────────────────── */
|
||||
|
||||
@@ -40,6 +40,11 @@ interface ModelOption {
|
||||
provider: string
|
||||
}
|
||||
|
||||
interface AgentActivityEntry {
|
||||
time: string
|
||||
text: string
|
||||
}
|
||||
|
||||
/* ── Status Mapping ───────────────────────────────── */
|
||||
|
||||
function mapStatus(isActive: boolean, currentTask: string | null): AgentNodeData['status'] {
|
||||
@@ -148,6 +153,7 @@ export function buildAgentDetail(data: AgentNodeData, models: { id: string; alia
|
||||
lastActive: data.elapsed !== '—' ? 'Vor ' + data.elapsed : 'Nicht aktiv',
|
||||
activeTaskCount: data.task ? 1 : 0,
|
||||
thinking: buildThinkingItems(data),
|
||||
activity: [],
|
||||
availableModels: models,
|
||||
}
|
||||
}
|
||||
@@ -159,6 +165,7 @@ export const useAgentStore = defineStore('agents', {
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
selectedAgentId: null as string | null,
|
||||
activityByAgentId: {} as Record<string, AgentActivityItem[]>,
|
||||
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||
isConnected: false,
|
||||
}),
|
||||
@@ -179,7 +186,10 @@ export const useAgentStore = defineStore('agents', {
|
||||
if (!state.selectedAgentId) return null
|
||||
const data = state.agents.find(a => a.id === state.selectedAgentId)
|
||||
if (!data) return null
|
||||
return buildAgentDetail(data, state.models)
|
||||
return {
|
||||
...buildAgentDetail(data, state.models),
|
||||
activity: state.activityByAgentId[data.id] ?? [],
|
||||
}
|
||||
},
|
||||
|
||||
/** Is the modal open? */
|
||||
@@ -249,9 +259,24 @@ export const useAgentStore = defineStore('agents', {
|
||||
}
|
||||
},
|
||||
|
||||
async fetchAgentActivity(agentId: string) {
|
||||
try {
|
||||
const res = await apiFetch(`/api/dashboard/agents/${encodeURIComponent(agentId)}/activity?limit=5`)
|
||||
if (!res.ok) return
|
||||
const data: AgentActivityEntry[] = await res.json()
|
||||
this.activityByAgentId[agentId] = data.map(entry => ({
|
||||
time: entry.time,
|
||||
text: entry.text,
|
||||
}))
|
||||
} catch (err) {
|
||||
console.warn('[AgentStore] fetchAgentActivity failed', err)
|
||||
}
|
||||
},
|
||||
|
||||
/* ── Selection ───────────────────────────────── */
|
||||
selectAgent(id: string | null) {
|
||||
this.selectedAgentId = id
|
||||
if (id) void this.fetchAgentActivity(id)
|
||||
},
|
||||
|
||||
/* ── Polling ─────────────────────────────────── */
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { apiFetch } from '../services/api'
|
||||
|
||||
interface DashboardStatusDto {
|
||||
gatewayOk: boolean
|
||||
irisStatus: string
|
||||
activeAgents: number
|
||||
pendingTasks: number
|
||||
}
|
||||
|
||||
interface FeedEntryDto {
|
||||
agent: string
|
||||
action: string
|
||||
timestamp: string
|
||||
time: string
|
||||
agentId?: string | null
|
||||
type?: string | null
|
||||
}
|
||||
|
||||
interface QueueItemDto {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
priority: string
|
||||
source: string
|
||||
waitTime: string
|
||||
}
|
||||
|
||||
export const useDashboardStore = defineStore('dashboard', {
|
||||
state: () => ({
|
||||
status: null as DashboardStatusDto | null,
|
||||
operations: [] as FeedEntryDto[],
|
||||
queue: [] as QueueItemDto[],
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
refreshInterval: null as ReturnType<typeof setInterval> | null,
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isGatewayConnected: state => state.status?.gatewayOk ?? false,
|
||||
irisStatusLabel: state => state.status?.irisStatus ?? 'Offline',
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchStatus() {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/status')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
this.status = await res.json()
|
||||
} catch (err) {
|
||||
console.warn('[DashboardStore] fetchStatus failed', err)
|
||||
this.status = null
|
||||
}
|
||||
},
|
||||
|
||||
async fetchOperations() {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/operations?limit=20')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
this.operations = await res.json()
|
||||
} catch (err) {
|
||||
console.warn('[DashboardStore] fetchOperations failed', err)
|
||||
this.operations = []
|
||||
}
|
||||
},
|
||||
|
||||
async fetchQueue() {
|
||||
try {
|
||||
const res = await apiFetch('/api/dashboard/queue')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
this.queue = await res.json()
|
||||
} catch (err) {
|
||||
console.warn('[DashboardStore] fetchQueue failed', err)
|
||||
this.queue = []
|
||||
}
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
this.loading = true
|
||||
try {
|
||||
await Promise.all([
|
||||
this.fetchStatus(),
|
||||
this.fetchOperations(),
|
||||
this.fetchQueue(),
|
||||
])
|
||||
this.error = null
|
||||
} catch (err) {
|
||||
console.warn('[DashboardStore] refresh failed', err)
|
||||
this.error = 'Dashboard metadata could not be loaded'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
startPolling() {
|
||||
if (this.refreshInterval) return
|
||||
this.refresh()
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.refresh()
|
||||
}, 30000)
|
||||
},
|
||||
|
||||
stopPolling() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval)
|
||||
this.refreshInterval = null
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -56,6 +56,8 @@ function mapTask(t: DashboardTaskDto): TaskItem {
|
||||
priority: mapPriority(t.priority),
|
||||
status: mapState(t.state),
|
||||
progress: mapProgress(t.state),
|
||||
detail: t.detail,
|
||||
source: t.source,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
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'
|
||||
@@ -26,6 +27,7 @@ import { useFlowBoardState } from '../../composables/useFlowBoardState'
|
||||
/* ── Stores ──────────────────────────────────────── */
|
||||
const agentStore = useAgentStore()
|
||||
const chatStore = useChatStore()
|
||||
const dashboardStore = useDashboardStore()
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
const {
|
||||
@@ -44,16 +46,28 @@ function handleBlockerClick() {
|
||||
console.log('[FlowBoard] blocker clicked')
|
||||
}
|
||||
|
||||
function blockerLabel() {
|
||||
const blockedTask = taskStore.taskList.find(task => task.status === 'blocked')
|
||||
if (!blockedTask) return undefined
|
||||
return `${taskStore.taskList.filter(task => task.status === 'blocked').length} Blocker — ${blockedTask.title}`
|
||||
}
|
||||
|
||||
function blockerCount() {
|
||||
return taskStore.taskList.filter(task => task.status === 'blocked').length
|
||||
}
|
||||
|
||||
/* ── Lifecycle ────────────────────────────────────── */
|
||||
onMounted(() => {
|
||||
agentStore.startPolling()
|
||||
chatStore.startPolling()
|
||||
dashboardStore.startPolling()
|
||||
taskStore.startPolling()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
agentStore.stopPolling()
|
||||
chatStore.stopPolling()
|
||||
dashboardStore.stopPolling()
|
||||
taskStore.stopPolling()
|
||||
})
|
||||
</script>
|
||||
@@ -68,9 +82,10 @@ onUnmounted(() => {
|
||||
:active-count="agentStore.activeCount"
|
||||
:think-count="agentStore.thinkCount"
|
||||
:idle-count="agentStore.idleCount"
|
||||
:blocker-count="agentStore.blockerCount"
|
||||
:blocker-count="blockerCount()"
|
||||
:today-cost="agentStore.todayCost"
|
||||
:today-tokens="agentStore.todayTokens"
|
||||
:blocker-label="blockerLabel()"
|
||||
@blocker-click="handleBlockerClick"
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user