synapse-gateway 2.0.0

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 (135) hide show
  1. package/README.md +385 -0
  2. package/bin/synapse.js +242 -0
  3. package/docs/PLAN.md +1723 -0
  4. package/docs/PRD.md +1799 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.ts +8 -0
  7. package/package.json +82 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/analytics/cost/route.ts +13 -0
  15. package/src/app/api/analytics/usage/route.ts +16 -0
  16. package/src/app/api/auth/login/route.ts +42 -0
  17. package/src/app/api/cache/route.ts +19 -0
  18. package/src/app/api/dashboard/route.ts +35 -0
  19. package/src/app/api/distill/route.ts +10 -0
  20. package/src/app/api/events/route.ts +54 -0
  21. package/src/app/api/health/route.ts +10 -0
  22. package/src/app/api/intelligence/forensics/route.ts +23 -0
  23. package/src/app/api/intelligence/neural-router/route.ts +23 -0
  24. package/src/app/api/keys/route.ts +34 -0
  25. package/src/app/api/mcp/route.ts +49 -0
  26. package/src/app/api/memory/route.ts +10 -0
  27. package/src/app/api/models/benchmark/route.ts +13 -0
  28. package/src/app/api/models/route.ts +39 -0
  29. package/src/app/api/namespace/route.ts +25 -0
  30. package/src/app/api/plugins/route.ts +41 -0
  31. package/src/app/api/providers/accounts/route.ts +91 -0
  32. package/src/app/api/providers/fetch-models/route.ts +52 -0
  33. package/src/app/api/providers/health/route.ts +10 -0
  34. package/src/app/api/providers/route.ts +46 -0
  35. package/src/app/api/routes/pipeline/route.ts +20 -0
  36. package/src/app/api/settings/route.ts +33 -0
  37. package/src/app/api/skills/route.ts +39 -0
  38. package/src/app/api/v1/chat/completions/route.ts +156 -0
  39. package/src/app/api/v1/models/route.ts +44 -0
  40. package/src/app/dashboard/intelligence/loading.tsx +14 -0
  41. package/src/app/dashboard/intelligence/page.tsx +125 -0
  42. package/src/app/dashboard/layout.tsx +143 -0
  43. package/src/app/dashboard/loading.tsx +17 -0
  44. package/src/app/dashboard/memory/loading.tsx +15 -0
  45. package/src/app/dashboard/memory/page.tsx +71 -0
  46. package/src/app/dashboard/models/loading.tsx +13 -0
  47. package/src/app/dashboard/models/page.tsx +107 -0
  48. package/src/app/dashboard/page.tsx +183 -0
  49. package/src/app/dashboard/playground/loading.tsx +17 -0
  50. package/src/app/dashboard/playground/page.tsx +212 -0
  51. package/src/app/dashboard/providers/loading.tsx +15 -0
  52. package/src/app/dashboard/providers/page.tsx +248 -0
  53. package/src/app/dashboard/routes/loading.tsx +15 -0
  54. package/src/app/dashboard/routes/page.tsx +72 -0
  55. package/src/app/dashboard/settings/loading.tsx +20 -0
  56. package/src/app/dashboard/settings/page.tsx +208 -0
  57. package/src/app/dashboard/skills/loading.tsx +26 -0
  58. package/src/app/dashboard/skills/page.tsx +137 -0
  59. package/src/app/dashboard/vault/loading.tsx +18 -0
  60. package/src/app/dashboard/vault/page.tsx +139 -0
  61. package/src/app/favicon.ico +0 -0
  62. package/src/app/globals.css +59 -0
  63. package/src/app/layout.tsx +32 -0
  64. package/src/app/login/page.tsx +87 -0
  65. package/src/app/page.tsx +5 -0
  66. package/src/components/ui/badge.tsx +32 -0
  67. package/src/components/ui/button.tsx +38 -0
  68. package/src/components/ui/card.tsx +50 -0
  69. package/src/components/ui/error-boundary.tsx +47 -0
  70. package/src/components/ui/index.ts +11 -0
  71. package/src/components/ui/input.tsx +26 -0
  72. package/src/components/ui/select.tsx +24 -0
  73. package/src/components/ui/skeleton.tsx +53 -0
  74. package/src/components/ui/toast.tsx +51 -0
  75. package/src/instrumentation.ts +6 -0
  76. package/src/lib/__tests__/auth.test.ts +42 -0
  77. package/src/lib/__tests__/format.test.ts +94 -0
  78. package/src/lib/__tests__/namespace.test.ts +102 -0
  79. package/src/lib/__tests__/squeezer.test.ts +93 -0
  80. package/src/lib/__tests__/utils.test.ts +28 -0
  81. package/src/lib/analytics/index.ts +187 -0
  82. package/src/lib/auth/guard.tsx +71 -0
  83. package/src/lib/auth/index.ts +105 -0
  84. package/src/lib/auth/middleware.ts +64 -0
  85. package/src/lib/benchmark/index.ts +137 -0
  86. package/src/lib/bootstrap.ts +122 -0
  87. package/src/lib/cache/index.ts +1 -0
  88. package/src/lib/cache/semantic.ts +211 -0
  89. package/src/lib/config/defaults.ts +61 -0
  90. package/src/lib/config/index.ts +72 -0
  91. package/src/lib/config/schema.ts +63 -0
  92. package/src/lib/db/index.ts +22 -0
  93. package/src/lib/db/migrate.ts +327 -0
  94. package/src/lib/db/schema.ts +303 -0
  95. package/src/lib/distiller/index.ts +331 -0
  96. package/src/lib/fallback/index.ts +153 -0
  97. package/src/lib/forensics/index.ts +188 -0
  98. package/src/lib/format/anthropic.ts +139 -0
  99. package/src/lib/format/gemini.ts +130 -0
  100. package/src/lib/format/index.ts +3 -0
  101. package/src/lib/format/openai.ts +78 -0
  102. package/src/lib/health/index.ts +158 -0
  103. package/src/lib/mcp/builtin.ts +83 -0
  104. package/src/lib/mcp/index.ts +1 -0
  105. package/src/lib/mcp/registry.ts +49 -0
  106. package/src/lib/memory/index.ts +3 -0
  107. package/src/lib/memory/store.ts +215 -0
  108. package/src/lib/memory/types.ts +56 -0
  109. package/src/lib/namespace/index.ts +89 -0
  110. package/src/lib/neural/features.ts +74 -0
  111. package/src/lib/neural/index.ts +85 -0
  112. package/src/lib/neural/strategies.ts +124 -0
  113. package/src/lib/pipeline/index.ts +84 -0
  114. package/src/lib/pipeline/types.ts +77 -0
  115. package/src/lib/plugins/builtin.ts +79 -0
  116. package/src/lib/plugins/index.ts +65 -0
  117. package/src/lib/prediction/index.ts +113 -0
  118. package/src/lib/providers/api-key/anthropic.ts +96 -0
  119. package/src/lib/providers/api-key/deepseek.ts +108 -0
  120. package/src/lib/providers/api-key/gemini.ts +112 -0
  121. package/src/lib/providers/api-key/openai.ts +122 -0
  122. package/src/lib/providers/api-key/openrouter.ts +112 -0
  123. package/src/lib/providers/base-adapter.ts +122 -0
  124. package/src/lib/providers/registry.ts +46 -0
  125. package/src/lib/providers/types.ts +121 -0
  126. package/src/lib/router/index.ts +82 -0
  127. package/src/lib/skills/forge.ts +57 -0
  128. package/src/lib/skills/index.ts +3 -0
  129. package/src/lib/skills/registry.ts +195 -0
  130. package/src/lib/skills/types.ts +44 -0
  131. package/src/lib/squeezer/index.ts +158 -0
  132. package/src/lib/utils/cn.ts +6 -0
  133. package/src/lib/utils/logger.ts +16 -0
  134. package/src/middleware.ts +60 -0
  135. package/tsconfig.json +34 -0
