Files
warmbly/web-admin
Matthew Meszaros c20971790c web-admin: Cloud Providers, Provisioning Templates, Provision modal, Jobs page
Three new admin surfaces on the existing Vite/React app, fully wired
against /admin/cloud-credentials, /admin/provisioning-templates,
/admin/provisioning-jobs.

Settings -> Cloud Providers: paste Hetzner API token, masked display,
Test connection button + green/red status banner with account email and
quota when healthy.

Settings -> Provisioning Templates: CRUD with every Hetzner option
exposed (provider, location, datacenter, server_type with cores/RAM/
price, server count, IPv4/IPv6 per server, worker tier, profile,
egress kind, image, placement group, private network, firewall, key=
value labels). Tier-exclusive 'auto-provision' checkbox enforced
client-side. Live cost preview card.

Workers page: amber 'Provision new' button opens two-tab modal (From
template / Custom). After submit, swaps to live state-machine progress
panel with checkmarks per state, polling every 2s via TanStack Query's
refetchInterval (stops on terminal state).

/workers/provisioning-jobs page: in-flight jobs at top (5s refresh),
30d history below with state + provider filters. Row click opens
timeline detail with retry button on failed jobs.

Fallback catalog for Hetzner locations + server types so the template
form stays usable even when the backend catalog endpoints are down
(small amber 'using built-in fallback list' note).

pnpm install + typecheck + build all clean. ~570KB production bundle.
2026-05-27 15:57:21 +00:00
..

web-admin

Warmbly's internal admin control plane. Separate Vite + React app, parallel to web/, that drives the /admin/* endpoints on the same backend.

Why a separate app

The dashboard at web/ is the product surface for customers. The admin app is the surface for the Warmbly team running the platform. Splitting them gives us:

  • a smaller, faster admin bundle (no tiptap, no marketing chrome, no onboarding flow)
  • independent deployment cadence (admin can ship without touching customer code)
  • different origin in production, so a stolen dashboard session can't quietly use admin endpoints
  • a clear visual marker (the amber ADMIN badge + stripe + sidebar tint) so anyone with both tabs open knows which one is which

Both apps share the same backend, the same Bearer-token auth shape, and the same shadcn primitives.

Run it locally

pnpm install
pnpm dev          # boots on http://localhost:5174
pnpm build        # production bundle into ./dist
pnpm typecheck    # tsc -b
pnpm lint

The dev server defaults to port 5174 so it coexists with the dashboard's 5173.

Set up .env.local from .env.example:

cp .env.example .env.local
Variable Purpose
VITE_API_URL Same Warmbly backend the dashboard talks to. Reuses /admin/*.
VITE_ENV_LABEL Drives the Production / Staging / Development pill in the topbar.
VITE_DASHBOARD_URL Used by the "Open dashboard" link in the user menu.

Visual differentiation (do not strip)

This app is intentionally tinted differently from the dashboard. If you find yourself "cleaning up" the amber accent, stop and read this section first.

  • ADMIN badge in the sidebar header and on the login card. Amber pill, ShieldAlert icon. Always visible.
  • 3px stripe along the top of the app shell (admin-stripe utility). First thing the eye lands on.
  • Sidebar tint (--sidebar shifted warm + faint diagonal pattern via admin-sidebar-pattern) so the rail reads as a different surface than the dashboard's near-white sidebar.
  • Amber active-nav state instead of the dashboard's blue.
  • Env pill in the topbar — different colour per environment.
  • Title prefix: index.html ships <title>Admin · Warmbly</title> and the favicon is an amber-bordered shield (public/admin-icon.svg).

These signals are layered on purpose. A single one (e.g. just the badge) is easy to overlook in a tab strip. Stacked, they make it obvious that the user is in the privileged surface.

What's wired vs. stubbed

Real data:

  • Overview — /admin/analytics/overview plus /admin/workers/managed for the fleet card
  • Workers list — /admin/workers/managed
  • Worker detail — /admin/workers/:id/managed, /admin/workers/:id/live-status, /admin/workers/:id/logs, plus the SSH lifecycle mutations (test, install, restart, uninstall)
  • Egresses — wired to /admin/workers/managed re-framed as sending identities (TODO when /admin/egresses exists)
  • Audit Log — /admin/audit-logs
  • Settings (Encryption, Storage, Messaging, Cache, Transports) — /admin/settings/backends with kind filter; renders an "endpoint pending" placeholder when the registry isn't wired yet

Stubs (page exists, no backend wire-up yet):

  • Mailboxes
  • Users
  • Organizations
  • Plans & Billing
  • Warmup pools
  • Campaigns
  • Analytics (cross-platform charts; the Overview page already feeds from the same family of endpoints)

Layout

web-admin/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig*.json
├── eslint.config.js
├── components.json          # shadcn config, mirrors web/
├── public/
│   └── admin-icon.svg       # amber-stroked shield favicon
└── src/
    ├── main.tsx             # router + query client + providers
    ├── global.css           # design tokens (mirror of web/) + admin-only tokens
    ├── app/
    │   ├── auth/LoginPage.tsx
    │   ├── dashboard/       # Overview, Workers, Egresses, Audit, stubs
    │   └── settings/        # Encryption/Storage/Messaging/Cache/Transports
    ├── components/
    │   ├── layout/          # AppShell, Sidebar, Topbar, AdminBadge, EnvPill, …
    │   └── ui/              # shadcn primitives copied from web/src/components/ui
    ├── hooks/
    │   └── useMe.ts
    └── lib/
        ├── env.ts
        ├── utils.ts
        ├── auth/storage.ts  # Bearer token persistence
        └── api/
            ├── client.ts    # axios instance + Request<T>
            ├── client/
            │   ├── auth/    # login, getMe, logout
            │   └── admin/   # workers, audit, analytics, settings
            └── models/