arkaos 4.13.1 → 4.13.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.
Files changed (35) hide show
  1. package/VERSION +1 -1
  2. package/bin/arka-doctor +15 -1
  3. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  4. package/core/governance/evidence_checks.py +85 -4
  5. package/core/workflow/__pycache__/design_authorization.cpython-313.pyc +0 -0
  6. package/core/workflow/__pycache__/frontend_gate.cpython-313.pyc +0 -0
  7. package/core/workflow/design_authorization.py +106 -0
  8. package/core/workflow/frontend_gate.py +54 -22
  9. package/dashboard/app/app.config.ts +2 -2
  10. package/dashboard/app/assets/css/main.css +285 -13
  11. package/dashboard/app/components/ArkaConstellation.vue +239 -0
  12. package/dashboard/app/components/ArkaCountUp.vue +35 -0
  13. package/dashboard/app/components/ArkaGlowCard.vue +25 -0
  14. package/dashboard/app/components/ArkaLiveFeed.vue +122 -0
  15. package/dashboard/app/components/ArkaNavGroupLabel.vue +11 -0
  16. package/dashboard/app/components/ArkaPageHero.vue +42 -0
  17. package/dashboard/app/components/ArkaSection.vue +20 -0
  18. package/dashboard/app/components/ArkaStarfield.vue +3 -0
  19. package/dashboard/app/components/ArkaStatCard.vue +38 -0
  20. package/dashboard/app/components/ArkaSystemPill.vue +54 -0
  21. package/dashboard/app/components/ArkaTrendChart.vue +68 -0
  22. package/dashboard/app/composables/useCountUp.ts +30 -0
  23. package/dashboard/app/composables/useDashboard.ts +2 -1
  24. package/dashboard/app/composables/useTaskStream.ts +39 -0
  25. package/dashboard/app/layouts/default.vue +11 -2
  26. package/dashboard/app/pages/design-lab.vue +257 -0
  27. package/dashboard/app/pages/index.vue +268 -213
  28. package/dashboard/nuxt.config.ts +21 -0
  29. package/dashboard/package.json +1 -0
  30. package/departments/content/skills/video-produce/SKILL.md +9 -1
  31. package/departments/content/skills/video-setup/SKILL.md +15 -4
  32. package/installer/doctor.js +13 -8
  33. package/package.json +1 -1
  34. package/pyproject.toml +1 -1
  35. package/scripts/start-dashboard.sh +3 -1
@@ -1,10 +1,8 @@
1
1
  <script setup lang="ts">
2
- // PR66 v2.83.0 Command center.
3
- //
4
- // Replaces the 6-stat-card Overview that just counted things you
5
- // already knew (agents=62, skills=256, ...) with telemetry-driven
6
- // information the operator actually uses: greeting, today's cost,
7
- // per-project state, recent incidents, quick actions.
2
+ // Mission Control (Pulse v2). Editorial sci-fi recomposition of the
3
+ // command center: numbered sections, count-up telemetry, agent
4
+ // constellation, live signal feed. Primary gate stays on
5
+ // /api/overview/command-center; section fetches are non-blocking.
8
6
 
