Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css
deleted file mode 100644
index 6a25b1b..0000000
--- a/dashboard/src/app/globals.css
+++ /dev/null
@@ -1,130 +0,0 @@
-@import "tailwindcss";
-@import "tw-animate-css";
-@import "shadcn/tailwind.css";
-
-@custom-variant dark (&:is(.dark *));
-
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-sans);
- --font-mono: var(--font-geist-mono);
- --font-heading: var(--font-sans);
- --color-sidebar-ring: var(--sidebar-ring);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar: var(--sidebar);
- --color-chart-5: var(--chart-5);
- --color-chart-4: var(--chart-4);
- --color-chart-3: var(--chart-3);
- --color-chart-2: var(--chart-2);
- --color-chart-1: var(--chart-1);
- --color-ring: var(--ring);
- --color-input: var(--input);
- --color-border: var(--border);
- --color-destructive: var(--destructive);
- --color-accent-foreground: var(--accent-foreground);
- --color-accent: var(--accent);
- --color-muted-foreground: var(--muted-foreground);
- --color-muted: var(--muted);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-secondary: var(--secondary);
- --color-primary-foreground: var(--primary-foreground);
- --color-primary: var(--primary);
- --color-popover-foreground: var(--popover-foreground);
- --color-popover: var(--popover);
- --color-card-foreground: var(--card-foreground);
- --color-card: var(--card);
- --radius-sm: calc(var(--radius) * 0.6);
- --radius-md: calc(var(--radius) * 0.8);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) * 1.4);
- --radius-2xl: calc(var(--radius) * 1.8);
- --radius-3xl: calc(var(--radius) * 2.2);
- --radius-4xl: calc(var(--radius) * 2.6);
-}
-
-:root {
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
- --radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
-}
-
-.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.87 0 0);
- --chart-2: oklch(0.556 0 0);
- --chart-3: oklch(0.439 0 0);
- --chart-4: oklch(0.371 0 0);
- --chart-5: oklch(0.269 0 0);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
-}
-
-@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
- html {
- @apply font-sans;
- }
-}
diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx
deleted file mode 100644
index bbce3d5..0000000
--- a/dashboard/src/app/layout.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import { Toaster } from "@/components/ui/sonner";
-import Sidebar from "@/components/Sidebar";
-import { AuthProvider } from "@/hooks/useAuth";
-import { ProjectProvider } from "@/hooks/useProjectKey";
-import "./globals.css";
-
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
-});
-
-export const metadata: Metadata = {
- title: "Notifkit Observability & Admin",
- description: "Real-time delivery observability dashboard and admin console for Notifkit",
-};
-
-export default function RootLayout({ children }: { children: React.ReactNode }) {
- return (
-
-
-
-
-
- {children}
-
-
-
-
-
- );
-}
diff --git a/dashboard/src/app/login/page.tsx b/dashboard/src/app/login/page.tsx
deleted file mode 100644
index 0d2f8ce..0000000
--- a/dashboard/src/app/login/page.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { useAuth } from "@/hooks/useAuth";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { Bell, Lock, User, Loader2, AlertCircle, ArrowRight, ShieldCheck } from "lucide-react";
-import { toast } from "sonner";
-
-export default function LoginPage() {
- const { login } = useAuth();
- const [identifier, setIdentifier] = useState("");
- const [password, setPassword] = useState("");
- const [loading, setLoading] = useState(false);
- const [errorMessage, setErrorMessage] = useState(null);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!identifier.trim() || !password) {
- setErrorMessage("Please enter both your email/user ID and password.");
- return;
- }
-
- setLoading(true);
- setErrorMessage(null);
-
- const res = await login(identifier.trim(), password);
- setLoading(false);
-
- if (res.success) {
- toast.success("Welcome back!", { description: "Logged in successfully." });
- } else {
- setErrorMessage(res.error || "Invalid email/user ID or password.");
- toast.error("Authentication failed", { description: res.error });
- }
- };
-
- return (
-
- {/* Background Decorative Gradients */}
-
-
-
-
-
-
-
-
-
-
- Notifkit Admin
-
-
- Sign in to access your notification engine and observability suite.
-
-
-
-
-
- Admin Login
-
- Enter your admin credentials below
-
-
-
- {errorMessage && (
-
- )}
-
-
-
-
-
- Protected by salted Scrypt & Redis session auth
-
-
-
-
-
- );
-}
diff --git a/dashboard/src/components/DashboardHeader.tsx b/dashboard/src/components/DashboardHeader.tsx
index 9185b08..5088f47 100644
--- a/dashboard/src/components/DashboardHeader.tsx
+++ b/dashboard/src/components/DashboardHeader.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { useEffect, useState } from "react";
import { Activity, Bell, FolderKey, AlertTriangle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
@@ -8,19 +6,21 @@ import { useAuth } from "@/hooks/useAuth";
export default function DashboardHeader({ isConnected }: { isConnected: boolean }) {
const { projects, selectedProjectId, setSelectedProjectId, projectApiKey } = useProject();
- const { apiUrl } = useAuth();
+ const { apiUrl, token } = useAuth();
const [highBackpressure, setHighBackpressure] = useState(false);
useEffect(() => {
- if (!projectApiKey || !selectedProjectId) return;
+ const authKey = projectApiKey || token;
+ if (!authKey) return;
const checkMetrics = async () => {
try {
- const res = await fetch(`${apiUrl}/v1/system/metrics`, {
- headers: {
- Authorization: `Bearer ${projectApiKey}`,
- "x-project-id": selectedProjectId,
- },
- });
+ const headers: Record = {
+ Authorization: `Bearer ${authKey}`,
+ };
+ if (selectedProjectId) {
+ headers["x-project-id"] = selectedProjectId;
+ }
+ const res = await fetch(`${apiUrl}/v1/system/metrics`, { headers });
if (res.ok) {
const data = await res.json();
const streams = data.streams || {};
@@ -35,24 +35,21 @@ export default function DashboardHeader({ isConnected }: { isConnected: boolean
void checkMetrics();
const interval = setInterval(() => void checkMetrics(), 10000);
return () => clearInterval(interval);
- }, [apiUrl, projectApiKey, selectedProjectId]);
+ }, [apiUrl, projectApiKey, selectedProjectId, token]);
return (
-
+
-
-
+
+
Notifkit Observability
{highBackpressure && (
-
+
Queue Backpressure Warning (>80% Cap)
@@ -62,7 +59,7 @@ export default function DashboardHeader({ isConnected }: { isConnected: boolean
)}
- {projects.length === 0 ? (
-
-
- No Project Created
-
- ) : (
-
- {isConnected ? (
-
-
-
-
- ) : (
-
- )}
- {isConnected ? "Live Stream Active" : "Disconnected"}
-
- )}
+
+
+ {isConnected ? "Live Stream Active" : "Disconnected"}
+
diff --git a/dashboard/src/components/LogsTable.tsx b/dashboard/src/components/LogsTable.tsx
index fc8a6c9..3405986 100644
--- a/dashboard/src/components/LogsTable.tsx
+++ b/dashboard/src/components/LogsTable.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import { useEffect, useState, useCallback } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -12,6 +10,7 @@ import {
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
import {
RefreshCcw,
Database,
@@ -26,6 +25,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { useProject } from "@/hooks/useProjectKey";
+import { useAuth } from "@/hooks/useAuth";
interface LogEntry {
id: string;
@@ -55,12 +55,12 @@ export default function LogsTable() {
const [loadingLifecycle, setLoadingLifecycle] = useState(false);
const { projects, selectedProjectId, projectApiKey, isLoadingProjects } = useProject();
+ const { apiUrl } = useAuth();
const fetchLogs = useCallback(
async (apiKey: string, projectId: string, cursor?: string | null) => {
setIsLoading(true);
try {
- const apiUrl = process.env.NEXT_PUBLIC_API_URL || "";
const query = new URLSearchParams({ limit: "25" });
if (cursor) query.set("cursor", cursor);
if (search) query.set("search", search);
@@ -88,7 +88,7 @@ export default function LogsTable() {
setIsLoading(false);
}
},
- [search, channelFilter, statusFilter],
+ [apiUrl, search, channelFilter, statusFilter],
);
useEffect(() => {
@@ -104,7 +104,6 @@ export default function LogsTable() {
useEffect(() => {
if (selectedLog && projectApiKey && selectedProjectId) {
setLoadingLifecycle(true);
- const apiUrl = process.env.NEXT_PUBLIC_API_URL || "";
fetch(`${apiUrl}/v1/notifications/${selectedLog.taskId}`, {
headers: {
Authorization: `Bearer ${projectApiKey}`,
@@ -114,7 +113,6 @@ export default function LogsTable() {
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data && data.logs) {
- // Sort ascending by timestamp for chronological lifecycle timeline
const sorted = [...data.logs].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
);
@@ -128,7 +126,7 @@ export default function LogsTable() {
} else {
setTaskLifecycle([]);
}
- }, [selectedLog, projectApiKey, selectedProjectId]);
+ }, [apiUrl, selectedLog, projectApiKey, selectedProjectId]);
const handleNextPage = () => {
if (nextCursor && projectApiKey && selectedProjectId) {
@@ -148,12 +146,12 @@ export default function LogsTable() {
};
return (
-
-
+
+
-
+
Historical Dispatches & Audit Logs
@@ -168,7 +166,7 @@ export default function LogsTable() {
void fetchLogs(projectApiKey, selectedProjectId, cursorHistory[pageIndex]);
}}
disabled={isLoading || !projectApiKey || !selectedProjectId}
- className="h-8 w-8 rounded-full border-border/50 hover:bg-primary/10 hover:text-primary transition-colors"
+ className="h-8 w-8 rounded-md border-border"
>
@@ -178,19 +176,19 @@ export default function LogsTable() {
- setSearch(e.target.value)}
- className="w-full pl-9 pr-3 py-1.5 bg-muted/30 border border-border/40 rounded-lg text-xs font-mono focus:outline-none focus:ring-1 focus:ring-primary"
+ className="pl-9 text-xs font-mono"
/>
|