diff --git a/drb-frontend/app/page.tsx b/drb-frontend/app/page.tsx
index cfe35b8..4fb7b79 100644
--- a/drb-frontend/app/page.tsx
+++ b/drb-frontend/app/page.tsx
@@ -1,13 +1,12 @@
"use client";
-import { useEffect } from "react";
import Link from "next/link";
-import { useRouter } from "next/navigation";
import { LinkButton } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Badge } from "@/components/ui/Badge";
import { PLANS } from "@/lib/billing";
import { useAuth } from "@/components/AuthProvider";
+import { LiveView } from "@/components/LiveView";
const CAPABILITIES = [
{
@@ -147,21 +146,15 @@ function MarketingHomePage() {
/**
* "/" is marketing for a signed-out visitor and Live (the map) for anyone
- * signed in — see UI_REDESIGN.md §3, "the map is the home screen". The Live
- * screen itself lands in chunk 6; until then a signed-in, provisioned user
- * is sent to /incidents rather than shown stale marketing copy. A user with
- * no org_id yet still sees marketing (unchanged — that's the pre-redesign
- * behaviour for an unprovisioned account, out of scope here).
+ * signed in — see UI_REDESIGN.md §3, "the map is the home screen". A user
+ * with no org_id yet still sees marketing (unchanged — that's the
+ * pre-redesign behaviour for an unprovisioned account, out of scope here;
+ * ChromeSwitcher's own no-claim guard only fires off marketing paths).
*/
export default function HomePage() {
const { user, loading, orgId } = useAuth();
- const router = useRouter();
- useEffect(() => {
- if (loading || !user || !orgId) return;
- router.replace("/incidents");
- }, [loading, user, orgId, router]);
-
- if (loading || (user && orgId)) return null;
+ if (loading) return null;
+ if (user && orgId) return ;
return ;
}
diff --git a/drb-frontend/components/LiveView.tsx b/drb-frontend/components/LiveView.tsx
new file mode 100644
index 0000000..47bd450
--- /dev/null
+++ b/drb-frontend/components/LiveView.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+/**
+ * Live — the default landing for every authenticated user (UI_REDESIGN.md
+ * §3, §5.1). Full-bleed map with the incident rail/legend MapView already
+ * renders (chunk 5), plus a time-density scrubber strip below it.
+ */
+import { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
+import { useNodes } from "@/lib/useNodes";
+import { useActiveCalls, useCalls } from "@/lib/useCalls";
+import { useActiveIncidents } from "@/lib/useIncidents";
+import { TimeScrubber } from "@/components/TimeScrubber";
+
+const MapView = dynamic(() => import("@/components/MapView"), { ssr: false });
+
+function timeAgo(date: Date): string {
+ const s = Math.floor((Date.now() - date.getTime()) / 1000);
+ if (s < 60) return `${s}s ago`;
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
+ return `${Math.floor(s / 3600)}h ago`;
+}
+
+export function LiveView() {
+ const { nodes, loading: nodesLoading } = useNodes();
+ const activeCalls = useActiveCalls();
+ const activeIncidents = useActiveIncidents();
+ // Wide-ish recent window: feeds both the incident-path polyline (chunk 5)
+ // and the scrubber's density bars. Not a fixture — real recent calls.
+ const { calls: recentCalls, loading: callsLoading } = useCalls(500);
+ const [lastUpdated, setLastUpdated] = useState(null);
+
+ useEffect(() => {
+ if (!nodesLoading) setLastUpdated(new Date());
+ }, [nodes, activeCalls, activeIncidents, nodesLoading]);
+
+ const configuredButQuiet = !nodesLoading && nodes.length > 0 && activeIncidents.length === 0;
+ const mostRecentSeen = configuredButQuiet
+ ? nodes
+ .map((n) => n.last_seen)
+ .filter((s): s is string => !!s)
+ .sort()
+ .at(-1)
+ : null;
+
+ return (
+
+
+ {nodesLoading || callsLoading ? (
+
+ Loading map…
+
+ ) : (
+
+ )}
+
+ {/* Configured-and-quiet — distinct from "no nodes at all" (chunk 10's
+ Activation screen handles the zero-node case). A node that's
+ online but has heard nothing is NOT the same empty state as an
+ org with no equipment. */}
+ {configuredButQuiet && (
+