@@ -0,0 +1,208 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Settings, Server, Brain, Shield, Database, Globe, Save } from 'lucide-react'
5
+
6
+ const sections = [
7
+ { id: 'general', label: 'General', icon: Settings },
8
+ { id: 'providers', label: 'Providers', icon: Server },
9
+ { id: 'models', label: 'Models', icon: Brain },
10
+ { id: 'security', label: 'Security', icon: Shield },
11
+ { id: 'database', label: 'Database', icon: Database },
12
+ { id: 'network', label: 'Network', icon: Globe },
13
+ ]
14
+
15
+ export default function SettingsPage() {
16
+ const [active, setActive] = useState('general')
17
+ const [config, setConfig] = useState<Record<string, string>>({})
18
+ const [saved, setSaved] = useState(false)
19
+
20
+ useEffect(() => {
21
+ fetch('/api/settings').then((r) => r.json()).then((data) => setConfig((data as { settings: Record<string, string> }).settings || {})).catch(() => {})
22
+ }, [])
23
+
24
+ async function handleSave(updates: Record<string, string>) {
25
+ await fetch('/api/settings', {
26
+ method: 'PUT',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify(updates),
29
+ })
30
+ setConfig((prev) => ({ ...prev, ...updates }))
31
+ setSaved(true)
32
+ setTimeout(() => setSaved(false), 2000)
33
+ }
34
+
35
+ return (
36
+ <div className="flex gap-6 h-[calc(100vh-8rem)]">
37
+ <nav className="w-48 shrink-0 space-y-0.5">
38
+ {sections.map((s) => (
39
+ <button
40
+ key={s.id}
41
+ onClick={() => setActive(s.id)}
42
+ className={`w-full flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors ${active === s.id ? 'bg-primary/10 text-primary font-medium' : 'text-muted-foreground hover:bg-muted hover:text-foreground'}`}
43
+ >
44
+ <s.icon className="h-4 w-4" />
45
+ {s.label}
46
+ </button>
47
+ ))}
48
+ </nav>
49
+
50
+ <div className="flex-1 bg-card border border-border rounded-lg p-6 overflow-y-auto">
51
+ {active === 'general' && <GeneralSettings config={config} onSave={handleSave} saved={saved} />}
52
+ {active === 'providers' && <ProviderSettings config={config} onSave={handleSave} saved={saved} />}
53
+ {active === 'security' && <SecuritySettings config={config} onSave={handleSave} saved={saved} />}
54
+ {active === 'database' && <DatabaseSettings config={config} />}
55
+ {active === 'network' && <NetworkSettings config={config} onSave={handleSave} saved={saved} />}
56
+ {active === 'models' && (
57
+ <div className="flex items-center justify-center h-64 text-sm text-muted-foreground">
58
+ Model settings are managed in the Models page
59
+ </div>
60
+ )}
61
+ </div>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ function GeneralSettings({ config, onSave, saved }: { config: Record<string, string>; onSave: (u: Record<string, string>) => void; saved: boolean }) {
67
+ const [port, setPort] = useState(config.port || '3333')
68
+ const [logLevel, setLogLevel] = useState(config.logLevel || 'info')
69
+
70
+ return (
71
+ <div className="space-y-6">
72
+ <div className="flex items-center justify-between">
73
+ <h2 className="text-lg font-semibold">General Settings</h2>
74
+ <button onClick={() => onSave({ port, logLevel })} className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm hover:bg-primary/90">
75
+ <Save className="h-4 w-4" /> {saved ? 'Saved!' : 'Save'}
76
+ </button>
77
+ </div>
78
+ <div className="space-y-4">
79
+ <div>
80
+ <label className="text-sm font-medium block mb-1.5">Gateway Port</label>
81
+ <input type="number" value={port} onChange={(e) => setPort(e.target.value)} className="bg-muted border border-border rounded-md px-3 py-1.5 text-sm w-40 focus:outline-none focus:ring-1 focus:ring-ring" />
82
+ </div>
83
+ <div>
84
+ <label className="text-sm font-medium block mb-1.5">Log Level</label>
85
+ <select value={logLevel} onChange={(e) => setLogLevel(e.target.value)} className="bg-muted border border-border rounded-md px-3 py-1.5 text-sm w-40 focus:outline-none focus:ring-1 focus:ring-ring">
86
+ <option>debug</option>
87
+ <option>info</option>
88
+ <option>warn</option>
89
+ <option>error</option>
90
+ </select>
91
+ </div>
92
+ <div>
93
+ <label className="text-sm font-medium block mb-1.5">Data Directory</label>
94
+ <code className="text-sm bg-muted border border-border rounded-md px-3 py-1.5 block">~/.synapse/</code>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ function ProviderSettings({ config, onSave, saved }: { config: Record<string, string>; onSave: (u: Record<string, string>) => void; saved: boolean }) {
102
+ const [timeout, setTimeout_] = useState(config.requestTimeout || '30000')
103
+ const [retries, setRetries] = useState(config.maxRetries || '3')
104
+
105
+ return (
106
+ <div className="space-y-6">
107
+ <div className="flex items-center justify-between">
108
+ <h2 className="text-lg font-semibold">Provider Settings</h2>
109
+ <button onClick={() => onSave({ requestTimeout: timeout, maxRetries: retries })} className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm hover:bg-primary/90">
110
+ <Save className="h-4 w-4" /> {saved ? 'Saved!' : 'Save'}
111
+ </button>
112
+ </div>
113
+ <p className="text-sm text-muted-foreground">Configure global provider behavior, timeouts, and retries</p>
114
+ <div className="space-y-4">
115
+ <div>
116
+ <label className="text-sm font-medium block mb-1.5">Request Timeout (ms)</label>
117
+ <input type="number" value={timeout} onChange={(e) => setTimeout_(e.target.value)} className="bg-muted border border-border rounded-md px-3 py-1.5 text-sm w-40 focus:outline-none focus:ring-1 focus:ring-ring" />
118
+ </div>
119
+ <div>
120
+ <label className="text-sm font-medium block mb-1.5">Max Retries</label>
121
+ <input type="number" value={retries} onChange={(e) => setRetries(e.target.value)} className="bg-muted border border-border rounded-md px-3 py-1.5 text-sm w-40 focus:outline-none focus:ring-1 focus:ring-ring" />
122
+ </div>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ function SecuritySettings({ config, onSave, saved }: { config: Record<string, string>; onSave: (u: Record<string, string>) => void; saved: boolean }) {
129
+ const [rateLimit, setRateLimit] = useState(config.rateLimitEnabled !== 'false' ? 'true' : 'false')
130
+ const [keyRotation, setKeyRotation] = useState(config.keyRotationDays || '90')
131
+
132
+ return (
133
+ <div className="space-y-6">
134
+ <div className="flex items-center justify-between">
135
+ <h2 className="text-lg font-semibold">Security Settings</h2>
136
+ <button onClick={() => onSave({ rateLimitEnabled: rateLimit, keyRotationDays: keyRotation })} className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm hover:bg-primary/90">
137
+ <Save className="h-4 w-4" /> {saved ? 'Saved!' : 'Save'}
138
+ </button>
139
+ </div>
140
+ <div className="space-y-4">
141
+ <div>
142
+ <label className="text-sm font-medium block mb-1.5">Key Rotation (days)</label>
143
+ <input type="number" value={keyRotation} onChange={(e) => setKeyRotation(e.target.value)} className="bg-muted border border-border rounded-md px-3 py-1.5 text-sm w-40 focus:outline-none focus:ring-1 focus:ring-ring" />
144
+ </div>
145
+ <div className="flex items-center gap-3">
146
+ <input type="checkbox" checked={rateLimit === 'true'} onChange={(e) => setRateLimit(e.target.checked ? 'true' : 'false')} className="accent-primary" id="rate-limit" />
147
+ <label htmlFor="rate-limit" className="text-sm">Enable rate limiting</label>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ )
152
+ }
153
+
154
+ function DatabaseSettings({ config }: { config: Record<string, string> }) {
155
+ return (
156
+ <div className="space-y-6">
157
+ <h2 className="text-lg font-semibold">Database</h2>
158
+ <div className="space-y-3">
159
+ <div className="flex justify-between text-sm">
160
+ <span className="text-muted-foreground">Engine</span>
161
+ <span className="font-mono text-xs">SQLite (better-sqlite3)</span>
162
+ </div>
163
+ <div className="flex justify-between text-sm">
164
+ <span className="text-muted-foreground">ORM</span>
165
+ <span className="font-mono text-xs">Drizzle</span>
166
+ </div>
167
+ <div className="flex justify-between text-sm">
168
+ <span className="text-muted-foreground">Journal Mode</span>
169
+ <span className="font-mono text-xs">WAL</span>
170
+ </div>
171
+ <div className="flex justify-between text-sm">
172
+ <span className="text-muted-foreground">Location</span>
173
+ <span className="font-mono text-xs">{config.dbPath || '~/.synapse/synapse.db'}</span>
174
+ </div>
175
+ <div className="flex justify-between text-sm">
176
+ <span className="text-muted-foreground">Tables</span>
177
+ <span>21</span>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ function NetworkSettings({ config, onSave, saved }: { config: Record<string, string>; onSave: (u: Record<string, string>) => void; saved: boolean }) {
185
+ const [cors, setCors] = useState(config.corsEnabled !== 'false' ? 'true' : 'false')
186
+ const [allowedOrigins, setAllowedOrigins] = useState(config.allowedOrigins || '*')
187
+
188
+ return (
189
+ <div className="space-y-6">
190
+ <div className="flex items-center justify-between">
191
+ <h2 className="text-lg font-semibold">Network Settings</h2>
192
+ <button onClick={() => onSave({ corsEnabled: cors, allowedOrigins })} className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm hover:bg-primary/90">
193
+ <Save className="h-4 w-4" /> {saved ? 'Saved!' : 'Save'}
194
+ </button>
195
+ </div>
196
+ <div className="space-y-4">
197
+ <div className="flex items-center gap-3">
198
+ <input type="checkbox" checked={cors === 'true'} onChange={(e) => setCors(e.target.checked ? 'true' : 'false')} className="accent-primary" id="cors" />
199
+ <label htmlFor="cors" className="text-sm">Enable CORS</label>
200
+ </div>
201
+ <div>
202
+ <label className="text-sm font-medium block mb-1.5">Allowed Origins</label>
203
+ <input type="text" value={allowedOrigins} onChange={(e) => setAllowedOrigins(e.target.value)} className="bg-muted border border-border rounded-md px-3 py-1.5 text-sm w-full max-w-md focus:outline-none focus:ring-1 focus:ring-ring" />
204
+ </div>
205
+ </div>
206
+ </div>
207
+ )
208
+ }
@@ -0,0 +1,26 @@
1
+ import { CardSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function SkillsLoading() {
4
+ return (
5
+ <div className="space-y-6">
6
+ <div className="flex items-center justify-between">
7
+ <div className="h-4 w-64 animate-pulse rounded bg-muted" />
8
+ <div className="flex gap-2">
9
+ <div className="h-8 w-24 animate-pulse rounded bg-muted" />
10
+ <div className="h-8 w-32 animate-pulse rounded bg-muted" />
11
+ </div>
12
+ </div>
13
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
14
+ <div className="lg:col-span-2 space-y-3">
15
+ {Array.from({ length: 3 }).map((_, i) => (
16
+ <CardSkeleton key={i} />
17
+ ))}
18
+ </div>
19
+ <div className="space-y-2">
20
+ <CardSkeleton />
21
+ <CardSkeleton />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,137 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Wrench, Plus, RotateCw, Package, Download } from 'lucide-react'
5
+
6
+ interface SkillsData {
7
+ skills: Array<{
8
+ id: string
9
+ name: string
10
+ description: string | null
11
+ enabled: boolean
12
+ groupId: string | null
13
+ usageCount: number | null
14
+ qualityScore: number | null
15
+ tags: string | null
16
+ createdAt: string
17
+ }>
18
+ groups: Array<{
19
+ id: string
20
+ name: string
21
+ description: string | null
22
+ rotationStrategy: string
23
+ enabled: boolean
24
+ }>
25
+ }
26
+
27
+ export default function SkillsPage() {
28
+ const [data, setData] = useState<SkillsData | null>(null)
29
+ const [showCreate, setShowCreate] = useState(false)
30
+ const [form, setForm] = useState({ name: '', description: '', systemPrompt: '' })
31
+
32
+ useEffect(() => {
33
+ fetch('/api/skills').then((r) => r.json()).then(setData).catch(() => {})
34
+ }, [])
35
+
36
+ async function handleCreate() {
37
+ if (!form.name || !form.systemPrompt) return
38
+ await fetch('/api/skills', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ action: 'create', ...form }),
42
+ })
43
+ setShowCreate(false)
44
+ setForm({ name: '', description: '', systemPrompt: '' })
45
+ const res = await fetch('/api/skills')
46
+ setData(await res.json())
47
+ }
48
+
49
+ const skills = data?.skills || []
50
+ const groups = data?.groups || []
51
+
52
+ return (
53
+ <div className="space-y-6">
54
+ <div className="flex items-center justify-between">
55
+ <p className="text-sm text-muted-foreground">Manage skills, groups, and rotation strategies</p>
56
+ <div className="flex items-center gap-2">
57
+ <button className="flex items-center gap-2 px-3 py-1.5 border border-border rounded-md text-sm hover:bg-muted transition-colors">
58
+ <Download className="h-4 w-4" /> Import
59
+ </button>
60
+ <button onClick={() => setShowCreate(!showCreate)} className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors">
61
+ <Plus className="h-4 w-4" /> Create Skill
62
+ </button>
63
+ </div>
64
+ </div>
65
+
66
+ {showCreate && (
67
+ <div className="bg-card border border-border rounded-lg p-4 space-y-3">
68
+ <h3 className="text-sm font-medium">New Skill</h3>
69
+ <input placeholder="Skill name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className="w-full bg-muted border border-border rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring" />
70
+ <input placeholder="Description" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} className="w-full bg-muted border border-border rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring" />
71
+ <textarea placeholder="System prompt..." value={form.systemPrompt} onChange={(e) => setForm({ ...form, systemPrompt: e.target.value })} rows={4} className="w-full bg-muted border border-border rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring resize-none" />
72
+ <div className="flex gap-2">
73
+ <button onClick={handleCreate} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm hover:bg-primary/90">Create</button>
74
+ <button onClick={() => setShowCreate(false)} className="px-3 py-1.5 border border-border rounded-md text-sm hover:bg-muted">Cancel</button>
75
+ </div>
76
+ </div>
77
+ )}
78
+
79
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
80
+ <div className="lg:col-span-2 space-y-3">
81
+ <h2 className="text-sm font-medium flex items-center gap-2">
82
+ <Wrench className="h-4 w-4 text-muted-foreground" /> Skills ({skills.length})
83
+ </h2>
84
+ {skills.length === 0 ? (
85
+ <div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
86
+ No skills created yet. Click &quot;Create Skill&quot; to add one.
87
+ </div>
88
+ ) : (
89
+ skills.map((s) => (
90
+ <div key={s.id} className="bg-card border border-border rounded-lg p-4">
91
+ <div className="flex items-start justify-between">
92
+ <div>
93
+ <h3 className="font-medium text-sm">{s.name}</h3>
94
+ <p className="text-xs text-muted-foreground mt-0.5">{s.description || 'No description'}</p>
95
+ </div>
96
+ <span className={`text-xs px-2 py-0.5 rounded-full ${s.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
97
+ {s.enabled ? 'Active' : 'Disabled'}
98
+ </span>
99
+ </div>
100
+ <div className="flex items-center gap-4 mt-3 text-xs text-muted-foreground">
101
+ <span>{s.usageCount || 0} uses</span>
102
+ {s.qualityScore != null && <span>Quality: {Number(s.qualityScore).toFixed(1)}</span>}
103
+ {s.groupId && <span className="bg-muted px-1.5 py-0.5 rounded">Grouped</span>}
104
+ </div>
105
+ </div>
106
+ ))
107
+ )}
108
+ </div>
109
+
110
+ <div className="space-y-4">
111
+ <div>
112
+ <h2 className="text-sm font-medium flex items-center gap-2 mb-3">
113
+ <Package className="h-4 w-4 text-muted-foreground" /> Groups ({groups.length})
114
+ </h2>
115
+ {groups.length === 0 ? (
116
+ <div className="text-xs text-muted-foreground">No groups yet.</div>
117
+ ) : (
118
+ <div className="space-y-2">
119
+ {groups.map((g) => (
120
+ <div key={g.id} className="bg-card border border-border rounded-lg p-3 flex items-center justify-between">
121
+ <div>
122
+ <div className="text-sm font-medium">{g.name}</div>
123
+ <div className="text-xs text-muted-foreground flex items-center gap-1">
124
+ <RotateCw className="h-3 w-3" /> {g.rotationStrategy.replace('_', ' ')}
125
+ </div>
126
+ </div>
127
+ <span className={`h-2 w-2 rounded-full ${g.enabled ? 'bg-primary' : 'bg-muted-foreground'}`} />
128
+ </div>
129
+ ))}
130
+ </div>
131
+ )}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ )
137
+ }
@@ -0,0 +1,18 @@
1
+ import { CardSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function VaultLoading() {
4
+ return (
5
+ <div className="space-y-6">
6
+ <div className="h-4 w-64 animate-pulse rounded bg-muted" />
7
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
8
+ <div className="lg:col-span-2 space-y-4">
9
+ <div className="h-8 w-32 animate-pulse rounded bg-muted" />
10
+ {Array.from({ length: 2 }).map((_, i) => (
11
+ <CardSkeleton key={i} />
12
+ ))}
13
+ </div>
14
+ <CardSkeleton />
15
+ </div>
16
+ </div>
17
+ )
18
+ }
@@ -0,0 +1,139 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Shield, Key, Plus, Trash2, Copy } from 'lucide-react'
5
+
6
+ interface ApiKey {
7
+ id: string
8
+ name: string
9
+ keyPrefix: string
10
+ createdAt: string
11
+ lastUsedAt: string | null
12
+ requestCount: number
13
+ }
14
+
15
+ export default function VaultPage() {
16
+ const [keys, setKeys] = useState<ApiKey[]>([])
17
+ const [newKeyName, setNewKeyName] = useState('')
18
+ const [showCreate, setShowCreate] = useState(false)
19
+ const [createdKey, setCreatedKey] = useState<string | null>(null)
20
+
21
+ useEffect(() => {
22
+ fetch('/api/keys').then((r) => r.json()).then((data) => setKeys((data as { keys: ApiKey[] }).keys || [])).catch(() => {})
23
+ }, [])
24
+
25
+ async function handleCreate() {
26
+ if (!newKeyName.trim()) return
27
+ const res = await fetch('/api/keys', {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ name: newKeyName.trim() }),
31
+ })
32
+ const data = await res.json()
33
+ if (data.key) {
34
+ setCreatedKey(data.key)
35
+ setNewKeyName('')
36
+ setShowCreate(false)
37
+ const refreshed = await fetch('/api/keys').then((r) => r.json())
38
+ setKeys((refreshed as { keys: ApiKey[] }).keys || [])
39
+ }
40
+ }
41
+
42
+ async function handleRevoke(id: string) {
43
+ await fetch('/api/keys', {
44
+ method: 'DELETE',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify({ id }),
47
+ })
48
+ setKeys((prev) => prev.filter((k) => k.id !== id))
49
+ }
50
+
51
+ return (
52
+ <div className="space-y-6">
53
+ <p className="text-sm text-muted-foreground">Manage API keys and access control</p>
54
+
55
+ {createdKey && (
56
+ <div className="bg-primary/10 border border-primary/20 rounded-lg p-4">
57
+ <div className="flex items-center justify-between mb-2">
58
+ <h3 className="text-sm font-medium text-primary">API Key Created</h3>
59
+ <button onClick={() => { navigator.clipboard.writeText(createdKey) }} className="flex items-center gap-1 text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90">
60
+ <Copy className="h-3 w-3" /> Copy
61
+ </button>
62
+ </div>
63
+ <code className="text-xs font-mono break-all block bg-muted/50 p-2 rounded">{createdKey}</code>
64
+ <p className="text-xs text-muted-foreground mt-2">Copy this key now. You won&apos;t be able to see it again.</p>
65
+ <button onClick={() => setCreatedKey(null)} className="mt-2 text-xs text-muted-foreground hover:text-foreground">Dismiss</button>
66
+ </div>
67
+ )}
68
+
69
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
70
+ <div className="lg:col-span-2 space-y-4">
71
+ <div className="flex items-center justify-between">
72
+ <h2 className="text-sm font-medium flex items-center gap-2">
73
+ <Key className="h-4 w-4 text-muted-foreground" /> API Keys ({keys.length})
74
+ </h2>
75
+ <button onClick={() => setShowCreate(!showCreate)} className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors">
76
+ <Plus className="h-4 w-4" /> Generate Key
77
+ </button>
78
+ </div>
79
+
80
+ {showCreate && (
81
+ <div className="bg-card border border-border rounded-lg p-4 flex items-center gap-2">
82
+ <input placeholder="Key name (e.g. Production)" value={newKeyName} onChange={(e) => setNewKeyName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleCreate()} className="flex-1 bg-muted border border-border rounded-md px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring" />
83
+ <button onClick={handleCreate} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm hover:bg-primary/90">Create</button>
84
+ <button onClick={() => { setShowCreate(false); setNewKeyName('') }} className="px-3 py-1.5 border border-border rounded-md text-sm hover:bg-muted">Cancel</button>
85
+ </div>
86
+ )}
87
+
88
+ {keys.length === 0 ? (
89
+ <div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
90
+ No API keys generated yet. Create one to start using the gateway.
91
+ </div>
92
+ ) : (
93
+ keys.map((k) => (
94
+ <div key={k.id} className="bg-card border border-border rounded-lg p-4">
95
+ <div className="flex items-center justify-between mb-2">
96
+ <h3 className="font-medium text-sm">{k.name}</h3>
97
+ <button onClick={() => handleRevoke(k.id)} className="p-1 hover:bg-destructive/10 rounded text-muted-foreground hover:text-destructive transition-colors">
98
+ <Trash2 className="h-3.5 w-3.5" />
99
+ </button>
100
+ </div>
101
+ <code className="text-xs font-mono bg-muted px-2 py-1 rounded block mb-3">{k.keyPrefix}</code>
102
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
103
+ <span>Created: {new Date(k.createdAt).toLocaleDateString()}</span>
104
+ <span>Last used: {k.lastUsedAt ? new Date(k.lastUsedAt).toLocaleDateString() : 'Never'}</span>
105
+ <span>{k.requestCount.toLocaleString()} requests</span>
106
+ </div>
107
+ </div>
108
+ ))
109
+ )}
110
+ </div>
111
+
112
+ <div className="bg-card border border-border rounded-lg">
113
+ <div className="px-4 py-3 border-b border-border flex items-center gap-2">
114
+ <Shield className="h-4 w-4 text-primary" />
115
+ <h2 className="text-sm font-medium">Security</h2>
116
+ </div>
117
+ <div className="p-4 space-y-3">
118
+ <div className="flex justify-between text-sm">
119
+ <span className="text-muted-foreground">Key Format</span>
120
+ <span className="font-mono text-xs">adn_...</span>
121
+ </div>
122
+ <div className="flex justify-between text-sm">
123
+ <span className="text-muted-foreground">Hashing</span>
124
+ <span className="font-mono text-xs">SHA-256</span>
125
+ </div>
126
+ <div className="flex justify-between text-sm">
127
+ <span className="text-muted-foreground">Rate Limit</span>
128
+ <span>100 req/min</span>
129
+ </div>
130
+ <div className="flex justify-between text-sm">
131
+ <span className="text-muted-foreground">Active Keys</span>
132
+ <span>{keys.length}</span>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ )
139
+ }
Binary file
@@ -0,0 +1,59 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme inline {
4
+ --color-background: #0a0a0b;
5
+ --color-foreground: #fafafa;
6
+ --color-card: #111113;
7
+ --color-card-foreground: #fafafa;
8
+ --color-popover: #111113;
9
+ --color-popover-foreground: #fafafa;
10
+ --color-primary: #10b981;
11
+ --color-primary-foreground: #0a0a0b;
12
+ --color-secondary: #27272a;
13
+ --color-secondary-foreground: #fafafa;
14
+ --color-muted: #18181b;
15
+ --color-muted-foreground: #a1a1aa;
16
+ --color-accent: #8b5cf6;
17
+ --color-accent-foreground: #fafafa;
18
+ --color-destructive: #ef4444;
19
+ --color-destructive-foreground: #fafafa;
20
+ --color-border: #27272a;
21
+ --color-input: #27272a;
22
+ --color-ring: #10b981;
23
+ --color-sidebar: #0f0f11;
24
+ --color-sidebar-foreground: #a1a1aa;
25
+ --color-sidebar-active: #10b981;
26
+ --font-sans: var(--font-geist-sans);
27
+ --font-mono: var(--font-geist-mono);
28
+ --radius-sm: 0.25rem;
29
+ --radius-md: 0.375rem;
30
+ --radius-lg: 0.5rem;
31
+ }
32
+
33
+ * {
34
+ border-color: var(--color-border);
35
+ }
36
+
37
+ body {
38
+ background: var(--color-background);
39
+ color: var(--color-foreground);
40
+ font-family: var(--font-sans), system-ui, sans-serif;
41
+ }
42
+
43
+ ::-webkit-scrollbar {
44
+ width: 6px;
45
+ height: 6px;
46
+ }
47
+
48
+ ::-webkit-scrollbar-track {
49
+ background: var(--color-background);
50
+ }
51
+
52
+ ::-webkit-scrollbar-thumb {
53
+ background: var(--color-border);
54
+ border-radius: 3px;
55
+ }
56
+
57
+ ::-webkit-scrollbar-thumb:hover {
58
+ background: var(--color-muted-foreground);
59
+ }
@@ -0,0 +1,32 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "Synapse — Next-Gen AI Gateway",
17
+ description: "Smart AI Router & Token Optimizer with Neural Routing, Semantic Cache, and Self-Learning",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html lang="en" className={`${geistSans.variable} ${geistMono.variable} dark h-full antialiased`}>
27
+ <body className="min-h-full flex flex-col bg-background text-foreground">
28
+ {children}
29
+ </body>
30
+ </html>
31
+ );
32
+ }