Files
nexus/frontend/src/stores/tasks.ts
T
devops 95495a8332
CI - Build & Test / Backend (.NET) (push) Failing after 31s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 20s
CI - Build & Test / Security Check (push) Successful in 3s
feat: complete task board workflow gates
2026-06-24 01:23:49 +02:00

390 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Task Store V2 Dashboard + Task Board
*
* Fetches tasks from /api/dashboard/tasks and /api/dashboard/tasks/board
* and maps them into TaskItem[] format for the TaskStrip component.
*
* Board state: grouped by column (offen, inProgress, review, blocked, done)
* Auto-refresh: every 30 seconds.
*/
import { defineStore } from 'pinia'
import { apiFetch } from '../services/api'
import type { TaskItem } from '../components/dashboard/v2/types'
/* ── API Response Shapes ──────────────────────────── */
export interface DashboardTaskDto {
id: string
title: string
detail: string | null
source: string
state: string
priority: string
assignedTo: string | null
parentTaskId?: string | null
dueDate?: string | null
createdAt: string
updatedAt: string
isAgentTask?: boolean
expectedFrom?: string | null
lastActivityMessage?: string | null
lastActivityAt?: string | null
childTasks?: DashboardTaskDto[] | null
childTaskCount?: number
openChildTaskCount?: number
hasVisibleDelegation?: boolean
}
export interface BoardGroup {
offen: DashboardTaskDto[]
inProgress: DashboardTaskDto[]
review: DashboardTaskDto[]
blocked: DashboardTaskDto[]
done: DashboardTaskDto[]
}
export interface AgentWorkflowOverview {
waitingForBao: DashboardTaskDto[]
waitingForIris: DashboardTaskDto[]
waitingForOthers: DashboardTaskDto[]
staleTasks: DashboardTaskDto[]
staleThreshold: string
}
/* ── State Mapping ────────────────────────────────── */
function mapPriority(priority: string): TaskItem['priority'] {
const p = priority.toLowerCase()
if (p === 'high' || p === 'critical' || p === 'urgent') return 'high'
if (p === 'low' || p === 'minor') return 'low'
return 'medium'
}
function mapState(state: string): TaskItem['status'] {
const s = state.toLowerCase()
if (s === 'in progress' || s === 'active' || s === 'working') return 'active'
if (s === 'blocked' || s === 'block') return 'blocked'
return 'pending'
}
function mapProgress(state: string): number {
const s = state.toLowerCase()
if (s === 'in progress' || s === 'active' || s === 'working') return 50
if (s === 'done') return 100
if (s === 'blocked') return 30
return 0
}
function mapTask(t: DashboardTaskDto): TaskItem {
return {
id: t.id,
title: t.title,
agent: t.assignedTo ?? '—',
priority: mapPriority(t.priority),
status: mapState(t.state),
progress: mapProgress(t.state),
detail: t.detail,
source: t.source,
}
}
export const useTaskStore = defineStore('tasks', {
state: () => ({
tasks: [] as TaskItem[],
loading: false,
error: null as string | null,
refreshInterval: null as ReturnType<typeof setInterval> | null,
boardRefreshInterval: null as ReturnType<typeof setInterval> | null,
// Board state
board: {
offen: [] as DashboardTaskDto[],
inProgress: [] as DashboardTaskDto[],
review: [] as DashboardTaskDto[],
blocked: [] as DashboardTaskDto[],
done: [] as DashboardTaskDto[],
} as BoardGroup,
boardLoading: false,
boardError: null as string | null,
// Agent Workflow Overview (for Iris)
agentOverview: null as AgentWorkflowOverview | null,
agentOverviewLoading: false,
agentOverviewError: null as string | null,
}),
getters: {
taskList: (state) => state.tasks,
// Iris helpers
waitingForIrisTasks: (state) => state.agentOverview?.waitingForIris ?? [],
waitingForBaoTasks: (state) => state.agentOverview?.waitingForBao ?? [],
waitingForOthersTasks: (state) => state.agentOverview?.waitingForOthers ?? [],
staleTasksList: (state) => state.agentOverview?.staleTasks ?? [],
agentTaskCount: (state) => {
if (!state.agentOverview) return 0
return state.agentOverview.waitingForBao.length +
state.agentOverview.waitingForIris.length +
state.agentOverview.waitingForOthers.length +
state.agentOverview.staleTasks.length
},
},
actions: {
/* ── API: Fetch tasks (for TaskStrip) ─────────── */
async fetchTasks() {
this.loading = true
try {
const res = await apiFetch('/api/dashboard/tasks')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: DashboardTaskDto[] = await res.json()
this.tasks = data.map(mapTask)
this.error = null
} catch (err) {
console.warn('[TaskStore] fetchTasks failed', err)
this.error = 'Tasks could not be loaded'
} finally {
this.loading = false
}
},
/* ── API: Fetch board (for TaskBoardView) ─────── */
async fetchBoard() {
this.boardLoading = true
try {
const res = await apiFetch('/api/dashboard/tasks/board')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: BoardGroup = await res.json()
this.board = data
this.boardError = null
} catch (err) {
console.warn('[TaskStore] fetchBoard failed', err)
this.boardError = 'Board could not be loaded'
} finally {
this.boardLoading = false
}
},
/* ── API: Move task (Drag & Drop) ─────────────── */
async moveTask(id: string, newState: string) {
// Map board group key to canonical state string for the API payload
const canonicalMap: Record<string, string> = {
offen: 'Backlog',
inProgress: 'In progress',
review: 'Review',
blocked: 'Blocked',
done: 'Done',
}
// Save previous state for rollback
const prevBoard = JSON.parse(JSON.stringify(this.board)) as BoardGroup
// Optimistic: find the task in current board and move it
const findAndRemove = (arr: DashboardTaskDto[]): DashboardTaskDto | null => {
const idx = arr.findIndex(t => t.id === id)
if (idx === -1) return null
return arr.splice(idx, 1)[0]
}
const task =
findAndRemove(this.board.offen) ??
findAndRemove(this.board.inProgress) ??
findAndRemove(this.board.review) ??
findAndRemove(this.board.blocked) ??
findAndRemove(this.board.done)
if (task) {
const canonicalState = canonicalMap[newState] ?? newState
task.state = canonicalState
const targetKey = newState as keyof BoardGroup
if (this.board[targetKey]) {
this.board[targetKey].push(task)
}
}
// Actually call API with the board group key (backend handles mapping)
try {
const res = await apiFetch(`/api/dashboard/tasks/${id}/move`, {
method: 'PATCH',
body: JSON.stringify({ state: newState }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
} catch (err) {
console.warn('[TaskStore] moveTask failed, rolling back', err)
this.board = prevBoard
}
},
/* ── API: Create task ─────────────────────────── */
async createTask(data: { title: string; detail?: string | null; priority?: string; assignedTo?: string }) {
try {
const res = await apiFetch('/api/dashboard/tasks', {
method: 'POST',
body: JSON.stringify({
title: data.title,
detail: data.detail ?? null,
priority: data.priority ?? 'Medium',
assignedTo: data.assignedTo ?? 'bao',
source: 'bao',
}),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
// Refresh board + task list
await this.fetchBoard()
await this.fetchTasks()
} catch (err) {
console.warn('[TaskStore] createTask failed', err)
throw err
}
},
/* ── API: Add task (for TaskStrip) ────────────── */
async addTask(title: string, detail?: string, priority?: string, assignedTo?: string) {
try {
const res = await apiFetch('/api/dashboard/tasks', {
method: 'POST',
body: JSON.stringify({
title,
detail: detail ?? null,
priority: priority ?? null,
assignedTo: assignedTo ?? null,
source: 'bao',
}),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
// Refresh task list
await this.fetchTasks()
} catch (err) {
console.warn('[TaskStore] addTask failed', err)
}
},
/* ── API: Update task ─────────────────────────── */
async updateTask(id: string, updates: { title?: string; detail?: string | null; priority?: string; assignedTo?: string | null; dueDate?: string | null }) {
try {
const res = await apiFetch(`/api/dashboard/tasks/${id}`, {
method: 'PUT',
body: JSON.stringify(updates),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
await this.fetchTasks()
await this.fetchBoard()
} catch (err) {
console.warn('[TaskStore] updateTask failed', err)
throw err
}
},
async fetchTaskChildren(id: string) {
try {
const res = await apiFetch(`/api/dashboard/tasks/${id}/children`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json() as DashboardTaskDto[]
} catch (err) {
console.warn('[TaskStore] fetchTaskChildren failed', err)
throw err
}
},
async fetchTaskActivity(id: string) {
try {
const res = await apiFetch(`/api/dashboard/tasks/${id}/activity`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return await res.json() as Array<{ id?: string; message?: string; type?: string; createdAt?: string; timestamp?: string }>
} catch (err) {
console.warn('[TaskStore] fetchTaskActivity failed', err)
throw err
}
},
/* ── API: Fetch agent workflow overview ──────── */
async fetchAgentOverview(staleHours = 2) {
this.agentOverviewLoading = true
try {
const res = await apiFetch(`/api/dashboard/tasks/agent-overview?staleHours=${staleHours}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: AgentWorkflowOverview = await res.json()
this.agentOverview = data
this.agentOverviewError = null
} catch (err) {
console.warn('[TaskStore] fetchAgentOverview failed', err)
this.agentOverviewError = 'Agent overview could not be loaded'
} finally {
this.agentOverviewLoading = false
}
},
/* ── API: Create agent task ───────────────────── */
async createAgentTask(data: {
title: string
detail?: string | null
source?: string
priority?: string
assignedTo?: string
expectedFrom?: string
parentTaskId?: string | null
startsInProgress?: boolean
initialState?: string | null
}) {
try {
const res = await apiFetch('/api/dashboard/tasks/agent', {
method: 'POST',
body: JSON.stringify({
title: data.title,
detail: data.detail ?? null,
source: data.source ?? 'iris',
priority: data.priority ?? 'Medium',
assignedTo: data.assignedTo ?? null,
expectedFrom: data.expectedFrom ?? null,
parentTaskId: data.parentTaskId ?? null,
startsInProgress: data.startsInProgress ?? true,
initialState: data.initialState ?? null,
}),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
await this.fetchBoard()
await this.fetchAgentOverview()
return await res.json() as DashboardTaskDto
} catch (err) {
console.warn('[TaskStore] createAgentTask failed', err)
throw err
}
},
/* ── Polling ──────────────────────────────────── */
startPolling() {
if (this.refreshInterval) return
this.fetchTasks()
this.refreshInterval = setInterval(() => {
this.fetchTasks()
}, 30000)
},
stopPolling() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval)
this.refreshInterval = null
}
},
startBoardPolling(force = false) {
if (this.boardRefreshInterval && !force) return
if (this.boardRefreshInterval && force) {
clearInterval(this.boardRefreshInterval)
this.boardRefreshInterval = null
}
this.fetchBoard()
this.boardRefreshInterval = setInterval(() => {
this.fetchBoard()
}, 30000)
},
stopBoardPolling() {
if (this.boardRefreshInterval) {
clearInterval(this.boardRefreshInterval)
this.boardRefreshInterval = null
}
},
},
})