arkaos 2.3.0 → 2.3.1

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.
Files changed (64) hide show
  1. package/VERSION +1 -1
  2. package/arka/skills/conclave/SKILL.md +194 -0
  3. package/arka/skills/human-writing/SKILL.md +143 -0
  4. package/config/agent-memory-template.md +28 -0
  5. package/config/disc-profiles.json +108 -0
  6. package/config/disc-team-validator.sh +94 -0
  7. package/config/gotchas-fixes.json +148 -0
  8. package/config/profile-template.json +12 -0
  9. package/config/providers-registry.json +56 -0
  10. package/config/settings-template.json +42 -0
  11. package/config/standards/communication.md +64 -0
  12. package/config/standards/orchestration.md +91 -0
  13. package/config/statusline-v2.sh +101 -0
  14. package/config/statusline.sh +139 -0
  15. package/config/system-prompt.sh +190 -0
  16. package/dashboard/LICENSE +21 -0
  17. package/dashboard/README.md +64 -0
  18. package/dashboard/app/app.config.ts +8 -0
  19. package/dashboard/app/app.vue +42 -0
  20. package/dashboard/app/assets/css/main.css +18 -0
  21. package/dashboard/app/composables/useApi.ts +8 -0
  22. package/dashboard/app/composables/useDashboard.ts +19 -0
  23. package/dashboard/app/error.vue +24 -0
  24. package/dashboard/app/layouts/default.vue +114 -0
  25. package/dashboard/app/pages/agents/[id].vue +506 -0
  26. package/dashboard/app/pages/agents/index.vue +225 -0
  27. package/dashboard/app/pages/budget.vue +132 -0
  28. package/dashboard/app/pages/commands.vue +180 -0
  29. package/dashboard/app/pages/health.vue +98 -0
  30. package/dashboard/app/pages/index.vue +126 -0
  31. package/dashboard/app/pages/knowledge.vue +729 -0
  32. package/dashboard/app/pages/personas.vue +597 -0
  33. package/dashboard/app/pages/settings.vue +146 -0
  34. package/dashboard/app/pages/tasks.vue +203 -0
  35. package/dashboard/app/types/index.d.ts +181 -0
  36. package/dashboard/app/utils/index.ts +7 -0
  37. package/dashboard/nuxt.config.ts +39 -0
  38. package/dashboard/package.json +37 -0
  39. package/dashboard/pnpm-workspace.yaml +7 -0
  40. package/dashboard/tsconfig.json +10 -0
  41. package/knowledge/INDEX.md +34 -0
  42. package/knowledge/agents-registry.json +254 -0
  43. package/knowledge/channels-config.json +6 -0
  44. package/knowledge/commands-keywords.json +466 -0
  45. package/knowledge/commands-registry.json +2791 -0
  46. package/knowledge/commands-registry.json.bak +2791 -0
  47. package/knowledge/ecosystems.json +7 -0
  48. package/knowledge/obsidian-config.json +112 -0
  49. package/package.json +10 -6
  50. package/pyproject.toml +1 -1
  51. package/scripts/check-version.js +13 -0
  52. package/scripts/dashboard-api.py +636 -0
  53. package/scripts/knowledge-index.py +113 -0
  54. package/scripts/skill_validator.py +217 -0
  55. package/scripts/start-dashboard.sh +54 -0
  56. package/scripts/synapse-bridge.py +199 -0
  57. package/scripts/tools/brand_voice_analyzer.py +192 -0
  58. package/scripts/tools/dcf_calculator.py +168 -0
  59. package/scripts/tools/headline_scorer.py +215 -0
  60. package/scripts/tools/okr_cascade.py +207 -0
  61. package/scripts/tools/rice_prioritizer.py +230 -0
  62. package/scripts/tools/saas_metrics.py +234 -0
  63. package/scripts/tools/seo_checker.py +197 -0
  64. package/scripts/tools/tech_debt_analyzer.py +206 -0
