feat: complete Nexus mission-control workflows
This commit is contained in:
@@ -4,7 +4,8 @@ import { Bot, CheckCircle2, Clock3, MessageSquareText, Send, ShieldAlert, Zap, C
|
||||
import type { AgentInfo, OperationsSnapshot, RoutingTarget } from '../types'
|
||||
import { TASK_STATES } from '../types'
|
||||
import { apiFetch } from '../services/api'
|
||||
import { useOperationsStore } from '../stores/operations'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useOperationsStore, type PendingApprovalTask } from '../stores/operations'
|
||||
|
||||
const props = defineProps<{ view: string; snapshot: OperationsSnapshot; routing: RoutingTarget[] }>()
|
||||
const emit = defineEmits<{
|
||||
@@ -13,8 +14,13 @@ const emit = defineEmits<{
|
||||
updateTaskState: [id: string, state: string]
|
||||
}>()
|
||||
const store = useOperationsStore()
|
||||
const auth = useAuthStore()
|
||||
const agents = ref<AgentInfo[]>([])
|
||||
const agentsLoading = ref(false)
|
||||
const pendingApprovals = ref<PendingApprovalTask[]>([])
|
||||
const pendingApprovalsLoading = ref(false)
|
||||
const pendingApprovalsError = ref('')
|
||||
const canModerateApprovals = computed(() => auth.user?.role === 'owner')
|
||||
|
||||
async function loadAgents() {
|
||||
if (agentsLoading.value) return
|
||||
@@ -23,12 +29,43 @@ async function loadAgents() {
|
||||
agentsLoading.value = false
|
||||
}
|
||||
|
||||
async function loadPendingApprovals() {
|
||||
if (!canModerateApprovals.value) {
|
||||
pendingApprovals.value = []
|
||||
pendingApprovalsError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
pendingApprovalsLoading.value = true
|
||||
pendingApprovalsError.value = ''
|
||||
try {
|
||||
pendingApprovals.value = await store.fetchPendingApprovals()
|
||||
} catch (e) {
|
||||
pendingApprovalsError.value = e instanceof Error ? e.message : 'Failed to load pending approvals'
|
||||
} finally {
|
||||
pendingApprovalsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.view === 'Agents') loadAgents()
|
||||
if (props.view === 'Task Board') void loadPendingApprovals()
|
||||
})
|
||||
|
||||
watch(() => props.view, (v) => {
|
||||
if (v === 'Agents') loadAgents()
|
||||
if (v === 'Task Board') void loadPendingApprovals()
|
||||
})
|
||||
|
||||
watch(canModerateApprovals, (value) => {
|
||||
if (props.view !== 'Task Board') return
|
||||
if (value) {
|
||||
void loadPendingApprovals()
|
||||
return
|
||||
}
|
||||
|
||||
pendingApprovals.value = []
|
||||
pendingApprovalsError.value = ''
|
||||
})
|
||||
|
||||
const newProject = ref('')
|
||||
@@ -51,6 +88,7 @@ async function handleApproveTask(id: string) {
|
||||
taskActionError.value = ''
|
||||
try {
|
||||
await store.approveTask(id)
|
||||
pendingApprovals.value = pendingApprovals.value.filter(task => task.id !== id)
|
||||
} catch (e) {
|
||||
taskActionError.value = e instanceof Error ? e.message : 'Failed to approve task'
|
||||
} finally {
|
||||
@@ -63,6 +101,7 @@ async function handleRejectTask(id: string) {
|
||||
taskActionError.value = ''
|
||||
try {
|
||||
await store.rejectTask(id)
|
||||
pendingApprovals.value = pendingApprovals.value.filter(task => task.id !== id)
|
||||
} catch (e) {
|
||||
taskActionError.value = e instanceof Error ? e.message : 'Failed to reject task'
|
||||
} finally {
|
||||
@@ -187,6 +226,31 @@ async function sendMessage() {
|
||||
</div>
|
||||
|
||||
<form v-else-if="view === 'Task Board'" class="quick-create" @submit.prevent="newTask.trim() && (emit('createTask', newTask.trim(), 'Normal'), newTask = '')"><input v-model="newTask" placeholder="New task title" /><button>Create task</button></form>
|
||||
<section v-if="view === 'Task Board' && canModerateApprovals" class="approval-strip">
|
||||
<header class="approval-strip-head">
|
||||
<div>
|
||||
<span class="kicker">Owner approvals</span>
|
||||
<h3>Pending approvals</h3>
|
||||
</div>
|
||||
<span class="badge">{{ pendingApprovals.length }}</span>
|
||||
</header>
|
||||
<p v-if="pendingApprovalsLoading" class="approval-strip-note">Loading owner approval queue…</p>
|
||||
<p v-else-if="pendingApprovalsError" class="approval-strip-note error">{{ pendingApprovalsError }}</p>
|
||||
<p v-else-if="!pendingApprovals.length" class="approval-strip-note">No tasks are waiting for Bao approval.</p>
|
||||
<div v-else class="approval-list">
|
||||
<article v-for="task in pendingApprovals" :key="task.id" class="approval-card">
|
||||
<div>
|
||||
<strong>{{ task.title }}</strong>
|
||||
<p>{{ task.priority }} · {{ new Date(task.updatedAt).toLocaleString() }}</p>
|
||||
</div>
|
||||
<div class="approval-actions">
|
||||
<button class="task-approve-btn" :disabled="approvingTaskId === task.id" @click="handleApproveTask(task.id)"><CheckCircle2 :size="13" /></button>
|
||||
<button class="task-reject-btn" :disabled="approvingTaskId === task.id" @click="handleRejectTask(task.id)"><X :size="13" /></button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<p v-if="taskActionError" class="approval-strip-note error">{{ taskActionError }}</p>
|
||||
</section>
|
||||
<div v-if="view === 'Task Board'" class="kanban">
|
||||
<section v-for="column in columns" :key="column.name" class="kanban-column">
|
||||
<header><span>{{ column.name }}</span><b>{{ column.items.length }}</b></header>
|
||||
@@ -214,7 +278,7 @@ async function sendMessage() {
|
||||
<div class="task-card-head">
|
||||
<span :class="['priority', task.priority.toLowerCase()]">{{ task.priority }}</span>
|
||||
<div class="task-card-actions">
|
||||
<template v-if="task.state === 'In progress'">
|
||||
<template v-if="task.state === 'In progress' && canModerateApprovals">
|
||||
<button
|
||||
class="task-approve-btn"
|
||||
title="Approve"
|
||||
@@ -338,6 +402,53 @@ async function sendMessage() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-strip {
|
||||
margin: 0 0 18px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--line, #1e2030);
|
||||
border-radius: 14px;
|
||||
background: rgba(255,255,255,.025);
|
||||
}
|
||||
.approval-strip-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.approval-strip-head h3 {
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
.approval-strip-note {
|
||||
margin: 10px 0 0;
|
||||
color: #8e96a8;
|
||||
}
|
||||
.approval-strip-note.error {
|
||||
color: #e16e75;
|
||||
}
|
||||
.approval-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.approval-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255,255,255,.06);
|
||||
border-radius: 12px;
|
||||
background: rgba(8, 10, 18, .35);
|
||||
}
|
||||
.approval-card p {
|
||||
margin: 4px 0 0;
|
||||
color: #8e96a8;
|
||||
font-size: 12px;
|
||||
}
|
||||
.approval-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.task-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -10,6 +10,9 @@ defineProps<{
|
||||
saving: boolean
|
||||
saveStatus: 'idle' | 'saved' | 'error'
|
||||
saveMessage: string
|
||||
backupStatus: string
|
||||
reloadStatus: string
|
||||
reloadMessage: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
@@ -60,6 +63,12 @@ function onInput(event: Event) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="reloadMessage" class="editor-health">
|
||||
<span class="health-pill" :class="backupStatus">Backup {{ backupStatus }}</span>
|
||||
<span class="health-pill" :class="reloadStatus">Reload {{ reloadStatus }}</span>
|
||||
<span class="health-note">{{ reloadMessage }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Text editor -->
|
||||
<textarea
|
||||
class="config-editor"
|
||||
@@ -89,6 +98,17 @@ function onInput(event: Event) {
|
||||
border-bottom: 1px solid var(--line, #1e2030);
|
||||
gap: 12px;
|
||||
}
|
||||
.editor-health {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--line, #1e2030);
|
||||
background: rgba(255,255,255,.015);
|
||||
color: #8e96a8;
|
||||
font-size: 10.5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.editor-file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -165,6 +185,30 @@ function onInput(event: Event) {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.health-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--line, #1e2030);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.health-pill.created {
|
||||
color: #51d49a;
|
||||
border-color: rgba(81,212,154,.3);
|
||||
}
|
||||
.health-pill.not_applicable {
|
||||
color: #d4b26a;
|
||||
border-color: rgba(212,178,106,.25);
|
||||
}
|
||||
.health-pill.not_supported {
|
||||
color: #9aa4bb;
|
||||
border-color: rgba(154,164,187,.25);
|
||||
}
|
||||
.health-note {
|
||||
color: #7e8799;
|
||||
}
|
||||
|
||||
.config-editor {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user