feat: real-time alerts via WebSocket push notifications
- Add ws WebSocket server (port 3001) with JWT auth and user-socket mapping - Add WebSocket client with exponential backoff reconnection and heartbeat - Add useRealtimeAlerts hook with toast notifications and unread badge - Add alert.publisher service (WS → push → email fallback) - Integrate publisher into DarkWatch, VoicePrint, HomeTitle, SpamShield, RemoveBrokers - Update Navbar with connection status indicator and unread count - Add comprehensive tests (14 passing) for server, client, and publisher
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { createSignal, onMount, onCleanup, Show, Suspense } from "solid-js";
|
||||
import { createSignal, onMount, onCleanup, Show } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Button } from "~/components/ui";
|
||||
import { Typewriter } from "~/components/ui/Typewriter";
|
||||
import { useTheme } from "~/lib/theme";
|
||||
import { SignedIn, SignedOut, UserButton } from "clerk-solidjs";
|
||||
import { useRealtimeAlerts } from "~/hooks/useRealtimeAlerts";
|
||||
|
||||
function ShieldLogo() {
|
||||
return (
|
||||
@@ -125,6 +126,51 @@ const navLinks = [
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
];
|
||||
|
||||
function RealtimeIndicator() {
|
||||
const { connectionStatus, unreadCount, clearUnread } = useRealtimeAlerts();
|
||||
|
||||
return (
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={unreadCount() > 0}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearUnread}
|
||||
class="relative flex items-center justify-center w-6 h-6 rounded-full bg-[var(--color-error)] text-white text-[10px] font-bold leading-none transition-transform hover:scale-110"
|
||||
aria-label={`${unreadCount()} unread alerts`}
|
||||
>
|
||||
{unreadCount() > 99 ? "99+" : unreadCount()}
|
||||
</button>
|
||||
</Show>
|
||||
<Show
|
||||
when={connectionStatus() === "connected"}
|
||||
fallback={
|
||||
connectionStatus() === "reconnecting" || connectionStatus() === "connecting" ? (
|
||||
<div class="flex items-center gap-1 text-[10px] text-[var(--color-warning)]" aria-label="Reconnecting">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-warning)] opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-warning)]" />
|
||||
</span>
|
||||
<span class="hidden sm:inline">Reconnecting</span>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]" aria-label="Offline">
|
||||
<span class="inline-flex rounded-full h-2 w-2 bg-[var(--color-text-muted)]" />
|
||||
<span class="hidden sm:inline">Offline</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div class="flex items-center gap-1" aria-label="Connected">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-[var(--color-success)] opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-[var(--color-success)]" />
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = createSignal(false);
|
||||
const [scrolled, setScrolled] = createSignal(false);
|
||||
@@ -169,6 +215,7 @@ export default function Navbar() {
|
||||
<ThemeToggle />
|
||||
<SignedIn>
|
||||
<UserButton showName />
|
||||
<RealtimeIndicator />
|
||||
<Button variant="secondary" size="sm">
|
||||
<A href="/dashboard">Dashboard</A>
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user