diff --git a/frontend/src/components/dashboard/v2/AgentDetailModal.vue b/frontend/src/components/dashboard/v2/AgentDetailModal.vue index 537fa59..f050143 100644 --- a/frontend/src/components/dashboard/v2/AgentDetailModal.vue +++ b/frontend/src/components/dashboard/v2/AgentDetailModal.vue @@ -160,6 +160,16 @@ const statusColors: Record = {
{{ thinkDisplay }}
+
+

Agent Activity

+
+
+ {{ entry.time }} + {{ entry.text }} +
+
+
+

Modell wählen

@@ -519,6 +529,35 @@ const statusColors: Record = { @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; diff --git a/frontend/src/components/dashboard/v2/AlertBar.vue b/frontend/src/components/dashboard/v2/AlertBar.vue index 966e79d..d1f2146 100644 --- a/frontend/src/components/dashboard/v2/AlertBar.vue +++ b/frontend/src/components/dashboard/v2/AlertBar.vue @@ -19,6 +19,7 @@ defineProps<{ blockerCount: number todayCost: string todayTokens: string + blockerLabel?: string }>() defineEmits<{ @@ -62,7 +63,7 @@ defineEmits<{ @click="$emit('blockerClick')" > - {{ blockerCount }} Blocker + {{ blockerLabel || `${blockerCount} Blocker` }}
diff --git a/frontend/src/components/dashboard/v2/types.ts b/frontend/src/components/dashboard/v2/types.ts index 6c844e0..695fabf 100644 --- a/frontend/src/components/dashboard/v2/types.ts +++ b/frontend/src/components/dashboard/v2/types.ts @@ -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 }[] } diff --git a/frontend/src/components/layout/Topbar.vue b/frontend/src/components/layout/Topbar.vue index cf07afe..fba0091 100644 --- a/frontend/src/components/layout/Topbar.vue +++ b/frontend/src/components/layout/Topbar.vue @@ -3,6 +3,7 @@ import { icons } from '../../composables/icons' defineProps<{ connected?: boolean + statusLabel?: string }>() defineEmits<{ @@ -27,7 +28,7 @@ defineEmits<{ - {{ connected ? 'OpenClaw verbunden' : 'Preview' }} + {{ connected ? (statusLabel || 'OpenClaw verbunden') : 'Preview' }} diff --git a/frontend/src/composables/useFlowBoardState.ts b/frontend/src/composables/useFlowBoardState.ts index bb69a68..accc966 100644 --- a/frontend/src/composables/useFlowBoardState.ts +++ b/frontend/src/composables/useFlowBoardState.ts @@ -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 : {} + } catch { + return {} + } +} + export function useFlowBoardState(agentStore: FlowBoardAgentStore, chatStore: FlowBoardChatStore) { - const agentPositions = ref>({}) + const agentPositions = ref>(readStoredPositions()) const enteringIds = ref([]) const localAgentPool = ref([...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) { agentPositions.value = { ...positions } + if (typeof window !== 'undefined') { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(agentPositions.value)) + } } function sendChatMessage(text: string) { diff --git a/frontend/src/layouts/NexusLayout.vue b/frontend/src/layouts/NexusLayout.vue index 0efb994..ccab2d8 100644 --- a/frontend/src/layouts/NexusLayout.vue +++ b/frontend/src/layouts/NexusLayout.vue @@ -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() {
diff --git a/frontend/src/stores/agents.ts b/frontend/src/stores/agents.ts index 8cf7c7f..d228dbf 100644 --- a/frontend/src/stores/agents.ts +++ b/frontend/src/stores/agents.ts @@ -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, refreshInterval: null as ReturnType | 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 ─────────────────────────────────── */ diff --git a/frontend/src/stores/dashboard.ts b/frontend/src/stores/dashboard.ts new file mode 100644 index 0000000..1fd5f26 --- /dev/null +++ b/frontend/src/stores/dashboard.ts @@ -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 | 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 + } + }, + }, +}) diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index 55d1eb0..d93d5af 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -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, } } diff --git a/frontend/src/views/Dashboard/FlowBoard.vue b/frontend/src/views/Dashboard/FlowBoard.vue index c5af6fc..daf4958 100644 --- a/frontend/src/views/Dashboard/FlowBoard.vue +++ b/frontend/src/views/Dashboard/FlowBoard.vue @@ -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() }) @@ -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" />