let’s add a clean Admin shell (sidebar + tabs) and answer the “who sets admin?” bit.

Who sets Admin in Firebase?

First admin: must be set via the Firebase Admin SDK (server-side) — e.g., the little script I gave you earlier (setAdminClaim) or a one-time backend call.

After that: any existing admin can grant/revoke admin for staff via a protected admin API (I’m adding that below so you can do it in the UI).

Staff themselves cannot make themselves admin (custom claims require Admin SDK).

Admin Shell (Sidebar + Tabs)
1) Admin layout + sidebar

src/pages/Admin/AdminLayout.tsx

import { NavLink, Outlet } from "react-router-dom";
import { adminSignOut } from "@/admin/adminGuard";

function NavItem({ to, label }: { to: string; label: string }) {
  return (
    <NavLink
      to={to}
      className={({ isActive }) =>
        `block rounded-xl px-3 py-2 text-sm ${isActive ? "bg-black text-white" : "hover:bg-gray-100"}`
      }
    >
      {label}
    </NavLink>
  );
}

export default function AdminLayout() {
  return (
    <div className="max-w-6xl mx-auto py-8 grid grid-cols-12 gap-6">
      <aside className="col-span-12 md:col-span-3">
        <div className="rounded-2xl border p-4">
          <div className="font-semibold mb-3">Admin</div>
          <nav className="space-y-2">
            <NavItem to="/admin/dashboard" label="Overview" />
            <NavItem to="/admin/users" label="Users" />
            <NavItem to="/admin/payments" label="Payments" />
            <NavItem to="/admin/logs" label="Logs" />
          </nav>
          <button
            className="mt-4 w-full rounded-xl border py-2 text-sm"
            onClick={adminSignOut}
          >
            Sign out
          </button>
        </div>
      </aside>
      <main className="col-span-12 md:col-span-9">
        <Outlet />
      </main>
    </div>
  );
}

2) Hook the admin routes into your router
// in your router file
import AdminHome from "@/pages/Admin/AdminHome";
import AdminLogin from "@/pages/Admin/AdminLogin";
import AdminLayout from "@/pages/Admin/AdminLayout";
import Dashboard from "@/pages/Admin/Dashboard";
import AdminUsers from "@/pages/Admin/Users";
import AdminPayments from "@/pages/Admin/Payments";
import AdminLogs from "@/pages/Admin/Logs";

// ...
<Route path="/admin" element={<AdminHome />} />
<Route path="/admin/login" element={<AdminLogin />} />
<Route path="/admin" element={<AdminLayout />}>
  <Route path="dashboard" element={<Dashboard />} />
  <Route path="users" element={<AdminUsers />} />
  <Route path="payments" element={<AdminPayments />} />
  <Route path="logs" element={<AdminLogs />} />
</Route>

3) Pages (stubs you can build on)

src/pages/Admin/Users.tsx

import { useEffect, useState } from "react";
import { requireAdminOrRedirect } from "@/admin/adminGuard";

type UserRow = { id: number; email: string; is_paid: number; created_at: string };

export default function AdminUsers() {
  const [rows, setRows] = useState<UserRow[]>([]);
  const [email, setEmail] = useState("");
  const [opMsg, setOpMsg] = useState<string | null>(null);

  useEffect(() => {
    const token = requireAdminOrRedirect();
    if (!token) return;
    fetch("/api/admin/users/list", { headers: { Authorization: `Bearer ${token}` } })
      .then(r => r.json()).then(d => setRows(d.users || []))
      .catch(() => setRows([]));
  }, []);

  const setAdmin = async (admin: boolean) => {
    const token = sessionStorage.getItem("adminToken")!;
    setOpMsg(null);
    const res = await fetch("/api/admin/users/set-admin", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
      body: JSON.stringify({ email, admin })
    });
    const out = await res.json();
    setOpMsg(res.ok ? `Admin ${admin ? "granted" : "revoked"} for ${email}` : (out.error || "Failed"));
  };

  const togglePaid = async (userId: number, isPaid: boolean) => {
    const token = sessionStorage.getItem("adminToken")!;
    await fetch("/api/admin/users/set-paid", {
      method: "POST",
      headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
      body: JSON.stringify({ userId, isPaid })
    });
    // refresh
    const list = await fetch("/api/admin/users/list", { headers: { Authorization: `Bearer ${token}` } }).then(r=>r.json());
    setRows(list.users || []);
  };

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">Users</h1>

      <div className="rounded-2xl border p-4 mb-6">
        <div className="font-semibold mb-2">Grant/Revoke Staff Admin</div>
        <div className="flex gap-2">
          <input className="border rounded-xl px-3 py-2 flex-1" placeholder="staff@yourco.com"
                 value={email} onChange={e=>setEmail(e.target.value)} />
          <button className="rounded-xl bg-black text-white px-3" onClick={()=>setAdmin(true)}>Grant</button>
          <button className="rounded-xl border px-3" onClick={()=>setAdmin(false)}>Revoke</button>
        </div>
        {opMsg && <div className="text-sm mt-2">{opMsg}</div>}
      </div>

      <div className="rounded-2xl border overflow-hidden">
        <table className="w-full text-sm">
          <thead className="bg-gray-50">
            <tr>
              <th className="text-left p-3">ID</th>
              <th className="text-left p-3">Email</th>
              <th className="text-left p-3">Created</th>
              <th className="text-left p-3">Plan</th>
              <th className="text-left p-3">Actions</th>
            </tr>
          </thead>
          <tbody>
            {rows.map(u => (
              <tr key={u.id} className="border-t">
                <td className="p-3">{u.id}</td>
                <td className="p-3">{u.email}</td>
                <td className="p-3">{new Date(u.created_at).toLocaleString()}</td>
                <td className="p-3">{u.is_paid ? "Pro" : "Free"}</td>
                <td className="p-3">
                  <button className="rounded-xl border px-2 py-1 mr-2"
                          onClick={()=>togglePaid(u.id, true)}>Set Pro</button>
                  <button className="rounded-xl border px-2 py-1"
                          onClick={()=>togglePaid(u.id, false)}>Set Free</button>
                </td>
              </tr>
            ))}
            {!rows.length && (
              <tr><td className="p-3 text-gray-500" colSpan={5}>No users yet</td></tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}


src/pages/Admin/Payments.tsx

import { useEffect, useState } from "react";
import { requireAdminOrRedirect } from "@/admin/adminGuard";

export default function AdminPayments() {
  const [stats, setStats] = useState<any>(null);

  useEffect(() => {
    const token = requireAdminOrRedirect();
    if (!token) return;
    fetch("/api/admin/payments/overview", { headers: { Authorization: `Bearer ${token}` } })
      .then(r=>r.json()).then(setStats).catch(()=>setStats({ error: "Failed to load" }));
  }, []);

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">Payments</h1>
      <pre className="text-sm bg-gray-50 p-4 rounded-xl overflow-auto">
        {JSON.stringify(stats, null, 2)}
      </pre>
    </div>
  );
}


