Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

admin-shell.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

admin-shell.tsxBlame340 lines · 1 contributor
f4a1547ccantynz-alt1import type { FC, PropsWithChildren } from "hono/jsx";
2import type { User } from "../db/schema";
3import { Layout } from "./layout";
4
5// ---------------------------------------------------------------------------
6// AdminShell — the single unified chrome for every /admin/* surface.
7//
8// Before this, the ~15 admin pages each rendered the plain customer `Layout`
9// with no admin navigation, so the only way between admin screens was the
10// two rival hub pages (/admin and /admin/command). AdminShell wraps `Layout`
11// (so the html shell, top nav, footer, scripts all still work) and adds a
12// persistent left sidebar driven by ONE canonical `ADMIN_NAV` array plus a
13// sticky page header (title + optional actions slot).
14//
15// This module is the single source of truth for the admin IA. Every admin
16// destination in the app lives in `ADMIN_NAV` below — nothing stays orphaned.
17// Migrate a page by replacing its `<Layout …>` wrapper with
18// `<AdminShell active="<key>" title="…" user={user}> … </AdminShell>`.
19// ---------------------------------------------------------------------------
20
21export type AdminNavKey =
22 // Overview
23 | "dashboard"
24 | "command"
25 // Operations & Health
26 | "ops"
27 | "health"
28 | "env-health"
29 | "status"
30 | "diagnose"
31 | "autopilot"
32 // Deploys & Infra
33 | "deploys"
34 | "self-host"
35 | "servers"
36 // Users & Access
37 | "users"
38 | "repos"
39 | "security"
40 | "deletions"
41 // Sign-in & SSO
42 | "google-oauth"
43 | "github-oauth"
44 | "sso"
45 // Growth & Billing
46 | "growth"
47 | "billing"
48 | "stripe"
49 | "digests"
50 | "ai-costs"
51 // Platform Config
52 | "flags"
53 | "integrations"
54 | "advancement"
55 | "connect-claude";
56
57interface AdminNavItem {
58 key: AdminNavKey;
59 href: string;
60 label: string;
61}
62
63interface AdminNavGroup {
64 label: string;
65 items: AdminNavItem[];
66}
67
68/**
69 * Canonical admin navigation. Grouped by concern; the union of both legacy
70 * hubs (/admin + /admin/command) plus the previously-orphaned /admin/servers
71 * and /admin/billing surfaces. This array is the ONLY place the admin IA is
72 * defined — add new admin pages here and every shell picks them up.
73 */
74export const ADMIN_NAV: AdminNavGroup[] = [
75 {
76 label: "Overview",
77 items: [
78 { key: "dashboard", href: "/admin", label: "Dashboard" },
79 { key: "command", href: "/admin/command", label: "Command Center" },
80 ],
81 },
82 {
83 label: "Operations & Health",
84 items: [
85 { key: "ops", href: "/admin/ops", label: "Operations" },
86 { key: "health", href: "/admin/health", label: "Health (traffic lights)" },
87 { key: "env-health", href: "/admin/env-health", label: "Env / feature health" },
88 { key: "status", href: "/admin/status", label: "Platform monitor" },
89 { key: "diagnose", href: "/admin/diagnose", label: "AI background tasks" },
90 { key: "autopilot", href: "/admin/autopilot", label: "Autopilot" },
91 ],
92 },
93 {
94 label: "Deploys & Infra",
95 items: [
96 { key: "deploys", href: "/admin/deploys", label: "Deploys" },
97 { key: "self-host", href: "/admin/self-host", label: "Self-host status" },
98 { key: "servers", href: "/admin/servers", label: "Server targets" },
99 ],
100 },
101 {
102 label: "Users & Access",
103 items: [
104 { key: "users", href: "/admin/users", label: "Manage users" },
105 { key: "repos", href: "/admin/repos", label: "Manage repos" },
106 { key: "security", href: "/admin/security", label: "Security" },
107 { key: "deletions", href: "/admin/deletions", label: "GDPR deletions" },
108 ],
109 },
110 {
111 label: "Sign-in & SSO",
112 items: [
113 { key: "google-oauth", href: "/admin/google-oauth", label: "Sign in with Google" },
114 { key: "github-oauth", href: "/admin/github-oauth", label: "Sign in with GitHub" },
115 { key: "sso", href: "/admin/sso", label: "Enterprise SSO" },
116 ],
117 },
118 {
119 label: "Growth & Billing",
120 items: [
121 { key: "growth", href: "/admin/growth", label: "User growth" },
122 { key: "billing", href: "/admin/billing", label: "Billing" },
123 { key: "stripe", href: "/admin/stripe", label: "Stripe sync" },
124 { key: "digests", href: "/admin/digests", label: "Email digests" },
125 { key: "ai-costs", href: "/admin/ai-costs", label: "AI cost breakdown" },
126 ],
127 },
128 {
129 label: "Platform Config",
130 items: [
131 { key: "flags", href: "/admin/flags", label: "Site flags" },
132 { key: "integrations", href: "/admin/integrations", label: "Integrations" },
133 { key: "advancement", href: "/admin/advancement", label: "Advancement" },
134 { key: "connect-claude", href: "/connect/claude", label: "Connect Claude" },
135 ],
136 },
137];
138
139export const AdminShell: FC<
140 PropsWithChildren<{
141 active: AdminNavKey | string;
142 title: string;
143 user?: User | null;
144 // Optional right-aligned actions slot rendered in the sticky page header.
145 actions?: any;
146 }>
147> = ({ active, title, user, actions, children }) => (
148 <Layout title={`${title} — Admin`} user={user}>
149 <style dangerouslySetInnerHTML={{ __html: adminShellStyles }} />
150 <div class="admin-shell">
151 <aside class="admin-shell-sidebar">
152 <div class="admin-shell-brand">
153 <span class="admin-shell-brand-dot" aria-hidden="true" />
154 Site admin
155 </div>
156 <nav class="adminnav" aria-label="Admin sections">
157 {ADMIN_NAV.map((group) => (
158 <div class="adminnav-group">
159 <div class="adminnav-label">{group.label}</div>
160 {group.items.map((it) => (
161 <a
162 href={it.href}
163 class={it.key === active ? "is-active" : ""}
164 aria-current={it.key === active ? "page" : undefined}
165 >
166 {it.label}
167 </a>
168 ))}
169 </div>
170 ))}
171 </nav>
172 </aside>
173 <div class="admin-shell-main">
174 <header class="admin-shell-head">
175 <h1 class="admin-shell-title">{title}</h1>
176 {actions ? (
177 <div class="admin-shell-actions">{actions}</div>
178 ) : null}
179 </header>
180 <div class="admin-shell-content">{children}</div>
181 </div>
182 </div>
183 </Layout>
184);
185
186/**
187 * Shell CSS. Follows the SettingsNav pattern (grouped sidebar, sticky, token
188 * based, theme-aware). No hardcoded hex — every colour resolves from a design
189 * token so light/dark both track automatically. Injected once per page; a
190 * duplicate identical <style> is harmless.
191 */
192export const adminShellStyles = `
193 .admin-shell {
194 display: grid;
195 grid-template-columns: 240px minmax(0, 1fr);
196 gap: var(--space-6);
197 align-items: start;
198 max-width: 1280px;
199 margin: 0 auto;
200 padding: var(--space-6) var(--space-6) var(--space-16);
201 }
202 .admin-shell-sidebar {
203 position: sticky;
204 top: var(--space-4);
205 display: flex;
206 flex-direction: column;
207 gap: var(--space-4);
208 min-width: 0;
209 }
210 .admin-shell-brand {
211 display: flex;
212 align-items: center;
213 gap: var(--space-2);
214 font-family: var(--font-mono);
215 font-size: var(--font-size-xs);
216 font-weight: 500;
217 letter-spacing: 0.12em;
218 text-transform: uppercase;
219 color: var(--text-muted);
220 padding: 0 var(--space-2);
221 }
222 .admin-shell-brand-dot {
223 width: 7px;
224 height: 7px;
225 border-radius: var(--radius-full);
226 background: var(--accent);
227 box-shadow: var(--accent-glow);
228 }
229 .adminnav {
230 display: flex;
231 flex-direction: column;
232 gap: var(--space-5);
233 }
234 .adminnav-group {
235 display: flex;
236 flex-direction: column;
237 gap: 1px;
238 }
239 .adminnav-label {
240 font-size: 11px;
241 font-weight: 700;
242 letter-spacing: 0.06em;
243 text-transform: uppercase;
244 color: var(--text-muted);
245 padding: 4px 10px;
246 margin-bottom: 2px;
247 }
248 .adminnav a {
249 display: block;
250 padding: 6px 10px;
251 border-radius: 6px;
252 font-size: 13.5px;
253 font-weight: 500;
254 color: var(--text-muted);
255 text-decoration: none;
256 white-space: nowrap;
257 overflow: hidden;
258 text-overflow: ellipsis;
259 transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
260 }
261 .adminnav a:hover {
262 color: var(--text-strong);
263 background: var(--bg-hover);
264 }
265 .adminnav a.is-active {
266 color: var(--text-strong);
267 background: var(--accent-gradient-soft);
268 box-shadow: var(--accent-glow) inset;
269 font-weight: 600;
270 }
271 .admin-shell-main {
272 min-width: 0;
273 }
274 .admin-shell-head {
275 position: sticky;
276 top: 0;
277 z-index: var(--z-sticky);
278 display: flex;
279 align-items: center;
280 justify-content: space-between;
281 gap: var(--space-4);
282 flex-wrap: wrap;
283 padding: var(--space-4) 0;
284 margin-bottom: var(--space-5);
285 border-bottom: 1px solid var(--border);
286 background: var(--bg);
287 }
288 .admin-shell-title {
289 font-family: var(--font-display);
290 font-size: var(--font-size-xl);
291 font-weight: 700;
292 letter-spacing: -0.022em;
293 color: var(--text-strong);
294 margin: 0;
295 min-width: 0;
296 }
297 .admin-shell-actions {
298 display: flex;
299 align-items: center;
300 gap: var(--space-2);
301 flex-wrap: wrap;
302 }
303 .admin-shell-content {
304 min-width: 0;
305 }
306 @media (max-width: 860px) {
307 .admin-shell {
308 display: block;
309 padding: var(--space-4);
310 }
311 .admin-shell-sidebar {
312 position: static;
313 margin-bottom: var(--space-5);
314 }
315 .adminnav {
316 flex-direction: row;
317 flex-wrap: wrap;
318 gap: var(--space-3);
319 padding: var(--space-2);
320 background: var(--bg-elevated);
321 border: 1px solid var(--border);
322 border-radius: var(--radius);
323 }
324 .adminnav-group {
325 flex-direction: row;
326 flex-wrap: wrap;
327 gap: 2px;
328 }
329 .adminnav-label {
330 display: none;
331 }
332 .adminnav a {
333 padding: 6px 12px;
334 border-radius: var(--radius-full);
335 }
336 .admin-shell-head {
337 position: static;
338 }
339 }
340`;