9
7
  interface ProjectRow {
10
8
  name: string
@@ -67,15 +65,39 @@ interface CommandCenterPayload {
67
65
  quick_actions: QuickAction[]
68
66
  }
69
67
 
68
+ interface AgentRow {
69
+ id: string
70
+ name: string
71
+ department: string
72
+ tier: number
73
+ }
74
+
75
+ interface DeptRow {
76
+ department: string
77
+ agent_count: number
78
+ calls_30d: number
79
+ cost_usd_30d: number | null
80
+ }
81
+
82
+ interface TrendDay {
83
+ date: string
84
+ cost_usd: number
85
+ }
86
+
70
87
  const { fetchApi } = useApi()
71
88
 
72
89
  const {
73
90
  data,
74
91
  status,
75
92
  error,
76
- refresh,
93
+ refresh
77
94
  } = await fetchApi<CommandCenterPayload>('/api/overview/command-center')
78
95
 
96
+ // Section fetches — lazy so a slow endpoint never blocks the hero.
97
+ const { data: agentsData } = fetchApi<{ agents: AgentRow[] }>('/api/agents', { lazy: true })
98
+ const { data: deptsData } = fetchApi<{ departments: DeptRow[] }>('/api/departments', { lazy: true })
99
+ const { data: trendData } = fetchApi<{ days: TrendDay[] }>('/api/llm-costs/trend?days=30', { lazy: true })
100
+
79
101
  const greetingLabel = computed(() => {
80
102
  const name = data.value?.greeting?.name?.trim()
81
103
  const language = data.value?.greeting?.language ?? 'en'
@@ -85,16 +107,18 @@ const greetingLabel = computed(() => {
85
107
 
86
108
  const todayCost = computed(() => data.value?.today_cost)
87
109
 
88
- function formatCost(value: number | null | undefined): string {
89
- if (value === null || value === undefined) return 'n/a'
90
- if (value === 0) return '$0'
91
- if (value < 0.01) return `$${value.toFixed(4)}`
92
- return `$${value.toFixed(2)}`
93
- }
110
+ const trendSeries = computed(() =>
111
+ (trendData.value?.days ?? []).map(d => ({ date: d.date, value: d.cost_usd ?? 0 }))
112
+ )
113
+
114
+ const constellationAgents = computed(() => agentsData.value?.agents ?? [])
115
+ const constellationDepts = computed(() =>
116
+ (deptsData.value?.departments ?? []).map(d => ({ department: d.department, calls_30d: d.calls_30d }))
117
+ )
94
118
 
95
119
  function formatTokens(n: number): string {
96
120
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
97
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
121
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`
98
122
  return n.toString()
99
123
  }
100
124
 
@@ -108,13 +132,13 @@ function statusColor(status: string): 'success' | 'warning' | 'neutral' | 'error
108
132
  }
109
133
  }
110
134
 
111
- function commitFreshness(days: number | null): { color: string; label: string } {
135
+ function commitFreshness(days: number | null): { color: string, label: string } {
112
136
  if (days === null) return { color: 'text-muted', label: 'no git' }
113
- if (days === 0) return { color: 'text-green-500', label: 'today' }
114
- if (days === 1) return { color: 'text-green-500', label: '1 day ago' }
137
+ if (days === 0) return { color: 'text-primary', label: 'today' }
138
+ if (days === 1) return { color: 'text-primary', label: '1 day ago' }
115
139
  if (days < 7) return { color: 'text-primary', label: `${days} days ago` }
116
- if (days < 30) return { color: 'text-yellow-500', label: `${days} days ago` }
117
- return { color: 'text-red-500', label: `${days} days ago` }
140
+ if (days < 30) return { color: 'text-warning', label: `${days} days ago` }
141
+ return { color: 'text-error', label: `${days} days ago` }
118
142
  }
119
143
 
120
144
  function formatIncidentTs(iso: string): string {
@@ -124,7 +148,7 @@ function formatIncidentTs(iso: string): string {
124
148
  month: 'short',
125
149
  day: 'numeric',
126
150
  hour: '2-digit',
127
- minute: '2-digit',
151
+ minute: '2-digit'
128
152
  }).format(new Date(iso))
129
153
  } catch {
130
154
  return iso
@@ -146,7 +170,7 @@ function copyCommand(cmd: string) {
146
170
  <template>
147
171
  <UDashboardPanel id="overview">
148
172
  <template #header>
149
- <UDashboardNavbar title="Command Center">
173
+ <UDashboardNavbar title="Mission Control">
150
174
  <template #leading>
151
175
  <UDashboardSidebarCollapse />
152
176
  </template>
@@ -166,157 +190,135 @@ function copyCommand(cmd: string) {
166
190
  <DashboardState
167
191
  :status="status"
168
192
  :error="error"
169
- loading-label="Loading command center"
193
+ loading-label="Loading mission control"
170
194
  :on-retry="() => refresh()"
171
195
  >
172
- <div class="space-y-6">
173
- <!-- Hero: greeting + today's cost -->
174
- <UCard>
175
- <div class="flex flex-col gap-2 md:flex-row md:items-baseline md:justify-between">
176
- <div>
177
- <h1 class="text-2xl font-bold">{{ greetingLabel }}.</h1>
178
- <p class="text-sm text-muted mt-1">
179
- <template v-if="data?.greeting?.role && data?.greeting?.company">
180
- {{ data.greeting.role }} @ {{ data.greeting.company }}
181
- </template>
182
- <template v-else>
183
- Set your profile in Settings to personalise this view.
184
- </template>
185
- </p>
186
- </div>
187
- <div v-if="todayCost" class="flex items-baseline gap-6 text-right">
188
- <div>
189
- <p class="text-xs font-semibold text-muted uppercase tracking-wider">
190
- Today's cost
191
- </p>
192
- <p class="text-2xl font-bold">{{ formatCost(todayCost.total_usd) }}</p>
196
+ <div class="mx-auto w-full max-w-6xl space-y-12 py-2">
197
+ <!-- I. STATUS -->
198
+ <div class="space-y-6">
199
+ <ArkaPageHero
200
+ numeral="I"
201
+ label="mission control"
202
+ :title="greetingLabel"
203
+ :subtitle="data?.greeting?.role && data?.greeting?.company
204
+ ? `${data.greeting.role} @ ${data.greeting.company} — status of every agent, every memory, every signal.`
205
+ : 'Status of every agent, every memory, every signal.'"
206
+ >
207
+ <template #stats>
208
+ <div class="grid grid-cols-2 gap-3 pt-4 lg:grid-cols-4">
209
+ <ArkaStatCard
210
+ label="today’s cost"
211
+ :value="todayCost?.total_usd ?? null"
212
+ format="currency"
213
+ :decimals="2"
214
+ accent
215
+ />
216
+ <ArkaStatCard
217
+ label="calls"
218
+ :value="todayCost?.call_count ?? null"
219
+ />
220
+ <ArkaStatCard
221
+ label="tokens in / out"
222
+ :value="(todayCost?.tokens_in ?? 0) + (todayCost?.tokens_out ?? 0)"
223
+ format="tokens"
224
+ :hint="todayCost ? `${formatTokens(todayCost.tokens_in ?? 0)} in · ${formatTokens(todayCost.tokens_out ?? 0)} out` : ''"
225
+ />
226
+ <ArkaStatCard
227
+ label="cache hit rate"
228
+ :value="todayCost?.cache_hit_rate ?? null"
229
+ format="percent"
230
+ />
193
231
  </div>
194
- <div>
195
- <p class="text-xs font-semibold text-muted uppercase tracking-wider">
196
- Calls
197
- </p>
198
- <p class="text-2xl font-bold">{{ todayCost.call_count }}</p>
199
- </div>
200
- <div>
201
- <p class="text-xs font-semibold text-muted uppercase tracking-wider">
202
- Cache
203
- </p>
204
- <p class="text-2xl font-bold">
205
- {{ (todayCost.cache_hit_rate * 100).toFixed(0) }}%
206
- </p>
207
- </div>
208
- </div>
209
- </div>
210
- </UCard>
232
+ </template>
233
+ </ArkaPageHero>
211
234
 
212
- <!-- PR91a v3.35.0 — Agent gap suggestions -->
213
- <AgentSuggestionsCard class="mb-6" />
235
+ <div class="arka-pulse-line" aria-hidden="true" />
214
236
 
215
- <!-- PR84d v3.10.0 Top departments + Recent personas row -->
216
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
217
- <div>
218
- <h2 class="text-sm font-semibold uppercase tracking-wider text-muted mb-3">
219
- Top departments (30d)
220
- </h2>
221
- <DashboardState
222
- :status="status"
223
- :empty="!data?.top_departments_30d?.length"
224
- empty-title="No telemetry yet"
225
- empty-description="Department spend will appear once agents start running."
226
- empty-icon="i-lucide-bar-chart"
227
- >
228
- <div class="space-y-2">
229
- <div
230
- v-for="(d, idx) in data?.top_departments_30d"
231
- :key="d.department"
232
- class="rounded-lg border border-default p-3 flex items-center gap-3"
233
- >
234
- <span class="text-xs font-mono font-semibold text-muted w-6">#{{ idx + 1 }}</span>
235
- <span class="flex-1 font-semibold capitalize">{{ d.department }}</span>
236
- <span class="text-xs font-mono text-muted">{{ d.calls }} calls</span>
237
- <span class="text-sm font-mono font-semibold">
238
- {{ d.cost_usd === null ? '—' : `$${d.cost_usd.toFixed(2)}` }}
239
- </span>
240
- </div>
241
- </div>
242
- </DashboardState>
243
- </div>
237
+ <ArkaGlowCard v-if="trendSeries.length" :padded="false" class="px-2 pt-4 pb-1">
238
+ <div class="flex items-center justify-between px-3 pb-2">
239
+ <span class="arka-eyebrow">cumulative spend · 30d</span>
240
+ <span class="arka-data text-xs text-muted">
241
+ ${{ trendSeries.reduce((s, d) => s + d.value, 0).toFixed(2) }} total
242
+ </span>
243
+ </div>
244
+ <ClientOnly>
245
+ <ArkaTrendChart :series="trendSeries" :height="150" />
246
+ </ClientOnly>
247
+ </ArkaGlowCard>
248
+ </div>
244
249
 
245
- <div>
246
- <h2 class="text-sm font-semibold uppercase tracking-wider text-muted mb-3">
247
- Recent personas
248
- </h2>
249
- <DashboardState
250
- :status="status"
251
- :empty="!data?.recent_personas?.length"
252
- empty-title="No personas yet"
253
- empty-description="Create one from /personas → New Persona."
254
- empty-icon="i-lucide-user-plus"
255
- >
256
- <div class="space-y-2">
257
- <NuxtLink
258
- v-for="p in data?.recent_personas"
259
- :key="p.id"
260
- :to="`/personas/${p.id}`"
261
- class="block rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
262
- >
263
- <div class="flex items-center justify-between gap-3">
264
- <div class="min-w-0">
265
- <p class="text-sm font-semibold truncate">{{ p.name }}</p>
266
- <p class="text-xs text-muted truncate">{{ p.title || '—' }}</p>
267
- </div>
268
- <div class="flex items-center gap-2 shrink-0">
269
- <UBadge
270
- v-if="p.mbti"
271
- :label="p.mbti"
272
- variant="subtle"
273
- size="xs"
274
- />
275
- <UBadge
276
- v-if="p.source_store === 'obsidian'"
277
- icon="i-lucide-file-text"
278
- label="Obsidian"
279
- color="primary"
280
- variant="soft"
281
- size="xs"
282
- />
283
- </div>
284
- </div>
285
- </NuxtLink>
250
+ <!-- II. AGENT CONSTELLATION -->
251
+ <ArkaSection numeral="II" label="agent constellation">
252
+ <template #actions>
253
+ <NuxtLink to="/agents" class="arka-data text-xs text-muted transition-colors hover:text-primary">
254
+ open agents →
255
+ </NuxtLink>
256
+ </template>
257
+ <ArkaGlowCard :padded="false">
258
+ <ClientOnly>
259
+ <ArkaConstellation
260
+ v-if="constellationAgents.length"
261
+ :agents="constellationAgents"
262
+ :departments="constellationDepts"
263
+ :height="380"
264
+ />
265
+ <div v-else class="flex h-64 items-center justify-center">
266
+ <p class="arka-data text-xs text-muted">
267
+ mapping the constellation…
268
+ </p>
286
269
  </div>
287
- </DashboardState>
270
+ </ClientOnly>
271
+ </ArkaGlowCard>
272
+ <div v-if="data?.top_departments_30d?.length" class="flex flex-wrap gap-2">
273
+ <NuxtLink
274
+ v-for="(d, idx) in data.top_departments_30d"
275
+ :key="d.department"
276
+ :to="`/departments/${d.department}`"
277
+ class="flex items-center gap-2 rounded-full border border-default bg-elevated px-3 py-1.5 transition-colors hover:border-primary/40"
278
+ >
279
+ <span class="arka-data text-[10px] text-muted">#{{ idx + 1 }}</span>
280
+ <span class="text-xs font-medium capitalize">{{ d.department }}</span>
281
+ <span class="arka-data text-[10px] text-muted">{{ d.calls }} calls</span>
282
+ <span class="arka-data text-[10px]" :class="d.cost_usd === null ? 'text-muted' : 'text-primary'">
283
+ {{ d.cost_usd === null ? '—' : `$${d.cost_usd.toFixed(2)}` }}
284
+ </span>
285
+ </NuxtLink>
288
286
  </div>
289
- </div>
287
+ </ArkaSection>
290
288
 
291
- <!-- Two columns: projects + incidents -->
292
- <div class="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-6">
293
- <!-- Projects -->
294
- <div>
295
- <div class="flex items-baseline justify-between mb-3">
296
- <h2 class="text-sm font-semibold uppercase tracking-wider text-muted">
297
- Projects
298
- </h2>
299
- <NuxtLink to="/settings" class="text-xs text-muted hover:text-primary">
300
- Configure dirs →
301
- </NuxtLink>
302
- </div>
303
- <DashboardState
304
- :status="status"
305
- :empty="!data?.projects?.length"
306
- empty-title="No projects discovered yet"
307
- empty-description="Add your project directories in Settings → Projects."
308
- empty-icon="i-lucide-folder-open"
309
- >
310
- <div class="space-y-2">
311
- <div
289
+ <!-- III. LIVE FEED -->
290
+ <ArkaSection numeral="III" label="live feed">
291
+ <template #actions>
292
+ <span class="flex items-center gap-2">
293
+ <span class="arka-live-dot" />
294
+ <span class="arka-data text-[10px] text-muted uppercase">live</span>
295
+ </span>
296
+ </template>
297
+ <ArkaLiveFeed />
298
+ </ArkaSection>
299
+
300
+ <!-- IV. PROJECTS & ACTIONS -->
301
+ <ArkaSection numeral="IV" label="projects & actions">
302
+ <AgentSuggestionsCard />
303
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-[2fr_1fr]">
304
+ <div class="space-y-2">
305
+ <DashboardState
306
+ :status="status"
307
+ :empty="!data?.projects?.length"
308
+ empty-title="No projects discovered yet"
309
+ empty-description="Add your project directories in Settings → Projects."
310
+ empty-icon="i-lucide-folder-open"
311
+ >
312
+ <ArkaGlowCard
312
313
  v-for="p in data?.projects"
313
314
  :key="p.name"
314
- class="rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
315
+ interactive
316
+ class="mb-2"
315
317
  >
316
318
  <div class="flex items-start justify-between gap-3">
317
319
  <div class="min-w-0 flex-1">
318
- <div class="flex items-center gap-2 mb-1">
319
- <span class="text-sm font-semibold truncate">{{ p.name }}</span>
320
+ <div class="mb-1 flex items-center gap-2">
321
+ <span class="truncate text-sm font-semibold">{{ p.name }}</span>
320
322
  <UBadge
321
323
  v-if="p.status"
322
324
  :label="p.status"
@@ -332,8 +334,10 @@ function copyCommand(cmd: string) {
332
334
  size="xs"
333
335
  />
334
336
  </div>
335
- <p class="text-xs text-muted font-mono truncate">{{ p.path }}</p>
336
- <div class="flex items-center gap-2 mt-2 flex-wrap">
337
+ <p class="arka-data truncate text-xs text-muted">
338
+ {{ p.path }}
339
+ </p>
340
+ <div class="mt-2 flex flex-wrap items-center gap-2">
337
341
  <UBadge
338
342
  v-for="s in p.stack"
339
343
  :key="s"
@@ -344,75 +348,126 @@ function copyCommand(cmd: string) {
344
348
  </div>
345
349
  </div>
346
350
  <span
347
- class="text-xs font-mono shrink-0"
351
+ class="arka-data shrink-0 text-xs"
348
352
  :class="commitFreshness(p.last_commit_days).color"
349
353
  >
350
354
  {{ commitFreshness(p.last_commit_days).label }}
351
355
  </span>
352
356
  </div>
353
- </div>
354
- </div>
355
- </DashboardState>
356
- </div>
357
+ </ArkaGlowCard>
358
+ </DashboardState>
359
+ </div>
357
360
 
358
- <!-- Incidents + Quick actions -->
359
- <div class="space-y-6">
360
- <div>
361
- <h2 class="text-sm font-semibold uppercase tracking-wider text-muted mb-3">
362
- Recent incidents
363
- </h2>
364
- <DashboardState
365
- :status="status"
366
- :empty="!data?.recent_incidents?.length"
367
- empty-title="No incidents"
368
- empty-description="Bypass uses and flow blocks show up here."
369
- empty-icon="i-lucide-shield-check"
370
- >
371
- <div class="space-y-2">
372
- <div
373
- v-for="(i, idx) in data?.recent_incidents"
374
- :key="idx"
375
- class="rounded-lg border border-default p-3"
361
+ <div class="space-y-6">
362
+ <div>
363
+ <p class="arka-eyebrow mb-3">
364
+ quick launch
365
+ </p>
366
+ <div class="space-y-1.5">
367
+ <button
368
+ v-for="a in data?.quick_actions"
369
+ :key="a.command"
370
+ type="button"
371
+ class="arka-glow-card w-full p-3 text-left"
372
+ data-interactive="true"
373
+ @click="copyCommand(a.command)"
376
374
  >
377
- <div class="flex items-center gap-2 mb-1">
378
- <UBadge
379
- :label="i.kind"
380
- :color="i.kind === 'bypass' ? 'warning' : 'error'"
381
- variant="subtle"
382
- size="xs"
383
- />
384
- <span class="text-xs text-muted">{{ formatIncidentTs(i.ts) }}</span>
375
+ <div class="flex items-center gap-2">
376
+ <code class="arka-data text-sm font-semibold text-primary">{{ a.command }}</code>
377
+ <UIcon name="i-lucide-clipboard" class="ml-auto size-3 text-muted" />
385
378
  </div>
386
- <p class="text-xs font-mono text-muted truncate" :title="i.reason">
387
- {{ i.tool }} — {{ i.reason }}
379
+ <p class="mt-1 text-xs text-muted">
380
+ {{ a.description }}
388
381
  </p>
389
- </div>
382
+ </button>
390
383
  </div>
391
- </DashboardState>
392
- </div>
384
+ </div>
393
385
 
394
- <div>
395
- <h2 class="text-sm font-semibold uppercase tracking-wider text-muted mb-3">
396
- Quick actions
397
- </h2>
398
- <div class="space-y-1">
399
- <button
400
- v-for="a in data?.quick_actions"
401
- :key="a.command"
402
- type="button"
403
- class="w-full text-left p-3 rounded-lg border border-default hover:border-primary/40 hover:bg-elevated/30 transition-colors"
404
- @click="copyCommand(a.command)"
386
+ <div>
387
+ <p class="arka-eyebrow mb-3">
388
+ recent incidents
389
+ </p>
390
+ <DashboardState
391
+ :status="status"
392
+ :empty="!data?.recent_incidents?.length"
393
+ empty-title="No incidents"
394
+ empty-description="Bypass uses and flow blocks show up here."
395
+ empty-icon="i-lucide-shield-check"
405
396
  >
406
- <div class="flex items-center gap-2">
407
- <code class="text-sm font-mono font-semibold text-primary">{{ a.command }}</code>
408
- <UIcon name="i-lucide-clipboard" class="size-3 text-muted ml-auto" />
397
+ <div class="space-y-1.5">
398
+ <div
399
+ v-for="(i, idx) in data?.recent_incidents"
400
+ :key="idx"
401
+ class="rounded-lg border border-default p-3"
402
+ >
403
+ <div class="mb-1 flex items-center gap-2">
404
+ <UBadge
405
+ :label="i.kind"
406
+ :color="i.kind === 'bypass' ? 'warning' : 'error'"
407
+ variant="subtle"
408
+ size="xs"
409
+ />
410
+ <span class="arka-data text-xs text-muted">{{ formatIncidentTs(i.ts) }}</span>
411
+ </div>
412
+ <p class="arka-data truncate text-xs text-muted" :title="i.reason">
413
+ {{ i.tool }} — {{ i.reason }}
414
+ </p>
415
+ </div>
409
416
  </div>
410
- <p class="text-xs text-muted mt-1">{{ a.description }}</p>
411
- </button>
417
+ </DashboardState>
418
+ </div>
419
+
420
+ <div>
421
+ <p class="arka-eyebrow mb-3">
422
+ recent personas
423
+ </p>
424
+ <DashboardState
425
+ :status="status"
426
+ :empty="!data?.recent_personas?.length"
427
+ empty-title="No personas yet"
428
+ empty-description="Create one from /personas → New Persona."
429
+ empty-icon="i-lucide-user-plus"
430
+ >
431
+ <div class="space-y-1.5">
432
+ <NuxtLink
433
+ v-for="p in data?.recent_personas"
434
+ :key="p.id"
435
+ :to="`/personas/${p.id}`"
436
+ class="block rounded-lg border border-default p-3 transition-colors hover:border-primary/40"
437
+ >
438
+ <div class="flex items-center justify-between gap-3">
439
+ <div class="min-w-0">
440
+ <p class="truncate text-sm font-semibold">
441
+ {{ p.name }}
442
+ </p>
443
+ <p class="truncate text-xs text-muted">
444
+ {{ p.title || '—' }}
445
+ </p>
446
+ </div>
447
+ <div class="flex shrink-0 items-center gap-2">
448
+ <UBadge
449
+ v-if="p.mbti"
450
+ :label="p.mbti"
451
+ variant="subtle"
452
+ size="xs"
453
+ />
454
+ <UBadge
455
+ v-if="p.source_store === 'obsidian'"
456
+ icon="i-lucide-file-text"
457
+ label="Obsidian"
458
+ color="primary"
459
+ variant="soft"
460
+ size="xs"
461
+ />
462
+ </div>
463
+ </div>
464
+ </NuxtLink>
465
+ </div>
466
+ </DashboardState>
412
467
  </div>
413
468
  </div>
414
469
  </div>
415
- </div>
470
+ </ArkaSection>
416
471
  </div>
417
472
  </DashboardState>
418
473
  </template>
@@ -2,6 +2,7 @@
2
2
  export default defineNuxtConfig({
3
3
  modules: [
4
4
  '@nuxt/eslint',
5
+ '@nuxt/fonts',
5
6
  '@nuxt/ui',
6
7
  '@vueuse/nuxt'
7
8
  ],
@@ -12,8 +13,20 @@ export default defineNuxtConfig({
12
13
  enabled: true
13
14
  },
14
15
 
16
+ app: {
17
+ pageTransition: {
18
+ name: 'arka-page',
19
+ mode: 'out-in'
20
+ }
21
+ },
22
+
15
23
  css: ['~/assets/css/main.css'],
16
24
 
25
+ colorMode: {
26
+ preference: 'dark',
27
+ fallback: 'dark'
28
+ },
29
+
17
30
  runtimeConfig: {
18
31
  public: {
19
32
  apiBase: 'http://localhost:3334'
@@ -35,5 +48,13 @@ export default defineNuxtConfig({
35
48
  braceStyle: '1tbs'
36
49
  }
37
50
  }
51
+ },
52
+
53
+ fonts: {
54
+ families: [
55
+ // Explicit entry guarantees the italic axis (Instrument Serif ships
56
+ // 400 normal+italic only); the other families auto-resolve from @theme.
57
+ { name: 'Instrument Serif', provider: 'google', weights: [400], styles: ['normal', 'italic'] }
58
+ ]
38
59
  }
39
60
  })
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@nuxt/eslint": "^1.15.2",
38
+ "@nuxt/fonts": "^0.14.0",
38
39
  "@playwright/test": "^1.60.0",
39
40
  "eslint": "^10.1.0",
40
41
  "typescript": "^6.0.2",