@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.
- package/LICENSE +21 -0
- package/app/agents/page.tsx +408 -0
- package/app/analytics/page.tsx +226 -0
- package/app/globals.css +33 -0
- package/app/layout.tsx +38 -0
- package/app/logs/page.tsx +233 -0
- package/app/page.tsx +140 -0
- package/app/playground/page.tsx +44 -0
- package/app/queue/page.tsx +295 -0
- package/app/sessions/page.tsx +529 -0
- package/app/settings/api-keys/page.tsx +222 -0
- package/app/settings/credentials/page.tsx +250 -0
- package/components/nav.tsx +151 -0
- package/components/providers.tsx +18 -0
- package/components/ui/badge.tsx +43 -0
- package/components/ui/button.tsx +44 -0
- package/components/ui/card.tsx +43 -0
- package/components/ui/empty-state.tsx +20 -0
- package/components/ui/input.tsx +36 -0
- package/components/ui/select.tsx +50 -0
- package/components/ui/shimmer.tsx +53 -0
- package/lib/client.ts +41 -0
- package/lib/exports.ts +55 -0
- package/lib/hooks.ts +169 -0
- package/lib/utils.ts +44 -0
- package/next.config.ts +28 -0
- package/out/404/index.html +1 -0
- package/out/404.html +1 -0
- package/out/_next/static/J9asKIV7Gq221ygeAP958/_buildManifest.js +1 -0
- package/out/_next/static/J9asKIV7Gq221ygeAP958/_ssgManifest.js +1 -0
- package/out/_next/static/chunks/322-bab4df5c5188e993.js +1 -0
- package/out/_next/static/chunks/432-11ec8af7ccfbd019.js +1 -0
- package/out/_next/static/chunks/447.6d3368efa2d996b0.js +1 -0
- package/out/_next/static/chunks/513-c4683887323154aa.js +1 -0
- package/out/_next/static/chunks/522-cf174cf1bbbe9557.js +1 -0
- package/out/_next/static/chunks/53-b012ce05184a4754.js +1 -0
- package/out/_next/static/chunks/929-6faf1adeb65ee383.js +1 -0
- package/out/_next/static/chunks/app/_not-found/page-04f9d3958a76bc38.js +1 -0
- package/out/_next/static/chunks/app/agents/page-8d68e3019b4d7077.js +1 -0
- package/out/_next/static/chunks/app/analytics/page-6b725a46e9c48019.js +1 -0
- package/out/_next/static/chunks/app/layout-9cae773a790a15b2.js +1 -0
- package/out/_next/static/chunks/app/logs/page-2efd945345a44a0e.js +1 -0
- package/out/_next/static/chunks/app/page-06f62e11f9cd82d5.js +1 -0
- package/out/_next/static/chunks/app/playground/page-10d3461f118bfb21.js +1 -0
- package/out/_next/static/chunks/app/queue/page-38e79b84cbd59335.js +1 -0
- package/out/_next/static/chunks/app/sessions/page-2a67c9eddfac029e.js +1 -0
- package/out/_next/static/chunks/app/settings/api-keys/page-619682cf8a1c26eb.js +1 -0
- package/out/_next/static/chunks/app/settings/credentials/page-106d0ba4f12afe81.js +1 -0
- package/out/_next/static/chunks/b59f762a-cea625f74e98e0aa.js +1 -0
- package/out/_next/static/chunks/framework-5a02266cf144994c.js +1 -0
- package/out/_next/static/chunks/main-994a6af9cdcd7fb9.js +1 -0
- package/out/_next/static/chunks/main-app-5bcd4dcc44c4ae09.js +1 -0
- package/out/_next/static/chunks/pages/_app-9fd734050704698a.js +1 -0
- package/out/_next/static/chunks/pages/_error-310ed5880fd5d5e6.js +1 -0
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/out/_next/static/chunks/webpack-a50a78a04aed446d.js +1 -0
- package/out/_next/static/css/4b2beada31dbc623.css +1 -0
- package/out/agents/index.html +1 -0
- package/out/agents/index.txt +22 -0
- package/out/analytics/index.html +1 -0
- package/out/analytics/index.txt +22 -0
- package/out/index.html +1 -0
- package/out/index.txt +22 -0
- package/out/logs/index.html +1 -0
- package/out/logs/index.txt +22 -0
- package/out/playground/index.html +1 -0
- package/out/playground/index.txt +22 -0
- package/out/queue/index.html +1 -0
- package/out/queue/index.txt +22 -0
- package/out/sessions/index.html +1 -0
- package/out/sessions/index.txt +22 -0
- package/out/settings/api-keys/index.html +1 -0
- package/out/settings/api-keys/index.txt +22 -0
- package/out/settings/credentials/index.html +1 -0
- package/out/settings/credentials/index.txt +22 -0
- package/package.json +40 -0
- package/postcss.config.mjs +7 -0
- 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,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";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>
|