@ash-ai/dashboard 0.0.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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/app/agents/page.tsx +408 -0
  3. package/app/analytics/page.tsx +226 -0
  4. package/app/globals.css +33 -0
  5. package/app/layout.tsx +38 -0
  6. package/app/logs/page.tsx +233 -0
  7. package/app/page.tsx +140 -0
  8. package/app/playground/page.tsx +44 -0
  9. package/app/queue/page.tsx +295 -0
  10. package/app/sessions/page.tsx +529 -0
  11. package/app/settings/api-keys/page.tsx +222 -0
  12. package/app/settings/credentials/page.tsx +250 -0
  13. package/components/nav.tsx +151 -0
  14. package/components/providers.tsx +18 -0
  15. package/components/ui/badge.tsx +43 -0
  16. package/components/ui/button.tsx +44 -0
  17. package/components/ui/card.tsx +43 -0
  18. package/components/ui/empty-state.tsx +20 -0
  19. package/components/ui/input.tsx +36 -0
  20. package/components/ui/select.tsx +50 -0
  21. package/components/ui/shimmer.tsx +53 -0
  22. package/lib/client.ts +41 -0
  23. package/lib/exports.ts +55 -0
  24. package/lib/hooks.ts +169 -0
  25. package/lib/utils.ts +44 -0
  26. package/next.config.ts +28 -0
  27. package/out/404/index.html +1 -0
  28. package/out/404.html +1 -0
  29. package/out/_next/static/J9asKIV7Gq221ygeAP958/_buildManifest.js +1 -0
  30. package/out/_next/static/J9asKIV7Gq221ygeAP958/_ssgManifest.js +1 -0
  31. package/out/_next/static/chunks/322-bab4df5c5188e993.js +1 -0
  32. package/out/_next/static/chunks/432-11ec8af7ccfbd019.js +1 -0
  33. package/out/_next/static/chunks/447.6d3368efa2d996b0.js +1 -0
  34. package/out/_next/static/chunks/513-c4683887323154aa.js +1 -0
  35. package/out/_next/static/chunks/522-cf174cf1bbbe9557.js +1 -0
  36. package/out/_next/static/chunks/53-b012ce05184a4754.js +1 -0
  37. package/out/_next/static/chunks/929-6faf1adeb65ee383.js +1 -0
  38. package/out/_next/static/chunks/app/_not-found/page-04f9d3958a76bc38.js +1 -0
  39. package/out/_next/static/chunks/app/agents/page-8d68e3019b4d7077.js +1 -0
  40. package/out/_next/static/chunks/app/analytics/page-6b725a46e9c48019.js +1 -0
  41. package/out/_next/static/chunks/app/layout-9cae773a790a15b2.js +1 -0
  42. package/out/_next/static/chunks/app/logs/page-2efd945345a44a0e.js +1 -0
  43. package/out/_next/static/chunks/app/page-06f62e11f9cd82d5.js +1 -0
  44. package/out/_next/static/chunks/app/playground/page-10d3461f118bfb21.js +1 -0
  45. package/out/_next/static/chunks/app/queue/page-38e79b84cbd59335.js +1 -0
  46. package/out/_next/static/chunks/app/sessions/page-2a67c9eddfac029e.js +1 -0
  47. package/out/_next/static/chunks/app/settings/api-keys/page-619682cf8a1c26eb.js +1 -0
  48. package/out/_next/static/chunks/app/settings/credentials/page-106d0ba4f12afe81.js +1 -0
  49. package/out/_next/static/chunks/b59f762a-cea625f74e98e0aa.js +1 -0
  50. package/out/_next/static/chunks/framework-5a02266cf144994c.js +1 -0
  51. package/out/_next/static/chunks/main-994a6af9cdcd7fb9.js +1 -0
  52. package/out/_next/static/chunks/main-app-5bcd4dcc44c4ae09.js +1 -0
  53. package/out/_next/static/chunks/pages/_app-9fd734050704698a.js +1 -0
  54. package/out/_next/static/chunks/pages/_error-310ed5880fd5d5e6.js +1 -0
  55. package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  56. package/out/_next/static/chunks/webpack-a50a78a04aed446d.js +1 -0
  57. package/out/_next/static/css/4b2beada31dbc623.css +1 -0
  58. package/out/agents/index.html +1 -0
  59. package/out/agents/index.txt +22 -0
  60. package/out/analytics/index.html +1 -0
  61. package/out/analytics/index.txt +22 -0
  62. package/out/index.html +1 -0
  63. package/out/index.txt +22 -0
  64. package/out/logs/index.html +1 -0
  65. package/out/logs/index.txt +22 -0
  66. package/out/playground/index.html +1 -0
  67. package/out/playground/index.txt +22 -0
  68. package/out/queue/index.html +1 -0
  69. package/out/queue/index.txt +22 -0
  70. package/out/sessions/index.html +1 -0
  71. package/out/sessions/index.txt +22 -0
  72. package/out/settings/api-keys/index.html +1 -0
  73. package/out/settings/api-keys/index.txt +22 -0
  74. package/out/settings/credentials/index.html +1 -0
  75. package/out/settings/credentials/index.txt +22 -0
  76. package/package.json +40 -0
  77. package/postcss.config.mjs +7 -0
  78. package/tsconfig.json +27 -0