@@ -0,0 +1,225 @@
1
+ <script setup lang="ts">
2
+ import type { TableColumn } from '@nuxt/ui'
3
+ import type { Agent } from '~/types'
4
+
5
+ const { fetchApi } = useApi()
6
+
7
+ const { data, status, error, refresh } = await fetchApi<{ agents: Agent[], total: number }>('/api/agents')
8
+
9
+ const agents = computed(() => data.value?.agents ?? [])
10
+
11
+ const search = ref('')
12
+ const departmentFilter = ref('all')
13
+ const tierFilter = ref('all')
14
+ const page = ref(1)
15
+ const pageSize = 15
16
+
17
+ const departments = computed(() => {
18
+ const depts = new Set(agents.value.map(a => a.department))
19
+ return [
20
+ { label: 'All Departments', value: 'all' },
21
+ ...Array.from(depts).sort().map(d => ({ label: d, value: d }))
22
+ ]
23
+ })
24
+
25
+ const tierOptions = [
26
+ { label: 'All Tiers', value: 'all' },
27
+ { label: 'Tier 0 — C-Suite', value: '0' },
28
+ { label: 'Tier 1 — Squad Leads', value: '1' },
29
+ { label: 'Tier 2 — Specialists', value: '2' },
30
+ { label: 'Tier 3 — Support', value: '3' }
31
+ ]
32
+
33
+ const filteredAgents = computed(() => {
34
+ let result = agents.value
35
+ const query = search.value.toLowerCase()
36
+
37
+ if (query) {
38
+ result = result.filter(agent =>
39
+ agent.name.toLowerCase().includes(query)
40
+ || agent.role.toLowerCase().includes(query)
41
+ || agent.department.toLowerCase().includes(query)
42
+ )
43
+ }
44
+
45
+ if (departmentFilter.value !== 'all') {
46
+ result = result.filter(agent => agent.department === departmentFilter.value)
47
+ }
48
+
49
+ if (tierFilter.value !== 'all') {
50
+ result = result.filter(agent => String(agent.tier) === tierFilter.value)
51
+ }
52
+
53
+ return result
54
+ })
55
+
56
+ const totalFiltered = computed(() => filteredAgents.value.length)
57
+
58
+ const paginatedAgents = computed(() => {
59
+ const start = (page.value - 1) * pageSize
60
+ return filteredAgents.value.slice(start, start + pageSize)
61
+ })
62
+
63
+ const totalPages = computed(() => Math.max(1, Math.ceil(totalFiltered.value / pageSize)))
64
+
65
+ watch([search, departmentFilter, tierFilter], () => {
66
+ page.value = 1
67
+ })
68
+
69
+ const tierColor = (tier: number) => {
70
+ const colors: Record<number, string> = {
71
+ 0: 'error',
72
+ 1: 'warning',
73
+ 2: 'primary',
74
+ 3: 'neutral'
75
+ }
76
+ return (colors[tier] ?? 'neutral') as 'error' | 'warning' | 'primary' | 'neutral'
77
+ }
78
+
79
+ const columns: TableColumn<Agent>[] = [
80
+ {
81
+ accessorKey: 'name',
82
+ header: 'Name'
83
+ },
84
+ {
85
+ accessorKey: 'role',
86
+ header: 'Role'
87
+ },
88
+ {
89
+ accessorKey: 'department',
90
+ header: 'Department'
91
+ },
92
+ {
93
+ accessorKey: 'tier',
94
+ header: 'Tier'
95
+ },
96
+ {
97
+ accessorFn: (row: Agent) => row.disc?.primary ?? '-',
98
+ id: 'disc',
99
+ header: 'DISC'
100
+ },
101
+ {
102
+ accessorKey: 'mbti',
103
+ header: 'MBTI'
104
+ },
105
+ {
106
+ id: 'actions',
107
+ header: ''
108
+ }
109
+ ]
110
+
111
+ function goToAgent(id: string) {
112
+ navigateTo(`/agents/${id}`)
113
+ }
114
+ </script>
115
+
116
+ <template>
117
+ <UDashboardPanel id="agents">
118
+ <template #header>
119
+ <UDashboardNavbar title="Agents">
120
+ <template #leading>
121
+ <UDashboardSidebarCollapse />
122
+ </template>
123
+ <template #trailing>
124
+ <UBadge v-if="data?.total" :label="data.total" variant="subtle" />
125
+ </template>
126
+ </UDashboardNavbar>
127
+ </template>
128
+
129
+ <template #body>
130
+ <!-- Loading -->
131
+ <div v-if="status === 'pending'" class="flex items-center justify-center py-12">
132
+ <UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
133
+ </div>
134
+
135
+ <!-- Error -->
136
+ <div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
137
+ <UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
138
+ <p class="text-sm text-muted">Failed to load agents.</p>
139
+ <UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
140
+ </div>
141
+
142
+ <!-- Empty -->
143
+ <div v-else-if="!agents.length" class="flex flex-col items-center justify-center gap-4 py-12">
144
+ <UIcon name="i-lucide-users" class="size-12 text-muted" />
145
+ <p class="text-sm text-muted">No agents found.</p>
146
+ </div>
147
+
148
+ <!-- Content -->
149
+ <template v-else>
150
+ <div class="flex flex-wrap items-center gap-3 mb-4">
151
+ <UInput
152
+ v-model="search"
153
+ class="max-w-sm"
154
+ icon="i-lucide-search"
155
+ placeholder="Search agents..."
156
+ aria-label="Search agents by name, role, or department"
157
+ />
158
+
159
+ <USelect
160
+ v-model="departmentFilter"
161
+ :items="departments"
162
+ :ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
163
+ placeholder="Department"
164
+ class="min-w-48"
165
+ aria-label="Filter by department"
166
+ />
167
+
168
+ <USelect
169
+ v-model="tierFilter"
170
+ :items="tierOptions"
171
+ :ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
172
+ placeholder="Tier"
173
+ class="min-w-44"
174
+ aria-label="Filter by tier"
175
+ />
176
+
177
+ <span class="ml-auto text-xs text-muted">
178
+ {{ totalFiltered }} agent{{ totalFiltered !== 1 ? 's' : '' }}
179
+ </span>
180
+ </div>
181
+
182
+ <UTable
183
+ :data="paginatedAgents"
184
+ :columns="columns"
185
+ :loading="status === 'pending'"
186
+ class="shrink-0"
187
+ :ui="{
188
+ base: 'table-fixed border-separate border-spacing-0',
189
+ thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
190
+ tbody: '[&>tr]:last:[&>td]:border-b-0 [&>tr]:cursor-pointer [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
191
+ th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
192
+ td: 'border-b border-default'
193
+ }"
194
+ >
195
+ <template #name-cell="{ row }">
196
+ <button class="text-left font-medium text-primary hover:underline" @click="goToAgent(row.original.id)">
197
+ {{ row.original.name }}
198
+ </button>
199
+ </template>
200
+ <template #department-cell="{ row }">
201
+ <UBadge :label="row.original.department" variant="subtle" size="sm" />
202
+ </template>
203
+ <template #tier-cell="{ row }">
204
+ <UBadge :label="`Tier ${row.original.tier}`" :color="tierColor(row.original.tier)" variant="subtle" size="sm" />
205
+ </template>
206
+ <template #mbti-cell="{ row }">
207
+ <span class="font-mono text-sm">{{ row.original.mbti || '-' }}</span>
208
+ </template>
209
+ <template #actions-cell="{ row }">
210
+ <UButton size="xs" variant="ghost" icon="i-lucide-arrow-right" @click="goToAgent(row.original.id)" />
211
+ </template>
212
+ </UTable>
213
+
214
+ <div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
215
+ <UPagination
216
+ :page="page"
217
+ :total="totalFiltered"
218
+ :items-per-page="pageSize"
219
+ @update:page="(val) => page = val"
220
+ />
221
+ </div>
222
+ </template>
223
+ </template>
224
+ </UDashboardPanel>
225
+ </template>
@@ -0,0 +1,132 @@
1
+ <script setup lang="ts">
2
+ const { fetchApi } = useApi()
3
+
4
+ const { data, status, error, refresh } = fetchApi<any>('/api/budget')
5
+
6
+ const summary = computed(() => data.value?.summary ?? { total_tokens: 0, total_ops: 0, active_departments: 0, estimated_cost_usd: 0 })
7
+ const departments = computed(() => data.value?.departments ?? [])
8
+ const tiers = computed(() => data.value?.tiers ?? [])
9
+
10
+ const showLimits = ref(false)
11
+
12
+ const tierLabels: Record<number, string> = {
13
+ 0: 'C-Suite (Unlimited)',
14
+ 1: 'Squad Leads',
15
+ 2: 'Specialists',
16
+ 3: 'Support'
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <UDashboardPanel id="budget">
22
+ <template #header>
23
+ <UDashboardNavbar title="Usage & Budget">
24
+ <template #leading>
25
+ <UDashboardSidebarCollapse />
26
+ </template>
27
+ <template #right>
28
+ <UButton
29
+ label="Refresh"
30
+ variant="ghost"
31
+ icon="i-lucide-refresh-cw"
32
+ size="sm"
33
+ @click="refresh()"
34
+ />
35
+ </template>
36
+ </UDashboardNavbar>
37
+ </template>
38
+
39
+ <template #body>
40
+ <div v-if="status === 'pending'" class="flex items-center justify-center py-12">
41
+ <UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
42
+ </div>
43
+
44
+ <div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
45
+ <UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
46
+ <p class="text-sm text-muted">Failed to load budget data.</p>
47
+ <UButton label="Retry" variant="outline" icon="i-lucide-refresh-cw" @click="refresh()" />
48
+ </div>
49
+
50
+ <div v-else class="space-y-6">
51
+ <!-- Monthly Summary -->
52
+ <UCard>
53
+ <div class="space-y-3">
54
+ <p class="text-xs font-semibold text-muted uppercase tracking-wider">This Month's Usage</p>
55
+ <div class="flex flex-wrap items-baseline gap-6">
56
+ <div>
57
+ <span class="text-3xl font-bold">{{ summary.total_tokens.toLocaleString() }}</span>
58
+ <span class="text-sm text-muted ml-1">tokens</span>
59
+ </div>
60
+ <div>
61
+ <span class="text-xl font-semibold">{{ summary.total_ops }}</span>
62
+ <span class="text-sm text-muted ml-1">operations</span>
63
+ </div>
64
+ <div>
65
+ <span class="text-xl font-semibold">{{ summary.active_departments }}</span>
66
+ <span class="text-sm text-muted ml-1">departments active</span>
67
+ </div>
68
+ <div v-if="summary.estimated_cost_usd > 0">
69
+ <span class="text-sm text-muted">Est. cost: ~${{ summary.estimated_cost_usd.toFixed(4) }}</span>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </UCard>
74
+
75
+ <!-- Department Breakdown -->
76
+ <div v-if="departments.length">
77
+ <h2 class="text-sm font-semibold text-muted uppercase tracking-wider mb-4">Usage by Department</h2>
78
+ <div class="space-y-3">
79
+ <div
80
+ v-for="dept in departments"
81
+ :key="dept.department"
82
+ class="flex items-center gap-4"
83
+ >
84
+ <span class="w-28 text-sm font-medium truncate">{{ dept.department }}</span>
85
+ <div class="flex-1 h-3 rounded-full bg-muted/20 overflow-hidden">
86
+ <div
87
+ class="h-3 rounded-full bg-primary transition-none"
88
+ :style="{ width: `${dept.percent}%` }"
89
+ />
90
+ </div>
91
+ <span class="w-24 text-right text-sm font-mono">{{ dept.tokens.toLocaleString() }}</span>
92
+ <span class="w-16 text-right text-xs text-muted">{{ dept.operations }} ops</span>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Empty state -->
98
+ <div v-else class="flex flex-col items-center justify-center gap-4 py-12">
99
+ <UIcon name="i-lucide-bar-chart-3" class="size-12 text-muted" />
100
+ <p class="text-sm text-muted">No usage data yet.</p>
101
+ <p class="text-xs text-muted">Token usage is tracked automatically when ArkaOS processes prompts and indexes knowledge.</p>
102
+ </div>
103
+
104
+ <!-- System Limits (collapsible) -->
105
+ <div class="pt-4 border-t border-default">
106
+ <button
107
+ class="flex items-center gap-2 text-xs text-muted hover:text-highlighted transition-colors"
108
+ @click="showLimits = !showLimits"
109
+ >
110
+ <UIcon :name="showLimits ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'" class="size-3" />
111
+ System Limits
112
+ </button>
113
+
114
+ <div v-if="showLimits" class="mt-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
115
+ <div
116
+ v-for="tier in tiers"
117
+ :key="tier.tier"
118
+ class="rounded-lg border border-default p-3"
119
+ >
120
+ <p class="text-xs font-semibold">Tier {{ tier.tier }}</p>
121
+ <p class="text-xs text-muted">{{ tierLabels[tier.tier] ?? '' }}</p>
122
+ <p class="text-xs text-muted mt-1">
123
+ <template v-if="tier.is_unlimited">Unlimited</template>
124
+ <template v-else>{{ (tier.allocated ?? 0).toLocaleString() }} tokens/month</template>
125
+ </p>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </template>
131
+ </UDashboardPanel>
132
+ </template>
@@ -0,0 +1,180 @@
1
+ <script setup lang="ts">
2
+ import type { TableColumn } from '@nuxt/ui'
3
+ import type { Command } from '~/types'
4
+
5
+ const { fetchApi } = useApi()
6
+
7
+ const { data, status, error, refresh } = await fetchApi<{ commands: Command[], total: number }>('/api/commands')
8
+
9
+ const commands = computed(() => data.value?.commands ?? [])
10
+
11
+ const search = ref('')
12
+ const departmentFilter = ref('all')
13
+ const page = ref(1)
14
+ const pageSize = 20
15
+ const expandedRow = ref<string | null>(null)
16
+
17
+ const departments = computed(() => {
18
+ const depts = new Set(commands.value.map(c => c.department))
19
+ return [
20
+ { label: 'All Departments', value: 'all' },
21
+ ...Array.from(depts).sort().map(d => ({ label: d, value: d }))
22
+ ]
23
+ })
24
+
25
+ const filteredCommands = computed(() => {
26
+ let result = commands.value
27
+ const query = search.value.toLowerCase()
28
+
29
+ if (query) {
30
+ result = result.filter(cmd =>
31
+ cmd.command.toLowerCase().includes(query)
32
+ || cmd.description.toLowerCase().includes(query)
33
+ )
34
+ }
35
+
36
+ if (departmentFilter.value !== 'all') {
37
+ result = result.filter(cmd => cmd.department === departmentFilter.value)
38
+ }
39
+
40
+ return result
41
+ })
42
+
43
+ const totalFiltered = computed(() => filteredCommands.value.length)
44
+
45
+ const paginatedCommands = computed(() => {
46
+ const start = (page.value - 1) * pageSize
47
+ return filteredCommands.value.slice(start, start + pageSize)
48
+ })
49
+
50
+ const totalPages = computed(() => Math.max(1, Math.ceil(totalFiltered.value / pageSize)))
51
+
52
+ watch([search, departmentFilter], () => {
53
+ page.value = 1
54
+ })
55
+
56
+ function toggleExpand(commandId: string) {
57
+ expandedRow.value = expandedRow.value === commandId ? null : commandId
58
+ }
59
+
60
+ const columns: TableColumn<Command>[] = [
61
+ {
62
+ accessorKey: 'command',
63
+ header: 'Command'
64
+ },
65
+ {
66
+ accessorKey: 'department',
67
+ header: 'Department'
68
+ },
69
+ {
70
+ accessorKey: 'description',
71
+ header: 'Description'
72
+ }
73
+ ]
74
+ </script>
75
+
76
+ <template>
77
+ <UDashboardPanel id="commands">
78
+ <template #header>
79
+ <UDashboardNavbar title="Commands">
80
+ <template #leading>
81
+ <UDashboardSidebarCollapse />
82
+ </template>
83
+ <template #trailing>
84
+ <UBadge v-if="data?.total" :label="data.total" variant="subtle" />
85
+ </template>
86
+ </UDashboardNavbar>
87
+ </template>
88
+
89
+ <template #body>
90
+ <!-- Loading -->
91
+ <div v-if="status === 'pending'" class="flex items-center justify-center py-12">
92
+ <UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
93
+ </div>
94
+
95
+ <!-- Error -->
96
+ <div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
97
+ <UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
98
+ <p class="text-sm text-muted">Failed to load commands.</p>
99
+ <UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
100
+ </div>
101
+
102
+ <!-- Empty -->
103
+ <div v-else-if="!commands.length" class="flex flex-col items-center justify-center gap-4 py-12">
104
+ <UIcon name="i-lucide-terminal" class="size-12 text-muted" />
105
+ <p class="text-sm text-muted">No commands found.</p>
106
+ </div>
107
+
108
+ <!-- Content -->
109
+ <template v-else>
110
+ <div class="flex flex-wrap items-center gap-3 mb-4">
111
+ <UInput
112
+ v-model="search"
113
+ class="max-w-sm"
114
+ icon="i-lucide-search"
115
+ placeholder="Search commands..."
116
+ aria-label="Search commands by name or description"
117
+ />
118
+
119
+ <USelect
120
+ v-model="departmentFilter"
121
+ :items="departments"
122
+ :ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
123
+ placeholder="Department"
124
+ class="min-w-48"
125
+ aria-label="Filter by department"
126
+ />
127
+
128
+ <span class="ml-auto text-xs text-muted">
129
+ {{ totalFiltered }} command{{ totalFiltered !== 1 ? 's' : '' }}
130
+ </span>
131
+ </div>
132
+
133
+ <UTable
134
+ :data="paginatedCommands"
135
+ :columns="columns"
136
+ :loading="status === 'pending'"
137
+ class="shrink-0"
138
+ :ui="{
139
+ base: 'table-fixed border-separate border-spacing-0',
140
+ thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
141
+ tbody: '[&>tr]:last:[&>td]:border-b-0 [&>tr]:cursor-pointer [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
142
+ th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
143
+ td: 'border-b border-default'
144
+ }"
145
+ @select="(row: Command) => toggleExpand(row.id)"
146
+ >
147
+ <template #command-cell="{ row }">
148
+ <code class="font-mono text-sm text-primary">{{ row.original.command }}</code>
149
+ </template>
150
+ <template #department-cell="{ row }">
151
+ <UBadge :label="row.original.department" variant="subtle" size="sm" />
152
+ </template>
153
+ <template #expanded="{ row }">
154
+ <div v-if="expandedRow === row.original.id && row.original.keywords?.length" class="px-4 py-3 bg-elevated/30">
155
+ <p class="text-xs font-semibold text-muted uppercase tracking-wider mb-2">Keywords</p>
156
+ <div class="flex flex-wrap gap-1.5">
157
+ <UBadge
158
+ v-for="kw in row.original.keywords"
159
+ :key="kw"
160
+ :label="kw"
161
+ variant="outline"
162
+ size="xs"
163
+ />
164
+ </div>
165
+ </div>
166
+ </template>
167
+ </UTable>
168
+
169
+ <div v-if="totalPages > 1" class="flex items-center justify-center mt-6">
170
+ <UPagination
171
+ :page="page"
172
+ :total="totalFiltered"
173
+ :items-per-page="pageSize"
174
+ @update:page="(val) => page = val"
175
+ />
176
+ </div>
177
+ </template>
178
+ </template>
179
+ </UDashboardPanel>
180
+ </template>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ import type { HealthCheck } from '~/types'
3
+
4
+ const { fetchApi } = useApi()
5
+
6
+ const { data, status, error, refresh } = await fetchApi<{ checks: HealthCheck[], passed: number, total: number }>('/api/health')
7
+
8
+ const checks = computed(() => data.value?.checks ?? [])
9
+ const passed = computed(() => data.value?.passed ?? 0)
10
+ const total = computed(() => data.value?.total ?? 0)
11
+ const allPassed = computed(() => passed.value === total.value && total.value > 0)
12
+ </script>
13
+
14
+ <template>
15
+ <UDashboardPanel id="health">
16
+ <template #header>
17
+ <UDashboardNavbar title="Health Checks">
18
+ <template #leading>
19
+ <UDashboardSidebarCollapse />
20
+ </template>
21
+ <template #trailing>
22
+ <UBadge
23
+ v-if="data"
24
+ :label="`${passed}/${total}`"
25
+ :color="allPassed ? 'success' : 'warning'"
26
+ variant="subtle"
27
+ />
28
+ </template>
29
+ </UDashboardNavbar>
30
+ </template>
31
+
32
+ <template #body>
33
+ <!-- Loading -->
34
+ <div v-if="status === 'pending'" class="flex items-center justify-center py-12">
35
+ <UIcon name="i-lucide-loader-2" class="size-8 animate-spin text-muted" />
36
+ </div>
37
+
38
+ <!-- Error -->
39
+ <div v-else-if="error" class="flex flex-col items-center justify-center gap-4 py-12" role="alert">
40
+ <UIcon name="i-lucide-alert-triangle" class="size-12 text-red-500" />
41
+ <p class="text-sm text-muted">Failed to load health checks.</p>
42
+ <UButton label="Retry" variant="outline" color="primary" icon="i-lucide-refresh-cw" @click="refresh()" />
43
+ </div>
44
+
45
+ <!-- Empty -->
46
+ <div v-else-if="!checks.length" class="flex flex-col items-center justify-center gap-4 py-12">
47
+ <UIcon name="i-lucide-heart-pulse" class="size-12 text-muted" />
48
+ <p class="text-sm text-muted">No health checks available.</p>
49
+ </div>
50
+
51
+ <!-- Content -->
52
+ <template v-else>
53
+ <!-- Overall Status -->
54
+ <div
55
+ class="mb-6 rounded-lg border p-6 text-center"
56
+ :class="allPassed ? 'border-green-500/30 bg-green-500/5' : 'border-yellow-500/30 bg-yellow-500/5'"
57
+ >
58
+ <UIcon
59
+ :name="allPassed ? 'i-lucide-check-circle' : 'i-lucide-alert-circle'"
60
+ :class="allPassed ? 'text-green-500' : 'text-yellow-500'"
61
+ class="size-12"
62
+ />
63
+ <p class="mt-2 text-lg font-semibold text-highlighted">
64
+ {{ allPassed ? 'All Checks Passing' : `${total - passed} Check(s) Failing` }}
65
+ </p>
66
+ <p class="text-sm text-muted">{{ passed }} of {{ total }} checks passed</p>
67
+ </div>
68
+
69
+ <!-- Individual Checks -->
70
+ <div class="space-y-3">
71
+ <div
72
+ v-for="check in checks"
73
+ :key="check.name"
74
+ class="flex items-start gap-3 rounded-lg border border-default p-4"
75
+ >
76
+ <UIcon
77
+ :name="check.passed ? 'i-lucide-check-circle' : 'i-lucide-x-circle'"
78
+ :class="check.passed ? 'text-green-500' : 'text-red-500'"
79
+ class="mt-0.5 size-5 shrink-0"
80
+ />
81
+ <div class="flex-1">
82
+ <h4 class="font-medium text-highlighted">{{ check.name }}</h4>
83
+ <p v-if="!check.passed && check.fix" class="mt-1 text-sm text-muted">
84
+ Fix: {{ check.fix }}
85
+ </p>
86
+ </div>
87
+ <UBadge
88
+ :label="check.passed ? 'Pass' : 'Fail'"
89
+ :color="check.passed ? 'success' : 'error'"
90
+ variant="subtle"
91
+ size="sm"
92
+ />
93
+ </div>
94
+ </div>
95
+ </template>
96
+ </template>
97
+ </UDashboardPanel>
98
+ </template>