src/pages/Admin/Logs.tsx

import { useEffect, useState } from "react";
import { requireAdminOrRedirect } from "@/admin/adminGuard";

export default function AdminLogs() {
  const [logs, setLogs] = useState<any[]>([]);

  useEffect(() => {
    const token = requireAdminOrRedirect();
    if (!token) return;
    fetch("/api/admin/logs", { headers: { Authorization: `Bearer ${token}` } })
      .then(r=>r.json()).then(d=>setLogs(d.logs || [])).catch(()=>setLogs([]));
  }, []);

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">Logs</h1>
      <div className="rounded-2xl border p-4 text-sm space-y-2 max-h-[60vh] overflow-auto">
        {logs.length ? logs.map((l,i)=>(
          <div key={i} className="font-mono">{l.ts} — {l.msg}</div>
        )) : <div className="text-gray-500">No logs yet</div>}
      </div>
    </div>
  );
}

Admin APIs (server)

Extend your existing server/admin/routes.ts to support:

List users from SQLite

Grant/Revoke admin (Firebase custom claim)

Set paid (QA toggle)

Payments overview (simple counts; you can wire Stripe later)

Logs (stubbed)

// server/admin/routes.ts
import { Router } from "express";
import { verifyAdminBearer } from "./firebaseAdmin";
import { db, Users } from "../db";
import * as admin from "firebase-admin";

const router = Router();

async function guard(req: any, res: any, next: any) {
  try {
    await verifyAdminBearer(req.headers.authorization);
    next();
  } catch {
    res.status(403).json({ error: "Forbidden" });
  }
}

router.get("/api/admin/users/list", guard, (req, res) => {
  const rows = db.prepare("SELECT id, email, is_paid, created_at FROM users ORDER BY id DESC LIMIT 500").all();
  res.json({ users: rows });
});

router.post("/api/admin/users/set-paid", guard, (req, res) => {
  const { userId, isPaid } = req.body || {};
  if (!userId || typeof isPaid !== "boolean") return res.status(400).json({ error: "Bad request" });
  Users.setPaid(Number(userId), !!isPaid);
  res.json({ ok: true });
});

router.post("/api/admin/users/set-admin", guard, async (req, res) => {
  const { email, admin: isAdmin } = req.body || {};
  if (!email || typeof isAdmin !== "boolean") return res.status(400).json({ error: "Bad request" });
  try {
    const u = await admin.auth().getUserByEmail(email);
    await admin.auth().setCustomUserClaims(u.uid, { ...(u.customClaims || {}), admin: isAdmin, email });
    res.json({ ok: true });
  } catch (e: any) {
    res.status(400).json({ error: e.message || "Failed to set admin" });
  }
});

router.get("/api/admin/payments/overview", guard, (req, res) => {
  const total = db.prepare("SELECT COUNT(*) as c FROM users").get().c;
  const pros = db.prepare("SELECT COUNT(*) as c FROM users WHERE is_paid = 1").get().c;
  res.json({ users_total: total, pro_count: pros, arpu_guess: pros * 19 }); // placeholder
});

router.get("/api/admin/logs", guard, (req, res) => {
  // wire to your real logs later
  const logs = [
    { ts: new Date().toISOString(), msg: "Admin endpoint OK" }
  ];
  res.json({ logs });
});

export default router;


Reminder: you already mounted adminRoutes in server/index.ts.

Flow recap

You (owner) run the one-time script to give yourself admin claim.

You log into /admin/login, then use Admin → Users to grant staff admin by email.

Sidebar gives you Users / Payments / Logs from day one, and you can expand as you go.