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-env-health.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-env-health.tsxBlame378 lines · 2 contributors
bc519aeClaude1/**
2 * /admin/env-health — environment / feature health panel.
3 *
4 * GET /admin/env-health — table of every env-gated feature, grouped by
5 * severity (critical / recommended / optional), with green "Configured"
6 * / red "Missing" pills, the controlling env var names, and a one-line
7 * impact description.
8 *
9 * Makes silently-disabled features visible: today a dozen major features
10 * quietly turn off when their env vars are unset and the operator has no
11 * single place to see what's live. Data comes from
12 * `collectEnvHealthWithDb()` in `src/lib/env-health.ts` — set/unset
13 * booleans only, never the values.
14 *
15 * Gated by `isSiteAdmin` using the same `gate()` pattern as
16 * `src/routes/admin.tsx`. Scoped CSS prefixed `.admin-envh-` to avoid
17 * collisions with the parent admin polish.
18 */
19
20import { Hono } from "hono";
21import { Layout } from "../views/layout";
f4a1547ccantynz-alt22import { AdminShell } from "../views/admin-shell";
bc519aeClaude23import { softAuth } from "../middleware/auth";
24import type { AuthEnv } from "../middleware/auth";
25import { isSiteAdmin } from "../lib/admin";
26import {
27 collectEnvHealthWithDb,
28 groupBySeverity,
29 type EnvHealthSeverity,
30} from "../lib/env-health";
31
32const envHealth = new Hono<AuthEnv>();
33envHealth.use("*", softAuth);
34
35/* ─────────────────────────────────────────────────────────────────────────
36 * Scoped CSS — every class prefixed `.admin-envh-` so this surface can't
37 * bleed into the wider admin panel. Mirrors the gradient-hairline hero +
38 * table patterns from /admin and /admin/integrations.
39 * ───────────────────────────────────────────────────────────────────── */
40const styles = `
41 .admin-envh-wrap { max-width: 1320px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
42
43 .admin-envh-hero {
44 position: relative;
45 margin-bottom: var(--space-5);
46 padding: var(--space-5) var(--space-6);
47 background: var(--bg-elevated);
48 border: 1px solid var(--border);
49 border-radius: 16px;
50 overflow: hidden;
51 }
52 .admin-envh-hero::before {
53 content: '';
54 position: absolute;
55 top: 0; left: 0; right: 0;
56 height: 2px;
6fd5915Claude57 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
bc519aeClaude58 opacity: 0.7;
59 pointer-events: none;
60 }
61 .admin-envh-hero-orb {
62 position: absolute;
63 inset: -20% -10% auto auto;
64 width: 380px; height: 380px;
6fd5915Claude65 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
bc519aeClaude66 filter: blur(80px);
67 opacity: 0.7;
68 pointer-events: none;
69 z-index: 0;
70 }
71 .admin-envh-hero-inner { position: relative; z-index: 1; max-width: 720px; }
72 .admin-envh-eyebrow {
73 font-size: 12px;
74 color: var(--text-muted);
75 margin-bottom: var(--space-2);
76 letter-spacing: 0.02em;
77 display: inline-flex;
78 align-items: center;
79 gap: 8px;
80 }
81 .admin-envh-eyebrow .pill {
82 display: inline-flex;
83 align-items: center;
84 justify-content: center;
85 width: 18px; height: 18px;
86 border-radius: 6px;
6fd5915Claude87 background: rgba(91,110,232,0.14);
e589f77ccantynz-alt88 color: var(--accent);
6fd5915Claude89 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.35);
bc519aeClaude90 }
91 .admin-envh-title {
92 font-size: clamp(28px, 4vw, 40px);
93 font-family: var(--font-display);
94 font-weight: 800;
95 letter-spacing: -0.028em;
96 line-height: 1.05;
97 margin: 0 0 var(--space-2);
98 color: var(--text-strong);
99 }
100 .admin-envh-title-grad {
6fd5915Claude101 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
bc519aeClaude102 -webkit-background-clip: text;
103 background-clip: text;
104 -webkit-text-fill-color: transparent;
105 color: transparent;
106 }
107 .admin-envh-sub {
108 font-size: 15px;
109 color: var(--text-muted);
110 margin: 0;
111 line-height: 1.5;
112 max-width: 620px;
113 }
114
115 /* ─── Severity section headers ─── */
116 .admin-envh-h3 {
117 display: flex;
118 align-items: baseline;
119 justify-content: space-between;
120 gap: var(--space-3);
121 margin: var(--space-5) 0 var(--space-3);
122 }
123 .admin-envh-h3 h3 {
124 font-family: var(--font-display);
125 font-size: 16px;
126 font-weight: 700;
127 letter-spacing: -0.014em;
128 margin: 0;
129 color: var(--text-strong);
130 }
131 .admin-envh-h3-meta {
132 font-size: 12px;
133 color: var(--text-muted);
134 }
135
136 /* ─── Table (mirrors .admin-ap-table from /admin) ─── */
137 .admin-envh-table {
138 width: 100%;
139 border-collapse: collapse;
140 background: var(--bg-elevated);
141 border: 1px solid var(--border);
142 border-radius: 14px;
143 overflow: hidden;
144 }
145 .admin-envh-table thead th {
146 text-align: left;
147 font-size: 11px;
148 font-weight: 600;
149 letter-spacing: 0.08em;
150 text-transform: uppercase;
151 color: var(--text-muted);
152 padding: 10px 14px;
153 background: rgba(255,255,255,0.015);
154 border-bottom: 1px solid var(--border);
155 }
156 .admin-envh-table tbody td {
157 padding: 10px 14px;
158 border-bottom: 1px solid var(--border-subtle);
159 font-size: 13px;
160 color: var(--text);
161 vertical-align: top;
162 }
163 .admin-envh-table tbody tr:last-child td { border-bottom: none; }
164 .admin-envh-table code {
165 font-family: var(--font-mono);
166 font-size: 12px;
167 color: var(--text-strong);
168 background: var(--bg-tertiary);
169 padding: 1px 5px;
170 border-radius: 4px;
171 white-space: nowrap;
172 }
173 .admin-envh-feature {
174 font-weight: 600;
175 color: var(--text-strong);
176 }
177 .admin-envh-impact { color: var(--text-muted); line-height: 1.45; }
178
179 /* ─── Status pills ─── */
180 .admin-envh-status {
181 display: inline-flex;
182 align-items: center;
183 gap: 4px;
184 padding: 2px 8px;
185 border-radius: 9999px;
186 font-size: 10.5px;
187 font-weight: 600;
188 letter-spacing: 0.04em;
189 text-transform: uppercase;
190 white-space: nowrap;
191 }
192 .admin-envh-status.is-set {
193 background: rgba(52,211,153,0.14);
e589f77ccantynz-alt194 color: var(--green);
bc519aeClaude195 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
196 }
197 .admin-envh-status.is-missing {
198 background: rgba(248,113,113,0.10);
e589f77ccantynz-alt199 color: var(--red);
bc519aeClaude200 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
201 }
202 .admin-envh-status .dot {
203 width: 6px; height: 6px;
204 border-radius: 9999px;
205 background: currentColor;
206 }
207
208 .admin-envh-foot {
209 margin-top: var(--space-5);
210 padding: var(--space-3) var(--space-4);
211 border: 1px solid var(--border-subtle);
212 background: rgba(255,255,255,0.015);
213 border-radius: 10px;
214 color: var(--text-muted);
215 font-size: 12.5px;
216 }
217 .admin-envh-foot a { color: var(--accent); text-decoration: none; }
218 .admin-envh-foot a:hover { text-decoration: underline; }
219
220 .admin-envh-403 {
221 max-width: 540px;
222 margin: var(--space-12) auto;
223 padding: var(--space-6);
224 text-align: center;
225 background: var(--bg-elevated);
226 border: 1px solid var(--border);
227 border-radius: 16px;
228 }
229 .admin-envh-403 h2 {
230 font-family: var(--font-display);
231 font-size: 22px;
232 margin: 0 0 8px;
233 color: var(--text-strong);
234 }
235 .admin-envh-403 p { color: var(--text-muted); margin: 0; font-size: 14px; }
236
237 @media (max-width: 720px) {
238 .admin-envh-wrap { padding: var(--space-4) var(--space-3); }
239 .admin-envh-hero { padding: var(--space-4); }
240 .admin-envh-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
241 }
242`;
243
244/** Human labels for the three severity buckets. */
245const SEVERITY_LABELS: Record<EnvHealthSeverity, { title: string; blurb: string }> = {
246 critical: {
247 title: "Critical",
248 blurb: "Core product surface degrades without these.",
249 },
250 recommended: {
251 title: "Recommended",
252 blurb: "Feature works, but in a degraded mode.",
253 },
254 optional: {
255 title: "Optional",
256 blurb: "Opt-ins and scale-out knobs.",
257 },
258};
259
260async function gate(c: any): Promise<{ user: any } | Response> {
261 const user = c.get("user");
262 if (!user) return c.redirect("/login?next=/admin/env-health");
263 if (!(await isSiteAdmin(user.id))) {
264 return c.html(
265 <Layout title="Forbidden" user={user}>
266 <div class="admin-envh-403">
267 <h2>403 — Not a site admin</h2>
268 <p>You don't have permission to view this page.</p>
269 </div>
270 <style dangerouslySetInnerHTML={{ __html: styles }} />
271 </Layout>,
272 403
273 );
274 }
275 return { user };
276}
277
278envHealth.get("/admin/env-health", async (c) => {
279 const g = await gate(c);
280 if (g instanceof Response) return g;
281 const { user } = g;
282
283 const items = await collectEnvHealthWithDb();
284 const groups = groupBySeverity(items);
285 const configured = items.filter((i) => i.configured).length;
286
287 return c.html(
f4a1547ccantynz-alt288 <AdminShell active="env-health" title="Environment health" user={user}>
bc519aeClaude289 <div class="admin-envh-wrap">
290 <section class="admin-envh-hero">
291 <div class="admin-envh-hero-orb" aria-hidden="true" />
292 <div class="admin-envh-hero-inner">
293 <div class="admin-envh-eyebrow">
294 <span class="pill" aria-hidden="true">
295 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
296 <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
297 </svg>
298 </span>
299 Environment health · Site admin · <span style="color:var(--accent);font-weight:600">{user.username}</span>
300 </div>
301 <h2 class="admin-envh-title">
302 <span class="admin-envh-title-grad">What's actually on.</span>
303 </h2>
304 <p class="admin-envh-sub">
305 Every feature that silently turns off when its env vars are
306 unset — in one place. {configured} of {items.length} live.
307 Only set/unset is shown; values never leave the server.
308 </p>
309 </div>
310 </section>
311
312 {groups.map(({ severity, items: rows }) => {
313 const label = SEVERITY_LABELS[severity];
314 const live = rows.filter((r) => r.configured).length;
315 return (
316 <>
317 <div class="admin-envh-h3">
318 <h3>{label.title}</h3>
319 <span class="admin-envh-h3-meta">
320 {label.blurb} · {live}/{rows.length} configured
321 </span>
322 </div>
323 <table class="admin-envh-table">
324 <thead>
325 <tr>
326 <th>Feature</th>
327 <th>Status</th>
328 <th>Env vars</th>
329 <th>When missing</th>
330 </tr>
331 </thead>
332 <tbody>
333 {rows.map((item) => (
334 <tr>
335 <td class="admin-envh-feature">{item.feature}</td>
336 <td>
337 <span
338 class={
339 "admin-envh-status " +
340 (item.configured ? "is-set" : "is-missing")
341 }
342 >
343 <span class="dot" aria-hidden="true" />
344 {item.configured ? "Configured" : "Missing"}
345 </span>
346 </td>
347 <td>
348 {item.envVars.map((v, i) => (
349 <>
350 {i > 0 && " "}
351 <code>{v}</code>
352 </>
353 ))}
354 </td>
355 <td class="admin-envh-impact">{item.impact}</td>
356 </tr>
357 ))}
358 </tbody>
359 </table>
360 </>
361 );
362 })}
363
364 <div class="admin-envh-foot">
365 Most keys can be set without a restart on{" "}
366 <a href="/admin/integrations">/admin/integrations</a> · Google
367 OAuth credentials saved at{" "}
368 <a href="/admin/google-oauth">/admin/google-oauth</a> also satisfy
369 the Google login check · runtime checks live on{" "}
370 <a href="/admin/health">/admin/health</a>.
371 </div>
372 </div>
373 <style dangerouslySetInnerHTML={{ __html: styles }} />
f4a1547ccantynz-alt374 </AdminShell>
bc519aeClaude375 );
376});
377
378export default envHealth;