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

status.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.

status.tsxBlame324 lines · 1 contributor
2316be6Claude1/**
2 * Public /status — human-readable platform health dashboard.
3 *
4 * Unlike /healthz (LB liveness JSON) and /readyz (DB readiness JSON),
5 * /status renders a full HTML page anyone can load. Shows DB reachability,
6 * autopilot state, totals (users/repos/gate runs), and the most recent
7 * autopilot tick's task breakdown.
8 *
9 * Accessible without auth. Uses softAuth so the nav bar renders correctly
10 * for logged-in visitors.
11 */
12
13import { Hono } from "hono";
14import { sql } from "drizzle-orm";
15import { db } from "../db";
16import { users, repositories, gateRuns } from "../db/schema";
17import { Layout } from "../views/layout";
18import { softAuth } from "../middleware/auth";
19import type { AuthEnv } from "../middleware/auth";
20import { getLastTick, getTickCount } from "../lib/autopilot";
21
22const status = new Hono<AuthEnv>();
23status.use("*", softAuth);
24
25const started = Date.now();
26
27function fmtUptime(ms: number): string {
28 const s = Math.floor(ms / 1000);
29 const d = Math.floor(s / 86400);
30 const h = Math.floor((s % 86400) / 3600);
31 const m = Math.floor((s % 3600) / 60);
32 if (d > 0) return `${d}d ${h}h`;
33 if (h > 0) return `${h}h ${m}m`;
34 return `${m}m`;
35}
36
37status.get("/status", async (c) => {
38 const user = c.get("user");
39
40 let dbOk = false;
41 try {
42 await db.execute(sql`SELECT 1`);
43 dbOk = true;
44 } catch {
45 dbOk = false;
46 }
47
48 let userCount = 0;
49 let repoCount = 0;
50 let publicRepoCount = 0;
51 let gateRunCount = 0;
52 let greenRate: number | null = null;
53 try {
54 const [u] = await db.select({ n: sql<number>`count(*)::int` }).from(users);
55 userCount = Number(u?.n ?? 0);
56 const [r] = await db
57 .select({ n: sql<number>`count(*)::int` })
58 .from(repositories);
59 repoCount = Number(r?.n ?? 0);
60 const [pr] = await db
61 .select({ n: sql<number>`count(*)::int` })
62 .from(repositories)
63 .where(sql`${repositories.isPrivate} = false`);
64 publicRepoCount = Number(pr?.n ?? 0);
65 const [gr] = await db
66 .select({ n: sql<number>`count(*)::int` })
67 .from(gateRuns);
68 gateRunCount = Number(gr?.n ?? 0);
69 if (gateRunCount > 0) {
70 const [g] = await db
71 .select({ n: sql<number>`count(*)::int` })
72 .from(gateRuns)
73 .where(sql`${gateRuns.status} IN ('passed','repaired')`);
74 greenRate = (Number(g?.n ?? 0) / gateRunCount) * 100;
75 }
76 } catch {
77 // counts stay 0
78 }
79
80 const tick = getLastTick();
81 const ticks = getTickCount();
82 const autopilotDisabled = process.env.AUTOPILOT_DISABLED === "1";
83 const uptimeMs = Date.now() - started;
84
85 const overallOk = dbOk;
86
87 return c.html(
88 <Layout title="Status — gluecron" user={user}>
89 <div style="max-width: 960px; margin: 0 auto; padding: 24px 16px">
90 <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px">
91 <span
92 style={`display: inline-block; width: 14px; height: 14px; border-radius: 50%; background: ${overallOk ? "var(--green, #2da44e)" : "var(--red, #cf222e)"}`}
93 />
94 <h1 style="margin: 0; font-size: 28px">
95 {overallOk ? "All systems operational" : "Service degraded"}
96 </h1>
97 </div>
98 <p style="color: var(--text-muted); margin-bottom: 32px">
99 Live platform status. Reloads on refresh; no client-side polling.
100 </p>
101
102 <h2 style="margin-bottom: 12px; font-size: 18px">Components</h2>
103 <div class="panel" style="margin-bottom: 24px">
104 <div
105 class="panel-item"
106 style="justify-content: space-between; align-items: center"
107 >
108 <div>
109 <strong>Database</strong>
110 <div style="font-size: 12px; color: var(--text-muted)">
111 Neon PostgreSQL
112 </div>
113 </div>
114 <span
115 style={`padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; background: ${dbOk ? "rgba(45, 164, 78, 0.15)" : "rgba(207, 34, 46, 0.15)"}; color: ${dbOk ? "var(--green, #2da44e)" : "var(--red, #cf222e)"}`}
116 >
117 {dbOk ? "operational" : "down"}
118 </span>
119 </div>
120 <div
121 class="panel-item"
122 style="justify-content: space-between; align-items: center"
123 >
124 <div>
125 <strong>Autopilot</strong>
126 <div style="font-size: 12px; color: var(--text-muted)">
127 Periodic platform-maintenance loop
128 </div>
129 </div>
130 <span
131 style={`padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; background: ${autopilotDisabled ? "rgba(150, 150, 150, 0.15)" : "rgba(45, 164, 78, 0.15)"}; color: ${autopilotDisabled ? "var(--text-muted)" : "var(--green, #2da44e)"}`}
132 >
133 {autopilotDisabled ? "disabled" : "running"}
134 </span>
135 </div>
136 <div
137 class="panel-item"
138 style="justify-content: space-between; align-items: center"
139 >
140 <div>
141 <strong>Git Smart HTTP</strong>
142 <div style="font-size: 12px; color: var(--text-muted)">
143 Clone, fetch, push
144 </div>
145 </div>
146 <span style="padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; background: rgba(45, 164, 78, 0.15); color: var(--green, #2da44e)">
147 operational
148 </span>
149 </div>
150 </div>
151
152 <h2 style="margin-bottom: 12px; font-size: 18px">Platform stats</h2>
153 <div
154 style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 24px"
155 >
156 <div class="panel" style="padding: 14px; text-align: center">
157 <div style="font-size: 24px; font-weight: 700">
158 {userCount.toLocaleString()}
159 </div>
160 <div
161 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
162 >
163 Developers
164 </div>
165 </div>
166 <div class="panel" style="padding: 14px; text-align: center">
167 <div style="font-size: 24px; font-weight: 700">
168 {repoCount.toLocaleString()}
169 </div>
170 <div
171 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
172 >
173 Repositories
174 </div>
175 </div>
176 <div class="panel" style="padding: 14px; text-align: center">
177 <div style="font-size: 24px; font-weight: 700">
178 {publicRepoCount.toLocaleString()}
179 </div>
180 <div
181 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
182 >
183 Public repos
184 </div>
185 </div>
186 <div class="panel" style="padding: 14px; text-align: center">
187 <div style="font-size: 24px; font-weight: 700">
188 {gateRunCount.toLocaleString()}
189 </div>
190 <div
191 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
192 >
193 Gate runs
194 </div>
195 </div>
196 <div class="panel" style="padding: 14px; text-align: center">
197 <div style="font-size: 24px; font-weight: 700">
198 {greenRate === null ? "—" : `${greenRate.toFixed(1)}%`}
199 </div>
200 <div
201 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
202 >
203 Green rate
204 </div>
205 </div>
206 <div class="panel" style="padding: 14px; text-align: center">
207 <div style="font-size: 24px; font-weight: 700">
208 {fmtUptime(uptimeMs)}
209 </div>
210 <div
211 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
212 >
213 Uptime
214 </div>
215 </div>
216 </div>
217
218 <h2 style="margin-bottom: 12px; font-size: 18px">
219 Latest autopilot tick
220 </h2>
221 {tick ? (
222 <div class="panel" style="margin-bottom: 24px">
223 <div
224 class="panel-item"
225 style="justify-content: space-between; font-size: 13px"
226 >
227 <span>Finished</span>
228 <code>{tick.finishedAt}</code>
229 </div>
230 <div
231 class="panel-item"
232 style="justify-content: space-between; font-size: 13px"
233 >
234 <span>Total ticks this process</span>
235 <code>{ticks}</code>
236 </div>
237 {tick.tasks.map((t) => (
238 <div
239 class="panel-item"
240 style="justify-content: space-between; font-size: 13px"
241 >
242 <code>{t.name}</code>
243 <span
244 style={
245 t.ok
246 ? "color: var(--green, #2da44e)"
247 : "color: var(--red, #cf222e)"
248 }
249 >
250 {t.ok ? "ok" : `failed: ${t.error || "unknown"}`}
251 <span
252 style="color: var(--text-muted); margin-left: 8px"
253 >
254 {t.durationMs}ms
255 </span>
256 </span>
257 </div>
258 ))}
259 </div>
260 ) : (
261 <p
262 style="color: var(--text-muted); margin-bottom: 24px; font-size: 14px"
263 >
264 {autopilotDisabled
265 ? "Autopilot is disabled via AUTOPILOT_DISABLED=1."
266 : "No ticks have completed yet. Check back after the first 5-minute interval elapses."}
267 </p>
268 )}
269
270 <p
271 style="color: var(--text-muted); font-size: 12px; margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border)"
272 >
273 Liveness: <a href="/healthz">/healthz</a> &middot; Readiness:{" "}
274 <a href="/readyz">/readyz</a> &middot; Metrics:{" "}
275 <a href="/metrics">/metrics</a> &middot; Platform JSON:{" "}
276 <a href="/api/platform-status">/api/platform-status</a>
277 </p>
278 </div>
279 </Layout>
280 );
281});
282
9b07ca9Claude283/**
284 * Shields-style status badge. Reads the latest autopilot tick + DB
285 * reachability and returns an SVG. Embed in READMEs with:
286 * ![status](https://your-host/status.svg)
287 */
288status.get("/status.svg", async (c) => {
289 let dbOk = false;
290 try {
291 await db.execute(sql`SELECT 1`);
292 dbOk = true;
293 } catch {
294 dbOk = false;
295 }
296 const tick = getLastTick();
297 const lastOk = tick ? tick.tasks.every((t) => t.ok) : true;
298 const overall = dbOk && lastOk;
299 const label = "gluecron";
300 const value = overall ? "operational" : "degraded";
301 const fill = overall ? "#2da44e" : "#cf222e";
302
303 const labelW = 70;
304 const valueW = overall ? 78 : 68;
305 const totalW = labelW + valueW;
306 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="20" role="img" aria-label="${label}: ${value}">
307 <linearGradient id="s" x2="0" y2="100%">
308 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
309 <stop offset="1" stop-opacity=".1"/>
310 </linearGradient>
311 <rect width="${totalW}" height="20" rx="3" fill="#555"/>
312 <rect x="${labelW}" width="${valueW}" height="20" rx="3" fill="${fill}"/>
313 <rect width="${totalW}" height="20" rx="3" fill="url(#s)"/>
314 <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,sans-serif" font-size="11">
315 <text x="${labelW / 2}" y="15">${label}</text>
316 <text x="${labelW + valueW / 2}" y="15">${value}</text>
317 </g>
318</svg>`;
319 c.header("Content-Type", "image/svg+xml; charset=utf-8");
320 c.header("Cache-Control", "no-cache, max-age=0");
321 return c.body(svg);
322});
323
2316be6Claude324export default status;