diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx
index c2db356..e4e53bc 100644
--- a/apps/web/app/(app)/layout.tsx
+++ b/apps/web/app/(app)/layout.tsx
@@ -1,14 +1,6 @@
-import { redirect } from "next/navigation";
-import { createClient } from "@/lib/supabase/server";
import { TopNav } from "@/components/app-shell/top-nav";
-export default async function AppLayout({ children }: { children: React.ReactNode }) {
- const supabase = await createClient();
- const {
- data: { user },
- } = await supabase.auth.getUser();
- if (!user) redirect("/login");
-
+export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
diff --git a/apps/web/app/(auth)/layout.tsx b/apps/web/app/(auth)/layout.tsx
deleted file mode 100644
index 197d2fd..0000000
--- a/apps/web/app/(auth)/layout.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import Image from "next/image";
-
-export default function AuthLayout({ children }: { children: React.ReactNode }) {
- return (
-
-
-
-
-
- Real-time crime intelligence
-
-
- {children}
-
-
- );
-}
diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx
deleted file mode 100644
index de5300d..0000000
--- a/apps/web/app/(auth)/login/page.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client";
-
-import { Suspense, useState } from "react";
-import { useRouter, useSearchParams } from "next/navigation";
-import type { Route } from "next";
-import { createClient } from "@/lib/supabase/browser";
-
-function LoginForm() {
- const router = useRouter();
- const params = useSearchParams();
- const [email, setEmail] = useState("");
- const [password, setPassword] = useState("");
- const [error, setError] = useState
(null);
- const [loading, setLoading] = useState(false);
-
- async function onSubmit(e: React.FormEvent) {
- e.preventDefault();
- setLoading(true);
- setError(null);
- const supabase = createClient();
- const { error } = await supabase.auth.signInWithPassword({ email, password });
- setLoading(false);
- if (error) {
- setError(error.message);
- return;
- }
- router.replace((params.get("next") ?? "/") as Route);
- router.refresh();
- }
-
- return (
-
- );
-}
-
-export default function LoginPage() {
- return (
-
-
-
- );
-}
diff --git a/apps/web/app/auth/callback/route.ts b/apps/web/app/auth/callback/route.ts
deleted file mode 100644
index b639332..0000000
--- a/apps/web/app/auth/callback/route.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { NextResponse, type NextRequest } from "next/server";
-import { createClient } from "@/lib/supabase/server";
-
-export async function GET(request: NextRequest) {
- const { searchParams, origin } = new URL(request.url);
- const code = searchParams.get("code");
- const next = searchParams.get("next") ?? "/";
-
- if (code) {
- const supabase = await createClient();
- await supabase.auth.exchangeCodeForSession(code);
- }
-
- return NextResponse.redirect(`${origin}${next}`);
-}
diff --git a/apps/web/components/cameras/camera-wall.tsx b/apps/web/components/cameras/camera-wall.tsx
index 7a59b83..4ed4926 100644
--- a/apps/web/components/cameras/camera-wall.tsx
+++ b/apps/web/components/cameras/camera-wall.tsx
@@ -26,7 +26,12 @@ export function CameraWall({ cameras }: Props) {
const [stream, setStream] = useState("hls");
const [query, setQuery] = useState("");
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE);
- const [hideOffline, setHideOffline] = useState(true);
+ // Default OFF: offline tiles stay in their slot with the "offline" overlay
+ // instead of being filtered out of `visible`, which triggers CSS-grid
+ // reflow and visually shifts every surviving tile. Re-applies the fix
+ // from 9672f31, which was reverted in 0010078 without restoring any
+ // in-place behavior.
+ const [hideOffline, setHideOffline] = useState(false);
const [offlineIds, setOfflineIds] = useState>(() => new Set());
const reportStatus = useCallback((id: string, status: CameraStatus) => {
diff --git a/apps/web/components/landing/business-owner-view.tsx b/apps/web/components/landing/business-owner-view.tsx
index 6aa15f5..9e3b774 100644
--- a/apps/web/components/landing/business-owner-view.tsx
+++ b/apps/web/components/landing/business-owner-view.tsx
@@ -1,6 +1,12 @@
"use client";
import { useEffect, useRef, useState } from "react";
+import { LiveStream } from "@/components/cameras/live-stream";
+
+// Public Caltrans D4 HLS feed — proxied through /api/hls.
+// Falls back to an "offline" overlay if the stream is down.
+const LANDING_PREVIEW_STREAM =
+ "https://wzmedia.dot.ca.gov/D4/N101_at_6th.stream/playlist.m3u8";
// IntersectionObserver: returns [ref, inView]. Latches to true so reveal
// animations play exactly once when the section scrolls into view.
@@ -147,11 +153,13 @@ function OwnerAlertMock() {
CAM 14B · live
-
-
- ⟶ HLS feed preview
-
-
+
request.cookies.getAll(),
- setAll: (
- cookiesToSet: { name: string; value: string; options: CookieOptions }[],
- ) => {
- for (const { name, value } of cookiesToSet) {
- request.cookies.set(name, value);
- }
- response = NextResponse.next({ request });
- for (const { name, value, options } of cookiesToSet) {
- response.cookies.set(name, value, options);
- }
- },
- },
- },
- );
-
- const {
- data: { user },
- } = await supabase.auth.getUser();
-
- const { pathname } = request.nextUrl;
- const isAuthRoute = pathname.startsWith("/login") || pathname.startsWith("/auth");
- const isPublicRoute = pathname === "/";
-
- if (!user && !isAuthRoute && !isPublicRoute) {
- const redirect = request.nextUrl.clone();
- redirect.pathname = "/login";
- redirect.searchParams.set("next", pathname);
- return NextResponse.redirect(redirect);
- }
-
- if (user && pathname === "/login") {
- const redirect = request.nextUrl.clone();
- redirect.pathname = "/";
- return NextResponse.redirect(redirect);
- }
-
- return response;
-}
-
-export const config = {
- matcher: [
- "/((?!_next/static|_next/image|favicon.ico|api/cron|api/hls|api/dispatch|api/live|api/openclaw|api/seed|api/contribute|api/contributor-waitlist|c/|contribute|.*\\.[a-zA-Z0-9]+$).*)",
- ],
-};