arkaos 2.3.0 → 2.3.2
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/arka/skills/conclave/SKILL.md +194 -0
- package/arka/skills/human-writing/SKILL.md +143 -0
- package/config/agent-memory-template.md +28 -0
- package/config/disc-profiles.json +108 -0
- package/config/disc-team-validator.sh +94 -0
- package/config/gotchas-fixes.json +148 -0
- package/config/profile-template.json +12 -0
- package/config/providers-registry.json +56 -0
- package/config/settings-template.json +42 -0
- package/config/standards/communication.md +64 -0
- package/config/standards/orchestration.md +91 -0
- package/config/statusline-v2.sh +101 -0
- package/config/statusline.sh +139 -0
- package/config/system-prompt.sh +190 -0
- package/dashboard/LICENSE +21 -0
- package/dashboard/README.md +64 -0
- package/dashboard/app/app.config.ts +8 -0
- package/dashboard/app/app.vue +42 -0
- package/dashboard/app/assets/css/main.css +18 -0
- package/dashboard/app/composables/useApi.ts +8 -0
- package/dashboard/app/composables/useDashboard.ts +19 -0
- package/dashboard/app/error.vue +24 -0
- package/dashboard/app/layouts/default.vue +114 -0
- package/dashboard/app/pages/agents/[id].vue +506 -0
- package/dashboard/app/pages/agents/index.vue +225 -0
- package/dashboard/app/pages/budget.vue +132 -0
- package/dashboard/app/pages/commands.vue +180 -0
- package/dashboard/app/pages/health.vue +98 -0
- package/dashboard/app/pages/index.vue +126 -0
- package/dashboard/app/pages/knowledge.vue +729 -0
- package/dashboard/app/pages/personas.vue +597 -0
- package/dashboard/app/pages/settings.vue +146 -0
- package/dashboard/app/pages/tasks.vue +203 -0
- package/dashboard/app/types/index.d.ts +181 -0
- package/dashboard/app/utils/index.ts +7 -0
- package/dashboard/nuxt.config.ts +39 -0
- package/dashboard/package.json +37 -0
- package/dashboard/pnpm-workspace.yaml +7 -0
- package/dashboard/tsconfig.json +10 -0
- package/knowledge/INDEX.md +34 -0
- package/knowledge/agents-registry.json +254 -0
- package/knowledge/channels-config.json +6 -0
- package/knowledge/commands-keywords.json +466 -0
- package/knowledge/commands-registry.json +2791 -0
- package/knowledge/commands-registry.json.bak +2791 -0
- package/knowledge/ecosystems.json +7 -0
- package/knowledge/obsidian-config.json +112 -0
- package/package.json +10 -6
- package/pyproject.toml +1 -1
- package/scripts/check-version.js +13 -0
- package/scripts/dashboard-api.py +636 -0
- package/scripts/knowledge-index.py +113 -0
- package/scripts/skill_validator.py +217 -0
- package/scripts/start-dashboard.sh +103 -0
- package/scripts/synapse-bridge.py +199 -0
- package/scripts/tools/brand_voice_analyzer.py +192 -0
- package/scripts/tools/dcf_calculator.py +168 -0
- package/scripts/tools/headline_scorer.py +215 -0
- package/scripts/tools/okr_cascade.py +207 -0
- package/scripts/tools/rice_prioritizer.py +230 -0
- package/scripts/tools/saas_metrics.py +234 -0
- package/scripts/tools/seo_checker.py +197 -0
- 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>
|