Files
OmniRoute/src/shared/components/MaintenanceBanner.js
diegosouzapw 034f1a7e1d feat(pages): Phase 8 — Missing Flows & Pages
8.1 — 403 Forbidden Page
  - New: src/app/forbidden/page.js
  - Gradient code + Access Denied message + Dashboard link
  - Consistent design with not-found.js

8.2 — Password Recovery Flow
  - New: src/app/forgot-password/page.js
  - Two methods: CLI reset + manual database reset
  - Added 'Forgot password?' link to login page

8.3 — Health/Status Dashboard
  - New: src/app/(dashboard)/dashboard/health/page.js
  - New: src/app/api/monitoring/health/route.js
  - Cards: Uptime, Version, Memory, Provider count
  - Provider health (circuit breaker states with color coding)
  - Rate limit status table
  - Active lockouts list
  - Auto-refresh every 15s
  - Added Health nav link to Sidebar

8.4 — Maintenance Banner
  - New: src/shared/components/MaintenanceBanner.js
  - Auto-detects server health issues every 10s
  - Shows/hides automatically, dismissible
  - Wired into DashboardLayout

8.5 — Empty States
  - Verified EmptyState component applied in 6+ pages
  - Providers page has its own empty handling

Bonus:
  - Fixed stale GitHub link in Sidebar (decolua → diegosouzapw)

Tests: 295 pass | Build: success
2026-02-15 10:46:11 -03:00

64 lines
1.9 KiB
JavaScript

"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 (
<div className="bg-amber-500/10 border-b border-amber-500/20 px-4 py-2.5 flex items-center justify-between gap-3 animate-in slide-in-from-top">
<div className="flex items-center gap-2.5">
<span className="material-symbols-outlined text-amber-500 text-[18px] animate-pulse">
warning
</span>
<span className="text-sm text-amber-200">{message}</span>
</div>
<button
onClick={() => setShow(false)}
className="p-1 rounded hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
aria-label="Dismiss"
>
<span className="material-symbols-outlined text-[16px]">close</span>
</button>
</div>
);
}