@@ -0,0 +1,44 @@
1
+ import { cn } from '@/lib/utils'
2
+ import { forwardRef } from 'react'
3
+
4
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
6
+ size?: 'sm' | 'md' | 'lg'
7
+ }
8
+
9
+ const variantStyles = {
10
+ primary:
11
+ 'bg-indigo-500 text-white font-medium hover:bg-indigo-400',
12
+ secondary:
13
+ 'border border-white/20 bg-white/5 text-white hover:bg-white/10 hover:border-white/30',
14
+ ghost: 'text-white/70 hover:text-white hover:bg-white/10',
15
+ danger:
16
+ 'bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30',
17
+ }
18
+
19
+ const sizeStyles = {
20
+ sm: 'h-8 px-3 text-xs rounded-lg',
21
+ md: 'h-9 px-4 text-sm rounded-xl',
22
+ lg: 'h-10 px-6 text-sm rounded-xl',
23
+ }
24
+
25
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
26
+ ({ className, variant = 'primary', size = 'md', disabled, ...props }, ref) => {
27
+ return (
28
+ <button
29
+ ref={ref}
30
+ className={cn(
31
+ 'inline-flex items-center justify-center font-medium transition-all duration-200',
32
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/50',
33
+ 'disabled:pointer-events-none disabled:opacity-50',
34
+ variantStyles[variant],
35
+ sizeStyles[size],
36
+ className
37
+ )}
38
+ disabled={disabled}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+ )
44
+ Button.displayName = 'Button'
@@ -0,0 +1,43 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface CardProps {
4
+ children: React.ReactNode
5
+ className?: string
6
+ }
7
+
8
+ export function Card({ children, className }: CardProps) {
9
+ return (
10
+ <div
11
+ className={cn(
12
+ 'rounded-2xl border border-white/10 bg-[#1c2129] transition-all duration-200',
13
+ className
14
+ )}
15
+ >
16
+ {children}
17
+ </div>
18
+ )
19
+ }
20
+
21
+ export function CardHeader({ children, className }: CardProps) {
22
+ return (
23
+ <div className={cn('flex flex-col space-y-1.5 px-4 pt-4 pb-0 sm:px-6 sm:pt-6 sm:pb-0', className)}>
24
+ {children}
25
+ </div>
26
+ )
27
+ }
28
+
29
+ export function CardTitle({ children, className }: CardProps) {
30
+ return (
31
+ <h3 className={cn('text-lg font-semibold leading-none tracking-tight text-white', className)}>
32
+ {children}
33
+ </h3>
34
+ )
35
+ }
36
+
37
+ export function CardContent({ children, className }: CardProps) {
38
+ return (
39
+ <div className={cn('p-4 sm:p-6', className)}>
40
+ {children}
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,20 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface EmptyStateProps {
4
+ icon?: React.ReactNode
5
+ title: string
6
+ description: string
7
+ action?: React.ReactNode
8
+ className?: string
9
+ }
10
+
11
+ export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
12
+ return (
13
+ <div className={cn('flex flex-col items-center justify-center py-16 text-center', className)}>
14
+ {icon && <div className="mb-4 text-white/30">{icon}</div>}
15
+ <h3 className="text-lg font-semibold text-white">{title}</h3>
16
+ <p className="mt-1 max-w-sm text-sm text-white/50">{description}</p>
17
+ {action && <div className="mt-4">{action}</div>}
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,36 @@
1
+ import { cn } from '@/lib/utils'
2
+ import { forwardRef } from 'react'
3
+
4
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
5
+ label?: string
6
+ error?: string
7
+ }
8
+
9
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
10
+ ({ className, label, error, id, ...props }, ref) => {
11
+ return (
12
+ <div className="space-y-1.5">
13
+ {label && (
14
+ <label htmlFor={id} className="block text-sm font-medium text-white/70">
15
+ {label}
16
+ </label>
17
+ )}
18
+ <input
19
+ ref={ref}
20
+ id={id}
21
+ className={cn(
22
+ 'flex h-9 w-full rounded-xl border px-3 py-1 text-sm transition-all duration-200',
23
+ 'bg-white/5 border-white/10 text-white placeholder:text-white/40',
24
+ 'focus-visible:outline-none focus-visible:border-indigo-500/50 focus-visible:bg-white/10',
25
+ 'disabled:cursor-not-allowed disabled:opacity-50',
26
+ error && 'border-red-500/50 focus-visible:border-red-400',
27
+ className
28
+ )}
29
+ {...props}
30
+ />
31
+ {error && <p className="text-xs text-red-400">{error}</p>}
32
+ </div>
33
+ )
34
+ }
35
+ )
36
+ Input.displayName = 'Input'
@@ -0,0 +1,50 @@
1
+ import { cn } from '@/lib/utils'
2
+ import { forwardRef } from 'react'
3
+
4
+ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
5
+ label?: string
6
+ options: Array<{ value: string; label: string; disabled?: boolean }>
7
+ placeholder?: string
8
+ }
9
+
10
+ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
11
+ ({ className, label, options, placeholder, id, ...props }, ref) => {
12
+ return (
13
+ <div className="space-y-1.5">
14
+ {label && (
15
+ <label htmlFor={id} className="block text-sm font-medium text-white/70">
16
+ {label}
17
+ </label>
18
+ )}
19
+ <select
20
+ ref={ref}
21
+ id={id}
22
+ className={cn(
23
+ 'flex h-9 w-full rounded-xl border px-3 py-1 text-sm transition-all duration-200',
24
+ 'bg-white/5 border-white/10 text-white',
25
+ 'focus-visible:outline-none focus-visible:border-indigo-500/50',
26
+ className
27
+ )}
28
+ {...props}
29
+ >
30
+ {placeholder && (
31
+ <option value="" className="bg-[#1c2129]">
32
+ {placeholder}
33
+ </option>
34
+ )}
35
+ {options.map((opt) => (
36
+ <option
37
+ key={opt.value}
38
+ value={opt.value}
39
+ disabled={opt.disabled}
40
+ className="bg-[#1c2129]"
41
+ >
42
+ {opt.label}
43
+ </option>
44
+ ))}
45
+ </select>
46
+ </div>
47
+ )
48
+ }
49
+ )
50
+ Select.displayName = 'Select'
@@ -0,0 +1,53 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ export function ShimmerLine({
4
+ width = '100%',
5
+ height = 16,
6
+ className,
7
+ }: {
8
+ width?: string | number
9
+ height?: number
10
+ className?: string
11
+ }) {
12
+ return (
13
+ <div
14
+ className={cn(
15
+ 'rounded-md',
16
+ 'bg-gradient-to-r from-white/5 via-white/10 to-white/5',
17
+ 'bg-[length:200%_100%]',
18
+ 'animate-[shimmer_1.5s_ease-in-out_infinite]',
19
+ className
20
+ )}
21
+ style={{
22
+ width: typeof width === 'number' ? `${width}px` : width,
23
+ height,
24
+ }}
25
+ />
26
+ )
27
+ }
28
+
29
+ export function ShimmerBlock({
30
+ width = '100%',
31
+ height = 100,
32
+ className,
33
+ }: {
34
+ width?: string | number
35
+ height?: number
36
+ className?: string
37
+ }) {
38
+ return (
39
+ <div
40
+ className={cn(
41
+ 'rounded-xl',
42
+ 'bg-gradient-to-r from-white/5 via-white/10 to-white/5',
43
+ 'bg-[length:200%_100%]',
44
+ 'animate-[shimmer_1.5s_ease-in-out_infinite]',
45
+ className
46
+ )}
47
+ style={{
48
+ width: typeof width === 'number' ? `${width}px` : width,
49
+ height,
50
+ }}
51
+ />
52
+ )
53
+ }
package/lib/client.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { AshClient } from '@ash-ai/sdk'
2
+
3
+ let client: AshClient | null = null
4
+ let cachedApiKey: string | undefined
5
+
6
+ declare global {
7
+ interface Window {
8
+ __ASH_CONFIG__?: { apiKey?: string; serverVersion?: string; serverUrl?: string }
9
+ }
10
+ }
11
+
12
+ export function getClient(): AshClient {
13
+ if (!client) {
14
+ // In the browser, prefer the server URL from config (points directly to the Ash server,
15
+ // bypassing the Next.js dev proxy which can break SSE streams).
16
+ // Falls back to window.location.origin (works when served by the Ash server itself).
17
+ const serverUrl =
18
+ typeof window !== 'undefined'
19
+ ? window.__ASH_CONFIG__?.serverUrl || window.location.origin
20
+ : process.env.NEXT_PUBLIC_ASH_API_URL || 'http://localhost:4100'
21
+
22
+ cachedApiKey =
23
+ typeof window !== 'undefined'
24
+ ? window.__ASH_CONFIG__?.apiKey
25
+ : process.env.ASH_API_KEY
26
+
27
+ client = new AshClient({ serverUrl, apiKey: cachedApiKey })
28
+ }
29
+ return client
30
+ }
31
+
32
+ /** Return auth headers matching the SDK client's config. */
33
+ export function getAuthHeaders(): Record<string, string> {
34
+ getClient() // ensure cachedApiKey is set
35
+ return cachedApiKey ? { Authorization: `Bearer ${cachedApiKey}` } : {}
36
+ }
37
+
38
+ export function resetClient(): void {
39
+ client = null
40
+ cachedApiKey = undefined
41
+ }
package/lib/exports.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @ash-ai/dashboard public API
3
+ *
4
+ * This barrel exports components, hooks, and utilities for use by consumers
5
+ * like the ash-cloud-platform. Pages are exported as named components so they
6
+ * can be wrapped with auth / context by the consuming app.
7
+ *
8
+ * NOTE: All page components are React Client Components ('use client').
9
+ * They rely on the AshProvider context or getClient() for data fetching.
10
+ */
11
+
12
+ // ─── Page Components ─────────────────────────────────────────────────────────
13
+ export { default as DashboardHomePage } from '@/app/page'
14
+ export { default as AgentsPage } from '@/app/agents/page'
15
+ export { default as SessionsPage } from '@/app/sessions/page'
16
+ export { default as PlaygroundPage } from '@/app/playground/page'
17
+ export { default as LogsPage } from '@/app/logs/page'
18
+ export { default as AnalyticsPage } from '@/app/analytics/page'
19
+ export { default as ApiKeysPage } from '@/app/settings/api-keys/page'
20
+ export { default as CredentialsPage } from '@/app/settings/credentials/page'
21
+ export { default as QueuePage } from '@/app/queue/page'
22
+
23
+ // ─── Navigation ──────────────────────────────────────────────────────────────
24
+ export { DashboardNav } from '@/components/nav'
25
+ export type { NavItem } from '@/components/nav'
26
+
27
+ // ─── UI Primitives ───────────────────────────────────────────────────────────
28
+ export { Badge, StatusBadge } from '@/components/ui/badge'
29
+ export type { BadgeProps } from '@/components/ui/badge'
30
+ export { Button } from '@/components/ui/button'
31
+ export { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
32
+ export { EmptyState } from '@/components/ui/empty-state'
33
+ export { Input } from '@/components/ui/input'
34
+ export { Select } from '@/components/ui/select'
35
+ export { ShimmerBlock } from '@/components/ui/shimmer'
36
+
37
+ // ─── Data Layer ──────────────────────────────────────────────────────────────
38
+ export { AshProvider, useAshClient } from '@/components/providers'
39
+ export { getClient, getAuthHeaders, resetClient } from '@/lib/client'
40
+ export {
41
+ useAgents,
42
+ useSessions,
43
+ useHealth,
44
+ useCredentials,
45
+ useUsageStats,
46
+ } from '@/lib/hooks'
47
+
48
+ // ─── Utilities ───────────────────────────────────────────────────────────────
49
+ export {
50
+ cn,
51
+ formatRelativeTime,
52
+ formatNumber,
53
+ formatDuration,
54
+ truncateId,
55
+ } from '@/lib/utils'
package/lib/hooks.ts ADDED
@@ -0,0 +1,169 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react'
4
+ import { getClient } from './client'
5
+ import type { Agent, Session, Credential } from '@ash-ai/shared'
6
+
7
+ // ─── useInterval ───
8
+
9
+ function useInterval(callback: () => void, delay: number | null) {
10
+ const savedCallback = useRef(callback)
11
+ useEffect(() => {
12
+ savedCallback.current = callback
13
+ }, [callback])
14
+ useEffect(() => {
15
+ if (delay === null) return
16
+ const id = setInterval(() => savedCallback.current(), delay)
17
+ return () => clearInterval(id)
18
+ }, [delay])
19
+ }
20
+
21
+ // ─── useAgents ───
22
+
23
+ export function useAgents() {
24
+ const [agents, setAgents] = useState<Agent[]>([])
25
+ const [loading, setLoading] = useState(true)
26
+ const [error, setError] = useState<Error | null>(null)
27
+
28
+ const refetch = useCallback(async () => {
29
+ try {
30
+ const result = await getClient().listAgents()
31
+ setAgents(result)
32
+ setError(null)
33
+ } catch (e) {
34
+ setError(e as Error)
35
+ } finally {
36
+ setLoading(false)
37
+ }
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ refetch()
42
+ }, [refetch])
43
+
44
+ return { agents, loading, error, refetch }
45
+ }
46
+
47
+ // ─── useSessions ───
48
+
49
+ export function useSessions(opts?: {
50
+ agent?: string
51
+ limit?: number
52
+ autoRefresh?: boolean
53
+ }) {
54
+ const [sessions, setSessions] = useState<Session[]>([])
55
+ const [loading, setLoading] = useState(true)
56
+ const [error, setError] = useState<Error | null>(null)
57
+
58
+ const refetch = useCallback(async () => {
59
+ try {
60
+ const result = await getClient().listSessions(opts?.agent)
61
+ setSessions(result)
62
+ setError(null)
63
+ } catch (e) {
64
+ setError(e as Error)
65
+ } finally {
66
+ setLoading(false)
67
+ }
68
+ }, [opts?.agent])
69
+
70
+ useEffect(() => {
71
+ refetch()
72
+ }, [refetch])
73
+
74
+ useInterval(
75
+ () => refetch(),
76
+ opts?.autoRefresh !== false ? 10_000 : null
77
+ )
78
+
79
+ return { sessions, loading, error, refetch }
80
+ }
81
+
82
+ // ─── useHealth ───
83
+
84
+ export interface HealthData {
85
+ status: string
86
+ version?: string
87
+ activeSessions?: number
88
+ activeSandboxes?: number
89
+ }
90
+
91
+ export function useHealth() {
92
+ const [health, setHealth] = useState<HealthData | null>(null)
93
+ const [error, setError] = useState<Error | null>(null)
94
+
95
+ const refetch = useCallback(async () => {
96
+ try {
97
+ const result = await getClient().health()
98
+ setHealth(result as HealthData)
99
+ setError(null)
100
+ } catch (e) {
101
+ setError(e as Error)
102
+ }
103
+ }, [])
104
+
105
+ useEffect(() => {
106
+ refetch()
107
+ }, [refetch])
108
+
109
+ useInterval(() => refetch(), 30_000)
110
+
111
+ return { health, error, refetch }
112
+ }
113
+
114
+ // ─── useCredentials ───
115
+
116
+ export function useCredentials() {
117
+ const [credentials, setCredentials] = useState<Credential[]>([])
118
+ const [loading, setLoading] = useState(true)
119
+ const [error, setError] = useState<Error | null>(null)
120
+
121
+ const refetch = useCallback(async () => {
122
+ try {
123
+ const result = await getClient().listCredentials()
124
+ setCredentials(result)
125
+ setError(null)
126
+ } catch (e) {
127
+ setError(e as Error)
128
+ } finally {
129
+ setLoading(false)
130
+ }
131
+ }, [])
132
+
133
+ useEffect(() => {
134
+ refetch()
135
+ }, [refetch])
136
+
137
+ return { credentials, loading, error, refetch }
138
+ }
139
+
140
+ // ─── useUsageStats ───
141
+
142
+ export type { UsageStats } from '@ash-ai/shared'
143
+
144
+ export function useUsageStats(opts?: {
145
+ agentName?: string
146
+ sessionId?: string
147
+ }) {
148
+ const [stats, setStats] = useState<import('@ash-ai/shared').UsageStats | null>(null)
149
+ const [loading, setLoading] = useState(true)
150
+ const [error, setError] = useState<Error | null>(null)
151
+
152
+ const refetch = useCallback(async () => {
153
+ try {
154
+ const result = await getClient().getUsageStats(opts)
155
+ setStats(result)
156
+ setError(null)
157
+ } catch (e) {
158
+ setError(e as Error)
159
+ } finally {
160
+ setLoading(false)
161
+ }
162
+ }, [opts?.agentName, opts?.sessionId])
163
+
164
+ useEffect(() => {
165
+ refetch()
166
+ }, [refetch])
167
+
168
+ return { stats, loading, error, refetch }
169
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
7
+
8
+ export function formatRelativeTime(date: string | Date): string {
9
+ const now = Date.now()
10
+ const then = new Date(date).getTime()
11
+ const diff = now - then
12
+
13
+ const seconds = Math.floor(diff / 1000)
14
+ if (seconds < 60) return `${seconds}s ago`
15
+
16
+ const minutes = Math.floor(seconds / 60)
17
+ if (minutes < 60) return `${minutes}m ago`
18
+
19
+ const hours = Math.floor(minutes / 60)
20
+ if (hours < 24) return `${hours}h ago`
21
+
22
+ const days = Math.floor(hours / 24)
23
+ if (days < 30) return `${days}d ago`
24
+
25
+ return new Date(date).toLocaleDateString()
26
+ }
27
+
28
+ export function formatNumber(n: number): string {
29
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
30
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
31
+ return n.toString()
32
+ }
33
+
34
+ export function formatDuration(seconds: number): string {
35
+ if (seconds < 60) return `${seconds.toFixed(1)}s`
36
+ const minutes = Math.floor(seconds / 60)
37
+ if (minutes < 60) return `${minutes}m ${Math.floor(seconds % 60)}s`
38
+ const hours = Math.floor(minutes / 60)
39
+ return `${hours}h ${minutes % 60}m`
40
+ }
41
+
42
+ export function truncateId(id: string, len = 8): string {
43
+ return id.length > len ? id.slice(0, len) : id
44
+ }
package/next.config.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { NextConfig } from 'next'
2
+
3
+ const isDev = process.env.NODE_ENV === 'development'
4
+
5
+ const nextConfig: NextConfig = {
6
+ output: isDev ? undefined : 'export',
7
+ basePath: '/dashboard',
8
+ trailingSlash: true,
9
+ images: {
10
+ unoptimized: true,
11
+ },
12
+ ...(isDev && {
13
+ // Skip trailing slash redirects in dev to avoid extra round-trips on /api/* proxy calls
14
+ skipTrailingSlashRedirect: true,
15
+ async rewrites() {
16
+ const ashUrl = process.env.ASH_API_URL || 'http://localhost:4100'
17
+ return {
18
+ beforeFiles: [
19
+ { source: '/api/:path*', destination: `${ashUrl}/api/:path*`, basePath: false as const },
20
+ { source: '/health', destination: `${ashUrl}/health`, basePath: false as const },
21
+ { source: '/dashboard/config.js', destination: `${ashUrl}/dashboard/config.js`, basePath: false as const },
22
+ ],
23
+ }
24
+ },
25
+ }),
26
+ }
27
+
28
+ export default nextConfig
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><!--J9asKIV7Gq221ygeAP958--><html lang="en" class="dark"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/dashboard/_next/static/css/4b2beada31dbc623.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/dashboard/_next/static/chunks/webpack-a50a78a04aed446d.js"/><script src="/dashboard/_next/static/chunks/b59f762a-cea625f74e98e0aa.js" async=""></script><script src="/dashboard/_next/static/chunks/522-cf174cf1bbbe9557.js" async=""></script><script src="/dashboard/_next/static/chunks/main-app-5bcd4dcc44c4ae09.js" async=""></script><script src="/dashboard/_next/static/chunks/513-c4683887323154aa.js" async=""></script><script src="/dashboard/_next/static/chunks/53-b012ce05184a4754.js" async=""></script><script src="/dashboard/_next/static/chunks/322-bab4df5c5188e993.js" async=""></script><script src="/dashboard/_next/static/chunks/432-11ec8af7ccfbd019.js" async=""></script><script src="/dashboard/_next/static/chunks/929-6faf1adeb65ee383.js" async=""></script><script src="/dashboard/_next/static/chunks/app/layout-9cae773a790a15b2.js" async=""></script><link rel="preload" href="/dashboard/config.js" as="script"/><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Ash Dashboard</title><meta name="description" content="Manage agents, sessions, and monitor your Ash server"/><script>(self.__next_s=self.__next_s||[]).push(["/dashboard/config.js",{}])</script><script src="/dashboard/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="bg-[#0d1117] text-zinc-100 antialiased"><div hidden=""><!--$--><!--/$--></div><div class="flex min-h-screen"><nav class="fixed inset-y-0 left-0 z-50 w-64 flex flex-col border-r border-white/10 bg-[#0d1117]"><div class="flex h-16 items-center gap-3 px-5 border-b border-white/10"><div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-500"><span class="text-sm font-bold text-white">A</span></div><div class="min-w-0"><span class="text-white font-bold tracking-tight block">Ash</span><div class="flex items-center gap-1.5 text-white/40 text-xs font-mono"><span class="w-1.5 h-1.5 rounded-full bg-red-400"></span>OFFLINE</div></div></div><div class="flex-1 overflow-auto px-3 py-4 scrollbar-thin"><div class="space-y-0.5"><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/60 hover:text-white hover:bg-white/5 border border-transparent" href="/dashboard/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-layout-dashboard h-4 w-4"><rect width="7" height="9" x="3" y="3" rx="1"></rect><rect width="7" height="5" x="14" y="3" rx="1"></rect><rect width="7" height="9" x="14" y="12" rx="1"></rect><rect width="7" height="5" x="3" y="16" rx="1"></rect></svg>Dashboard</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/60 hover:text-white hover:bg-white/5 border border-transparent" href="/dashboard/playground/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code-xml h-4 w-4"><path d="m18 16 4-4-4-4"></path><path d="m6 8-4 4 4 4"></path><path d="m14.5 4-5 16"></path></svg>Playground</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/60 hover:text-white hover:bg-white/5 border border-transparent" href="/dashboard/agents/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bot h-4 w-4"><path d="M12 8V4H8"></path><rect width="16" height="12" x="4" y="8" rx="2"></rect><path d="M2 14h2"></path><path d="M20 14h2"></path><path d="M15 13v2"></path><path d="M9 13v2"></path></svg>Agents</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/60 hover:text-white hover:bg-white/5 border border-transparent" href="/dashboard/sessions/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity h-4 w-4"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"></path></svg>Sessions</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/60 hover:text-white hover:bg-white/5 border border-transparent" href="/dashboard/logs/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text h-4 w-4"><path d="M15 12h-5"></path><path d="M15 8h-5"></path><path d="M19 17V5a2 2 0 0 0-2-2H4"></path><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"></path></svg>Logs</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/60 hover:text-white hover:bg-white/5 border border-transparent" href="/dashboard/analytics/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trending-up h-4 w-4"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"></polyline><polyline points="16 7 22 7 22 13"></polyline></svg>Analytics</a></div></div><div class="border-t border-white/10 px-3 py-3 space-y-0.5"><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/40 hover:text-white hover:bg-white/5" href="/dashboard/settings/api-keys/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-key h-4 w-4"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"></path><path d="m21 2-9.6 9.6"></path><circle cx="7.5" cy="15.5" r="5.5"></circle></svg>API Keys</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/40 hover:text-white hover:bg-white/5" href="/dashboard/settings/credentials/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lock h-4 w-4"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>Credentials</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 text-white/40 hover:text-white hover:bg-white/5" href="/dashboard/queue/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-ordered h-4 w-4"><path d="M10 12h11"></path><path d="M10 18h11"></path><path d="M10 6h11"></path><path d="M4 10h2"></path><path d="M4 6h1v4"></path><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"></path></svg>Queue</a></div></nav><main class="min-w-0 flex-1 pl-64 overflow-y-auto overflow-x-hidden"><div class="mx-auto max-w-[1600px] px-6 pb-6 pt-8 sm:px-8 sm:pb-8 sm:pt-10"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--></div></main></div><script src="/dashboard/_next/static/chunks/webpack-a50a78a04aed446d.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[9061,[\"513\",\"static/chunks/513-c4683887323154aa.js\",\"53\",\"static/chunks/53-b012ce05184a4754.js\",\"322\",\"static/chunks/322-bab4df5c5188e993.js\",\"432\",\"static/chunks/432-11ec8af7ccfbd019.js\",\"929\",\"static/chunks/929-6faf1adeb65ee383.js\",\"177\",\"static/chunks/app/layout-9cae773a790a15b2.js\"],\"\"]\n3:I[6412,[\"513\",\"static/chunks/513-c4683887323154aa.js\",\"53\",\"static/chunks/53-b012ce05184a4754.js\",\"322\",\"static/chunks/322-bab4df5c5188e993.js\",\"432\",\"static/chunks/432-11ec8af7ccfbd019.js\",\"929\",\"static/chunks/929-6faf1adeb65ee383.js\",\"177\",\"static/chunks/app/layout-9cae773a790a15b2.js\"],\"AshProvider\"]\n4:I[2553,[\"513\",\"static/chunks/513-c4683887323154aa.js\",\"53\",\"static/chunks/53-b012ce05184a4754.js\",\"322\",\"static/chunks/322-bab4df5c5188e993.js\",\"432\",\"static/chunks/432-11ec8af7ccfbd019.js\",\"929\",\"static/chunks/929-6faf1adeb65ee383.js\",\"177\",\"static/chunks/app/layout-9cae773a790a15b2.js\"],\"DashboardNav\"]\n5:I[2229,[],\"\"]\n6:I[1457,[],\"\"]\n7:I[1464,[],\"OutletBoundary\"]\n9:I[6673,[],\"AsyncMetadataOutlet\"]\nb:I[1464,[],\"ViewportBoundary\"]\nd:I[1464,[],\"MetadataBoundary\"]\ne:\"$Sreact.suspense\"\n10:I[5095,[],\"\"]\n:HL[\"/dashboard/_next/static/css/4b2beada31dbc623.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"J9asKIV7Gq221ygeAP958\",\"p\":\"/dashboard\",\"c\":[\"\",\"_not-found\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/dashboard/_next/static/css/4b2beada31dbc623.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"dark\",\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"$L2\",null,{\"src\":\"/dashboard/config.js\",\"strategy\":\"beforeInteractive\"}]}],[\"$\",\"body\",null,{\"className\":\"bg-[#0d1117] text-zinc-100 antialiased\",\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"div\",null,{\"className\":\"flex min-h-screen\",\"children\":[[\"$\",\"$L4\",null,{}],[\"$\",\"main\",null,{\"className\":\"min-w-0 flex-1 pl-64 overflow-y-auto overflow-x-hidden\",\"children\":[\"$\",\"div\",null,{\"className\":\"mx-auto max-w-[1600px] px-6 pb-6 pt-8 sm:px-8 sm:pb-8 sm:pt-10\",\"children\":[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L6\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L6\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L7\",null,{\"children\":[\"$L8\",[\"$\",\"$L9\",null,{\"promise\":\"$@a\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$Lb\",null,{\"children\":\"$Lc\"}],null],[\"$\",\"$Ld\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$e\",null,{\"fallback\":null,\"children\":\"$Lf\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$10\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"c:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n8:null\n"])</script><script>self.__next_f.push([1,"a:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Ash Dashboard\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Manage agents, sessions, and monitor your Ash server\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"f:\"$a:metadata\"\n"])</script></body></html>