Control-plane dashboard

Architecture of the HostSSH SaaS dashboard — stack, design system, RBAC, and how to add a page.

Control-plane dashboard

The hostssh.com SaaS dashboard — the "supreme" control surface for the whole fleet.

  • Location: control-plane/web/
  • Stack: Next.js 16 (App Router, RSC) · React 19 · TypeScript (strict) · Tailwind CSS v4 · lucide-react
  • Run: cd control-plane/web && npm install && npm run devhttp://localhost:3000

Layout

control-plane/web/
  app/
    layout.tsx                 root: <html> data-density/data-launch, fonts, ToastProvider
    globals.css                design tokens (@theme) + base styles + keyframes
    (god)/
      layout.tsx               shell: Sidebar + Topbar + Impersonation banner + ⌘K + shortcuts
      loading.tsx              premium route-level skeleton
      page.tsx                 Command Center (home)
      <section>/page.tsx       one folder per IA section
  components/
    shell/                     sidebar, topbar, command-palette, shortcuts, launch-mode-switcher, simulator-pill, impersonation-banner
    ui/                        the ONE component system (card, stat, badge, table, button, drawer, meter, toast, spinner, page-header, section, empty-state)
    <section>/                 co-located client components for a section
  lib/
    types.ts                   domain contracts (Server, Image, Connection, License, Tenant, …)
    data.ts                    async mock accessors (swap bodies for the real API later)
    nav.ts                     information architecture + keyboard SHORTCUTS (single source of truth)
    rbac.ts                    permission catalog + userHasPermission (fail-closed)
    session.ts                 god-mode session: launch-mode, impersonation, simulator (cookie-backed)
    actions.ts                 server actions that mutate the control state
    access.ts                  visibleNavKeys() + guard(permission)
    format.ts                  bytes/money/relativeTime/… (deterministic)
    cn.ts                      class merge

Design language

Ultra-refined, command-first, calm density. Encoded as Tailwind v4 @theme tokens in app/globals.css: a near-black surface ramp, one accent (infra cyan) plus the dark-red god chrome (--color-god) reserved for elevation, impersonation, and destructive actions. Two density modes via data-density. The shell tints crimson in God launch-mode via [data-launch='god'].

Use only the primitives in components/ui/*. Numbers use the tabular class. Never introduce new colors — extend the token set if a new semantic is truly needed.

God-mode layer

  • RBAC (lib/rbac.ts): granular permission catalog; super_admin → all, sub-admins → granted subset; admin.manage_admins + hardware.manage are super-only.
  • Launch mode (god / beta / production): switched from the top bar; recolors chrome and gates god-only sections (e.g. Hardware).
  • Impersonation: cookie-carried, expiring, read-only/read-write; a crimson banner is always present while active, with one-click exit.
  • Simulator: preview the product as any tier/tenant without leaving your identity.

Add a new section page

  1. Add the entry to lib/nav.ts (NAV group + a SHORTCUTS[key] chord).

  2. Create app/(god)/<key>/page.tsx as a server component:

    import { guard } from '@/lib/access'
    import { PageHeader } from '@/components/ui/page-header'
    import { LockedState } from '@/components/ui/empty-state'
    import { getThings } from '@/lib/data'
    
    export default async function ThingsPage() {
      const { allowed } = await guard('things.view')
      if (!allowed) return (<><PageHeader title="Things" /><LockedState /></>)
      const things = await getThings()
      return (
        <>
          <PageHeader title="Things" hint="…" />
          {/* compose ui/* primitives */}
        </>
      )
    }
    
  3. For interactivity (filters, drawers, action calls, toasts), add a 'use client' component under components/<key>/ and pass it serializable props only. Call server actions via useTransition; fire useToast() on success.

See app/(god)/fleet/page.tsx + components/fleet/fleet-table.tsx as the canonical example.

From mock to real

lib/data.ts returns realistic fixtures today (the live Girard Media fleet + sample tenants). Each accessor is async with the final signature, so wiring the real control-plane API / agent telemetry is a body swap — pages don't change. Likewise lib/session.ts seeds the immutable root super-admin (sgirard@girardmedia.com); replace getSession() with the real auth lookup, keeping the Session shape.