Three of the app's routes were referenced but never existed, so the redesign's
navigation pointed at 404s from several directions.
/dashboard was the post-login and fallback redirect target in nine places --
login, onboarding, middleware, the admin/nodes/systems/tokens/settings guards,
and the marketing header -- but app/dashboard/ was never created. Signing in
normally dropped the user on a 404. The real signed-in home is "/", which
app/page.tsx already renders as LiveView for an authed user with an org, and
which the nav labels "Live"; all nine now point there.
Nav also linked /watch and /network, neither of which existed. /watch is the
alerts screen under its redesign name, so it re-exports app/alerts/page.tsx
and /alerts stays reachable for old links. /network is new: the "my equipment"
hub the redesign moved /nodes, /systems and /tokens behind and then never
built, which had left /systems and /tokens with no entry point in the UI at
all. Its hooks all run before the admin/operator guard, per d041c86.
Separately, the admin page's guard read isAdmin without authLoading, so every
cold load of /admin -- typed URL, hard refresh, bookmark -- redirected away
while the Firebase claims were still resolving. Admin was only reachable by
clicking through from an already-mounted page. Now it waits, like every other
guarded route does.
And /incidents no longer lies about an empty list: a failed Firestore query
leaves `incidents` empty just as a quiet night does, and the page was printing
"No incidents recorded yet" over the top of a missing-composite-index error.
useIncidents already returned `error`; the page just ignored it. It now renders
an ErrorBanner instead, so the undeployed indexes in server-26#13 read as a
failure rather than as silence on the radio.
Closes server-26#30, server-26#31. server-26#13 stays open -- the rules and
indexes still have to be pushed to the live project by hand.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
3.2 KiB
TypeScript
88 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useAuth } from "@/components/AuthProvider";
|
|
import { c2api } from "@/lib/c2api";
|
|
import { Button } from "@/components/ui/Button";
|
|
|
|
/**
|
|
* Shown to any signed-in user with no org_id claim — see ChromeSwitcher's
|
|
* no-claim guard (SAAS_PLAN.md B3). Two ways to land here:
|
|
* 1. Just created an account via /signup, org name not collected yet.
|
|
* 2. Signed in via Google on /login (which auto-creates a Firebase account
|
|
* on first use) and was never provisioned into anything.
|
|
* Either way, this is the one screen an unprovisioned account can reach,
|
|
* and completing it is what POST /auth/signup uses to grant org_id/org_role.
|
|
*/
|
|
export default function OnboardingPage() {
|
|
const { user, loading, orgId, refreshClaims } = useAuth();
|
|
const router = useRouter();
|
|
const [orgName, setOrgName] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (loading) return;
|
|
if (!user) {
|
|
router.replace("/login");
|
|
return;
|
|
}
|
|
if (orgId) {
|
|
router.replace("/");
|
|
}
|
|
}, [loading, user, orgId, router]);
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!orgName.trim()) return;
|
|
setSubmitting(true);
|
|
setError(null);
|
|
try {
|
|
await c2api.signup(orgName.trim());
|
|
// Firebase custom claims only show up in a *freshly fetched* ID token —
|
|
// getIdTokenResult(true) inside refreshClaims forces that fetch, then
|
|
// AuthProvider's own state (orgId) updates and the effect above
|
|
// redirects to /dashboard.
|
|
await refreshClaims();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Could not set up your organization. Try again.");
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
if (loading || !user || orgId) return null;
|
|
|
|
return (
|
|
<div className="max-w-sm mx-auto pt-16">
|
|
<div className="bg-gray-900 border border-gray-700 rounded-xl p-8 space-y-5 font-mono">
|
|
<div>
|
|
<h1 className="text-white text-lg font-bold">Set up your organization</h1>
|
|
<p className="text-gray-400 text-xs mt-2 leading-relaxed">
|
|
One more step — name the organization your nodes, calls, and incidents will belong to. You can change this later.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label className="text-xs text-gray-400 block mb-1">Organization name</label>
|
|
<input
|
|
type="text"
|
|
value={orgName}
|
|
onChange={(e) => setOrgName(e.target.value)}
|
|
required
|
|
autoFocus
|
|
placeholder="e.g. Riverside County Scanner"
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
|
/>
|
|
</div>
|
|
{error && <p className="text-red-400 text-xs">{error}</p>}
|
|
<Button type="submit" disabled={submitting || !orgName.trim()} fullWidth>
|
|
{submitting ? "Setting up…" : "Continue"}
|
|
</Button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|