Files
nexus/frontend/src/stores/operations.ts
T
reviewer 6cedd8410f
CI - Build & Test / Backend (.NET) (push) Failing after 23s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 16s
CI - Build & Test / Security Check (push) Successful in 3s
refactor(frontend): deduplicate CSS keyframes, unify types, extract format utils, add UI states, trim mock data
- Remove duplicate @keyframes pulse-* from 3 component files (already in nexus-tokens.css)
- Rename AgentDetail → AgentDetailData in dashboard types to avoid collision with types/agent.ts
- Extract shared formatNumber/initials/formatTime to utils/format.ts
- Simplify FlowBoard: use agentStore modal/selection getters instead of duplicating local state
- Add error banner + empty state to IrisChat; add loading skeleton + error/empty states to TaskStrip
- Remove 105-line unused mockAgents array from useFlowLayout
- Reduce operations store fallbacks from hardcoded preview data to minimal safe defaults
- Update operations store tests to match lean fallback structure
- Net: -73 lines, cleaner imports, fewer magic strings
2026-06-12 17:02:50 +02:00

178 lines
7.5 KiB
TypeScript

import { defineStore } from 'pinia'
import type { AgentInfo, OperationsSnapshot, RoutingTarget } from '../types'
import { apiFetch } from '../services/api'
const fallback: OperationsSnapshot = {
generatedAt: new Date().toISOString(),
runtime: { runtime: 'OpenClaw', status: 'Unknown', detail: 'Awaiting connection…' },
models: [],
metrics: { activeAgents: 0, queuedTasks: 0, successRate: 0, incidents: 0 },
projects: [],
tasks: [],
activity: [],
}
const fallbackRouting: RoutingTarget[] = []
export const useOperationsStore = defineStore('operations', {
state: () => ({
snapshot: fallback,
routing: fallbackRouting,
loading: false,
connected: false,
}),
actions: {
async createProject(name: string) {
const response = await apiFetch('/api/v1/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
})
if (!response.ok) throw new Error('Project could not be created')
const project = await response.json()
this.snapshot.projects.unshift({
id: project.id,
name: project.name,
status: project.status,
progress: project.progress,
})
},
async createTask(title: string, priority: string) {
const response = await apiFetch('/api/v1/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, priority }),
})
if (!response.ok) throw new Error('Task could not be created')
const task = await response.json()
this.snapshot.tasks.unshift(task)
this.snapshot.metrics.queuedTasks += 1
},
async updateTaskState(id: string, state: string) {
const response = await apiFetch(`/api/v1/tasks/${id}/state`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state }),
})
if (!response.ok) throw new Error('Task state could not be updated')
const updatedTask = await response.json()
const index = this.snapshot.tasks.findIndex(task => task.id === id)
if (index !== -1) this.snapshot.tasks[index] = updatedTask
this.snapshot.metrics.queuedTasks = this.snapshot.tasks.filter(task => task.state !== 'Done').length
this.snapshot.metrics.incidents = this.snapshot.tasks.filter(task => task.state === 'Blocked').length
const completed = this.snapshot.tasks.filter(task => task.state === 'Done').length
this.snapshot.metrics.successRate = this.snapshot.tasks.length
? Math.round((completed * 1000) / this.snapshot.tasks.length) / 10
: 100
},
async updateTask(id: string, data: { title?: string; priority?: string; projectId?: string | null }) {
const response = await apiFetch(`/api/v1/tasks/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('Task could not be updated')
const updatedTask = await response.json()
const index = this.snapshot.tasks.findIndex(task => task.id === id)
if (index !== -1) this.snapshot.tasks[index] = updatedTask
},
async updateProject(id: string, data: { name?: string; description?: string; status?: string }) {
const response = await apiFetch(`/api/v1/projects/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('Project could not be updated')
const updatedProject = await response.json()
const index = this.snapshot.projects.findIndex(p => p.id === id)
if (index !== -1) this.snapshot.projects[index] = {
id: updatedProject.id,
name: updatedProject.name,
status: updatedProject.status,
progress: updatedProject.progress,
}
},
async deleteTask(id: string) {
const response = await apiFetch(`/api/v1/tasks/${id}`, {
method: 'DELETE',
})
if (response.status === 403) {
const err = await response.json().catch(() => ({ detail: 'Task cannot be deleted in its current state.' }))
throw new Error(err.detail || 'Task cannot be deleted in its current state.')
}
if (!response.ok) throw new Error('Task could not be deleted')
this.snapshot.tasks = this.snapshot.tasks.filter(t => t.id !== id)
this.snapshot.metrics.queuedTasks = this.snapshot.tasks.filter(t => t.state !== 'Done').length
const completed = this.snapshot.tasks.filter(t => t.state === 'Done').length
this.snapshot.metrics.successRate = this.snapshot.tasks.length
? Math.round((completed * 1000) / this.snapshot.tasks.length) / 10
: 100
},
async deleteProject(id: string) {
const response = await apiFetch(`/api/v1/projects/${id}`, {
method: 'DELETE',
})
if (!response.ok) throw new Error('Project could not be deleted')
this.snapshot.projects = this.snapshot.projects.filter(p => p.id !== id)
},
async refresh() {
this.loading = true
try {
const [snapshotResponse, routingResponse] = await Promise.all([
apiFetch('/api/v1/operations/snapshot'),
apiFetch('/api/v1/routing'),
])
if (!snapshotResponse.ok || !routingResponse.ok) throw new Error('Nexus API unavailable')
this.snapshot = await snapshotResponse.json()
this.routing = await routingResponse.json()
this.connected = true
} catch {
this.connected = false
} finally {
this.loading = false
}
},
async fetchAgents(): Promise<AgentInfo[]> {
try {
const response = await apiFetch('/api/v1/agents')
if (!response.ok) throw new Error('Failed to fetch agents')
return await response.json()
} catch {
return []
}
},
async approveTask(id: string) {
const response = await apiFetch(`/api/v1/tasks/${id}/approve`, {
method: 'POST',
})
if (!response.ok) throw new Error('Task could not be approved')
const index = this.snapshot.tasks.findIndex(task => task.id === id)
if (index !== -1) {
this.snapshot.tasks.splice(index, 1)
}
this.snapshot.metrics.queuedTasks = this.snapshot.tasks.filter(task => task.state !== 'Done').length
this.snapshot.metrics.incidents = this.snapshot.tasks.filter(task => task.state === 'Blocked').length
const completed = this.snapshot.tasks.filter(task => task.state === 'Done').length
this.snapshot.metrics.successRate = this.snapshot.tasks.length
? Math.round((completed * 1000) / this.snapshot.tasks.length) / 10
: 100
},
async rejectTask(id: string) {
const response = await apiFetch(`/api/v1/tasks/${id}/reject`, {
method: 'POST',
})
if (!response.ok) throw new Error('Task could not be rejected')
const index = this.snapshot.tasks.findIndex(task => task.id === id)
if (index !== -1) {
this.snapshot.tasks[index] = { ...this.snapshot.tasks[index], state: 'Backlog' }
}
this.snapshot.metrics.queuedTasks = this.snapshot.tasks.filter(task => task.state !== 'Done').length
this.snapshot.metrics.incidents = this.snapshot.tasks.filter(task => task.state === 'Blocked').length
const completed = this.snapshot.tasks.filter(task => task.state === 'Done').length
this.snapshot.metrics.successRate = this.snapshot.tasks.length
? Math.round((completed * 1000) / this.snapshot.tasks.length) / 10
: 100
},
},
})