Fix health endpoint routing order
Browse files- backend/main.py +7 -7
- backend/static/index.html +0 -1
- frontend/_src/app/analytics/page.tsx +59 -0
- frontend/_src/app/api-keys/page.tsx +26 -0
- frontend/_src/app/apps/page.tsx +94 -0
- frontend/_src/app/globals.css +22 -0
- frontend/_src/app/layout.tsx +20 -0
- frontend/_src/app/login/page.tsx +61 -0
- frontend/_src/app/page.tsx +90 -0
- frontend/_src/app/settings/page.tsx +40 -0
- frontend/_src/app/tools/page.tsx +64 -0
- frontend/_src/components/sidebar.tsx +93 -0
- frontend/_src/lib/api.ts +55 -0
- frontend/_src/lib/utils.ts +6 -0
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 |
+
}
|