Files
server-26/drb-frontend/app/settings/layout.tsx
T
Logan CusanoandClaude Opus 5 be79499635
Build & Deploy / Build & push images (push) Successful in 4m3s
Build & Deploy / Deploy to VM (push) Successful in 1m54s
Build & Deploy / Report a failed deploy (push) Skipped
Give the nav's dead links somewhere to land
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>
2026-08-23 02:28:02 -04:00

62 lines
2.1 KiB
TypeScript

"use client";
import { useEffect } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useAuth } from "@/components/AuthProvider";
import { PageHeader } from "@/components/ui/PageHeader";
const TABS = [
{ href: "/settings/organization", label: "Organization" },
{ href: "/settings/members", label: "Members" },
{ href: "/settings/nodes", label: "Node Ownership" },
{ href: "/settings/api-keys", label: "API Keys" },
{ href: "/settings/billing", label: "Billing" },
];
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
// SAAS_PLAN.md B7: this used to gate on isAdmin (platform admin) alone,
// which meant a paying customer who is their own org's owner couldn't
// reach their own billing/members/node-ownership settings — "admin" here
// conflated "platform operator" with "org owner". isAdmin still passes
// (support/debugging access to any org's settings), but org_role ===
// "owner" is now sufficient on its own.
const { isAdmin, isOrgOwner, loading } = useAuth();
const canAccess = isAdmin || isOrgOwner;
const pathname = usePathname();
const router = useRouter();
useEffect(() => {
if (!loading && !canAccess) router.replace("/");
}, [loading, canAccess, router]);
if (loading || !canAccess) return null;
return (
<div className="space-y-6">
<PageHeader
title="Settings"
description="Organization profile, team access, node ownership, API keys, and billing."
/>
<div className="flex flex-wrap gap-1 bg-gray-900 border border-gray-800 rounded-lg p-1 w-fit max-w-full overflow-x-auto">
{TABS.map((t) => (
<Link
key={t.href}
href={t.href}
className={`text-sm font-mono px-4 py-1.5 rounded-md transition-colors whitespace-nowrap ${
pathname === t.href || pathname.startsWith(t.href + "/")
? "bg-gray-800 text-white"
: "text-gray-500 hover:text-gray-300"
}`}
>
{t.label}
</Link>
))}
</div>
{children}
</div>
);
}