abc1181 commited on
Commit
de886f2
·
1 Parent(s): 74bcc24

Fix health endpoint routing order

Browse files
backend/main.py CHANGED
@@ -46,6 +46,11 @@ app.include_router(settings.router, prefix="/api/settings", tags=["settings"])
46
  app.include_router(mcp.router, prefix="/mcp", tags=["mcp"])
47
  app.include_router(auth_configs.router, prefix="/api/auth-configs", tags=["auth-configs"])
48
 
 
 
 
 
 
49
  STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
50
 
51
  if os.path.exists(STATIC_DIR):
@@ -53,7 +58,7 @@ if os.path.exists(STATIC_DIR):
53
 
54
  @app.get("/{full_path:path}")
55
  async def serve_spa(full_path: str):
56
- if full_path.startswith("api/") or full_path.startswith("mcp/"):
57
  return JSONResponse({"detail": "Not found"}, status_code=404)
58
  static_file = os.path.join(STATIC_DIR, full_path)
59
  if os.path.isfile(static_file):
@@ -62,9 +67,4 @@ if os.path.exists(STATIC_DIR):
62
 
63
  @app.get("/")
64
  async def root():
65
- return FileResponse(os.path.join(STATIC_DIR, "index.html"))
66
-
67
-
68
- @app.get("/health")
69
- async def health():
70
- return {"status": "healthy", "version": "1.0.0"}
 
46
  app.include_router(mcp.router, prefix="/mcp", tags=["mcp"])
47
  app.include_router(auth_configs.router, prefix="/api/auth-configs", tags=["auth-configs"])
48
 
49
+ @app.get("/health")
50
+ async def health():
51
+ return {"status": "healthy", "version": "1.0.0"}
52
+
53
+
54
  STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
55
 
56
  if os.path.exists(STATIC_DIR):
 
58
 
59
  @app.get("/{full_path:path}")
60
  async def serve_spa(full_path: str):
61
+ if full_path.startswith("api/") or full_path.startswith("mcp/") or full_path == "health":
62
  return JSONResponse({"detail": "Not found"}, status_code=404)
63
  static_file = os.path.join(STATIC_DIR, full_path)
64
  if os.path.isfile(static_file):
 
67
 
68
  @app.get("/")
69
  async def root():
70
+ return FileResponse(os.path.join(STATIC_DIR, "index.html"))
 
 
 
 
 
