Files
warmbly/admin
Matthew Meszaros ae012dd13f Clear the live error-tracking issues, and the workspace rename that renamed the wrong workspace (#533)
* feat: stop a managed Kafka cluster refusing topic creation from failing the publish, by treating a topic- or cluster-authorization failure from CreateTopics as a topic the cluster owns rather than one that is missing, which on Confluent Cloud dropped every warmup event and filed one issue per message because the topic never became known

* feat: drop a report whose error is a cancelled context in errs rather than at ninety call sites, so a browser navigating away or a container draining on deploy stops filing one issue per query that happened to be in flight, while a deadline this process set and blew through still reports

* feat: stop renaming one workspace from renaming another, by keying the workspace settings editor on the workspace id so a switch re-seeds the name field instead of leaving the previous workspace's name against the new workspace's autosave baseline, and pinning every save on the workspace, sending and tracking pages to the workspace its draft was hydrated from

* feat: drop Script error. and the ResizeObserver notice on the marketing site and the hosted form page the way the dashboard and admin panel already do, since those two carry no stack and no bug and between them were the largest issues in error tracking, all of it from warmbly.com

* feat: rename the forms Turnstile script module to turnstileScript.ts so it no longer differs only in case from the Turnstile.tsx component, which resolved both imports to one file on a case-insensitive filesystem and failed forms' typecheck with TS1149

* feat: upload source maps from the static build:pages build as well as the image build, so the dashboards served from a static host stop reporting every stack frame as a minified name beside 'Invalid source map: bad json', which is PostHog falling back to fetching the .map from a host that answers with its SPA fallback

* feat: build every admin list in pg_admin.go with make rather than declaring it nil, so an empty page serializes as [] instead of null, and guard the audit table's own empty check, which is what crashed admin.warmbly.com/audit with 'null is not an object (evaluating d.data.length)' whenever a filter matched nothing

* feat: match the whole broker description rather than a substring when deciding a topic create was refused for permissions, since that answer remembers the topic as present, and clear the cached promise and dead tag when the forms Turnstile script fails to load so a blocked first attempt no longer leaves every later mount with the same rejection and the captcha permanently missing
2026-09-15 09:05:53 -07:00
..
2026-05-27 16:17:47 +00:00
2026-05-27 16:17:47 +00:00
2026-05-27 16:17:47 +00:00
2026-05-27 16:17:47 +00:00
2026-05-27 16:17:47 +00:00
2026-05-27 16:17:47 +00:00

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

From the repo root you can also use make admin, which is a shortcut for cd admin && pnpm dev. make app does not start this app — admin lives outside the docker compose stack so it can ship on its own cadence.

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

First admin (local dev)

Admin access is gated by users.admin_permissions (bitmask) on the backend. Nothing in the codebase seeds the first admin — sign up through the dashboard as normal, then promote yourself from the repo root:

make grant-admin EMAIL=you@example.com               # super-admin
make grant-admin EMAIL=you@example.com ROLE=support  # or ops, analyst
make revoke-admin EMAIL=you@example.com              # drop back to 0

Role bitmasks mirror AdminRolePermissions in internal/models/admin_permission.go. For one-off permission combinations, pass a raw BITMASK=N instead of ROLE.

Once a super-admin exists they can grant the rest from Accounts > Admins, which goes through the audited GrantAdminPermissions path instead of raw SQL.

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 is in it

Every nav entry is backed by real endpoints under /admin/*; there are no stub pages.

Group Pages
Overview counters, trends, signups by channel, the instance problems strip
Operations Workers, Fleet (capacity, decision log, dedicated bindings), Mailboxes, Sync (backfill and fair-use throttle per mailbox), Warmup (pools, abuse signals, action history), Warmup Appeals, Warmup Content, Campaigns, Sends (in-flight reservations, dead letters, task failures, webhook delivery health)
Accounts Users, Organizations (with API keys, webhooks and transfer tabs), Limit requests, Outreach, Admins
Insight Live Events (the admin:platform socket firehose), Audit Log, Jobs (every background loop with last run, next run and "run now")
Instance Setup and health (findings and service probes), Configuration (settings, notifications, environment, effective limits), Transfers (workspace export and import)

Cmd/Ctrl K opens a command palette that jumps to any page and searches users, organizations, mailboxes and workers. Lists stay live through the realtime invalidation spine in src/lib/realtime/RealtimeManager.tsx; only pages whose data has no event (service probes, jobs, in-flight sends, capacity) poll.

The customer docs describe the panel page by page at docs/content/docs/development/admin-panel.mdx.

Layout

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/       # one file per page, tab bodies in subfolders
    ├── components/
    │   ├── data/            # DataTable, Explorer facet rail
    │   ├── layout/          # AppShell, Sidebar, MobileNav, Topbar, CommandPalette, PageTabs, …
    │   └── ui/              # shadcn primitives copied from web/src/components/ui
    ├── hooks/
    │   └── useMe.ts, useDocumentTitle.ts, useInstanceHealth.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/   # one module per backend area (workers, sync, sends, jobs, fleet, …)
            └── models/