arkaos 2.86.0 → 2.88.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 +1 -1
- package/core/__pycache__/keys.cpython-313.pyc +0 -0
- package/dashboard/app/pages/health.vue +211 -19
- package/dashboard/app/pages/knowledge.vue +103 -6
- package/dashboard/app/types/index.d.ts +3 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +80 -10
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.88.0
|
|
Binary file
|
|
@@ -1,14 +1,153 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
// PR70 v2.87.0 — Health page polish.
|
|
3
|
+
// - 30s auto-refresh (paused while tab is hidden) + manual refresh
|
|
4
|
+
// - Last-checked timestamp in header
|
|
5
|
+
// - Severity-aware rendering (fail = red, warn = yellow)
|
|
6
|
+
// - Copy-fix button when a check has a fix command
|
|
7
|
+
// - Healthy banner ignores warnings (only blocking failures matter)
|
|
8
|
+
|
|
9
|
+
interface HealthCheck {
|
|
10
|
+
name: string
|
|
11
|
+
passed: boolean
|
|
12
|
+
fix: string
|
|
13
|
+
severity: 'fail' | 'warn'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HealthPayload {
|
|
17
|
+
checks: HealthCheck[]
|
|
18
|
+
passed: number
|
|
19
|
+
total: number
|
|
20
|
+
failed_blocking: number
|
|
21
|
+
warning_count: number
|
|
22
|
+
healthy: boolean
|
|
23
|
+
ts: string
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
const { fetchApi } = useApi()
|
|
27
|
+
const toast = useToast()
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
data,
|
|
31
|
+
status,
|
|
32
|
+
error,
|
|
33
|
+
refresh,
|
|
34
|
+
} = await fetchApi<HealthPayload>('/api/health')
|
|
35
|
+
|
|
36
|
+
// ─── Auto-refresh ───────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
39
|
+
|
|
40
|
+
function startPolling() {
|
|
41
|
+
stopPolling()
|
|
42
|
+
pollTimer = setInterval(() => {
|
|
43
|
+
refresh()
|
|
44
|
+
}, 30_000)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stopPolling() {
|
|
48
|
+
if (pollTimer !== null) {
|
|
49
|
+
clearInterval(pollTimer)
|
|
50
|
+
pollTimer = null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleVisibility() {
|
|
55
|
+
if (typeof document === 'undefined') return
|
|
56
|
+
if (document.hidden) {
|
|
57
|
+
stopPolling()
|
|
58
|
+
} else {
|
|
59
|
+
refresh()
|
|
60
|
+
startPolling()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onMounted(() => {
|
|
65
|
+
if (typeof document !== 'undefined') {
|
|
66
|
+
document.addEventListener('visibilitychange', handleVisibility)
|
|
67
|
+
}
|
|
68
|
+
startPolling()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
onBeforeUnmount(() => {
|
|
72
|
+
stopPolling()
|
|
73
|
+
if (typeof document !== 'undefined') {
|
|
74
|
+
document.removeEventListener('visibilitychange', handleVisibility)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ─── Copy fix ───────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const copied = ref<string | null>(null)
|
|
81
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
|
82
|
+
|
|
83
|
+
async function copyFix(check: HealthCheck) {
|
|
84
|
+
if (!check.fix) return
|
|
85
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
86
|
+
toast.add({ title: 'Clipboard unavailable', color: 'warning' })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await navigator.clipboard.writeText(check.fix)
|
|
91
|
+
copied.value = check.name
|
|
92
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
93
|
+
copyTimer = setTimeout(() => { copied.value = null; copyTimer = null }, 1500)
|
|
94
|
+
toast.add({
|
|
95
|
+
title: 'Fix copied',
|
|
96
|
+
description: check.fix,
|
|
97
|
+
color: 'success',
|
|
98
|
+
})
|
|
99
|
+
} catch (err) {
|
|
100
|
+
toast.add({
|
|
101
|
+
title: 'Copy failed',
|
|
102
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
103
|
+
color: 'error',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
5
107
|
|
|
6
|
-
|
|
108
|
+
onBeforeUnmount(() => {
|
|
109
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
110
|
+
})
|
|
7
111
|
|
|
8
|
-
|
|
112
|
+
// ─── Format helpers ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function formatTs(iso: string | undefined): string {
|
|
115
|
+
if (!iso) return ''
|
|
116
|
+
try {
|
|
117
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
118
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
119
|
+
}).format(new Date(iso))
|
|
120
|
+
} catch {
|
|
121
|
+
return iso
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type CheckStatus = 'pass' | 'warn' | 'fail'
|
|
126
|
+
|
|
127
|
+
function statusOf(c: HealthCheck): CheckStatus {
|
|
128
|
+
if (c.passed) return 'pass'
|
|
129
|
+
return c.severity === 'warn' ? 'warn' : 'fail'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const STATUS_META: Record<CheckStatus, { icon: string; color: string; label: string }> = {
|
|
133
|
+
pass: { icon: 'i-lucide-check-circle', color: 'text-green-500', label: 'Pass' },
|
|
134
|
+
warn: { icon: 'i-lucide-alert-circle', color: 'text-yellow-500', label: 'Warn' },
|
|
135
|
+
fail: { icon: 'i-lucide-x-circle', color: 'text-red-500', label: 'Fail' },
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function statusBadgeColor(s: CheckStatus): 'success' | 'warning' | 'error' {
|
|
139
|
+
return s === 'pass' ? 'success' : s === 'warn' ? 'warning' : 'error'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Aggregate display ──────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const checks = computed<HealthCheck[]>(() => data.value?.checks ?? [])
|
|
9
145
|
const passed = computed(() => data.value?.passed ?? 0)
|
|
10
146
|
const total = computed(() => data.value?.total ?? 0)
|
|
11
|
-
const
|
|
147
|
+
const failedBlocking = computed(() => data.value?.failed_blocking ?? 0)
|
|
148
|
+
const warningCount = computed(() => data.value?.warning_count ?? 0)
|
|
149
|
+
const allPassed = computed(() => failedBlocking.value === 0 && warningCount.value === 0 && total.value > 0)
|
|
150
|
+
const someWarnings = computed(() => failedBlocking.value === 0 && warningCount.value > 0)
|
|
12
151
|
</script>
|
|
13
152
|
|
|
14
153
|
<template>
|
|
@@ -19,11 +158,28 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
|
|
|
19
158
|
<UDashboardSidebarCollapse />
|
|
20
159
|
</template>
|
|
21
160
|
<template #trailing>
|
|
161
|
+
<span
|
|
162
|
+
v-if="data?.ts"
|
|
163
|
+
class="text-xs text-muted"
|
|
164
|
+
:title="data.ts"
|
|
165
|
+
>
|
|
166
|
+
Last checked {{ formatTs(data.ts) }}
|
|
167
|
+
</span>
|
|
22
168
|
<UBadge
|
|
23
169
|
v-if="data"
|
|
24
170
|
:label="`${passed}/${total}`"
|
|
25
|
-
:color="allPassed ? 'success' : 'warning'"
|
|
171
|
+
:color="allPassed ? 'success' : someWarnings ? 'warning' : 'error'"
|
|
26
172
|
variant="subtle"
|
|
173
|
+
class="ml-3"
|
|
174
|
+
/>
|
|
175
|
+
</template>
|
|
176
|
+
<template #right>
|
|
177
|
+
<UButton
|
|
178
|
+
label="Refresh"
|
|
179
|
+
variant="ghost"
|
|
180
|
+
icon="i-lucide-refresh-cw"
|
|
181
|
+
size="sm"
|
|
182
|
+
@click="refresh()"
|
|
27
183
|
/>
|
|
28
184
|
</template>
|
|
29
185
|
</UDashboardNavbar>
|
|
@@ -39,45 +195,81 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
|
|
|
39
195
|
loading-label="Loading health checks"
|
|
40
196
|
:on-retry="() => refresh()"
|
|
41
197
|
>
|
|
42
|
-
<!-- Overall
|
|
198
|
+
<!-- Overall banner -->
|
|
43
199
|
<div
|
|
44
200
|
class="mb-6 rounded-lg border p-6 text-center"
|
|
45
|
-
:class="allPassed
|
|
201
|
+
:class="allPassed
|
|
202
|
+
? 'border-green-500/30 bg-green-500/5'
|
|
203
|
+
: someWarnings
|
|
204
|
+
? 'border-yellow-500/30 bg-yellow-500/5'
|
|
205
|
+
: 'border-red-500/30 bg-red-500/5'"
|
|
46
206
|
>
|
|
47
207
|
<UIcon
|
|
48
|
-
:name="allPassed
|
|
49
|
-
|
|
208
|
+
:name="allPassed
|
|
209
|
+
? 'i-lucide-check-circle'
|
|
210
|
+
: someWarnings
|
|
211
|
+
? 'i-lucide-alert-circle'
|
|
212
|
+
: 'i-lucide-x-circle'"
|
|
213
|
+
:class="allPassed
|
|
214
|
+
? 'text-green-500'
|
|
215
|
+
: someWarnings ? 'text-yellow-500' : 'text-red-500'"
|
|
50
216
|
class="size-12"
|
|
51
217
|
/>
|
|
52
218
|
<p class="mt-2 text-lg font-semibold text-highlighted">
|
|
53
|
-
|
|
219
|
+
<template v-if="allPassed">All Checks Passing</template>
|
|
220
|
+
<template v-else-if="someWarnings">
|
|
221
|
+
{{ warningCount }} Warning{{ warningCount === 1 ? '' : 's' }}
|
|
222
|
+
</template>
|
|
223
|
+
<template v-else>
|
|
224
|
+
{{ failedBlocking }} Blocking Failure{{ failedBlocking === 1 ? '' : 's' }}
|
|
225
|
+
</template>
|
|
226
|
+
</p>
|
|
227
|
+
<p class="text-sm text-muted">
|
|
228
|
+
{{ passed }} of {{ total }} checks passed
|
|
229
|
+
<template v-if="warningCount && failedBlocking">
|
|
230
|
+
· {{ warningCount }} warn · {{ failedBlocking }} blocking
|
|
231
|
+
</template>
|
|
54
232
|
</p>
|
|
55
|
-
<p class="text-sm text-muted">{{ passed }} of {{ total }} checks passed</p>
|
|
56
233
|
</div>
|
|
57
234
|
|
|
58
|
-
<!--
|
|
235
|
+
<!-- Check list -->
|
|
59
236
|
<div class="space-y-3">
|
|
60
237
|
<div
|
|
61
238
|
v-for="check in checks"
|
|
62
239
|
:key="check.name"
|
|
63
|
-
class="flex items-start gap-3 rounded-lg border
|
|
240
|
+
class="flex items-start gap-3 rounded-lg border p-4"
|
|
241
|
+
:class="{
|
|
242
|
+
'border-default': check.passed,
|
|
243
|
+
'border-yellow-500/30 bg-yellow-500/5': !check.passed && check.severity === 'warn',
|
|
244
|
+
'border-red-500/30 bg-red-500/5': !check.passed && check.severity === 'fail',
|
|
245
|
+
}"
|
|
64
246
|
>
|
|
65
247
|
<UIcon
|
|
66
|
-
:name="check.
|
|
67
|
-
:class="check.
|
|
248
|
+
:name="STATUS_META[statusOf(check)].icon"
|
|
249
|
+
:class="STATUS_META[statusOf(check)].color"
|
|
68
250
|
class="mt-0.5 size-5 shrink-0"
|
|
69
251
|
/>
|
|
70
|
-
<div class="flex-1">
|
|
252
|
+
<div class="flex-1 min-w-0">
|
|
71
253
|
<h4 class="font-medium text-highlighted">{{ check.name }}</h4>
|
|
72
254
|
<p v-if="!check.passed && check.fix" class="mt-1 text-sm text-muted">
|
|
73
|
-
Fix: {{ check.fix }}
|
|
255
|
+
Fix: <code class="font-mono text-xs">{{ check.fix }}</code>
|
|
74
256
|
</p>
|
|
75
257
|
</div>
|
|
258
|
+
<UButton
|
|
259
|
+
v-if="!check.passed && check.fix"
|
|
260
|
+
:icon="copied === check.name ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
261
|
+
:color="copied === check.name ? 'success' : 'neutral'"
|
|
262
|
+
variant="ghost"
|
|
263
|
+
size="xs"
|
|
264
|
+
aria-label="Copy fix command"
|
|
265
|
+
@click="copyFix(check)"
|
|
266
|
+
/>
|
|
76
267
|
<UBadge
|
|
77
|
-
:label="check.
|
|
78
|
-
:color="check
|
|
268
|
+
:label="STATUS_META[statusOf(check)].label"
|
|
269
|
+
:color="statusBadgeColor(statusOf(check))"
|
|
79
270
|
variant="subtle"
|
|
80
271
|
size="sm"
|
|
272
|
+
class="shrink-0"
|
|
81
273
|
/>
|
|
82
274
|
</div>
|
|
83
275
|
</div>
|
|
@@ -324,6 +324,90 @@ async function handleSearch() {
|
|
|
324
324
|
function formatScore(score: number): string {
|
|
325
325
|
return `${(score * 100).toFixed(0)}%`
|
|
326
326
|
}
|
|
327
|
+
|
|
328
|
+
// PR71 v2.88.0 — delete all chunks from a given source.
|
|
329
|
+
|
|
330
|
+
const deletingSource = ref<string | null>(null)
|
|
331
|
+
|
|
332
|
+
async function askDeleteSource(source: string) {
|
|
333
|
+
if (!source) return
|
|
334
|
+
if (typeof window === 'undefined') return
|
|
335
|
+
const ok = window.confirm(
|
|
336
|
+
`Delete every indexed chunk from this source?\n\n${source}\n\n`
|
|
337
|
+
+ 'This removes the source from search results but does not delete the original file. '
|
|
338
|
+
+ 'You can re-ingest the source later if needed.',
|
|
339
|
+
)
|
|
340
|
+
if (!ok) return
|
|
341
|
+
await deleteSource(source)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function deleteSource(source: string) {
|
|
345
|
+
deletingSource.value = source
|
|
346
|
+
try {
|
|
347
|
+
const res = await $fetch<{ deleted?: number, source?: string, error?: string }>(
|
|
348
|
+
`${apiBase}/api/knowledge/sources`,
|
|
349
|
+
{ method: 'DELETE', query: { source } },
|
|
350
|
+
)
|
|
351
|
+
if (res.error) {
|
|
352
|
+
toast.add({
|
|
353
|
+
title: 'Delete failed',
|
|
354
|
+
description: res.error,
|
|
355
|
+
color: 'error',
|
|
356
|
+
})
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
const deleted = res.deleted ?? 0
|
|
360
|
+
// Drop the matching rows from the in-memory list without a full re-fetch.
|
|
361
|
+
searchResults.value = searchResults.value.filter((r) => r.source !== source)
|
|
362
|
+
searchTotal.value = searchResults.value.length
|
|
363
|
+
// Refresh stats so the chunk count in the header updates.
|
|
364
|
+
if (typeof refresh === 'function') {
|
|
365
|
+
await refresh()
|
|
366
|
+
}
|
|
367
|
+
toast.add({
|
|
368
|
+
title: deleted > 0
|
|
369
|
+
? `Deleted ${deleted} chunk${deleted === 1 ? '' : 's'}`
|
|
370
|
+
: 'Nothing to delete',
|
|
371
|
+
description: source,
|
|
372
|
+
color: 'success',
|
|
373
|
+
})
|
|
374
|
+
} catch (err) {
|
|
375
|
+
toast.add({
|
|
376
|
+
title: 'Delete failed',
|
|
377
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
378
|
+
color: 'error',
|
|
379
|
+
})
|
|
380
|
+
} finally {
|
|
381
|
+
deletingSource.value = null
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// PR71 — highlight the search query in the preview text.
|
|
386
|
+
// Tolerates malformed regex (escapes special characters) and HTML-
|
|
387
|
+
// escapes the input so v-html'd output is safe from XSS via DB rows.
|
|
388
|
+
function highlightMatches(text: string, query: string): string {
|
|
389
|
+
const safe = escapeHtml(text || '')
|
|
390
|
+
const q = (query || '').trim()
|
|
391
|
+
if (!q) return safe
|
|
392
|
+
const pattern = new RegExp(`(${escapeRegex(q)})`, 'gi')
|
|
393
|
+
return safe.replace(
|
|
394
|
+
pattern,
|
|
395
|
+
'<mark class="bg-primary/20 text-primary rounded px-0.5">$1</mark>',
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function escapeHtml(value: string): string {
|
|
400
|
+
return value
|
|
401
|
+
.replace(/&/g, '&')
|
|
402
|
+
.replace(/</g, '<')
|
|
403
|
+
.replace(/>/g, '>')
|
|
404
|
+
.replace(/"/g, '"')
|
|
405
|
+
.replace(/'/g, ''')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function escapeRegex(value: string): string {
|
|
409
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
410
|
+
}
|
|
327
411
|
</script>
|
|
328
412
|
|
|
329
413
|
<template>
|
|
@@ -771,17 +855,30 @@ function formatScore(score: number): string {
|
|
|
771
855
|
{{ result.heading }}
|
|
772
856
|
</span>
|
|
773
857
|
</div>
|
|
774
|
-
<
|
|
775
|
-
|
|
776
|
-
|
|
858
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
859
|
+
<span class="text-xs text-muted whitespace-nowrap">
|
|
860
|
+
Score: {{ formatScore(result.score) }}
|
|
861
|
+
</span>
|
|
862
|
+
<UButton
|
|
863
|
+
v-if="result.source"
|
|
864
|
+
:icon="deletingSource === result.source
|
|
865
|
+
? 'i-lucide-loader-2'
|
|
866
|
+
: 'i-lucide-trash-2'"
|
|
867
|
+
:loading="deletingSource === result.source"
|
|
868
|
+
variant="ghost"
|
|
869
|
+
color="error"
|
|
870
|
+
size="xs"
|
|
871
|
+
aria-label="Delete all chunks from this source"
|
|
872
|
+
@click.stop="askDeleteSource(result.source)"
|
|
873
|
+
/>
|
|
874
|
+
</div>
|
|
777
875
|
</div>
|
|
778
876
|
<p v-if="result.source" class="text-xs text-muted mb-1 truncate">
|
|
779
877
|
<UIcon name="i-lucide-file-text" class="size-3 inline-block mr-1" />
|
|
780
878
|
{{ result.source }}
|
|
781
879
|
</p>
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
</p>
|
|
880
|
+
<!-- PR71 v2.88.0 — highlight query matches in the preview -->
|
|
881
|
+
<p class="text-sm text-muted line-clamp-3" v-html="highlightMatches(result.text || result.content, searchQuery)" />
|
|
785
882
|
</div>
|
|
786
883
|
</div>
|
|
787
884
|
|
|
@@ -175,6 +175,9 @@ export interface HealthCheck {
|
|
|
175
175
|
name: string
|
|
176
176
|
passed: boolean
|
|
177
177
|
fix: string
|
|
178
|
+
// PR70 v2.87.0 — backend now tags every check with a severity.
|
|
179
|
+
// 'fail' is must-pass; 'warn' is recommended but non-blocking.
|
|
180
|
+
severity?: 'fail' | 'warn'
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
export interface Persona {
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -612,20 +612,66 @@ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
|
|
|
612
612
|
return {"results": results, "query": q, "total": len(results)}
|
|
613
613
|
|
|
614
614
|
|
|
615
|
+
@app.delete("/api/knowledge/sources")
|
|
616
|
+
def knowledge_delete_source(source: str = Query(...)):
|
|
617
|
+
"""PR71 v2.88.0 — remove all chunks from a given source.
|
|
618
|
+
|
|
619
|
+
Operators sometimes ingest a noisy / wrong source and want to nuke
|
|
620
|
+
every chunk that came from it without rebuilding the whole vector
|
|
621
|
+
DB. The vector store already exposes `remove_file(source)` —
|
|
622
|
+
this endpoint just exposes it on the wire.
|
|
623
|
+
|
|
624
|
+
Returns ``{deleted: N, source: "..."}``. Refuses empty source
|
|
625
|
+
paths so a runaway client doesn't accidentally request "delete
|
|
626
|
+
everything that has no source".
|
|
627
|
+
"""
|
|
628
|
+
clean = (source or "").strip()
|
|
629
|
+
if not clean:
|
|
630
|
+
return {"error": "source query param is required"}
|
|
631
|
+
store = _get_vector_store()
|
|
632
|
+
if not store:
|
|
633
|
+
return {"error": "vector store unavailable", "deleted": 0}
|
|
634
|
+
try:
|
|
635
|
+
deleted = store.remove_file(clean)
|
|
636
|
+
except Exception as exc: # noqa: BLE001 — surface as 200+error
|
|
637
|
+
return {"error": f"delete failed: {exc}", "deleted": 0}
|
|
638
|
+
return {"deleted": int(deleted), "source": clean}
|
|
639
|
+
|
|
640
|
+
|
|
615
641
|
@app.get("/api/health")
|
|
616
642
|
def health():
|
|
617
|
-
|
|
643
|
+
"""PR70 v2.87.0 — per-check severity + response timestamp.
|
|
644
|
+
|
|
645
|
+
Each check now carries a `severity` field:
|
|
646
|
+
- "fail" — must-pass; missing breaks ArkaOS
|
|
647
|
+
- "warn" — recommended; missing means a degraded but workable env
|
|
648
|
+
|
|
649
|
+
Response also carries `ts` so the UI can show "last checked".
|
|
650
|
+
Frontend polls every 30s and surfaces copy-fix buttons.
|
|
651
|
+
"""
|
|
652
|
+
from datetime import datetime, timezone
|
|
653
|
+
|
|
654
|
+
checks: list[dict] = []
|
|
618
655
|
arkaos_home = Path.home() / ".arkaos"
|
|
619
656
|
|
|
620
|
-
def check(name, condition, fix=""):
|
|
621
|
-
checks.append({
|
|
657
|
+
def check(name: str, condition: bool, fix: str = "", severity: str = "fail"):
|
|
658
|
+
checks.append({
|
|
659
|
+
"name": name,
|
|
660
|
+
"passed": condition,
|
|
661
|
+
"fix": fix,
|
|
662
|
+
"severity": severity,
|
|
663
|
+
})
|
|
622
664
|
|
|
623
|
-
check("install_dir", arkaos_home.exists(), "
|
|
624
|
-
check("manifest", (arkaos_home / "install-manifest.json").exists(),
|
|
665
|
+
check("install_dir", arkaos_home.exists(), "npx arkaos install")
|
|
666
|
+
check("manifest", (arkaos_home / "install-manifest.json").exists(),
|
|
667
|
+
"npx arkaos install")
|
|
625
668
|
check("constitution", (ARKAOS_ROOT / "config" / "constitution.yaml").exists())
|
|
626
|
-
check("agents_registry",
|
|
627
|
-
|
|
628
|
-
check("
|
|
669
|
+
check("agents_registry",
|
|
670
|
+
(ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json").exists())
|
|
671
|
+
check("commands_registry",
|
|
672
|
+
(ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json").exists())
|
|
673
|
+
check("hooks_dir", (arkaos_home / "config" / "hooks").exists(),
|
|
674
|
+
"npx arkaos install")
|
|
629
675
|
|
|
630
676
|
try:
|
|
631
677
|
subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
|
|
@@ -633,10 +679,34 @@ def health():
|
|
|
633
679
|
except Exception:
|
|
634
680
|
check("python", False, "Install Python 3.11+")
|
|
635
681
|
|
|
636
|
-
|
|
682
|
+
# Telemetry + knowledge — warn-only; missing them is a degraded
|
|
683
|
+
# but workable state (new installs, never-indexed-anything).
|
|
684
|
+
check("knowledge_db", (arkaos_home / "knowledge.db").exists(),
|
|
685
|
+
"Open the Knowledge tab and ingest a source",
|
|
686
|
+
severity="warn")
|
|
687
|
+
check("profile",
|
|
688
|
+
(arkaos_home / "profile.json").exists(),
|
|
689
|
+
"Open Settings → Profile to introduce yourself",
|
|
690
|
+
severity="warn")
|
|
637
691
|
|
|
638
692
|
passed = sum(1 for c in checks if c["passed"])
|
|
639
|
-
|
|
693
|
+
failed_blocking = sum(
|
|
694
|
+
1 for c in checks
|
|
695
|
+
if not c["passed"] and c["severity"] == "fail"
|
|
696
|
+
)
|
|
697
|
+
warning_count = sum(
|
|
698
|
+
1 for c in checks
|
|
699
|
+
if not c["passed"] and c["severity"] == "warn"
|
|
700
|
+
)
|
|
701
|
+
return {
|
|
702
|
+
"checks": checks,
|
|
703
|
+
"passed": passed,
|
|
704
|
+
"total": len(checks),
|
|
705
|
+
"failed_blocking": failed_blocking,
|
|
706
|
+
"warning_count": warning_count,
|
|
707
|
+
"healthy": failed_blocking == 0,
|
|
708
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
709
|
+
}
|
|
640
710
|
|
|
641
711
|
|
|
642
712
|
# --- Personas ---
|