@splyntra/dashboard 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ <p align="center">
2
+ <img src="https://avatars.githubusercontent.com/u/291030557?s=200" alt="Splyntra" width="64" />
3
+ </p>
4
+
5
+ # @splyntra/dashboard
6
+
7
+ [![npm](https://img.shields.io/npm/v/@splyntra/dashboard)](https://www.npmjs.com/package/@splyntra/dashboard)
8
+ [![License](https://img.shields.io/badge/license-AGPL--3.0-blue.svg)](../../LICENSE)
9
+
10
+ The Splyntra open dashboard — a composable Next.js application providing trace visualization, agent metrics, cost analytics, evaluation results, alerts, and team management with RBAC.
11
+
12
+ Published as **source** (not a prebuilt library). Consumers compose it with their overlays and run `next build`.
13
+
14
+ ## Prerequisites
15
+
16
+ - Node.js 20+
17
+ - PostgreSQL (metadata store)
18
+ - Running Splyntra Collector (`localhost:4318`)
19
+ - Running Evaluation service (`localhost:8002`) — optional
20
+
21
+ ## Local Development
22
+
23
+ ```bash
24
+ # From the monorepo root
25
+ docker compose up -d # starts Postgres, ClickHouse, Collector, etc.
26
+
27
+ # From this directory
28
+ cp .env.local.example .env.local # configure environment
29
+ npm install
30
+ npm run dev # http://localhost:3000
31
+ ```
32
+
33
+ ## Environment Variables
34
+
35
+ | Variable | Default | Description |
36
+ |-------------------|----------------------------------|-------------------------------------|
37
+ | `NEXTAUTH_SECRET` | — | Secret for NextAuth.js session encryption |
38
+ | `NEXTAUTH_URL` | `http://localhost:3000` | Canonical app URL |
39
+ | `POSTGRES_DSN` | — | PostgreSQL connection string |
40
+ | `COLLECTOR_URL` | `http://localhost:4318` | Splyntra Collector base URL |
41
+ | `EVAL_URL` | `http://localhost:8002` | Evaluation service base URL |
42
+
43
+ ## Scripts
44
+
45
+ | Command | Description |
46
+ |------------------|----------------------------------|
47
+ | `npm run dev` | Start development server |
48
+ | `npm run build` | Production build |
49
+ | `npm run start` | Start production server |
50
+ | `npm run lint` | Run ESLint |
51
+ | `npm run test` | Run tests (Vitest) |
52
+
53
+ ## Tech Stack
54
+
55
+ - **Framework:** Next.js 14 (App Router)
56
+ - **Auth:** NextAuth.js v5
57
+ - **Styling:** Tailwind CSS
58
+ - **Data fetching:** TanStack Query
59
+ - **Charts:** Recharts
60
+ - **Testing:** Vitest + React Testing Library
61
+
62
+ ## Pages
63
+
64
+ | Route | Purpose |
65
+ |-------------------|----------------------------------------------|
66
+ | `/` | Dashboard overview |
67
+ | `/traces` | Trace list and detail viewer |
68
+ | `/agents` | Agent registry and status |
69
+ | `/metrics` | Time-series observability metrics |
70
+ | `/costs` | Token and cost analytics |
71
+ | `/evaluations` | Evaluation runs and regression results |
72
+ | `/alerts` | Alert configuration and history |
73
+ | `/projects` | Project management |
74
+ | `/settings/team` | Team members, invites, RBAC |
75
+ | `/settings/keys` | API key management |
76
+
77
+ ## License
78
+
79
+ AGPL-3.0 — see [LICENSE](../../LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splyntra/dashboard",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "description": "Splyntra open dashboard — the composable source the commercial cloud build overlays. Published as source (not a prebuilt library): consumers compose it with their overlays + `next build`.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
Binary file
@@ -5,5 +5,8 @@
5
5
  "start_url": "/",
6
6
  "display": "standalone",
7
7
  "background_color": "#ffffff",
8
- "theme_color": "#4c6ef5"
8
+ "theme_color": "#4c6ef5",
9
+ "icons": [
10
+ { "src": "/logo.png", "sizes": "1000x1000", "type": "image/png", "purpose": "any maskable" }
11
+ ]
9
12
  }
Binary file
@@ -23,7 +23,7 @@ export default function AgentsPage() {
23
23
  : 0;
24
24
 
25
25
  return (
26
- <div className="mx-auto max-w-6xl p-6">
26
+ <div className="mx-auto max-w-7xl p-6 lg:p-8">
27
27
  <PageHeader
28
28
  icon={Bot}
29
29
  title="Agents"
@@ -53,17 +53,17 @@ export default function AgentsPage() {
53
53
  </EmptyState>
54
54
  ) : (
55
55
  <table className="w-full text-sm">
56
- <thead className="bg-gray-50 dark:bg-gray-800 border-b">
56
+ <thead className="border-b border-gray-100 bg-gray-50/80 dark:border-gray-800 dark:bg-gray-900/50">
57
57
  <tr>
58
- <th className="px-4 py-3 text-left font-medium text-gray-500">Agent</th>
59
- <th className="px-4 py-3 text-left font-medium text-gray-500">Status</th>
60
- <th className="px-4 py-3 text-right font-medium text-gray-500">Traces</th>
61
- <th className="px-4 py-3 text-right font-medium text-gray-500">Errors</th>
62
- <th className="px-4 py-3 text-right font-medium text-gray-500">Avg Latency</th>
63
- <th className="px-4 py-3 text-right font-medium text-gray-500">P95 Latency</th>
64
- <th className="px-4 py-3 text-right font-medium text-gray-500">Cost</th>
65
- <th className="px-4 py-3 text-right font-medium text-gray-500">Detections</th>
66
- <th className="px-4 py-3 text-right font-medium text-gray-500">Last Seen</th>
58
+ <th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500">Agent</th>
59
+ <th className="px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-wider text-gray-500">Status</th>
60
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">Traces</th>
61
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">Errors</th>
62
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">Avg Latency</th>
63
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">P95 Latency</th>
64
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">Cost</th>
65
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">Detections</th>
66
+ <th className="px-4 py-3 text-right text-[11px] font-semibold uppercase tracking-wider text-gray-500">Last Seen</th>
67
67
  </tr>
68
68
  </thead>
69
69
  <tbody className="divide-y">
@@ -66,7 +66,7 @@ export default function AlertsPage() {
66
66
  }
67
67
 
68
68
  return (
69
- <div className="mx-auto max-w-5xl p-6">
69
+ <div className="mx-auto max-w-6xl p-6 lg:p-8">
70
70
  <PageHeader
71
71
  icon={Bell}
72
72
  title="Alerts"
@@ -76,7 +76,7 @@ export default function AlertsPage() {
76
76
  {/* Create form */}
77
77
  <form
78
78
  onSubmit={onCreate}
79
- className="bg-white dark:bg-gray-900 rounded-lg border p-4 mb-6 grid gap-3 md:grid-cols-4"
79
+ className="mb-6 grid gap-3 rounded-xl border border-gray-200/80 bg-white p-5 shadow-card dark:border-gray-800 dark:bg-gray-900 md:grid-cols-4"
80
80
  >
81
81
  <label className="flex flex-col text-sm md:col-span-2">
82
82
  <span className="text-xs text-gray-500 mb-1">Alert name</span>
@@ -6,6 +6,7 @@
6
6
  import { NextRequest, NextResponse } from "next/server";
7
7
  import { auth as getSession } from "@/auth";
8
8
  import { roleAtLeast } from "@/lib/db";
9
+ import "@/lib/collector-auth-providers"; // side-effect: registers the resolver (no-op in OSS, OAuth/org in cloud)
9
10
  import { resolveCollectorAuth } from "@/lib/collector-auth";
10
11
 
11
12
  export const dynamic = "force-dynamic";
@@ -10,6 +10,7 @@
10
10
  import { NextRequest, NextResponse } from "next/server";
11
11
  import { auth as getSession } from "@/auth";
12
12
  import { roleAtLeast } from "@/lib/db";
13
+ import "@/lib/collector-auth-providers"; // side-effect: registers the resolver (no-op in OSS, OAuth/org in cloud)
13
14
  import { resolveCollectorAuth } from "@/lib/collector-auth";
14
15
 
15
16
  export const dynamic = "force-dynamic";
@@ -20,7 +20,7 @@ export default function CostsPage() {
20
20
  const avgCostPerCall = totalCalls > 0 ? summary.avg_cost_per_call : 0;
21
21
 
22
22
  return (
23
- <div className="mx-auto max-w-6xl p-6">
23
+ <div className="mx-auto max-w-7xl p-6 lg:p-8">
24
24
  <PageHeader icon={DollarSign} title="Costs" subtitle="Token spend by run, model, and project" />
25
25
  {!hasRealData && !isLoading && (
26
26
  <p className="-mt-2 mb-4 text-xs text-amber-600">
@@ -46,7 +46,7 @@ export default function CostsPage() {
46
46
  {byProject.map((p) => (
47
47
  <div
48
48
  key={p.project_id}
49
- className="bg-white dark:bg-gray-900 rounded-lg border p-4"
49
+ className="rounded-xl border border-gray-200/80 bg-white p-5 shadow-card dark:border-gray-800 dark:bg-gray-900"
50
50
  >
51
51
  <div className="text-xs font-medium font-mono truncate" title={p.project_id}>
52
52
  {p.project_id}
@@ -2,41 +2,87 @@
2
2
  @tailwind components;
3
3
  @tailwind utilities;
4
4
 
5
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
6
+
5
7
  :root {
6
- --background: #f8fafc;
7
- --foreground: #1c2541;
8
+ --background: #fafbfc;
9
+ --foreground: #1e1b4b;
10
+ --sidebar-bg: #ffffff;
11
+ --card-bg: #ffffff;
12
+ --border: #e5e7eb;
13
+ --accent: #4f46e5;
14
+ --accent-light: #eef2ff;
8
15
  }
9
16
 
10
17
  @media (prefers-color-scheme: dark) {
11
18
  :root {
12
- --background: #0b0f14;
13
- --foreground: #e1e8ed;
19
+ --background: #09090b;
20
+ --foreground: #f4f4f5;
21
+ --sidebar-bg: #0f0f12;
22
+ --card-bg: #18181b;
23
+ --border: #27272a;
24
+ --accent: #6366f1;
25
+ --accent-light: #1e1b4b;
14
26
  }
15
27
  }
16
28
 
17
29
  body {
18
30
  color: var(--foreground);
19
31
  background: var(--background);
20
- font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
32
+ font-family: 'Inter', ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
21
33
  "Helvetica Neue", Arial, sans-serif;
22
34
  -webkit-font-smoothing: antialiased;
23
35
  -moz-osx-font-smoothing: grayscale;
24
36
  text-rendering: optimizeLegibility;
37
+ letter-spacing: -0.011em;
38
+ }
39
+
40
+ /* Focus states — accessible and consistent */
41
+ *:focus-visible {
42
+ outline: 2px solid var(--accent);
43
+ outline-offset: 2px;
44
+ border-radius: 4px;
25
45
  }
26
46
 
27
47
  /* Subtle, unobtrusive scrollbars */
28
48
  * {
29
49
  scrollbar-width: thin;
30
- scrollbar-color: rgba(148, 163, 184, 0.5) transparent;
50
+ scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
31
51
  }
32
52
  *::-webkit-scrollbar {
33
- width: 8px;
34
- height: 8px;
53
+ width: 6px;
54
+ height: 6px;
35
55
  }
36
56
  *::-webkit-scrollbar-thumb {
37
- background-color: rgba(148, 163, 184, 0.5);
57
+ background-color: rgba(148, 163, 184, 0.3);
38
58
  border-radius: 9999px;
39
59
  }
60
+ *::-webkit-scrollbar-thumb:hover {
61
+ background-color: rgba(148, 163, 184, 0.5);
62
+ }
40
63
  *::-webkit-scrollbar-track {
41
64
  background: transparent;
42
65
  }
66
+
67
+ /* Utility classes */
68
+ @layer utilities {
69
+ .text-gradient {
70
+ @apply bg-clip-text text-transparent bg-gradient-to-r from-splyntra-600 to-splyntra-400;
71
+ }
72
+ .glass-card {
73
+ @apply bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm border border-gray-200/60 dark:border-gray-800/60;
74
+ }
75
+ }
76
+
77
+ /* Table styling */
78
+ table {
79
+ border-collapse: separate;
80
+ border-spacing: 0;
81
+ }
82
+
83
+ /* Smooth transitions for interactive elements */
84
+ a, button, input, select, textarea {
85
+ transition-property: color, background-color, border-color, box-shadow, opacity;
86
+ transition-duration: 150ms;
87
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
88
+ }
@@ -5,8 +5,10 @@ import { Providers } from "./providers";
5
5
  import { AppShell } from "@/components/layout/AppShell";
6
6
 
7
7
  export const metadata: Metadata = {
8
- title: "Splyntra - Agent Observability & Security",
8
+ title: "Splyntra Agent Observability & Security",
9
9
  description: "Unified observability and security for AI agents",
10
+ icons: { icon: "/logo.png", apple: "/logo.png", shortcut: "/logo.png" },
11
+ manifest: "/manifest.json",
10
12
  };
11
13
 
12
14
  export default function RootLayout({
@@ -15,8 +17,8 @@ export default function RootLayout({
15
17
  children: React.ReactNode;
16
18
  }) {
17
19
  return (
18
- <html lang="en">
19
- <body className="min-h-screen bg-gray-50 dark:bg-gray-950">
20
+ <html lang="en" suppressHydrationWarning>
21
+ <body className="min-h-screen bg-[var(--background)] antialiased">
20
22
  <Providers>
21
23
  <AppShell>{children}</AppShell>
22
24
  </Providers>
@@ -29,21 +29,21 @@ export default function LoginPage() {
29
29
 
30
30
  return (
31
31
  <AuthCard title="Sign in to Splyntra">
32
- <form onSubmit={onSubmit} className="space-y-3">
32
+ <form onSubmit={onSubmit} className="space-y-4">
33
33
  <Field name="email" type="email" label="Email" />
34
34
  <Field name="password" type="password" label="Password" />
35
35
  {error && <p className="text-sm text-red-600">{error}</p>}
36
36
  <button
37
37
  type="submit"
38
38
  disabled={busy}
39
- className="w-full rounded-lg bg-splyntra-600 py-2 text-sm font-medium text-white hover:bg-splyntra-700 disabled:opacity-50"
39
+ className="w-full rounded-xl bg-gradient-to-r from-splyntra-600 to-splyntra-500 py-2.5 text-sm font-semibold text-white shadow-md shadow-splyntra-500/20 transition-all hover:from-splyntra-700 hover:to-splyntra-600 hover:shadow-lg hover:shadow-splyntra-500/30 disabled:opacity-50"
40
40
  >
41
41
  {busy ? "Signing in…" : "Sign in"}
42
42
  </button>
43
43
  </form>
44
- <p className="mt-4 text-center text-sm text-gray-500">
44
+ <p className="mt-5 text-center text-[13px] text-gray-500">
45
45
  No account?{" "}
46
- <Link href="/signup" className="text-splyntra-600 hover:underline">
46
+ <Link href="/signup" className="font-medium text-splyntra-600 hover:text-splyntra-700 hover:underline">
47
47
  Create one
48
48
  </Link>
49
49
  </p>
package/src/app/page.tsx CHANGED
@@ -8,12 +8,16 @@ import {
8
8
  Bell,
9
9
  ShieldCheck,
10
10
  ArrowRight,
11
+ LineChart,
12
+ ClipboardCheck,
11
13
  type LucideIcon,
12
14
  } from "lucide-react";
13
15
 
14
16
  const cards: { href: string; title: string; desc: string; icon: LucideIcon }[] = [
15
17
  { href: "/traces", title: "Traces", desc: "Full execution traces with unified risk scores", icon: Activity },
16
18
  { href: "/agents", title: "Agents", desc: "Monitor registered agents, latency, and errors", icon: Bot },
19
+ { href: "/metrics", title: "Metrics", desc: "Time-series observability metrics and trends", icon: LineChart },
20
+ { href: "/evaluations", title: "Evaluation", desc: "Scored evaluations and regression gates", icon: ClipboardCheck },
17
21
  { href: "/costs", title: "Costs", desc: "Token spend by run, model, and project", icon: DollarSign },
18
22
  { href: "/projects", title: "Projects", desc: "Scope every view to a project", icon: FolderKanban },
19
23
  { href: "/alerts", title: "Alerts", desc: "Fire on risk thresholds; view history", icon: Bell },
@@ -21,36 +25,48 @@ const cards: { href: string; title: string; desc: string; icon: LucideIcon }[] =
21
25
 
22
26
  export default function Home() {
23
27
  return (
24
- <div className="mx-auto flex min-h-full max-w-4xl flex-col justify-center px-6 py-16">
25
- <div className="mb-10 text-center">
26
- <div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-splyntra-600 text-white shadow-sm">
27
- <ShieldCheck className="h-7 w-7" />
28
+ <div className="mx-auto flex min-h-full max-w-5xl flex-col justify-center px-8 py-16">
29
+ {/* Hero */}
30
+ <div className="mb-12 text-center">
31
+ <div className="mb-5 inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-splyntra-500 to-splyntra-700 text-white shadow-lg shadow-splyntra-500/25">
32
+ <ShieldCheck className="h-8 w-8" />
28
33
  </div>
29
- <h1 className="text-3xl font-bold tracking-tight text-splyntra-900 dark:text-white">Splyntra</h1>
30
- <p className="mx-auto mt-3 max-w-xl text-base text-gray-600 dark:text-gray-400">
34
+ <h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
35
+ Welcome to <span className="text-gradient">Splyntra</span>
36
+ </h1>
37
+ <p className="mx-auto mt-4 max-w-lg text-[15px] leading-relaxed text-gray-500 dark:text-gray-400">
31
38
  Unified observability and security for AI agents. See what your agents did and
32
39
  whether it was safe — in one view.
33
40
  </p>
34
41
  </div>
35
42
 
43
+ {/* Navigation cards */}
36
44
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
37
45
  {cards.map(({ href, title, desc, icon: Icon }) => (
38
46
  <Link
39
47
  key={href}
40
48
  href={href}
41
- className="group rounded-xl border border-gray-200 bg-white p-5 shadow-sm transition-all hover:border-splyntra-400 hover:shadow-md dark:border-gray-800 dark:bg-gray-900"
49
+ className="group relative rounded-xl border border-gray-200/80 bg-white p-6 shadow-card transition-all duration-200 hover:border-splyntra-300 hover:shadow-card-hover dark:border-gray-800 dark:bg-gray-900 dark:hover:border-splyntra-700"
42
50
  >
43
- <div className="mb-3 flex h-9 w-9 items-center justify-center rounded-lg bg-splyntra-50 text-splyntra-600 dark:bg-splyntra-900/30 dark:text-splyntra-100">
51
+ <div className="mb-4 flex h-10 w-10 items-center justify-center rounded-xl bg-splyntra-50 text-splyntra-600 transition-colors group-hover:bg-splyntra-100 dark:bg-splyntra-950/40 dark:text-splyntra-300">
44
52
  <Icon className="h-5 w-5" />
45
53
  </div>
46
- <h2 className="flex items-center gap-1 font-semibold text-gray-900 dark:text-white">
54
+ <h2 className="flex items-center gap-1.5 text-[15px] font-semibold text-gray-900 dark:text-white">
47
55
  {title}
48
- <ArrowRight className="h-4 w-4 -translate-x-1 text-gray-300 opacity-0 transition-all group-hover:translate-x-0 group-hover:opacity-100" />
56
+ <ArrowRight className="h-4 w-4 -translate-x-1 text-gray-300 opacity-0 transition-all duration-200 group-hover:translate-x-0 group-hover:text-splyntra-500 group-hover:opacity-100" />
49
57
  </h2>
50
- <p className="mt-1 text-sm text-gray-500">{desc}</p>
58
+ <p className="mt-1.5 text-[13px] leading-relaxed text-gray-500 dark:text-gray-400">{desc}</p>
51
59
  </Link>
52
60
  ))}
53
61
  </div>
62
+
63
+ {/* Quick status */}
64
+ <div className="mt-12 text-center">
65
+ <div className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-[13px] text-gray-500 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400">
66
+ <span className="h-2 w-2 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/50" />
67
+ Collector connected at localhost:4318
68
+ </div>
69
+ </div>
54
70
  </div>
55
71
  );
56
72
  }
@@ -13,7 +13,7 @@ export default function TracesPage() {
13
13
  const traces = data?.traces || [];
14
14
 
15
15
  return (
16
- <div className="mx-auto max-w-6xl p-6">
16
+ <div className="mx-auto max-w-7xl p-6 lg:p-8">
17
17
  <PageHeader
18
18
  icon={Activity}
19
19
  title="Traces"
@@ -1,17 +1,15 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  "use client";
3
3
 
4
- import { ShieldCheck } from "lucide-react";
4
+ import Image from "next/image";
5
5
 
6
6
  export function AuthCard({ title, children }: { title: string; children: React.ReactNode }) {
7
7
  return (
8
- <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-gray-950">
9
- <div className="w-full max-w-sm rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
10
- <div className="mb-5 flex items-center gap-2">
11
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-splyntra-600 text-white">
12
- <ShieldCheck className="h-5 w-5" />
13
- </div>
14
- <h1 className="text-lg font-semibold tracking-tight">{title}</h1>
8
+ <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-gray-50 via-white to-splyntra-50/30 px-4 dark:from-gray-950 dark:via-gray-950 dark:to-splyntra-950/20">
9
+ <div className="w-full max-w-sm animate-slide-up rounded-2xl border border-gray-200/80 bg-white p-8 shadow-lg shadow-gray-200/50 dark:border-gray-800 dark:bg-gray-900 dark:shadow-none">
10
+ <div className="mb-6 flex flex-col items-center gap-3">
11
+ <Image src="/logo.png" alt="Splyntra" width={56} height={56} priority className="h-14 w-14 rounded-2xl shadow-lg shadow-splyntra-500/25" />
12
+ <h1 className="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">{title}</h1>
15
13
  </div>
16
14
  {children}
17
15
  </div>
@@ -32,13 +30,13 @@ export function Field({
32
30
  }) {
33
31
  return (
34
32
  <label className="block text-sm">
35
- <span className="mb-1 block text-gray-600 dark:text-gray-300">{label}</span>
33
+ <span className="mb-1.5 block text-[13px] font-medium text-gray-700 dark:text-gray-300">{label}</span>
36
34
  <input
37
35
  name={name}
38
36
  type={type}
39
37
  defaultValue={defaultValue}
40
38
  required
41
- className="w-full rounded-lg border border-gray-200 px-3 py-2 outline-none focus:border-splyntra-400 focus:ring-2 focus:ring-splyntra-100 dark:border-gray-700 dark:bg-gray-800"
39
+ className="w-full rounded-xl border border-gray-200 bg-gray-50 px-4 py-2.5 text-[14px] outline-none transition-all placeholder:text-gray-400 focus:border-splyntra-400 focus:bg-white focus:ring-2 focus:ring-splyntra-100 dark:border-gray-700 dark:bg-gray-800 dark:focus:bg-gray-800 dark:focus:ring-splyntra-900"
42
40
  />
43
41
  </label>
44
42
  );
@@ -11,12 +11,16 @@ const AUTH_ROUTES = ["/login", "/signup", "/accept-invite"];
11
11
  export function AppShell({ children }: { children: React.ReactNode }) {
12
12
  const pathname = usePathname();
13
13
  if (AUTH_ROUTES.some((p) => pathname.startsWith(p))) {
14
- return <div className="min-h-screen">{children}</div>;
14
+ return <div className="min-h-screen animate-fade-in">{children}</div>;
15
15
  }
16
16
  return (
17
- <div className="flex h-screen">
17
+ <div className="flex h-screen overflow-hidden">
18
18
  <Sidebar />
19
- <main className="flex-1 overflow-auto">{children}</main>
19
+ <main className="flex-1 overflow-auto bg-gray-50/50 dark:bg-gray-950/50">
20
+ <div className="animate-fade-in">
21
+ {children}
22
+ </div>
23
+ </main>
20
24
  </div>
21
25
  );
22
26
  }
@@ -2,6 +2,7 @@
2
2
  "use client";
3
3
 
4
4
  import Link from "next/link";
5
+ import Image from "next/image";
5
6
  import { usePathname } from "next/navigation";
6
7
  import { useSession, signOut } from "next-auth/react";
7
8
  import {
@@ -19,7 +20,6 @@ import {
19
20
  KeyRound,
20
21
  CreditCard,
21
22
  Building2,
22
- ShieldCheck,
23
23
  ChevronDown,
24
24
  LogOut,
25
25
  type LucideIcon,
@@ -68,22 +68,20 @@ export function Sidebar() {
68
68
  const items = resolveNavItems();
69
69
 
70
70
  return (
71
- <aside className="flex w-60 flex-col border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
71
+ <aside className="flex w-64 flex-col border-r border-gray-100 bg-white shadow-sidebar dark:border-gray-800/50 dark:bg-gray-950">
72
72
  {/* Logo */}
73
- <div className="flex h-16 items-center border-b border-gray-200 px-4 dark:border-gray-800">
74
- <Link href="/" className="flex items-center gap-2.5">
75
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-splyntra-600 text-white shadow-sm">
76
- <ShieldCheck className="h-5 w-5" />
77
- </div>
73
+ <div className="flex h-16 items-center border-b border-gray-100 px-5 dark:border-gray-800/50">
74
+ <Link href="/" className="flex items-center gap-3">
75
+ <Image src="/logo.png" alt="Splyntra" width={36} height={36} priority className="h-9 w-9 rounded-xl shadow-md shadow-splyntra-500/20" />
78
76
  <div className="leading-tight">
79
- <span className="block font-semibold tracking-tight text-splyntra-900 dark:text-white">Splyntra</span>
80
- <span className="block text-[10px] uppercase tracking-wider text-gray-400">Observability + Security</span>
77
+ <span className="block text-[15px] font-semibold tracking-tight text-gray-900 dark:text-white">Splyntra</span>
78
+ <span className="block text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">Observability · Security</span>
81
79
  </div>
82
80
  </Link>
83
81
  </div>
84
82
 
85
83
  {/* Sidebar-top widgets (e.g. org switcher in the cloud build) + project selector */}
86
- <div className="space-y-3 border-b border-gray-100 px-3 py-3 dark:border-gray-800">
84
+ <div className="space-y-3 border-b border-gray-100 px-4 py-4 dark:border-gray-800/50">
87
85
  {slotWidgets("sidebarTop").map((W, i) => (
88
86
  <W key={i} />
89
87
  ))}
@@ -91,7 +89,7 @@ export function Sidebar() {
91
89
  </div>
92
90
 
93
91
  {/* Nav */}
94
- <nav className="flex-1 space-y-0.5 p-3">
92
+ <nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
95
93
  {items.map((item) => {
96
94
  const isActive = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
97
95
  const Icon = item.icon;
@@ -99,13 +97,13 @@ export function Sidebar() {
99
97
  <Link
100
98
  key={item.href}
101
99
  href={item.href}
102
- className={`group flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors ${
100
+ className={`group flex items-center gap-3 rounded-lg px-3 py-2.5 text-[13px] font-medium transition-all ${
103
101
  isActive
104
- ? "bg-splyntra-50 font-medium text-splyntra-700 dark:bg-splyntra-900/30 dark:text-splyntra-100"
105
- : "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
102
+ ? "bg-splyntra-50 text-splyntra-700 shadow-sm shadow-splyntra-100/50 dark:bg-splyntra-950/40 dark:text-splyntra-200 dark:shadow-none"
103
+ : "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-900 dark:hover:text-gray-200"
106
104
  }`}
107
105
  >
108
- <Icon className={`h-4 w-4 ${isActive ? "text-splyntra-600 dark:text-splyntra-300" : "text-gray-400 group-hover:text-gray-500"}`} />
106
+ <Icon className={`h-[18px] w-[18px] flex-shrink-0 ${isActive ? "text-splyntra-600 dark:text-splyntra-400" : "text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300"}`} />
109
107
  {item.label}
110
108
  </Link>
111
109
  );
@@ -113,13 +111,15 @@ export function Sidebar() {
113
111
  </nav>
114
112
 
115
113
  {/* Footer */}
116
- <div className="border-t border-gray-200 px-3 py-3 dark:border-gray-800">
114
+ <div className="border-t border-gray-100 px-4 py-3 dark:border-gray-800/50">
117
115
  <UserFooter />
118
- <div className="mt-2 px-1 text-xs text-gray-400">
116
+ <div className="mt-2 flex items-center gap-3 px-2 text-[11px] text-gray-400">
119
117
  <span className="inline-flex items-center gap-1.5">
120
- <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> dev
118
+ <span className="h-1.5 w-1.5 rounded-full bg-emerald-500 shadow-sm shadow-emerald-500/50" />
119
+ Connected
121
120
  </span>
122
- <span className="ml-2">v0.1.0</span>
121
+ <span className="text-gray-300 dark:text-gray-700">·</span>
122
+ <span>v0.3.0</span>
123
123
  </div>
124
124
  </div>
125
125
  </aside>
@@ -131,15 +131,15 @@ function UserFooter() {
131
131
  const user = session?.user as { email?: string; role?: string } | undefined;
132
132
  if (!user?.email) return null;
133
133
  return (
134
- <div className="flex items-center justify-between gap-2 rounded-lg px-2 py-1.5">
134
+ <div className="flex items-center justify-between gap-2 rounded-lg px-2 py-2 hover:bg-gray-50 dark:hover:bg-gray-900">
135
135
  <div className="min-w-0">
136
- <div className="truncate text-xs font-medium text-gray-700 dark:text-gray-200">{user.email}</div>
137
- {user.role && <div className="text-[10px] uppercase tracking-wide text-gray-400">{user.role}</div>}
136
+ <div className="truncate text-[13px] font-medium text-gray-700 dark:text-gray-200">{user.email}</div>
137
+ {user.role && <div className="text-[10px] font-medium uppercase tracking-wide text-gray-400">{user.role}</div>}
138
138
  </div>
139
139
  <button
140
140
  onClick={() => signOut({ callbackUrl: "/login" })}
141
141
  title="Sign out"
142
- className="rounded-md p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800"
142
+ className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300"
143
143
  >
144
144
  <LogOut className="h-4 w-4" />
145
145
  </button>
@@ -156,14 +156,14 @@ function ProjectSelector() {
156
156
 
157
157
  return (
158
158
  <label className="block">
159
- <span className="mb-1 block text-[10px] font-medium uppercase tracking-wider text-gray-400">Project</span>
159
+ <span className="mb-1.5 block text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Project</span>
160
160
  <div className="relative">
161
161
  <select
162
162
  value={projectId}
163
163
  onChange={(e) => setProjectId(e.target.value)}
164
- className="w-full appearance-none rounded-lg border border-gray-200 bg-white py-1.5 pl-2.5 pr-8 text-sm text-gray-700 outline-none focus:border-splyntra-400 focus:ring-2 focus:ring-splyntra-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
164
+ className="w-full appearance-none rounded-lg border border-gray-200 bg-gray-50 py-2 pl-3 pr-8 text-[13px] font-medium text-gray-700 outline-none transition-colors focus:border-splyntra-400 focus:bg-white focus:ring-2 focus:ring-splyntra-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200 dark:focus:bg-gray-800"
165
165
  >
166
- <option value="">All / default</option>
166
+ <option value="">All projects</option>
167
167
  {projects.map((p) => (
168
168
  <option key={p.id} value={p.id}>
169
169
  {p.name} ({p.environment})
@@ -103,16 +103,16 @@ export function PageHeader({
103
103
  action?: ReactNode;
104
104
  }) {
105
105
  return (
106
- <div className="mb-6 flex items-start justify-between gap-4">
106
+ <div className="mb-8 flex items-start justify-between gap-4">
107
107
  <div className="flex items-start gap-3">
108
108
  {Icon && (
109
- <div className="mt-0.5 flex h-9 w-9 items-center justify-center rounded-lg bg-splyntra-50 text-splyntra-600 dark:bg-splyntra-900/30 dark:text-splyntra-100">
109
+ <div className="mt-0.5 flex h-10 w-10 items-center justify-center rounded-xl bg-splyntra-50 text-splyntra-600 dark:bg-splyntra-950/40 dark:text-splyntra-300">
110
110
  <Icon className="h-5 w-5" />
111
111
  </div>
112
112
  )}
113
113
  <div>
114
114
  <h1 className="text-xl font-semibold tracking-tight text-gray-900 dark:text-white">{title}</h1>
115
- {subtitle && <p className="mt-0.5 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>}
115
+ {subtitle && <p className="mt-1 text-[13px] text-gray-500 dark:text-gray-400">{subtitle}</p>}
116
116
  </div>
117
117
  </div>
118
118
  {action}
@@ -122,7 +122,7 @@ export function PageHeader({
122
122
 
123
123
  export function Card({ children, className = "" }: { children: ReactNode; className?: string }) {
124
124
  return (
125
- <div className={`rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900 ${className}`}>
125
+ <div className={`rounded-xl border border-gray-200/80 bg-white shadow-card dark:border-gray-800 dark:bg-gray-900 ${className}`}>
126
126
  {children}
127
127
  </div>
128
128
  );
@@ -140,12 +140,12 @@ export function StatCard({
140
140
  accent?: string;
141
141
  }) {
142
142
  return (
143
- <Card className="p-4">
143
+ <Card className="p-5">
144
144
  <div className="flex items-center justify-between">
145
- <span className="text-xs font-medium uppercase tracking-wide text-gray-500">{label}</span>
146
- {Icon && <Icon className="h-4 w-4 text-gray-400" />}
145
+ <span className="text-[11px] font-semibold uppercase tracking-wider text-gray-400">{label}</span>
146
+ {Icon && <Icon className="h-4 w-4 text-gray-300 dark:text-gray-600" />}
147
147
  </div>
148
- <div className={`mt-2 text-2xl font-semibold tabular-nums ${accent}`}>{value}</div>
148
+ <div className={`mt-2 text-2xl font-bold tabular-nums tracking-tight ${accent}`}>{value}</div>
149
149
  </Card>
150
150
  );
151
151
  }
@@ -160,12 +160,12 @@ export function EmptyState({
160
160
  children?: ReactNode;
161
161
  }) {
162
162
  return (
163
- <div className="flex flex-col items-center justify-center px-6 py-16 text-center">
164
- <div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-gray-100 text-gray-400 dark:bg-gray-800">
165
- <Icon className="h-6 w-6" />
163
+ <div className="flex flex-col items-center justify-center px-6 py-20 text-center">
164
+ <div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-gray-100 text-gray-400 dark:bg-gray-800">
165
+ <Icon className="h-7 w-7" />
166
166
  </div>
167
- <p className="text-base font-medium text-gray-700 dark:text-gray-200">{title}</p>
168
- {children && <div className="mt-1 max-w-md text-sm text-gray-500">{children}</div>}
167
+ <p className="text-[15px] font-medium text-gray-700 dark:text-gray-200">{title}</p>
168
+ {children && <div className="mt-2 max-w-md text-[13px] leading-relaxed text-gray-500">{children}</div>}
169
169
  </div>
170
170
  );
171
171
  }
@@ -4,13 +4,16 @@
4
4
  //
5
5
  // Open (Community) default: a single implicit org, so the BFF attaches the
6
6
  // server-side org key (SPLYNTRA_API_KEY) — or, only outside production, the dev
7
- // key. The commercial Cloud build registers a resolver (see the no-op
8
- // collector-auth-providers below, replaced by the cloud overlay) that instead
9
- // uses a trusted service token + X-Splyntra-Org-Id headers so each request is
10
- // scoped to the user's ACTIVE org (api_keys store only hashes, so a per-org key
11
- // can't be replayed). The collector honors that header path only when its
12
- // COLLECTOR_SERVICE_TOKEN matches.
13
- import "@/lib/collector-auth-providers"; // side-effect: cloud overlay registers its resolver
7
+ // key. The commercial Cloud build registers a resolver via the overlay module
8
+ // `@/lib/collector-auth-providers`, which the BFF routes import for its side
9
+ // effects (it registers a resolver that uses a trusted service token +
10
+ // X-Splyntra-Org-Id headers so each request is scoped to the user's ACTIVE org;
11
+ // api_keys store only hashes, so a per-org key can't be replayed). The collector
12
+ // honors that header path only when its COLLECTOR_SERVICE_TOKEN matches.
13
+ //
14
+ // NOTE: this module must NOT import the providers module — that would create an
15
+ // import cycle (providers → this module's registry), so the BFF routes do the
16
+ // side-effect import instead.
14
17
 
15
18
  type SessionLike = { user?: { id?: string; orgId?: string; role?: string } } | null | undefined;
16
19
  export interface CollectorAuth {
package/src/middleware.ts CHANGED
@@ -7,6 +7,8 @@ import { authConfig } from "@/auth.config";
7
7
  export const { auth: middleware } = NextAuth(authConfig);
8
8
 
9
9
  export const config = {
10
- // Protect everything except Next internals and static assets.
11
- matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
10
+ // Protect everything except Next internals and public static assets (the logo
11
+ // serves as favicon + PWA icon, fetched directly by the browser, so it must
12
+ // not be auth-gated).
13
+ matcher: ["/((?!_next/static|_next/image|favicon.ico|logo.png|manifest.json).*)"],
12
14
  };
@@ -2,22 +2,51 @@
2
2
  /** @type {import('tailwindcss').Config} */
3
3
  module.exports = {
4
4
  content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
5
+ darkMode: "class",
5
6
  theme: {
6
7
  extend: {
7
8
  colors: {
8
9
  splyntra: {
9
- 50: "#f0f4ff",
10
- 100: "#dbe4ff",
11
- 500: "#4c6ef5",
12
- 600: "#3b5bdb",
13
- 700: "#364fc7",
14
- 900: "#1c2541",
10
+ 50: "#eef2ff",
11
+ 100: "#e0e7ff",
12
+ 200: "#c7d2fe",
13
+ 300: "#a5b4fc",
14
+ 400: "#818cf8",
15
+ 500: "#6366f1",
16
+ 600: "#4f46e5",
17
+ 700: "#4338ca",
18
+ 800: "#3730a3",
19
+ 900: "#312e81",
20
+ 950: "#1e1b4b",
15
21
  },
16
22
  risk: {
17
- low: "#51cf66",
18
- medium: "#fcc419",
19
- high: "#ff6b6b",
20
- critical: "#c92a2a",
23
+ low: "#10b981",
24
+ medium: "#f59e0b",
25
+ high: "#ef4444",
26
+ critical: "#dc2626",
27
+ },
28
+ },
29
+ fontFamily: {
30
+ sans: ['Inter', 'ui-sans-serif', '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', 'sans-serif'],
31
+ mono: ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular', 'monospace'],
32
+ },
33
+ boxShadow: {
34
+ 'card': '0 1px 3px 0 rgb(0 0 0 / 0.04), 0 1px 2px -1px rgb(0 0 0 / 0.04)',
35
+ 'card-hover': '0 4px 6px -1px rgb(0 0 0 / 0.07), 0 2px 4px -2px rgb(0 0 0 / 0.05)',
36
+ 'sidebar': '1px 0 3px 0 rgb(0 0 0 / 0.04)',
37
+ },
38
+ animation: {
39
+ 'fade-in': 'fadeIn 0.3s ease-out',
40
+ 'slide-up': 'slideUp 0.3s ease-out',
41
+ },
42
+ keyframes: {
43
+ fadeIn: {
44
+ '0%': { opacity: '0' },
45
+ '100%': { opacity: '1' },
46
+ },
47
+ slideUp: {
48
+ '0%': { opacity: '0', transform: 'translateY(8px)' },
49
+ '100%': { opacity: '1', transform: 'translateY(0)' },
21
50
  },
22
51
  },
23
52
  },