@splyntra/dashboard 0.3.0 → 1.1.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 +79 -0
- package/package.json +1 -1
- package/public/logo.png +0 -0
- package/public/manifest.json +4 -1
- package/public/splyntra.png +0 -0
- package/src/app/agents/page.tsx +11 -11
- package/src/app/alerts/page.tsx +2 -2
- package/src/app/api/eval/[...path]/route.ts +1 -0
- package/src/app/api/v1/[...path]/route.ts +1 -0
- package/src/app/costs/page.tsx +2 -2
- package/src/app/globals.css +55 -9
- package/src/app/layout.tsx +5 -3
- package/src/app/login/page.tsx +4 -4
- package/src/app/page.tsx +27 -11
- package/src/app/traces/page.tsx +1 -1
- package/src/auth.ts +14 -3
- package/src/components/auth/AuthCard.tsx +8 -10
- package/src/components/layout/AppShell.tsx +7 -3
- package/src/components/layout/Sidebar.tsx +26 -26
- package/src/components/ui/primitives.tsx +13 -13
- package/src/lib/auth-extensions.ts +9 -1
- package/src/lib/collector-auth.ts +10 -7
- package/src/middleware.ts +4 -2
- package/tailwind.config.js +39 -10
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
|
+
[](https://www.npmjs.com/package/@splyntra/dashboard)
|
|
8
|
+
[](../../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": "
|
|
3
|
+
"version": "1.1.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": {
|
package/public/logo.png
ADDED
|
Binary file
|
package/public/manifest.json
CHANGED
|
Binary file
|
package/src/app/agents/page.tsx
CHANGED
|
@@ -23,7 +23,7 @@ export default function AgentsPage() {
|
|
|
23
23
|
: 0;
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<div className="mx-auto max-w-
|
|
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:
|
|
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-
|
|
59
|
-
<th className="px-4 py-3 text-left font-
|
|
60
|
-
<th className="px-4 py-3 text-right font-
|
|
61
|
-
<th className="px-4 py-3 text-right font-
|
|
62
|
-
<th className="px-4 py-3 text-right font-
|
|
63
|
-
<th className="px-4 py-3 text-right font-
|
|
64
|
-
<th className="px-4 py-3 text-right font-
|
|
65
|
-
<th className="px-4 py-3 text-right font-
|
|
66
|
-
<th className="px-4 py-3 text-right font-
|
|
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">
|
package/src/app/alerts/page.tsx
CHANGED
|
@@ -66,7 +66,7 @@ export default function AlertsPage() {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
|
-
<div className="mx-auto max-w-
|
|
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="
|
|
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";
|
package/src/app/costs/page.tsx
CHANGED
|
@@ -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-
|
|
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:
|
|
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}
|
package/src/app/globals.css
CHANGED
|
@@ -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: #
|
|
7
|
-
--foreground: #
|
|
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: #
|
|
13
|
-
--foreground: #
|
|
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.
|
|
50
|
+
scrollbar-color: rgba(148, 163, 184, 0.3) transparent;
|
|
31
51
|
}
|
|
32
52
|
*::-webkit-scrollbar {
|
|
33
|
-
width:
|
|
34
|
-
height:
|
|
53
|
+
width: 6px;
|
|
54
|
+
height: 6px;
|
|
35
55
|
}
|
|
36
56
|
*::-webkit-scrollbar-thumb {
|
|
37
|
-
background-color: rgba(148, 163, 184, 0.
|
|
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
|
+
}
|
package/src/app/layout.tsx
CHANGED
|
@@ -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
|
|
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-
|
|
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>
|
package/src/app/login/page.tsx
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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-
|
|
30
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
}
|
package/src/app/traces/page.tsx
CHANGED
package/src/auth.ts
CHANGED
|
@@ -11,11 +11,22 @@ import { registeredAuthProviders, registeredSignInHooks } from "@/lib/auth-exten
|
|
|
11
11
|
|
|
12
12
|
export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
13
13
|
...authConfig,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
callbacks: {
|
|
15
|
+
...authConfig.callbacks,
|
|
16
|
+
// Sign-in guards run BEFORE a session is issued and can DENY sign-in. The
|
|
17
|
+
// cloud build registers one that persists/links the OAuth identity (refusing
|
|
18
|
+
// unverified-email linking) and fails closed — so a user never ends up with a
|
|
19
|
+
// session but no backing user row. Open edition registers none (always true).
|
|
20
|
+
async signIn({ user, account, profile }) {
|
|
16
21
|
for (const hook of registeredSignInHooks()) {
|
|
17
|
-
|
|
22
|
+
try {
|
|
23
|
+
const ok = await hook(user as { id?: string; email?: string | null }, account, profile);
|
|
24
|
+
if (ok === false) return false;
|
|
25
|
+
} catch {
|
|
26
|
+
return false; // fail closed
|
|
27
|
+
}
|
|
18
28
|
}
|
|
29
|
+
return true;
|
|
19
30
|
},
|
|
20
31
|
},
|
|
21
32
|
providers: [
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import
|
|
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:
|
|
9
|
-
<div className="w-full max-w-sm rounded-
|
|
10
|
-
<div className="mb-
|
|
11
|
-
<
|
|
12
|
-
|
|
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-
|
|
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-
|
|
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">
|
|
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-
|
|
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-
|
|
74
|
-
<Link href="/" className="flex items-center gap-
|
|
75
|
-
<
|
|
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-
|
|
80
|
-
<span className="block text-[10px] uppercase tracking-wider text-gray-400">Observability
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
105
|
-
: "text-gray-600 hover:bg-gray-
|
|
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-
|
|
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-
|
|
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-
|
|
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" />
|
|
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="
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
143
|
+
<Card className="p-5">
|
|
144
144
|
<div className="flex items-center justify-between">
|
|
145
|
-
<span className="text-
|
|
146
|
-
{Icon && <Icon className="h-4 w-4 text-gray-
|
|
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-
|
|
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-
|
|
164
|
-
<div className="mb-
|
|
165
|
-
<Icon className="h-
|
|
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-
|
|
168
|
-
{children && <div className="mt-
|
|
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
|
}
|
|
@@ -12,7 +12,15 @@ import type { NextAuthConfig } from "next-auth";
|
|
|
12
12
|
|
|
13
13
|
type Provider = NonNullable<NextAuthConfig["providers"]>[number];
|
|
14
14
|
type SignInUser = { id?: string; email?: string | null; name?: string | null };
|
|
15
|
-
|
|
15
|
+
// A sign-in guard runs in the next-auth `signIn` callback (it can DENY sign-in).
|
|
16
|
+
// Return false (or throw) to reject; void/true allows. Used by the cloud build to
|
|
17
|
+
// link the OAuth identity and refuse unverified-email linking — and to fail
|
|
18
|
+
// closed if persistence fails (so a user never gets a session with no backing row).
|
|
19
|
+
type SignInHook = (
|
|
20
|
+
user: SignInUser,
|
|
21
|
+
account: unknown,
|
|
22
|
+
profile: unknown
|
|
23
|
+
) => Promise<boolean | void> | boolean | void;
|
|
16
24
|
|
|
17
25
|
const extraProviders: Provider[] = [];
|
|
18
26
|
const signInHooks: SignInHook[] = [];
|
|
@@ -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
|
|
8
|
-
// collector-auth-providers
|
|
9
|
-
// uses a trusted service token +
|
|
10
|
-
// scoped to the user's ACTIVE org
|
|
11
|
-
// can't be replayed). The collector
|
|
12
|
-
// COLLECTOR_SERVICE_TOKEN matches.
|
|
13
|
-
|
|
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
|
-
|
|
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
|
};
|
package/tailwind.config.js
CHANGED
|
@@ -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: "#
|
|
10
|
-
100: "#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: "#
|
|
18
|
-
medium: "#
|
|
19
|
-
high: "#
|
|
20
|
-
critical: "#
|
|
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
|
},
|