diff --git a/src/app/(dashboard)/dashboard/health/page.js b/src/app/(dashboard)/dashboard/health/page.js
new file mode 100644
index 0000000000..b2d8bfa842
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/health/page.js
@@ -0,0 +1,302 @@
+"use client";
+
+/**
+ * Health Dashboard — Phase 8.3
+ *
+ * System health overview with cards for:
+ * - System status (uptime, version, memory)
+ * - Provider health (circuit breaker states)
+ * - Rate limit status
+ * - Active lockouts
+ */
+
+import { useState, useEffect, useCallback } from "react";
+import { Card } from "@/shared/components";
+
+function formatUptime(seconds) {
+ const d = Math.floor(seconds / 86400);
+ const h = Math.floor((seconds % 86400) / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ if (d > 0) return `${d}d ${h}h ${m}m`;
+ if (h > 0) return `${h}h ${m}m`;
+ return `${m}m`;
+}
+
+function formatBytes(bytes) {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+const CB_COLORS = {
+ CLOSED: { bg: "bg-green-500/10", text: "text-green-500", label: "Healthy" },
+ OPEN: { bg: "bg-red-500/10", text: "text-red-500", label: "Open" },
+ HALF_OPEN: { bg: "bg-amber-500/10", text: "text-amber-500", label: "Half-Open" },
+};
+
+export default function HealthPage() {
+ const [data, setData] = useState(null);
+ const [error, setError] = useState(null);
+ const [lastRefresh, setLastRefresh] = useState(null);
+
+ const fetchHealth = useCallback(async () => {
+ try {
+ const res = await fetch("/api/monitoring/health");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const json = await res.json();
+ setData(json);
+ setError(null);
+ setLastRefresh(new Date());
+ } catch (err) {
+ setError(err.message);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchHealth();
+ const interval = setInterval(fetchHealth, 15000);
+ return () => clearInterval(interval);
+ }, [fetchHealth]);
+
+ if (!data && !error) {
+ return (
+
+
+
+
Loading health data...
+
+
+ );
+ }
+
+ if (error && !data) {
+ return (
+
+
+
error
+
Failed to load health data: {error}
+
+
+
+ );
+ }
+
+ const { system, providerHealth, rateLimitStatus, lockouts } = data;
+ const cbEntries = Object.entries(providerHealth || {});
+ const lockoutEntries = Object.entries(lockouts || {});
+
+ return (
+
+ {/* Header */}
+
+
+
System Health
+
+ Real-time monitoring of your OmniRoute instance
+
+
+
+ {lastRefresh && (
+
+ Updated {lastRefresh.toLocaleTimeString()}
+
+ )}
+
+
+
+
+ {/* Status Banner */}
+
+
+ {data.status === "healthy" ? "check_circle" : "error"}
+
+
+ {data.status === "healthy" ? "All systems operational" : "System issues detected"}
+
+
+
+ {/* System Info Cards */}
+
+
+
+ {formatUptime(system.uptime)}
+
+
+
+
+ v{system.version}
+ Node {system.nodeVersion}
+
+
+
+
+
+ memory
+
+
Memory (RSS)
+
+
+ {formatBytes(system.memoryUsage?.rss || 0)}
+
+
+ Heap: {formatBytes(system.memoryUsage?.heapUsed || 0)} /{" "}
+ {formatBytes(system.memoryUsage?.heapTotal || 0)}
+
+
+
+
+
+ {cbEntries.length}
+
+ {cbEntries.filter(([, v]) => v.state === "CLOSED").length} healthy
+
+
+
+
+ {/* Provider Health */}
+
+
+
+ health_and_safety
+
+ Provider Health
+
+ {cbEntries.length === 0 ? (
+
+ No circuit breaker data available. Make some requests first.
+
+ ) : (
+
+ {cbEntries.map(([provider, cb]) => {
+ const style = CB_COLORS[cb.state] || CB_COLORS.CLOSED;
+ return (
+
+
+ {provider}
+ {style.label}
+
+
+ Failures: {cb.failures || 0}
+ {cb.lastFailure && (
+
+ Last: {new Date(cb.lastFailure).toLocaleTimeString()}
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* Rate Limit Status */}
+ {rateLimitStatus && Object.keys(rateLimitStatus).length > 0 && (
+
+
+ speed
+ Rate Limit Status
+
+
+
+
+
+ | Provider |
+ Status |
+ Requests |
+
+
+
+ {Object.entries(rateLimitStatus).map(([provider, status]) => (
+
+ | {provider} |
+
+
+ {status.limited ? "Limited" : "OK"}
+
+ |
+
+ {status.requestsInWindow || 0} / {status.limit || "∞"}
+ |
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Active Lockouts */}
+ {lockoutEntries.length > 0 && (
+
+
+ lock
+ Active Lockouts
+
+
+ {lockoutEntries.map(([key, lockout]) => (
+
+
+ {key}
+ {lockout.reason && (
+ ({lockout.reason})
+ )}
+
+ {lockout.until && (
+
+ Until {new Date(lockout.until).toLocaleTimeString()}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/app/api/monitoring/health/route.js b/src/app/api/monitoring/health/route.js
new file mode 100644
index 0000000000..2d0263914f
--- /dev/null
+++ b/src/app/api/monitoring/health/route.js
@@ -0,0 +1,58 @@
+import { NextResponse } from "next/server";
+import { getSettings } from "@/lib/localDb";
+import { APP_CONFIG } from "@/shared/constants/config";
+
+/**
+ * GET /api/monitoring/health — System health overview
+ *
+ * Returns system info, provider health (circuit breakers),
+ * rate limit status, and database stats.
+ */
+export async function GET() {
+ try {
+ const { getAllCircuitBreakerStatuses } =
+ await import("@/../../src/shared/utils/circuitBreaker.js");
+ const { getAllRateLimitStatus } =
+ await import("@omniroute/open-sse/services/rateLimitManager.js");
+ const { getAllModelLockouts } = await import("@omniroute/open-sse/services/accountFallback.js");
+
+ const settings = await getSettings();
+ const circuitBreakers = getAllCircuitBreakerStatuses();
+ const rateLimitStatus = getAllRateLimitStatus();
+ const lockouts = getAllModelLockouts();
+
+ // System info
+ const system = {
+ version: APP_CONFIG.version,
+ nodeVersion: process.version,
+ uptime: process.uptime(),
+ memoryUsage: process.memoryUsage(),
+ pid: process.pid,
+ platform: process.platform,
+ };
+
+ // Provider health summary
+ const providerHealth = {};
+ for (const [key, cb] of Object.entries(circuitBreakers)) {
+ providerHealth[key] = {
+ state: cb.state,
+ failures: cb.failures,
+ lastFailure: cb.lastFailure,
+ nextRetry: cb.nextRetry,
+ };
+ }
+
+ return NextResponse.json({
+ status: "healthy",
+ timestamp: new Date().toISOString(),
+ system,
+ providerHealth,
+ rateLimitStatus,
+ lockouts,
+ setupComplete: settings?.setupComplete || false,
+ });
+ } catch (error) {
+ console.error("[API] GET /api/monitoring/health error:", error);
+ return NextResponse.json({ status: "error", error: error.message }, { status: 500 });
+ }
+}
diff --git a/src/app/forbidden/page.js b/src/app/forbidden/page.js
new file mode 100644
index 0000000000..4a700ccf7e
--- /dev/null
+++ b/src/app/forbidden/page.js
@@ -0,0 +1,43 @@
+"use client";
+
+/**
+ * 403 Forbidden Page — Phase 8.1
+ *
+ * Displayed when access is denied due to:
+ * - Invalid API key
+ * - IP not in allowlist
+ * - Rate limit exceeded
+ */
+
+import Link from "next/link";
+
+export default function ForbiddenPage() {
+ return (
+
+
+ 403
+
+
Access Denied
+
+ You don't have permission to access this resource. Check your API key or contact the
+ administrator.
+
+
+ Go to Dashboard
+
+
+ );
+}
diff --git a/src/app/forgot-password/page.js b/src/app/forgot-password/page.js
new file mode 100644
index 0000000000..487cee2bfe
--- /dev/null
+++ b/src/app/forgot-password/page.js
@@ -0,0 +1,91 @@
+"use client";
+
+/**
+ * Forgot Password Page — Phase 8.2
+ *
+ * Provides two recovery methods:
+ * 1. CLI reset via omniroute-reset-password command
+ * 2. Manual database reset instructions
+ */
+
+import Link from "next/link";
+import { Card } from "@/shared/components";
+
+export default function ForgotPasswordPage() {
+ return (
+
+
+
+
Reset Password
+
Choose a method to recover access to your dashboard
+
+
+ {/* Method 1: CLI Reset */}
+
+
+
+ terminal
+
+
+
Method 1: CLI Reset
+
+ Run the following command on the server where OmniRoute is running:
+
+
+ npx omniroute reset-password
+
+
+ This will prompt you to set a new password. The server must be stopped first.
+
+
+
+
+
+ {/* Method 2: Database Reset */}
+
+
+
+ database
+
+
+
Method 2: Manual Reset
+
+ Delete the password from the database and set a new one on startup:
+
+
+ - Stop the OmniRoute server
+ -
+ Set a new password in your{" "}
+
.env file:
+
+ INITIAL_PASSWORD=your_new_password
+
+
+ -
+ Delete{" "}
+
+ data/settings.json
+ {" "}
+ (or remove the{" "}
+ passwordHash{" "}
+ field)
+
+ - Restart the server — it will use the new password
+
+
+
+
+
+
+
+ arrow_back
+ Back to Login
+
+
+
+
+ );
+}
diff --git a/src/app/login/page.js b/src/app/login/page.js
index fb8408fe63..f114147c7a 100644
--- a/src/app/login/page.js
+++ b/src/app/login/page.js
@@ -111,6 +111,12 @@ export default function LoginPage() {
Default password is 123456
+
+
+
+ Forgot password?
+
+
diff --git a/src/shared/components/MaintenanceBanner.js b/src/shared/components/MaintenanceBanner.js
new file mode 100644
index 0000000000..9f792b87be
--- /dev/null
+++ b/src/shared/components/MaintenanceBanner.js
@@ -0,0 +1,63 @@
+"use client";
+
+/**
+ * Maintenance Banner — Phase 8.4
+ *
+ * Shows a warning banner at the top of the dashboard when the server
+ * is restarting or in maintenance mode. Auto-dismisses when the server
+ * comes back online.
+ */
+
+import { useState, useEffect, useCallback } from "react";
+
+export default function MaintenanceBanner() {
+ const [show, setShow] = useState(false);
+ const [message, setMessage] = useState("");
+
+ const checkHealth = useCallback(async () => {
+ try {
+ const res = await fetch("/api/monitoring/health", {
+ signal: AbortSignal.timeout(3000),
+ });
+ if (res.ok) {
+ // Server is healthy — hide banner if shown
+ if (show) {
+ setShow(false);
+ setMessage("");
+ }
+ } else {
+ setShow(true);
+ setMessage("Server is experiencing issues. Some features may be unavailable.");
+ }
+ } catch {
+ setShow(true);
+ setMessage("Server is unreachable. Reconnecting...");
+ }
+ }, [show]);
+
+ useEffect(() => {
+ // Check health every 10 seconds
+ const interval = setInterval(checkHealth, 10000);
+ return () => clearInterval(interval);
+ }, [checkHealth]);
+
+ if (!show) return null;
+
+ return (
+
+
+
+ warning
+
+ {message}
+
+
+
+ );
+}
diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js
index 3c6c41fe14..0c355a844e 100644
--- a/src/shared/components/Sidebar.js
+++ b/src/shared/components/Sidebar.js
@@ -14,6 +14,7 @@ const navItems = [
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
+ { href: "/dashboard/health", label: "Health", icon: "health_and_safety" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
];
@@ -25,7 +26,7 @@ const systemItems = [{ href: "/dashboard/settings", label: "Settings", icon: "se
const helpItems = [
{ href: "/docs", label: "Docs", icon: "menu_book" },
{
- href: "https://github.com/decolua/omniroute/issues",
+ href: "https://github.com/diegosouzapw/OmniRoute/issues",
label: "Issues",
icon: "bug_report",
external: true,
diff --git a/src/shared/components/layouts/DashboardLayout.js b/src/shared/components/layouts/DashboardLayout.js
index 42793619b2..07af78335a 100644
--- a/src/shared/components/layouts/DashboardLayout.js
+++ b/src/shared/components/layouts/DashboardLayout.js
@@ -5,6 +5,7 @@ import Sidebar from "../Sidebar";
import Header from "../Header";
import Breadcrumbs from "../Breadcrumbs";
import NotificationToast from "../NotificationToast";
+import MaintenanceBanner from "../MaintenanceBanner";
const SIDEBAR_COLLAPSED_KEY = "sidebar-collapsed";
@@ -55,6 +56,7 @@ export default function DashboardLayout({ children }) {
className="flex flex-col flex-1 h-full min-w-0 relative transition-colors duration-300"
>