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