backend/static/index.html DELETED
@@ -1 +0,0 @@
1
- <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/b8fe60b397de5bf4.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-16fd57b891a459c7.js"/><script src="/_next/static/chunks/fd9d1056-9e075b64004453d8.js" async=""></script><script src="/_next/static/chunks/117-4e7593576fd7f596.js" async=""></script><script src="/_next/static/chunks/main-app-c4d682a1ca6a9af7.js" async=""></script><script src="/_next/static/chunks/876-6377c47dc51ad221.js" async=""></script><script src="/_next/static/chunks/app/page-b6939147ba61bda4.js" async=""></script><title>Compost - AI Tool Platform</title><meta name="description" content="Unified AI tool integration platform"/><meta name="next-size-adjust"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_f367f3 font-sans bg-bg-primary text-text-primary"><div class="flex min-h-screen"><aside class="w-60 bg-bg-secondary border-r border-border flex flex-col shrink-0"><div class="px-5 py-4 border-b border-border"><div class="flex items-center gap-3"><div class="w-8 h-8 bg-accent-cyan rounded-lg flex items-center justify-center font-bold text-bg-primary text-sm">C</div><span class="text-base font-semibold tracking-tight">Compost</span></div></div><nav class="flex-1 px-3 py-4 space-y-1"><div class="px-3 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Main</div><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors bg-accent-cyan/10 text-accent-cyanLight font-medium" href="/"><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 w-4 h-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 transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/apps/"><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-plug w-4 h-4"><path d="M12 22v-5"></path><path d="M9 8V2"></path><path d="M15 8V2"></path><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"></path></svg>Apps</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/tools/"><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-wrench w-4 h-4"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>Tools</a><div class="px-3 mt-6 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Configuration</div><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/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 w-4 h-4"><circle cx="7.5" cy="15.5" r="5.5"></circle><path d="m21 2-9.6 9.6"></path><path d="m15.5 7.5 3 3L22 7l-3-3"></path></svg>API Keys</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/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-bar-chart3 w-4 h-4"><path d="M3 3v18h18"></path><path d="M18 17V9"></path><path d="M13 17V5"></path><path d="M8 17v-3"></path></svg>Analytics</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/settings/"><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-settings w-4 h-4"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>Settings</a><a href="/api/docs" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-text-secondary hover:bg-bg-hover hover:text-text-primary transition-colors"><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-external-link w-4 h-4"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg>API Docs</a></nav><div class="px-3 py-4 border-t border-border"><div class="flex items-center justify-between"><div class="flex items-center gap-2 min-w-0"><div class="w-7 h-7 bg-bg-tertiary rounded-full flex items-center justify-center text-xs font-semibold shrink-0">U</div><span class="text-xs text-text-secondary truncate">User</span></div><button class="p-1.5 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors"><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-log-out w-3.5 h-3.5"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" x2="9" y1="12" y2="12"></line></svg></button></div></div></aside><main class="flex-1 flex flex-col"><header class="h-14 bg-bg-secondary border-b border-border flex items-center px-6"><h1 class="text-base font-medium">Dashboard</h1></header><div class="flex-1 p-6 space-y-6 overflow-auto"><div class="grid grid-cols-4 gap-4"><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Total Executions</p><p class="text-2xl font-bold mt-1">...</p></div><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 w-5 h-5 text-accent-cyan"><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></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Integrations</p><p class="text-2xl font-bold mt-1">...</p></div><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-plug w-5 h-5 text-accent-green"><path d="M12 22v-5"></path><path d="M9 8V2"></path><path d="M15 8V2"></path><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"></path></svg></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Success Rate</p><p class="text-2xl font-bold mt-1">...</p></div><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-circle-check-big w-5 h-5 text-accent-blue"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><path d="m9 11 3 3L22 4"></path></svg></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Avg Latency</p><p class="text-2xl font-bold mt-1">...</p></div><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-clock w-5 h-5 text-accent-amber"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></div></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-6"><h3 class="text-sm font-medium mb-4">Quick Connect</h3><p class="text-text-muted text-sm">Loading...</p></div></div></main></div><script src="/_next/static/chunks/webpack-16fd57b891a459c7.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/_next/static/css/b8fe60b397de5bf4.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"3:I[2846,[],\"\"]\n5:I[9107,[],\"ClientPageRoot\"]\n6:I[7340,[\"876\",\"static/chunks/876-6377c47dc51ad221.js\",\"931\",\"static/chunks/app/page-b6939147ba61bda4.js\"],\"default\",1]\n7:I[4707,[],\"\"]\n8:I[6423,[],\"\"]\na:I[1060,[],\"\"]\nb:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L3\",null,{\"buildId\":\"Kf4VGOEAsTnd5eMvxpmU3\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"\"],\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[\"$\",\"$L5\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$6\"}],null],null],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b8fe60b397de5bf4.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__variable_f367f3 font-sans bg-bg-primary text-text-primary\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"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.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[null,\"$L9\"],\"globalErrorComponent\":\"$a\",\"missingSlots\":\"$Wb\"}]\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"Compost - AI Tool Platform\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Unified AI tool integration platform\"}],[\"$\",\"meta\",\"4\",{\"name\":\"next-size-adjust\"}]]\n4:null\n"])</script></body></html>
 
 
frontend/_src/app/analytics/page.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Sidebar } from '@/components/sidebar'
5
+ import { api, type Stats } from '@/lib/api'
6
+ import { Activity, CheckCircle, XCircle, Clock } from 'lucide-react'
7
+
8
+ export default function AnalyticsPage() {
9
+ const [stats, setStats] = useState<Stats | null>(null)
10
+ const [loading, setLoading] = useState(true)
11
+
12
+ useEffect(() => {
13
+ api.get<Stats>('/api/analytics/overview')
14
+ .then((data) => { setStats(data); setLoading(false) })
15
+ .catch(() => setLoading(false))
16
+ }, [])
17
+
18
+ const metrics = [
19
+ { label: 'Executions', value: stats?.total_executions ?? 0, icon: Activity, color: 'text-accent-cyan' },
20
+ { label: 'Successful', value: stats?.successful ?? 0, icon: CheckCircle, color: 'text-accent-green' },
21
+ { label: 'Failed', value: stats?.failed ?? 0, icon: XCircle, color: 'text-accent-red' },
22
+ { label: 'Avg Latency', value: `${stats?.avg_latency_ms ?? 0}ms`, icon: Clock, color: 'text-accent-amber' },
23
+ ]
24
+
25
+ return (
26
+ <div className="flex min-h-screen">
27
+ <Sidebar />
28
+ <main className="flex-1 flex flex-col">
29
+ <header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
30
+ <h1 className="text-base font-medium">Analytics</h1>
31
+ </header>
32
+ <div className="flex-1 p-6 space-y-6 overflow-auto">
33
+ <div className="grid grid-cols-4 gap-4">
34
+ {metrics.map((m) => (
35
+ <div key={m.label} className="bg-bg-tertiary border border-border rounded-xl p-5">
36
+ <div className="flex items-center justify-between">
37
+ <div>
38
+ <p className="text-sm text-text-muted">{m.label}</p>
39
+ <p className={`text-2xl font-bold mt-1 ${m.color}`}>{loading ? '...' : m.value}</p>
40
+ </div>
41
+ <m.icon className={`w-5 h-5 ${m.color}`} />
42
+ </div>
43
+ </div>
44
+ ))}
45
+ </div>
46
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
47
+ <h3 className="text-sm font-medium mb-4">Success Rate</h3>
48
+ <div className="flex items-center gap-4">
49
+ <div className="flex-1 h-3 bg-bg-secondary rounded-full overflow-hidden">
50
+ <div className="h-full bg-accent-green rounded-full transition-all" style={{ width: `${stats?.success_rate ?? 100}%` }} />
51
+ </div>
52
+ <span className="text-sm font-medium">{stats?.success_rate ?? 100}%</span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </main>
57
+ </div>
58
+ )
59
+ }
frontend/_src/app/api-keys/page.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { Sidebar } from '@/components/sidebar'
4
+ import { Key, Server } from 'lucide-react'
5
+
6
+ export default function APIKeysPage() {
7
+ return (
8
+ <div className="flex min-h-screen">
9
+ <Sidebar />
10
+ <main className="flex-1 flex flex-col">
11
+ <header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6 justify-between">
12
+ <h1 className="text-base font-medium">API Keys</h1>
13
+ </header>
14
+ <div className="flex-1 p-6">
15
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
16
+ <div className="flex items-center gap-3 mb-4">
17
+ <Key className="w-5 h-5 text-accent-cyan" />
18
+ <h3 className="text-sm font-medium">Manage API Keys</h3>
19
+ </div>
20
+ <p className="text-sm text-text-muted">Generate API keys for programmatic access to the platform.</p>
21
+ </div>
22
+ </div>
23
+ </main>
24
+ </div>
25
+ )
26
+ }
frontend/_src/app/apps/page.tsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Sidebar } from '@/components/sidebar'
5
+ import { api, type Integration } from '@/lib/api'
6
+ import { Search } from 'lucide-react'
7
+
8
+ const categories = ['All', 'Developer Tools', 'Communication', 'Productivity', 'CRM', 'Finance', 'Social', 'Other']
9
+
10
+ export default function AppsPage() {
11
+ const [integrations, setIntegrations] = useState<Integration[]>([])
12
+ const [loading, setLoading] = useState(true)
13
+ const [category, setCategory] = useState('All')
14
+ const [search, setSearch] = useState('')
15
+
16
+ useEffect(() => {
17
+ api.get<Integration[]>('/api/apps')
18
+ .then((data) => { setIntegrations(data); setLoading(false) })
19
+ .catch(() => setLoading(false))
20
+ }, [])
21
+
22
+ const filtered = integrations.filter((i) => {
23
+ const matchCat = category === 'All' || i.category === category
24
+ const matchSearch = !search || i.name.toLowerCase().includes(search.toLowerCase()) || i.description.toLowerCase().includes(search.toLowerCase())
25
+ return matchCat && matchSearch
26
+ })
27
+
28
+ return (
29
+ <div className="flex min-h-screen">
30
+ <Sidebar integrationCount={integrations.length} />
31
+ <main className="flex-1 flex flex-col">
32
+ <header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6 justify-between">
33
+ <h1 className="text-base font-medium">Apps</h1>
34
+ <div className="relative">
35
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-dim" />
36
+ <input
37
+ className="pl-9 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-64"
38
+ placeholder="Search apps..."
39
+ value={search}
40
+ onChange={(e) => setSearch(e.target.value)}
41
+ />
42
+ </div>
43
+ </header>
44
+ <div className="flex-1 p-6 overflow-auto">
45
+ <div className="flex gap-2 mb-6 flex-wrap">
46
+ {categories.map((c) => (
47
+ <button key={c} onClick={() => setCategory(c)}
48
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${category === c ? 'bg-accent-cyan text-bg-primary' : 'bg-bg-tertiary border border-border text-text-secondary hover:text-text-primary'}`}
49
+ >{c}</button>
50
+ ))}
51
+ </div>
52
+ {loading ? (
53
+ <div className="grid grid-cols-4 gap-4">
54
+ {Array.from({ length: 8 }).map((_, i) => (
55
+ <div key={i} className="bg-bg-tertiary border border-border rounded-xl p-5 animate-pulse">
56
+ <div className="flex items-center gap-3 mb-3">
57
+ <div className="w-9 h-9 bg-bg-hover rounded-lg" />
58
+ <div className="flex-1"><div className="h-3 bg-bg-hover rounded w-20 mb-1" /><div className="h-2 bg-bg-hover rounded w-14" /></div>
59
+ </div>
60
+ <div className="h-2 bg-bg-hover rounded w-full mb-2" /><div className="h-2 bg-bg-hover rounded w-3/4" />
61
+ </div>
62
+ ))}
63
+ </div>
64
+ ) : filtered.length === 0 ? (
65
+ <div className="text-center py-20"><p className="text-text-muted">No apps found</p></div>
66
+ ) : (
67
+ <div className="grid grid-cols-4 gap-4">
68
+ {filtered.map((i) => (
69
+ <div key={i.id} className="bg-bg-tertiary border border-border rounded-xl p-5 hover:border-accent-cyan/40 transition-colors group">
70
+ <div className="flex items-center gap-3 mb-3">
71
+ <div className="w-10 h-10 bg-bg-secondary rounded-lg flex items-center justify-center overflow-hidden border border-border">
72
+ <img src={i.logo_url} alt={i.name} className="w-6 h-6" onError={(e) => (e.currentTarget.style.display = 'none')} />
73
+ </div>
74
+ <div>
75
+ <p className="text-sm font-semibold">{i.name}</p>
76
+ <p className="text-[11px] text-text-muted">{i.category}</p>
77
+ </div>
78
+ </div>
79
+ <p className="text-xs text-text-secondary leading-relaxed line-clamp-2 mb-4">{i.description}</p>
80
+ <div className="flex items-center justify-between pt-3 border-t border-border">
81
+ <span className="text-[11px] text-text-muted">{i.tool_count} tools</span>
82
+ <span className={`text-[11px] px-2 py-0.5 rounded font-medium ${i.auth_type === 'oauth2' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'bg-accent-green/10 text-accent-green'}`}>
83
+ {i.auth_type}
84
+ </span>
85
+ </div>
86
+ </div>
87
+ ))}
88
+ </div>
89
+ )}
90
+ </div>
91
+ </main>
92
+ </div>
93
+ )
94
+ }
frontend/_src/app/globals.css ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --background: #09090b;
7
+ --foreground: #fafafa;
8
+ }
9
+
10
+ * { box-sizing: border-box; }
11
+
12
+ body {
13
+ background: var(--background);
14
+ color: var(--foreground);
15
+ font-family: 'Inter', system-ui, sans-serif;
16
+ -webkit-font-smoothing: antialiased;
17
+ }
18
+
19
+ ::-webkit-scrollbar { width: 6px; }
20
+ ::-webkit-scrollbar-track { background: #111113; }
21
+ ::-webkit-scrollbar-thumb { background: #27272a; border-radius: 3px; }
22
+ ::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
frontend/_src/app/layout.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import './globals.css'
4
+
5
+ const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
6
+
7
+ export const metadata: Metadata = {
8
+ title: 'Compost - AI Tool Platform',
9
+ description: 'Unified AI tool integration platform',
10
+ }
11
+
12
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
13
+ return (
14
+ <html lang="en">
15
+ <body className={`${inter.variable} font-sans bg-bg-primary text-text-primary`}>
16
+ {children}
17
+ </body>
18
+ </html>
19
+ )
20
+ }
frontend/_src/app/login/page.tsx ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+
5
+ export default function LoginPage() {
6
+ const [email, setEmail] = useState('')
7
+ const [password, setPassword] = useState('')
8
+ const [error, setError] = useState('')
9
+ const [loading, setLoading] = useState(false)
10
+
11
+ const handleSubmit = async (e: React.FormEvent) => {
12
+ e.preventDefault()
13
+ setError('')
14
+ setLoading(true)
15
+ try {
16
+ const formData = new URLSearchParams()
17
+ formData.append('username', email)
18
+ formData.append('password', password)
19
+ const res = await fetch('/api/auth/login', {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
22
+ body: formData,
23
+ })
24
+ const data = await res.json()
25
+ if (!res.ok) { setError(data.detail || 'Login failed'); return }
26
+ localStorage.setItem('token', data.access_token)
27
+ localStorage.setItem('email', data.user?.email || email)
28
+ window.location.href = '/'
29
+ } catch { setError('Network error') }
30
+ finally { setLoading(false) }
31
+ }
32
+
33
+ return (
34
+ <div className="min-h-screen flex items-center justify-center bg-bg-primary">
35
+ <div className="w-full max-w-sm">
36
+ <div className="text-center mb-8">
37
+ <div className="w-14 h-14 bg-accent-cyan rounded-2xl flex items-center justify-center mx-auto mb-4 font-bold text-xl text-bg-primary">C</div>
38
+ <h1 className="text-xl font-semibold">Compost</h1>
39
+ <p className="text-sm text-text-muted mt-1">AI Tool Integration Platform</p>
40
+ </div>
41
+ <div className="bg-bg-tertiary border border-border rounded-2xl p-6">
42
+ {error && <div className="mb-4 p-3 bg-accent-red/10 border border-accent-red/30 rounded-lg text-accent-red text-sm">{error}</div>}
43
+ <form onSubmit={handleSubmit} className="space-y-4">
44
+ <div>
45
+ <label className="block text-xs text-text-muted mb-1.5">Email</label>
46
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full px-3 py-2 bg-bg-secondary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan" placeholder="admin@platform.local" required />
47
+ </div>
48
+ <div>
49
+ <label className="block text-xs text-text-muted mb-1.5">Password</label>
50
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 bg-bg-secondary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan" placeholder="••••••••" required />
51
+ </div>
52
+ <button type="submit" disabled={loading} className="w-full py-2.5 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary font-medium text-sm rounded-lg transition-colors disabled:opacity-50">
53
+ {loading ? 'Signing in...' : 'Sign In'}
54
+ </button>
55
+ </form>
56
+ </div>
57
+ <p className="text-center text-xs text-text-dim mt-4">Default: admin@platform.local / admin123</p>
58
+ </div>
59
+ </div>
60
+ )
61
+ }
frontend/_src/app/page.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Sidebar } from '@/components/sidebar'
5
+ import { api, type Integration, type Stats } from '@/lib/api'
6
+ import { Activity, Plug, Zap, CheckCircle, Clock } from 'lucide-react'
7
+
8
+ export default function DashboardPage() {
9
+ const [integrations, setIntegrations] = useState<Integration[]>([])
10
+ const [stats, setStats] = useState<Stats | null>(null)
11
+ const [loading, setLoading] = useState(true)
12
+
13
+ useEffect(() => {
14
+ Promise.all([
15
+ api.get<Integration[]>('/api/apps').catch(() => []),
16
+ api.get<Stats>('/api/analytics/overview').catch(() => null),
17
+ ]).then(([apps, s]) => {
18
+ setIntegrations(apps)
19
+ setStats(s)
20
+ setLoading(false)
21
+ })
22
+ }, [])
23
+
24
+ const metrics = [
25
+ { label: 'Total Executions', value: stats?.total_executions ?? 0, icon: Activity, color: 'text-accent-cyan' },
26
+ { label: 'Integrations', value: integrations.length, icon: Plug, color: 'text-accent-green' },
27
+ { label: 'Success Rate', value: `${stats?.success_rate ?? 100}%`, icon: CheckCircle, color: 'text-accent-blue' },
28
+ { label: 'Avg Latency', value: `${stats?.avg_latency_ms ?? 0}ms`, icon: Clock, color: 'text-accent-amber' },
29
+ ]
30
+
31
+ return (
32
+ <div className="flex min-h-screen">
33
+ <Sidebar integrationCount={integrations.length} />
34
+ <main className="flex-1 flex flex-col">
35
+ <header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
36
+ <h1 className="text-base font-medium">Dashboard</h1>
37
+ </header>
38
+ <div className="flex-1 p-6 space-y-6 overflow-auto">
39
+ <div className="grid grid-cols-4 gap-4">
40
+ {metrics.map((m) => (
41
+ <div key={m.label} className="bg-bg-tertiary border border-border rounded-xl p-5">
42
+ <div className="flex items-center justify-between">
43
+ <div>
44
+ <p className="text-sm text-text-muted">{m.label}</p>
45
+ <p className="text-2xl font-bold mt-1">{loading ? '...' : m.value}</p>
46
+ </div>
47
+ <m.icon className={`w-5 h-5 ${m.color}`} />
48
+ </div>
49
+ </div>
50
+ ))}
51
+ </div>
52
+
53
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
54
+ <h3 className="text-sm font-medium mb-4">Quick Connect</h3>
55
+ {loading ? (
56
+ <p className="text-text-muted text-sm">Loading...</p>
57
+ ) : integrations.length === 0 ? (
58
+ <div className="text-center py-12">
59
+ <p className="text-text-muted">No integrations yet</p>
60
+ <p className="text-text-dim text-sm mt-1">Set COMPOSIO_API_KEY to sync all 1000+ tools</p>
61
+ </div>
62
+ ) : (
63
+ <div className="grid grid-cols-4 gap-4">
64
+ {integrations.slice(0, 8).map((i) => (
65
+ <div key={i.id} className="bg-bg-secondary border border-border rounded-lg p-4 hover:border-accent-cyan/40 transition-colors">
66
+ <div className="flex items-center gap-3 mb-3">
67
+ <div className="w-9 h-9 bg-bg-tertiary rounded-lg flex items-center justify-center overflow-hidden">
68
+ <img src={i.logo_url} alt={i.name} className="w-5 h-5" onError={(e) => (e.currentTarget.style.display = 'none')} />
69
+ </div>
70
+ <div>
71
+ <p className="text-sm font-medium">{i.name}</p>
72
+ <p className="text-[11px] text-text-muted">{i.category}</p>
73
+ </div>
74
+ </div>
75
+ <div className="flex items-center justify-between pt-3 border-t border-border">
76
+ <span className="text-[11px] text-text-muted">{i.tool_count} tools</span>
77
+ <span className={`text-[11px] px-2 py-0.5 rounded ${i.auth_type === 'oauth2' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'bg-accent-green/10 text-accent-green'}`}>
78
+ {i.auth_type}
79
+ </span>
80
+ </div>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ )}
85
+ </div>
86
+ </div>
87
+ </main>
88
+ </div>
89
+ )
90
+ }
frontend/_src/app/settings/page.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { Sidebar } from '@/components/sidebar'
4
+ import { Server, Database, Shield } from 'lucide-react'
5
+
6
+ export default function SettingsPage() {
7
+ return (
8
+ <div className="flex min-h-screen">
9
+ <Sidebar />
10
+ <main className="flex-1 flex flex-col">
11
+ <header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
12
+ <h1 className="text-base font-medium">Settings</h1>
13
+ </header>
14
+ <div className="flex-1 p-6 space-y-4 overflow-auto">
15
+ <div className="bg-bg-tertiary border border-border rounded-xl p-5">
16
+ <div className="flex items-center gap-3 mb-4"><Server className="w-5 h-5 text-accent-cyan" /><h3 className="text-sm font-medium">System</h3></div>
17
+ <div className="space-y-3 text-sm">
18
+ <div className="flex justify-between"><span className="text-text-muted">Version</span><span>1.0.0</span></div>
19
+ <div className="flex justify-between"><span className="text-text-muted">Data Dir</span><span className="font-mono">/data</span></div>
20
+ </div>
21
+ </div>
22
+ <div className="bg-bg-tertiary border border-border rounded-xl p-5">
23
+ <div className="flex items-center gap-3 mb-4"><Database className="w-5 h-5 text-accent-green" /><h3 className="text-sm font-medium">Database</h3></div>
24
+ <div className="space-y-3 text-sm">
25
+ <div className="flex justify-between"><span className="text-text-muted">Engine</span><span>SQLite</span></div>
26
+ <div className="flex justify-between"><span className="text-text-muted">Path</span><span className="font-mono">/data/platform.db</span></div>
27
+ </div>
28
+ </div>
29
+ <div className="bg-bg-tertiary border border-border rounded-xl p-5">
30
+ <div className="flex items-center gap-3 mb-4"><Shield className="w-5 h-5 text-accent-amber" /><h3 className="text-sm font-medium">Security</h3></div>
31
+ <div className="space-y-3 text-sm">
32
+ <div className="flex justify-between"><span className="text-text-muted">Auth</span><span>JWT (HS256)</span></div>
33
+ <div className="flex justify-between"><span className="text-text-muted">Vault</span><span>AES-256-GCM</span></div>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </main>
38
+ </div>
39
+ )
40
+ }
frontend/_src/app/tools/page.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Sidebar } from '@/components/sidebar'
5
+ import { api, type Tool } from '@/lib/api'
6
+ import { Search, Play } from 'lucide-react'
7
+
8
+ export default function ToolsPage() {
9
+ const [tools, setTools] = useState<Tool[]>([])
10
+ const [loading, setLoading] = useState(true)
11
+ const [search, setSearch] = useState('')
12
+
13
+ useEffect(() => {
14
+ api.get<Tool[]>('/api/tools')
15
+ .then((data) => { setTools(data); setLoading(false) })
16
+ .catch(() => setLoading(false))
17
+ }, [])
18
+
19
+ const filtered = tools.filter((t) =>
20
+ !search || t.name.toLowerCase().includes(search.toLowerCase()) || t.description.toLowerCase().includes(search.toLowerCase())
21
+ )
22
+
23
+ return (
24
+ <div className="flex min-h-screen">
25
+ <Sidebar />
26
+ <main className="flex-1 flex flex-col">
27
+ <header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6 justify-between">
28
+ <h1 className="text-base font-medium">Tools</h1>
29
+ <div className="relative">
30
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-dim" />
31
+ <input className="pl-9 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-64" placeholder="Search tools..." value={search} onChange={(e) => setSearch(e.target.value)} />
32
+ </div>
33
+ </header>
34
+ <div className="flex-1 p-6 overflow-auto">
35
+ {loading ? (
36
+ <div className="space-y-2">{Array.from({ length: 10 }).map((_, i) => <div key={i} className="bg-bg-tertiary border border-border rounded-lg p-4 animate-pulse"><div className="h-3 bg-bg-hover rounded w-32 mb-2" /><div className="h-2 bg-bg-hover rounded w-full" /></div>)}</div>
37
+ ) : filtered.length === 0 ? (
38
+ <div className="text-center py-20"><p className="text-text-muted">No tools found</p></div>
39
+ ) : (
40
+ <div className="space-y-2">
41
+ {filtered.map((t) => (
42
+ <div key={t.id} className="bg-bg-tertiary border border-border rounded-lg p-4 flex items-center justify-between hover:border-border-focus transition-colors">
43
+ <div className="flex items-center gap-4">
44
+ <div className="w-8 h-8 bg-bg-secondary border border-border rounded-lg flex items-center justify-center">
45
+ <Play className="w-3.5 h-3.5 text-text-muted" />
46
+ </div>
47
+ <div>
48
+ <p className="text-sm font-medium">{t.name}</p>
49
+ <p className="text-xs text-text-muted">{t.description}</p>
50
+ </div>
51
+ </div>
52
+ <div className="flex items-center gap-3">
53
+ <span className="text-[11px] bg-bg-secondary border border-border px-2 py-0.5 rounded text-text-muted">{t.integration_id}</span>
54
+ <span className="text-[11px] text-text-dim">{t.category}</span>
55
+ </div>
56
+ </div>
57
+ ))}
58
+ </div>
59
+ )}
60
+ </div>
61
+ </main>
62
+ </div>
63
+ )
64
+ }
frontend/_src/components/sidebar.tsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { LayoutDashboard, Plug, Wrench, Key, BarChart3, Settings, LogOut, ExternalLink } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ const navItems = [
9
+ { href: '/', icon: LayoutDashboard, label: 'Dashboard' },
10
+ { href: '/apps', icon: Plug, label: 'Apps' },
11
+ { href: '/tools', icon: Wrench, label: 'Tools' },
12
+ ]
13
+
14
+ const configItems = [
15
+ { href: '/api-keys', icon: Key, label: 'API Keys' },
16
+ { href: '/analytics', icon: BarChart3, label: 'Analytics' },
17
+ { href: '/settings', icon: Settings, label: 'Settings' },
18
+ ]
19
+
20
+ export function Sidebar({ integrationCount = 0 }: { integrationCount?: number }) {
21
+ const pathname = usePathname()
22
+
23
+ return (
24
+ <aside className="w-60 bg-bg-secondary border-r border-border flex flex-col shrink-0">
25
+ <div className="px-5 py-4 border-b border-border">
26
+ <div className="flex items-center gap-3">
27
+ <div className="w-8 h-8 bg-accent-cyan rounded-lg flex items-center justify-center font-bold text-bg-primary text-sm">C</div>
28
+ <span className="text-base font-semibold tracking-tight">Compost</span>
29
+ </div>
30
+ </div>
31
+
32
+ <nav className="flex-1 px-3 py-4 space-y-1">
33
+ <div className="px-3 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Main</div>
34
+ {navItems.map((item) => (
35
+ <Link key={item.href} href={item.href}
36
+ className={cn(
37
+ 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
38
+ pathname === item.href
39
+ ? 'bg-accent-cyan/10 text-accent-cyanLight font-medium'
40
+ : 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
41
+ )}
42
+ >
43
+ <item.icon className="w-4 h-4" />
44
+ {item.label}
45
+ {item.href === '/apps' && integrationCount > 0 && (
46
+ <span className="ml-auto text-[11px] bg-bg-tertiary text-text-muted px-1.5 py-0.5 rounded">{integrationCount}</span>
47
+ )}
48
+ </Link>
49
+ ))}
50
+
51
+ <div className="px-3 mt-6 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Configuration</div>
52
+ {configItems.map((item) => (
53
+ <Link key={item.href} href={item.href}
54
+ className={cn(
55
+ 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
56
+ pathname === item.href
57
+ ? 'bg-accent-cyan/10 text-accent-cyanLight font-medium'
58
+ : 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
59
+ )}
60
+ >
61
+ <item.icon className="w-4 h-4" />
62
+ {item.label}
63
+ </Link>
64
+ ))}
65
+
66
+ <a href="/api/docs" target="_blank" rel="noopener noreferrer"
67
+ className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-text-secondary hover:bg-bg-hover hover:text-text-primary transition-colors"
68
+ >
69
+ <ExternalLink className="w-4 h-4" />
70
+ API Docs
71
+ </a>
72
+ </nav>
73
+
74
+ <div className="px-3 py-4 border-t border-border">
75
+ <div className="flex items-center justify-between">
76
+ <div className="flex items-center gap-2 min-w-0">
77
+ <div className="w-7 h-7 bg-bg-tertiary rounded-full flex items-center justify-center text-xs font-semibold shrink-0">
78
+ {typeof window !== 'undefined' ? (localStorage.getItem('email')?.[0]?.toUpperCase() || 'U') : 'U'}
79
+ </div>
80
+ <span className="text-xs text-text-secondary truncate">
81
+ {typeof window !== 'undefined' ? localStorage.getItem('email') || 'User' : 'User'}
82
+ </span>
83
+ </div>
84
+ <button onClick={() => { localStorage.removeItem('token'); localStorage.removeItem('email'); window.location.href = '/login' }}
85
+ className="p-1.5 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors"
86
+ >
87
+ <LogOut className="w-3.5 h-3.5" />
88
+ </button>
89
+ </div>
90
+ </div>
91
+ </aside>
92
+ )
93
+ }
frontend/_src/lib/api.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Integration {
2
+ id: string
3
+ name: string
4
+ description: string
5
+ logo_url: string
6
+ category: string
7
+ auth_type: string
8
+ tool_count: number
9
+ trigger_count: number
10
+ is_active: boolean
11
+ }
12
+
13
+ export interface Tool {
14
+ id: string
15
+ integration_id: string
16
+ name: string
17
+ description: string
18
+ input_schema: Record<string, unknown>
19
+ output_schema?: Record<string, unknown>
20
+ category: string
21
+ is_active: boolean
22
+ }
23
+
24
+ export interface Stats {
25
+ total_executions: number
26
+ successful: number
27
+ failed: number
28
+ success_rate: number
29
+ avg_latency_ms: number
30
+ }
31
+
32
+ const API_BASE = typeof window !== 'undefined' ? '' : process.env.NEXT_PUBLIC_API_URL || ''
33
+
34
+ async function fetchApi(path: string, opts: RequestInit = {}) {
35
+ const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null
36
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' }
37
+ if (token) headers['Authorization'] = `Bearer ${token}`
38
+
39
+ const res = await fetch(`${API_BASE}${path}`, { ...opts, headers })
40
+ if (res.status === 401) {
41
+ if (typeof window !== 'undefined') {
42
+ localStorage.removeItem('token')
43
+ window.location.href = '/login'
44
+ }
45
+ throw new Error('Unauthorized')
46
+ }
47
+ return res.json()
48
+ }
49
+
50
+ export const api = {
51
+ get: <T>(path: string) => fetchApi(path) as Promise<T>,
52
+ post: <T>(path: string, body?: unknown) =>
53
+ fetchApi(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }) as Promise<T>,
54
+ delete: <T>(path: string) => fetchApi(path, { method: 'DELETE' }) as Promise<T>,
55
+ }
frontend/_src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }