arkaos 2.83.0 → 2.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.84.0
|
|
Binary file
|
|
@@ -1,85 +1,227 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
// PR67 v2.84.0 — Tasks page rewritten against /api/jobs.
|
|
3
|
+
//
|
|
4
|
+
// The legacy /api/tasks endpoint returns the deprecated TaskManager
|
|
5
|
+
// data, which has no live updates and no cancel. The SQLite job queue
|
|
6
|
+
// (/api/jobs) is the real workhorse — knowledge ingest fans out into
|
|
7
|
+
// jobs, WebSocket broadcasts every progress event, DELETE /api/jobs/{id}
|
|
8
|
+
// cancels queued work. This page now consumes the real thing.
|
|
9
|
+
//
|
|
10
|
+
// What changed vs the previous tasks.vue:
|
|
11
|
+
// - Polls /api/jobs (jobs are the new tasks)
|
|
12
|
+
// - Subscribes to /ws/tasks for live progress + status flips
|
|
13
|
+
// - Cancel button on queued/processing rows
|
|
14
|
+
// - Empty state suggests a real command (Knowledge → ingest) instead
|
|
15
|
+
// of the dead-link "npx arkaos index" hint
|
|
16
|
+
// - DashboardState (PR64) wraps loading/error/empty consistently
|
|
17
|
+
|
|
2
18
|
import type { TableColumn } from '@nuxt/ui'
|
|
3
|
-
import type {
|
|
19
|
+
import type { Job, JobSummary } from '~/types'
|
|
20
|
+
|
|
21
|
+
const { fetchApi, apiBase } = useApi()
|
|
22
|
+
const toast = useToast()
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
data,
|
|
26
|
+
status,
|
|
27
|
+
error,
|
|
28
|
+
refresh,
|
|
29
|
+
} = await fetchApi<{ jobs: Job[], summary: JobSummary }>('/api/jobs?limit=200')
|
|
4
30
|
|
|
5
|
-
const
|
|
31
|
+
const jobs = ref<Job[]>(data.value?.jobs ?? [])
|
|
32
|
+
const summary = ref<JobSummary>(data.value?.summary ?? {
|
|
33
|
+
total: 0, queued: 0, processing: 0, completed: 0, failed: 0,
|
|
34
|
+
})
|
|
6
35
|
|
|
7
|
-
|
|
36
|
+
watch(data, (d) => {
|
|
37
|
+
if (!d) return
|
|
38
|
+
jobs.value = d.jobs ?? []
|
|
39
|
+
summary.value = d.summary ?? {
|
|
40
|
+
total: 0, queued: 0, processing: 0, completed: 0, failed: 0,
|
|
41
|
+
}
|
|
42
|
+
})
|
|
8
43
|
|
|
9
|
-
|
|
10
|
-
const summary = computed(() => data.value?.summary ?? { total: 0, active: 0, queued: 0, completed: 0 })
|
|
44
|
+
// ─── Filter tabs ─────────────────────────────────────────────────────────
|
|
11
45
|
|
|
12
|
-
const activeTab = ref('all')
|
|
46
|
+
const activeTab = ref<'all' | 'active' | 'queued' | 'completed' | 'failed'>('all')
|
|
13
47
|
|
|
14
48
|
const tabItems = [
|
|
15
|
-
{ label: 'All', value: 'all' },
|
|
16
|
-
{ label: 'Active', value: 'active' },
|
|
17
|
-
{ label: 'Queued', value: 'queued' },
|
|
18
|
-
{ label: 'Completed', value: 'completed' },
|
|
19
|
-
{ label: 'Failed', value: 'failed' }
|
|
49
|
+
{ label: 'All', value: 'all' as const },
|
|
50
|
+
{ label: 'Active', value: 'active' as const },
|
|
51
|
+
{ label: 'Queued', value: 'queued' as const },
|
|
52
|
+
{ label: 'Completed', value: 'completed' as const },
|
|
53
|
+
{ label: 'Failed', value: 'failed' as const },
|
|
20
54
|
]
|
|
21
55
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
const ACTIVE_STATUSES: Job['status'][] = [
|
|
57
|
+
'processing', 'downloading', 'transcribing', 'embedding',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const filteredJobs = computed<Job[]>(() => {
|
|
61
|
+
if (activeTab.value === 'all') return jobs.value
|
|
62
|
+
if (activeTab.value === 'active') {
|
|
63
|
+
return jobs.value.filter((j) => ACTIVE_STATUSES.includes(j.status))
|
|
64
|
+
}
|
|
65
|
+
return jobs.value.filter((j) => j.status === activeTab.value)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const CANCELLABLE: Job['status'][] = ['queued', 'processing']
|
|
69
|
+
function isCancellable(job: Job): boolean {
|
|
70
|
+
return CANCELLABLE.includes(job.status)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── WebSocket — live updates ────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
let ws: WebSocket | null = null
|
|
76
|
+
const wsConnected = ref(false)
|
|
77
|
+
|
|
78
|
+
function connectWebSocket() {
|
|
79
|
+
if (ws && ws.readyState === WebSocket.OPEN) return
|
|
80
|
+
const wsUrl = apiBase.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws/tasks'
|
|
81
|
+
ws = new WebSocket(wsUrl)
|
|
82
|
+
ws.onopen = () => { wsConnected.value = true }
|
|
83
|
+
ws.onclose = () => { wsConnected.value = false }
|
|
84
|
+
ws.onerror = () => { wsConnected.value = false }
|
|
85
|
+
ws.onmessage = (event) => {
|
|
86
|
+
try {
|
|
87
|
+
const msg = JSON.parse(event.data) as {
|
|
88
|
+
type: string
|
|
89
|
+
job_id?: string
|
|
90
|
+
progress?: number
|
|
91
|
+
message?: string
|
|
92
|
+
status?: Job['status']
|
|
93
|
+
error?: string
|
|
94
|
+
chunks_created?: number
|
|
95
|
+
}
|
|
96
|
+
if (!msg.job_id) return
|
|
97
|
+
const idx = jobs.value.findIndex((j) => j.id === msg.job_id)
|
|
98
|
+
if (idx === -1) {
|
|
99
|
+
// We don't have the row yet (new job spawned mid-session).
|
|
100
|
+
// Refetch the list so it shows up.
|
|
101
|
+
refresh()
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const current = jobs.value[idx]
|
|
105
|
+
if (!current) return
|
|
106
|
+
const next: Job = { ...current }
|
|
107
|
+
if (msg.type === 'job_progress') {
|
|
108
|
+
if (typeof msg.progress === 'number') next.progress = msg.progress
|
|
109
|
+
if (typeof msg.message === 'string') next.message = msg.message
|
|
110
|
+
if (msg.status) next.status = msg.status
|
|
111
|
+
} else if (msg.type === 'job_complete') {
|
|
112
|
+
next.status = 'completed'
|
|
113
|
+
next.progress = 100
|
|
114
|
+
if (typeof msg.chunks_created === 'number') next.chunks_created = msg.chunks_created
|
|
115
|
+
} else if (msg.type === 'job_failed') {
|
|
116
|
+
next.status = 'failed'
|
|
117
|
+
if (typeof msg.error === 'string') next.error = msg.error
|
|
118
|
+
} else if (msg.type === 'job_cancelled') {
|
|
119
|
+
next.status = 'cancelled'
|
|
120
|
+
}
|
|
121
|
+
jobs.value.splice(idx, 1, next)
|
|
122
|
+
} catch {
|
|
123
|
+
/* ignore malformed messages */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function disconnectWebSocket() {
|
|
129
|
+
if (!ws) return
|
|
130
|
+
try { ws.close() } catch { /* already closed */ }
|
|
131
|
+
ws = null
|
|
132
|
+
wsConnected.value = false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onMounted(() => {
|
|
136
|
+
connectWebSocket()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
onBeforeUnmount(() => {
|
|
140
|
+
disconnectWebSocket()
|
|
28
141
|
})
|
|
29
142
|
|
|
30
|
-
|
|
143
|
+
// ─── Cancel ──────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const cancelling = ref<Set<string>>(new Set())
|
|
31
146
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
147
|
+
async function cancelJob(job: Job) {
|
|
148
|
+
if (!isCancellable(job)) return
|
|
149
|
+
cancelling.value.add(job.id)
|
|
150
|
+
try {
|
|
151
|
+
const res = await $fetch<{ cancelled?: boolean, error?: string }>(
|
|
152
|
+
`${apiBase}/api/jobs/${job.id}`,
|
|
153
|
+
{ method: 'DELETE' },
|
|
154
|
+
)
|
|
155
|
+
if (res.cancelled) {
|
|
156
|
+
// WS will broadcast 'job_cancelled' — handler will flip the row.
|
|
157
|
+
// Refresh as a safety net in case the WS message races.
|
|
158
|
+
await refresh()
|
|
159
|
+
toast.add({
|
|
160
|
+
title: 'Job cancelled',
|
|
161
|
+
description: job.source || job.id,
|
|
162
|
+
color: 'success',
|
|
163
|
+
})
|
|
164
|
+
} else {
|
|
165
|
+
toast.add({
|
|
166
|
+
title: 'Cancel rejected',
|
|
167
|
+
description: res.error || 'Only queued jobs can be cancelled.',
|
|
168
|
+
color: 'warning',
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
} catch (err) {
|
|
172
|
+
toast.add({
|
|
173
|
+
title: 'Cancel failed',
|
|
174
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
175
|
+
color: 'error',
|
|
176
|
+
})
|
|
177
|
+
} finally {
|
|
178
|
+
cancelling.value.delete(job.id)
|
|
40
179
|
}
|
|
41
|
-
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Render helpers ──────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
type StatusColor = 'success' | 'error' | 'primary' | 'warning' | 'neutral'
|
|
185
|
+
|
|
186
|
+
const STATUS_COLOR: Record<Job['status'], StatusColor> = {
|
|
187
|
+
queued: 'neutral',
|
|
188
|
+
processing: 'primary',
|
|
189
|
+
downloading: 'primary',
|
|
190
|
+
transcribing: 'primary',
|
|
191
|
+
embedding: 'primary',
|
|
192
|
+
completed: 'success',
|
|
193
|
+
failed: 'error',
|
|
194
|
+
cancelled: 'warning',
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function statusColor(s: Job['status']): StatusColor {
|
|
198
|
+
return STATUS_COLOR[s] ?? 'neutral'
|
|
42
199
|
}
|
|
43
200
|
|
|
44
201
|
function formatDate(dateStr: string) {
|
|
45
202
|
if (!dateStr) return '-'
|
|
46
203
|
try {
|
|
47
204
|
return new Intl.DateTimeFormat('en-US', {
|
|
48
|
-
month: 'short',
|
|
49
|
-
|
|
50
|
-
hour: '2-digit',
|
|
51
|
-
minute: '2-digit'
|
|
205
|
+
month: 'short', day: 'numeric',
|
|
206
|
+
hour: '2-digit', minute: '2-digit',
|
|
52
207
|
}).format(new Date(dateStr))
|
|
53
208
|
} catch {
|
|
54
209
|
return dateStr
|
|
55
210
|
}
|
|
56
211
|
}
|
|
57
212
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
header: '
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
accessorKey: 'status',
|
|
73
|
-
header: 'Status'
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
accessorKey: 'progress',
|
|
77
|
-
header: 'Progress'
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
accessorKey: 'created_at',
|
|
81
|
-
header: 'Created'
|
|
82
|
-
}
|
|
213
|
+
function truncate(value: string, max = 60): string {
|
|
214
|
+
if (!value) return ''
|
|
215
|
+
return value.length > max ? value.slice(0, max - 1) + '…' : value
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const columns: TableColumn<Job>[] = [
|
|
219
|
+
{ accessorKey: 'type', header: 'Type' },
|
|
220
|
+
{ accessorKey: 'source', header: 'Source' },
|
|
221
|
+
{ accessorKey: 'status', header: 'Status' },
|
|
222
|
+
{ accessorKey: 'progress', header: 'Progress' },
|
|
223
|
+
{ accessorKey: 'created_at', header: 'Created' },
|
|
224
|
+
{ accessorKey: 'actions', header: '' },
|
|
83
225
|
]
|
|
84
226
|
</script>
|
|
85
227
|
|
|
@@ -90,6 +232,24 @@ const columns: TableColumn<Task>[] = [
|
|
|
90
232
|
<template #leading>
|
|
91
233
|
<UDashboardSidebarCollapse />
|
|
92
234
|
</template>
|
|
235
|
+
<template #trailing>
|
|
236
|
+
<UBadge
|
|
237
|
+
:label="wsConnected ? 'Live' : 'Offline'"
|
|
238
|
+
:color="wsConnected ? 'success' : 'neutral'"
|
|
239
|
+
variant="subtle"
|
|
240
|
+
size="xs"
|
|
241
|
+
:icon="wsConnected ? 'i-lucide-radio-tower' : 'i-lucide-radio'"
|
|
242
|
+
/>
|
|
243
|
+
</template>
|
|
244
|
+
<template #right>
|
|
245
|
+
<UButton
|
|
246
|
+
label="Refresh"
|
|
247
|
+
variant="ghost"
|
|
248
|
+
icon="i-lucide-refresh-cw"
|
|
249
|
+
size="sm"
|
|
250
|
+
@click="refresh()"
|
|
251
|
+
/>
|
|
252
|
+
</template>
|
|
93
253
|
</UDashboardNavbar>
|
|
94
254
|
</template>
|
|
95
255
|
|
|
@@ -97,17 +257,17 @@ const columns: TableColumn<Task>[] = [
|
|
|
97
257
|
<DashboardState
|
|
98
258
|
:status="status"
|
|
99
259
|
:error="error"
|
|
100
|
-
loading-label="Loading
|
|
260
|
+
loading-label="Loading jobs"
|
|
101
261
|
:on-retry="() => refresh()"
|
|
102
262
|
>
|
|
103
|
-
<!-- Summary
|
|
104
|
-
<div class="grid grid-cols-2 gap-4 sm:grid-cols-
|
|
263
|
+
<!-- Summary cards -->
|
|
264
|
+
<div class="grid grid-cols-2 gap-4 sm:grid-cols-5">
|
|
105
265
|
<div class="rounded-lg border border-default p-4 text-center">
|
|
106
266
|
<p class="text-2xl font-semibold text-highlighted">{{ summary.total }}</p>
|
|
107
267
|
<p class="text-xs text-muted">Total</p>
|
|
108
268
|
</div>
|
|
109
269
|
<div class="rounded-lg border border-default p-4 text-center">
|
|
110
|
-
<p class="text-2xl font-semibold text-primary">{{ summary.
|
|
270
|
+
<p class="text-2xl font-semibold text-primary">{{ summary.processing ?? 0 }}</p>
|
|
111
271
|
<p class="text-xs text-muted">Active</p>
|
|
112
272
|
</div>
|
|
113
273
|
<div class="rounded-lg border border-default p-4 text-center">
|
|
@@ -118,77 +278,116 @@ const columns: TableColumn<Task>[] = [
|
|
|
118
278
|
<p class="text-2xl font-semibold text-green-500">{{ summary.completed }}</p>
|
|
119
279
|
<p class="text-xs text-muted">Completed</p>
|
|
120
280
|
</div>
|
|
281
|
+
<div class="rounded-lg border border-default p-4 text-center">
|
|
282
|
+
<p class="text-2xl font-semibold text-red-500">{{ summary.failed }}</p>
|
|
283
|
+
<p class="text-xs text-muted">Failed</p>
|
|
284
|
+
</div>
|
|
121
285
|
</div>
|
|
122
286
|
|
|
123
|
-
<!--
|
|
287
|
+
<!-- Filter tabs -->
|
|
124
288
|
<div class="mt-6">
|
|
125
289
|
<UTabs
|
|
126
290
|
:items="tabItems"
|
|
127
291
|
:model-value="activeTab"
|
|
128
|
-
@update:model-value="activeTab = $event as
|
|
292
|
+
@update:model-value="activeTab = $event as 'all' | 'active' | 'queued' | 'completed' | 'failed'"
|
|
129
293
|
/>
|
|
130
294
|
</div>
|
|
131
295
|
|
|
132
|
-
<!--
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
296
|
+
<!-- Global empty (no jobs at all) -->
|
|
297
|
+
<DashboardState
|
|
298
|
+
:status="status"
|
|
299
|
+
:empty="!jobs.length"
|
|
300
|
+
empty-title="No jobs yet"
|
|
301
|
+
empty-icon="i-lucide-list-checks"
|
|
302
|
+
>
|
|
303
|
+
<template #empty>
|
|
304
|
+
<UIcon name="i-lucide-list-checks" class="size-16 text-muted" />
|
|
305
|
+
<h3 class="text-lg font-semibold text-highlighted">No jobs yet</h3>
|
|
306
|
+
<p class="text-sm text-muted text-center max-w-md">
|
|
307
|
+
Jobs are created when you ingest content. Head to the Knowledge tab and paste a URL or upload a file — each one runs as a job here in real time.
|
|
308
|
+
</p>
|
|
309
|
+
<UButton
|
|
310
|
+
to="/knowledge"
|
|
311
|
+
label="Open Knowledge"
|
|
312
|
+
icon="i-lucide-brain"
|
|
313
|
+
size="md"
|
|
314
|
+
class="mt-2"
|
|
315
|
+
/>
|
|
316
|
+
</template>
|
|
144
317
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<!-- Task Table -->
|
|
152
|
-
<div v-else class="mt-4">
|
|
153
|
-
<UTable
|
|
154
|
-
:data="filteredTasks"
|
|
155
|
-
:columns="columns"
|
|
156
|
-
:loading="status === 'pending'"
|
|
157
|
-
class="shrink-0"
|
|
158
|
-
:ui="{
|
|
159
|
-
base: 'table-fixed border-separate border-spacing-0',
|
|
160
|
-
thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
|
|
161
|
-
tbody: '[&>tr]:last:[&>td]:border-b-0',
|
|
162
|
-
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
163
|
-
td: 'border-b border-default'
|
|
164
|
-
}"
|
|
318
|
+
<!-- Filtered empty (jobs exist but not in this tab) -->
|
|
319
|
+
<div
|
|
320
|
+
v-if="!filteredJobs.length"
|
|
321
|
+
class="mt-6 flex flex-col items-center justify-center gap-4 py-12"
|
|
165
322
|
>
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
323
|
+
<UIcon name="i-lucide-filter-x" class="size-12 text-muted" />
|
|
324
|
+
<p class="text-sm text-muted">No {{ activeTab }} jobs found.</p>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<!-- Job table -->
|
|
328
|
+
<div v-else class="mt-4">
|
|
329
|
+
<UTable
|
|
330
|
+
:data="filteredJobs"
|
|
331
|
+
:columns="columns"
|
|
332
|
+
:loading="status === 'pending'"
|
|
333
|
+
class="shrink-0"
|
|
334
|
+
:ui="{
|
|
335
|
+
base: 'table-fixed border-separate border-spacing-0',
|
|
336
|
+
thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
|
|
337
|
+
tbody: '[&>tr]:last:[&>td]:border-b-0',
|
|
338
|
+
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
339
|
+
td: 'border-b border-default',
|
|
340
|
+
}"
|
|
341
|
+
>
|
|
342
|
+
<template #type-cell="{ row }">
|
|
343
|
+
<UBadge :label="row.original.type || 'unknown'" variant="subtle" color="primary" size="sm" />
|
|
344
|
+
</template>
|
|
345
|
+
<template #source-cell="{ row }">
|
|
346
|
+
<span class="text-sm font-mono" :title="row.original.source">
|
|
347
|
+
{{ truncate(row.original.source, 50) }}
|
|
348
|
+
</span>
|
|
349
|
+
<p v-if="row.original.message" class="text-xs text-muted mt-0.5">
|
|
350
|
+
{{ truncate(row.original.message, 60) }}
|
|
351
|
+
</p>
|
|
352
|
+
<p v-if="row.original.error" class="text-xs text-red-400 mt-0.5">
|
|
353
|
+
{{ truncate(row.original.error, 80) }}
|
|
354
|
+
</p>
|
|
355
|
+
</template>
|
|
356
|
+
<template #status-cell="{ row }">
|
|
357
|
+
<UBadge
|
|
358
|
+
:label="row.original.status"
|
|
359
|
+
:color="statusColor(row.original.status)"
|
|
360
|
+
variant="subtle"
|
|
361
|
+
size="sm"
|
|
362
|
+
class="capitalize"
|
|
363
|
+
/>
|
|
364
|
+
</template>
|
|
365
|
+
<template #progress-cell="{ row }">
|
|
366
|
+
<div class="flex items-center gap-2 min-w-24">
|
|
367
|
+
<UProgress :value="row.original.progress" :max="100" size="xs" class="flex-1" />
|
|
368
|
+
<span class="text-xs text-muted font-mono w-8 text-right">
|
|
369
|
+
{{ row.original.progress }}%
|
|
370
|
+
</span>
|
|
371
|
+
</div>
|
|
372
|
+
</template>
|
|
373
|
+
<template #created_at-cell="{ row }">
|
|
374
|
+
<span class="text-xs text-muted">{{ formatDate(row.original.created_at) }}</span>
|
|
375
|
+
</template>
|
|
376
|
+
<template #actions-cell="{ row }">
|
|
377
|
+
<UButton
|
|
378
|
+
v-if="isCancellable(row.original)"
|
|
379
|
+
icon="i-lucide-x"
|
|
380
|
+
variant="ghost"
|
|
381
|
+
color="error"
|
|
382
|
+
size="xs"
|
|
383
|
+
:loading="cancelling.has(row.original.id)"
|
|
384
|
+
aria-label="Cancel job"
|
|
385
|
+
@click="cancelJob(row.original)"
|
|
386
|
+
/>
|
|
387
|
+
</template>
|
|
388
|
+
</UTable>
|
|
389
|
+
</div>
|
|
390
|
+
</DashboardState>
|
|
192
391
|
</DashboardState>
|
|
193
392
|
</template>
|
|
194
393
|
</UDashboardPanel>
|
|
@@ -95,6 +95,35 @@ export interface TaskSummary {
|
|
|
95
95
|
completed: number
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// PR67 v2.84.0 — Jobs shape from /api/jobs (SQLite job queue).
|
|
99
|
+
// Mirrors core/jobs/manager.Job dataclass.
|
|
100
|
+
export interface Job {
|
|
101
|
+
id: string
|
|
102
|
+
type: string
|
|
103
|
+
source: string
|
|
104
|
+
title: string
|
|
105
|
+
status:
|
|
106
|
+
| 'queued' | 'processing' | 'downloading' | 'transcribing'
|
|
107
|
+
| 'embedding' | 'completed' | 'failed' | 'cancelled'
|
|
108
|
+
progress: number
|
|
109
|
+
message: string
|
|
110
|
+
chunks_created: number
|
|
111
|
+
media_path: string
|
|
112
|
+
error: string
|
|
113
|
+
created_at: string
|
|
114
|
+
started_at: string
|
|
115
|
+
completed_at: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface JobSummary {
|
|
119
|
+
total: number
|
|
120
|
+
queued: number
|
|
121
|
+
processing: number
|
|
122
|
+
completed: number
|
|
123
|
+
failed: number
|
|
124
|
+
cancelled?: number
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
export interface KnowledgeStats {
|
|
99
128
|
total_chunks: number
|
|
100
129
|
total_files: number
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|