Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit0a50474unknown_key

polish(repo): 2026 cards for deps + migrations + wikis + semantic-search + symbols

Claude committed on May 25, 2026Parent: e3eb5ff
2 files changed+13733310a5047464ab92a891b891a94f47a0dbe7338e5f9
2 changed files+1373−331
Modifiedsrc/routes/status.tsx+698−196View fileUnifiedSplit
88 *
99 * Accessible without auth. Uses softAuth so the nav bar renders correctly
1010 * for logged-in visitors.
11 *
12 * 2026 polish: scoped `.status-` CSS, hero with eyebrow + gradient
13 * headline + aggregate uptime percentage, per-service health pills,
14 * recent incidents list with severity dots, and an empty-state orb
15 * for the autopilot tick card when no tick has run yet.
1116 */
1217
1318import { Hono } from "hono";
9499
95100 const overallOk = dbOk && recentIncidents.length === 0;
96101
102 // Aggregate uptime — process uptime over the last 24h window, capped
103 // at 100%. We don't track historical downtime in-process, so this is a
104 // best-effort number based on how long this Bun worker has been alive
105 // versus the rolling 24h window.
106 const WINDOW_MS = 24 * 60 * 60 * 1000;
107 const aggregatePct =
108 overallOk
109 ? Math.min(100, (uptimeMs / WINDOW_MS) * 100)
110 : Math.max(0, 100 - (recentIncidents.length * 100) / 24);
111 const aggregateStr = aggregatePct >= 100 ? "100.0" : aggregatePct.toFixed(1);
112
113 // Per-service status descriptors — kept here so the JSX is just markup.
114 const services: Array<{
115 name: string;
116 sub: string;
117 state: "ok" | "down" | "idle";
118 label: string;
119 }> = [
120 {
121 name: "Database",
122 sub: "Neon PostgreSQL",
123 state: dbOk ? "ok" : "down",
124 label: dbOk ? "operational" : "down",
125 },
126 {
127 name: "Autopilot",
128 sub: "Periodic platform-maintenance loop",
129 state: autopilotDisabled ? "idle" : "ok",
130 label: autopilotDisabled ? "disabled" : "running",
131 },
132 {
133 name: "Git Smart HTTP",
134 sub: "Clone, fetch, push",
135 state: "ok",
136 label: "operational",
137 },
138 ];
139
97140 return c.html(
98141 <Layout title="Status — gluecron" user={user}>
99 <div style="max-width: 960px; margin: 0 auto; padding: 24px 16px">
100 <div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px">
101 <span
102 style={`display: inline-block; width: 14px; height: 14px; border-radius: 50%; background: ${overallOk ? "var(--green, #2da44e)" : "var(--red, #cf222e)"}`}
103 />
104 <h1 style="margin: 0; font-size: 28px">
105 {overallOk ? "All systems operational" : "Service degraded"}
106 </h1>
107 </div>
108 <p style="color: var(--text-muted); margin-bottom: 32px">
109 Live platform status. Reloads on refresh; no client-side polling.
110 </p>
111
112 <h2 style="margin-bottom: 12px; font-size: 18px">Components</h2>
113 <div class="panel" style="margin-bottom: 24px">
114 <div
115 class="panel-item"
116 style="justify-content: space-between; align-items: center"
117 >
118 <div>
119 <strong>Database</strong>
120 <div style="font-size: 12px; color: var(--text-muted)">
121 Neon PostgreSQL
122 </div>
142 <style dangerouslySetInnerHTML={{ __html: statusStyles }} />
143 <div class="status-wrap">
144 {/* ─── Hero ─── */}
145 <section
146 class={"status-hero " + (overallOk ? "is-ok" : "is-degraded")}
147 >
148 <div class="status-hero-orb" aria-hidden="true" />
149 <div class="status-hero-inner">
150 <div class="status-eyebrow">
151 <span class="status-eyebrow-pill" aria-hidden="true">
152 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
153 <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
154 </svg>
155 </span>
156 Platform status · live · reloads on refresh
123157 </div>
124 <span
125 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)"}`}
126 >
127 {dbOk ? "operational" : "down"}
128 </span>
129 </div>
130 <div
131 class="panel-item"
132 style="justify-content: space-between; align-items: center"
133 >
134 <div>
135 <strong>Autopilot</strong>
136 <div style="font-size: 12px; color: var(--text-muted)">
137 Periodic platform-maintenance loop
158 <h1 class="status-title">
159 <span class="status-dot-big" aria-hidden="true" />
160 <span
161 class={
162 "status-title-text " +
163 (overallOk
164 ? "status-title-grad"
165 : "status-title-grad status-title-grad-warn")
166 }
167 >
168 {overallOk
169 ? "All systems operational"
170 : "Service degraded"}
171 </span>
172 </h1>
173 <p class="status-sub">
174 Live platform health for every Gluecron surface — database,
175 autopilot, git protocol, and the synthetic monitor.
176 </p>
177 <div class="status-hero-stats">
178 <div class="status-hero-stat">
179 <div class="status-hero-stat-num">{aggregateStr}%</div>
180 <div class="status-hero-stat-label">24h uptime</div>
138181 </div>
139 </div>
140 <span
141 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)"}`}
142 >
143 {autopilotDisabled ? "disabled" : "running"}
144 </span>
145 </div>
146 <div
147 class="panel-item"
148 style="justify-content: space-between; align-items: center"
149 >
150 <div>
151 <strong>Git Smart HTTP</strong>
152 <div style="font-size: 12px; color: var(--text-muted)">
153 Clone, fetch, push
182 <div class="status-hero-stat">
183 <div class="status-hero-stat-num">{fmtUptime(uptimeMs)}</div>
184 <div class="status-hero-stat-label">Process uptime</div>
185 </div>
186 <div class="status-hero-stat">
187 <div class="status-hero-stat-num">{recentIncidents.length}</div>
188 <div class="status-hero-stat-label">Incidents · 24h</div>
154189 </div>
155190 </div>
156 <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)">
157 operational
158 </span>
159191 </div>
160 </div>
192 </section>
161193
162 <h2 style="margin-bottom: 12px; font-size: 18px">Platform stats</h2>
163 <div
164 style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 24px"
165 >
166 <div class="panel" style="padding: 14px; text-align: center">
167 <div style="font-size: 24px; font-weight: 700">
168 {userCount.toLocaleString()}
169 </div>
170 <div
171 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
172 >
173 Developers
174 </div>
175 </div>
176 <div class="panel" style="padding: 14px; text-align: center">
177 <div style="font-size: 24px; font-weight: 700">
178 {repoCount.toLocaleString()}
179 </div>
180 <div
181 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
182 >
183 Repositories
184 </div>
185 </div>
186 <div class="panel" style="padding: 14px; text-align: center">
187 <div style="font-size: 24px; font-weight: 700">
188 {publicRepoCount.toLocaleString()}
189 </div>
190 <div
191 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
192 >
193 Public repos
194 {/* ─── Components / health pills ─── */}
195 <section class="status-section" aria-labelledby="status-comp-h">
196 <header class="status-section-head">
197 <div>
198 <p class="status-section-eyebrow">Components</p>
199 <h2 class="status-section-title" id="status-comp-h">
200 Per-service health
201 </h2>
194202 </div>
195 </div>
196 <div class="panel" style="padding: 14px; text-align: center">
197 <div style="font-size: 24px; font-weight: 700">
198 {gateRunCount.toLocaleString()}
203 </header>
204 <ul class="status-svc-list">
205 {services.map((svc) => (
206 <li class="status-svc-row">
207 <div class="status-svc-main">
208 <p class="status-svc-name">{svc.name}</p>
209 <p class="status-svc-sub">{svc.sub}</p>
210 </div>
211 <span
212 class={"status-pill status-pill-" + svc.state}
213 title={svc.label}
214 >
215 <span class="status-pill-dot" aria-hidden="true" />
216 {svc.label}
217 </span>
218 </li>
219 ))}
220 </ul>
221 </section>
222
223 {/* ─── Platform stats ─── */}
224 <section class="status-section" aria-labelledby="status-stats-h">
225 <header class="status-section-head">
226 <div>
227 <p class="status-section-eyebrow">Numbers</p>
228 <h2 class="status-section-title" id="status-stats-h">
229 Platform stats
230 </h2>
199231 </div>
200 <div
201 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
202 >
203 Gate runs
232 </header>
233 <div class="status-section-body">
234 <div class="status-stats-grid">
235 <div class="status-stat">
236 <div class="status-stat-num">{userCount.toLocaleString()}</div>
237 <div class="status-stat-label">Developers</div>
238 </div>
239 <div class="status-stat">
240 <div class="status-stat-num">{repoCount.toLocaleString()}</div>
241 <div class="status-stat-label">Repositories</div>
242 </div>
243 <div class="status-stat">
244 <div class="status-stat-num">
245 {publicRepoCount.toLocaleString()}
246 </div>
247 <div class="status-stat-label">Public repos</div>
248 </div>
249 <div class="status-stat">
250 <div class="status-stat-num">
251 {gateRunCount.toLocaleString()}
252 </div>
253 <div class="status-stat-label">Gate runs</div>
254 </div>
255 <div class="status-stat">
256 <div class="status-stat-num">
257 {greenRate === null ? "—" : `${greenRate.toFixed(1)}%`}
258 </div>
259 <div class="status-stat-label">Green rate</div>
260 </div>
261 <div class="status-stat">
262 <div class="status-stat-num">{fmtUptime(uptimeMs)}</div>
263 <div class="status-stat-label">Uptime</div>
264 </div>
204265 </div>
205266 </div>
206 <div class="panel" style="padding: 14px; text-align: center">
207 <div style="font-size: 24px; font-weight: 700">
208 {greenRate === null ? "—" : `${greenRate.toFixed(1)}%`}
209 </div>
210 <div
211 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
212 >
213 Green rate
267 </section>
268
269 {/* ─── Recent incidents ─── */}
270 <section class="status-section" aria-labelledby="status-inc-h">
271 <header class="status-section-head">
272 <div>
273 <p class="status-section-eyebrow">Last 24h</p>
274 <h2 class="status-section-title" id="status-inc-h">
275 Recent incidents
276 </h2>
277 <p class="status-section-sub">
278 Synthetic-monitor probes that returned a red result.
279 </p>
214280 </div>
281 {recentIncidents.length > 0 ? (
282 <span class="status-count-pill is-warn">
283 {recentIncidents.length} red
284 </span>
285 ) : (
286 <span class="status-count-pill is-ok">All green</span>
287 )}
288 </header>
289 <div class="status-section-body">
290 {recentIncidents.length === 0 ? (
291 <div class="status-empty">
292 <div class="status-empty-orb" aria-hidden="true" />
293 <div class="status-empty-inner">
294 <p class="status-empty-title">No incidents in the last 24h.</p>
295 <p class="status-empty-sub">
296 Every synthetic probe is green. We log every red here
297 with timestamps and error text.
298 </p>
299 </div>
300 </div>
301 ) : (
302 <ul class="status-inc-list">
303 {recentIncidents.map((r) => (
304 <li class="status-inc-row">
305 <span class="status-inc-dot" aria-hidden="true" />
306 <div class="status-inc-main">
307 <p class="status-inc-name">
308 <code>{r.name}</code>
309 </p>
310 <p class="status-inc-err">
311 {r.error || "(no error message)"}
312 </p>
313 </div>
314 <time class="status-inc-time">
315 {r.checkedAt.toISOString()}
316 </time>
317 </li>
318 ))}
319 </ul>
320 )}
215321 </div>
216 <div class="panel" style="padding: 14px; text-align: center">
217 <div style="font-size: 24px; font-weight: 700">
218 {fmtUptime(uptimeMs)}
219 </div>
220 <div
221 style="font-size: 11px; color: var(--text-muted); text-transform: uppercase"
222 >
223 Uptime
322 </section>
323
324 {/* ─── Autopilot tick ─── */}
325 <section class="status-section" aria-labelledby="status-tick-h">
326 <header class="status-section-head">
327 <div>
328 <p class="status-section-eyebrow">Autopilot</p>
329 <h2 class="status-section-title" id="status-tick-h">
330 Latest tick
331 </h2>
332 <p class="status-section-sub">
333 Per-task results from the most recent autopilot sweep.
334 </p>
224335 </div>
225 </div>
226 </div>
227
228 {recentIncidents.length > 0 ? (
229 <>
230 <h2 style="margin-bottom: 12px; font-size: 18px">
231 Recent incidents (last 24h)
232 </h2>
233 <div class="panel" style="margin-bottom: 24px">
234 {recentIncidents.map((r) => (
235 <div
236 class="panel-item"
237 style="justify-content: space-between; font-size: 13px"
238 >
239 <div>
240 <code>{r.name}</code>
241 <span style="color: var(--text-muted); margin-left: 8px">
242 — {r.error || "(no error message)"}
336 {tick ? (
337 <span class="status-count-pill">
338 {ticks} ticks this process
339 </span>
340 ) : null}
341 </header>
342 <div class="status-section-body">
343 {tick ? (
344 <ul class="status-tick-list">
345 <li class="status-tick-row">
346 <span class="status-tick-name">Finished</span>
347 <code class="status-tick-val">{tick.finishedAt}</code>
348 </li>
349 <li class="status-tick-row">
350 <span class="status-tick-name">Total ticks this process</span>
351 <code class="status-tick-val">{ticks}</code>
352 </li>
353 {tick.tasks.map((t) => (
354 <li class="status-tick-row">
355 <code class="status-tick-name">{t.name}</code>
356 <span
357 class={"status-tick-val " + (t.ok ? "is-ok" : "is-err")}
358 >
359 {t.ok ? "ok" : `failed: ${t.error || "unknown"}`}
360 <span class="status-tick-ms">{t.durationMs}ms</span>
243361 </span>
244 </div>
245 <span style="color: var(--text-muted); font-size: 12px">
246 {r.checkedAt.toISOString()}
247 </span>
362 </li>
363 ))}
364 </ul>
365 ) : (
366 <div class="status-empty">
367 <div class="status-empty-orb" aria-hidden="true" />
368 <div class="status-empty-inner">
369 <p class="status-empty-title">
370 {autopilotDisabled
371 ? "Autopilot is disabled."
372 : "No ticks yet."}
373 </p>
374 <p class="status-empty-sub">
375 {autopilotDisabled
376 ? "Set AUTOPILOT_DISABLED=0 to re-enable the periodic platform-maintenance loop."
377 : "The first tick runs within 5 minutes of process start. Check back shortly."}
378 </p>
248379 </div>
249 ))}
250 </div>
251 </>
252 ) : null}
253
254 <h2 style="margin-bottom: 12px; font-size: 18px">
255 Latest autopilot tick
256 </h2>
257 {tick ? (
258 <div class="panel" style="margin-bottom: 24px">
259 <div
260 class="panel-item"
261 style="justify-content: space-between; font-size: 13px"
262 >
263 <span>Finished</span>
264 <code>{tick.finishedAt}</code>
265 </div>
266 <div
267 class="panel-item"
268 style="justify-content: space-between; font-size: 13px"
269 >
270 <span>Total ticks this process</span>
271 <code>{ticks}</code>
272 </div>
273 {tick.tasks.map((t) => (
274 <div
275 class="panel-item"
276 style="justify-content: space-between; font-size: 13px"
277 >
278 <code>{t.name}</code>
279 <span
280 style={
281 t.ok
282 ? "color: var(--green, #2da44e)"
283 : "color: var(--red, #cf222e)"
284 }
285 >
286 {t.ok ? "ok" : `failed: ${t.error || "unknown"}`}
287 <span
288 style="color: var(--text-muted); margin-left: 8px"
289 >
290 {t.durationMs}ms
291 </span>
292 </span>
293380 </div>
294 ))}
381 )}
295382 </div>
296 ) : (
297 <p
298 style="color: var(--text-muted); margin-bottom: 24px; font-size: 14px"
299 >
300 {autopilotDisabled
301 ? "Autopilot is disabled via AUTOPILOT_DISABLED=1."
302 : "No ticks have completed yet. Check back after the first 5-minute interval elapses."}
303 </p>
304 )}
305
306 <p
307 style="color: var(--text-muted); font-size: 12px; margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border)"
308 >
309 Liveness: <a href="/healthz">/healthz</a> &middot; Readiness:{" "}
310 <a href="/readyz">/readyz</a> &middot; Metrics:{" "}
311 <a href="/metrics">/metrics</a> &middot; Platform JSON:{" "}
383 </section>
384
385 <p class="status-foot">
386 Liveness: <a href="/healthz">/healthz</a> · Readiness:{" "}
387 <a href="/readyz">/readyz</a> · Metrics:{" "}
388 <a href="/metrics">/metrics</a> · Platform JSON:{" "}
312389 <a href="/api/platform-status">/api/platform-status</a>
313390 </p>
314391 </div>
357434 return c.body(svg);
358435});
359436
437/* ─────────────────────────────────────────────────────────────────────────
438 * Scoped CSS — every class prefixed `.status-` so this surface can't
439 * bleed into the admin status page or any other route.
440 * ───────────────────────────────────────────────────────────────────── */
441const statusStyles = `
442 .status-wrap { max-width: 960px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
443
444 /* ─── Hero ─── */
445 .status-hero {
446 position: relative;
447 margin-bottom: var(--space-5);
448 padding: var(--space-5) var(--space-6);
449 background: var(--bg-elevated);
450 border: 1px solid var(--border);
451 border-radius: 16px;
452 overflow: hidden;
453 }
454 .status-hero::before {
455 content: '';
456 position: absolute;
457 top: 0; left: 0; right: 0;
458 height: 2px;
459 background: linear-gradient(90deg, transparent 0%, #34d399 30%, #36c5d6 70%, transparent 100%);
460 opacity: 0.75;
461 pointer-events: none;
462 }
463 .status-hero.is-degraded::before {
464 background: linear-gradient(90deg, transparent 0%, #f87171 30%, #fbbf24 70%, transparent 100%);
465 }
466 .status-hero-orb {
467 position: absolute;
468 inset: -20% -10% auto auto;
469 width: 380px; height: 380px;
470 background: radial-gradient(circle, rgba(52,211,153,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
471 filter: blur(80px);
472 opacity: 0.7;
473 pointer-events: none;
474 z-index: 0;
475 }
476 .status-hero.is-degraded .status-hero-orb {
477 background: radial-gradient(circle, rgba(248,113,113,0.22), rgba(251,191,36,0.10) 45%, transparent 70%);
478 }
479 .status-hero-inner { position: relative; z-index: 1; }
480 .status-eyebrow {
481 font-size: 12px;
482 color: var(--text-muted);
483 margin-bottom: var(--space-2);
484 letter-spacing: 0.02em;
485 display: inline-flex;
486 align-items: center;
487 gap: 8px;
488 }
489 .status-eyebrow-pill {
490 display: inline-flex;
491 align-items: center;
492 justify-content: center;
493 width: 18px; height: 18px;
494 border-radius: 6px;
495 background: rgba(52,211,153,0.14);
496 color: #6ee7b7;
497 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
498 }
499 .status-hero.is-degraded .status-eyebrow-pill {
500 background: rgba(248,113,113,0.14);
501 color: #fca5a5;
502 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.30);
503 }
504 .status-title {
505 font-size: clamp(28px, 4vw, 40px);
506 font-family: var(--font-display);
507 font-weight: 800;
508 letter-spacing: -0.028em;
509 line-height: 1.05;
510 margin: 0 0 var(--space-2);
511 color: var(--text-strong);
512 display: flex;
513 align-items: center;
514 gap: 14px;
515 flex-wrap: wrap;
516 }
517 .status-dot-big {
518 display: inline-block;
519 width: 14px; height: 14px;
520 border-radius: 50%;
521 background: #34d399;
522 box-shadow: 0 0 0 5px rgba(52,211,153,0.18);
523 flex-shrink: 0;
524 }
525 .status-hero.is-degraded .status-dot-big {
526 background: #f87171;
527 box-shadow: 0 0 0 5px rgba(248,113,113,0.18);
528 }
529 .status-title-grad {
530 background-image: linear-gradient(135deg, #6ee7b7 0%, #34d399 50%, #36c5d6 100%);
531 -webkit-background-clip: text;
532 background-clip: text;
533 -webkit-text-fill-color: transparent;
534 color: transparent;
535 }
536 .status-title-grad-warn {
537 background-image: linear-gradient(135deg, #fca5a5 0%, #f87171 50%, #fbbf24 100%);
538 }
539 .status-sub {
540 font-size: 15px;
541 color: var(--text-muted);
542 margin: 0 0 var(--space-4);
543 line-height: 1.55;
544 max-width: 620px;
545 }
546
547 /* Hero stats strip */
548 .status-hero-stats {
549 display: grid;
550 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
551 gap: var(--space-3);
552 }
553 .status-hero-stat {
554 padding: 12px 14px;
555 background: rgba(255,255,255,0.025);
556 border: 1px solid var(--border-subtle);
557 border-radius: 10px;
558 }
559 .status-hero-stat-num {
560 font-family: var(--font-display);
561 font-size: 22px;
562 font-weight: 700;
563 color: var(--text-strong);
564 letter-spacing: -0.02em;
565 }
566 .status-hero-stat-label {
567 font-size: 11px;
568 color: var(--text-muted);
569 text-transform: uppercase;
570 letter-spacing: 0.06em;
571 margin-top: 2px;
572 }
573
574 /* ─── Section cards ─── */
575 .status-section {
576 margin-bottom: var(--space-5);
577 background: var(--bg-elevated);
578 border: 1px solid var(--border);
579 border-radius: 14px;
580 overflow: hidden;
581 }
582 .status-section-head {
583 padding: var(--space-4) var(--space-5);
584 border-bottom: 1px solid var(--border);
585 display: flex;
586 align-items: flex-start;
587 justify-content: space-between;
588 gap: var(--space-3);
589 flex-wrap: wrap;
590 }
591 .status-section-eyebrow {
592 font-size: 11px;
593 font-weight: 600;
594 letter-spacing: 0.08em;
595 text-transform: uppercase;
596 color: var(--text-faint);
597 margin: 0 0 6px;
598 }
599 .status-section-title {
600 margin: 0;
601 font-family: var(--font-display);
602 font-size: 17px;
603 font-weight: 700;
604 letter-spacing: -0.018em;
605 color: var(--text-strong);
606 }
607 .status-section-sub {
608 margin: 6px 0 0;
609 font-size: 12.5px;
610 color: var(--text-muted);
611 line-height: 1.5;
612 }
613 .status-section-body { padding: var(--space-5); }
614
615 /* ─── Service pills ─── */
616 .status-svc-list { list-style: none; margin: 0; padding: 0; }
617 .status-svc-row {
618 display: flex;
619 align-items: center;
620 justify-content: space-between;
621 gap: var(--space-3);
622 padding: var(--space-3) var(--space-5);
623 border-bottom: 1px solid var(--border);
624 }
625 .status-svc-row:last-child { border-bottom: 0; }
626 .status-svc-main { flex: 1; }
627 .status-svc-name {
628 margin: 0;
629 font-weight: 600;
630 color: var(--text-strong);
631 font-size: 14px;
632 }
633 .status-svc-sub {
634 margin: 2px 0 0;
635 font-size: 12px;
636 color: var(--text-muted);
637 }
638 .status-pill {
639 display: inline-flex;
640 align-items: center;
641 gap: 6px;
642 padding: 4px 10px;
643 border-radius: 9999px;
644 font-size: 11.5px;
645 font-weight: 600;
646 letter-spacing: 0.04em;
647 text-transform: lowercase;
648 }
649 .status-pill-dot {
650 width: 7px; height: 7px;
651 border-radius: 9999px;
652 background: currentColor;
653 flex-shrink: 0;
654 }
655 .status-pill-ok {
656 background: rgba(52,211,153,0.14);
657 color: #6ee7b7;
658 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
659 }
660 .status-pill-down {
661 background: rgba(248,113,113,0.14);
662 color: #fca5a5;
663 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
664 }
665 .status-pill-idle {
666 background: rgba(110,118,129,0.18);
667 color: #c9d1d9;
668 box-shadow: inset 0 0 0 1px rgba(110,118,129,0.40);
669 }
670
671 /* ─── Platform stats ─── */
672 .status-stats-grid {
673 display: grid;
674 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
675 gap: var(--space-3);
676 }
677 .status-stat {
678 padding: var(--space-3) var(--space-4);
679 background: var(--bg-secondary);
680 border: 1px solid var(--border-subtle);
681 border-radius: 12px;
682 text-align: center;
683 transition: border-color 150ms ease;
684 }
685 .status-stat:hover { border-color: var(--border-strong); }
686 .status-stat-num {
687 font-family: var(--font-display);
688 font-size: 24px;
689 font-weight: 700;
690 color: var(--text-strong);
691 letter-spacing: -0.022em;
692 }
693 .status-stat-label {
694 font-size: 11px;
695 color: var(--text-muted);
696 text-transform: uppercase;
697 letter-spacing: 0.06em;
698 margin-top: 4px;
699 }
700
701 /* ─── Count pills ─── */
702 .status-count-pill {
703 display: inline-flex;
704 align-items: center;
705 padding: 4px 10px;
706 border-radius: 9999px;
707 font-family: var(--font-mono);
708 font-size: 11.5px;
709 font-weight: 600;
710 background: rgba(140,109,255,0.10);
711 color: #c5b3ff;
712 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.28);
713 letter-spacing: 0.02em;
714 }
715 .status-count-pill.is-ok {
716 background: rgba(52,211,153,0.12);
717 color: #6ee7b7;
718 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
719 }
720 .status-count-pill.is-warn {
721 background: rgba(248,113,113,0.12);
722 color: #fca5a5;
723 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
724 }
725
726 /* ─── Incidents list ─── */
727 .status-inc-list { list-style: none; margin: 0; padding: 0; }
728 .status-inc-row {
729 display: flex;
730 align-items: flex-start;
731 gap: 12px;
732 padding: var(--space-3) 0;
733 border-bottom: 1px solid var(--border-subtle);
734 }
735 .status-inc-row:last-child { border-bottom: 0; }
736 .status-inc-row:first-child { padding-top: 0; }
737 .status-inc-dot {
738 width: 8px; height: 8px;
739 border-radius: 9999px;
740 background: #f87171;
741 box-shadow: 0 0 0 3px rgba(248,113,113,0.18);
742 margin-top: 6px;
743 flex-shrink: 0;
744 }
745 .status-inc-main { flex: 1; min-width: 0; }
746 .status-inc-name {
747 margin: 0;
748 font-size: 13.5px;
749 color: var(--text-strong);
750 }
751 .status-inc-name code {
752 font-family: var(--font-mono);
753 font-size: 12.5px;
754 background: var(--bg-tertiary);
755 padding: 1px 6px;
756 border-radius: 4px;
757 }
758 .status-inc-err {
759 margin: 4px 0 0;
760 font-size: 12.5px;
761 color: var(--text-muted);
762 line-height: 1.5;
763 }
764 .status-inc-time {
765 font-family: var(--font-mono);
766 font-size: 11.5px;
767 color: var(--text-faint);
768 white-space: nowrap;
769 flex-shrink: 0;
770 }
771
772 /* ─── Empty state ─── */
773 .status-empty {
774 position: relative;
775 padding: var(--space-6) var(--space-5);
776 border: 1px dashed var(--border-strong);
777 border-radius: 14px;
778 background: rgba(255,255,255,0.02);
779 text-align: center;
780 overflow: hidden;
781 }
782 .status-empty-orb {
783 position: absolute;
784 inset: -40% -10% auto auto;
785 width: 320px; height: 320px;
786 background: radial-gradient(circle, rgba(52,211,153,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
787 filter: blur(70px);
788 opacity: 0.55;
789 pointer-events: none;
790 z-index: 0;
791 }
792 .status-empty-inner { position: relative; z-index: 1; }
793 .status-empty-title {
794 margin: 0 0 6px;
795 font-family: var(--font-display);
796 font-size: 16px;
797 font-weight: 700;
798 color: var(--text-strong);
799 letter-spacing: -0.012em;
800 }
801 .status-empty-sub {
802 margin: 0 auto;
803 max-width: 460px;
804 font-size: 13px;
805 color: var(--text-muted);
806 line-height: 1.5;
807 }
808
809 /* ─── Autopilot tick list ─── */
810 .status-tick-list { list-style: none; margin: 0; padding: 0; }
811 .status-tick-row {
812 display: flex;
813 align-items: center;
814 justify-content: space-between;
815 gap: 12px;
816 padding: 10px 0;
817 border-bottom: 1px solid var(--border-subtle);
818 font-size: 13px;
819 }
820 .status-tick-row:last-child { border-bottom: 0; }
821 .status-tick-row:first-child { padding-top: 0; }
822 .status-tick-name {
823 color: var(--text);
824 font-size: 13px;
825 }
826 code.status-tick-name {
827 font-family: var(--font-mono);
828 font-size: 12.5px;
829 background: var(--bg-tertiary);
830 padding: 1px 6px;
831 border-radius: 4px;
832 color: var(--text-strong);
833 }
834 .status-tick-val {
835 display: inline-flex;
836 align-items: center;
837 gap: 10px;
838 font-family: var(--font-mono);
839 font-size: 12.5px;
840 color: var(--text-muted);
841 }
842 .status-tick-val.is-ok { color: #6ee7b7; }
843 .status-tick-val.is-err { color: #fca5a5; }
844 .status-tick-ms { color: var(--text-faint); font-size: 11.5px; }
845
846 /* ─── Footer links ─── */
847 .status-foot {
848 margin-top: var(--space-5);
849 padding-top: var(--space-4);
850 border-top: 1px solid var(--border);
851 color: var(--text-muted);
852 font-size: 12.5px;
853 text-align: center;
854 }
855 .status-foot a {
856 color: var(--accent, #8c6dff);
857 text-decoration: none;
858 }
859 .status-foot a:hover { text-decoration: underline; }
860`;
861
360862export default status;
Modifiedsrc/routes/symbols.tsx+675−135View fileUnifiedSplit
55 * GET /:owner/:repo/symbols/search — search by name (prefix match)
66 * GET /:owner/:repo/symbols/:name — definitions list
77 * POST /:owner/:repo/symbols/reindex — owner-only, runs indexer
8 *
9 * 2026 polish:
10 * - Scoped `.sym-*` CSS — no bleed into RepoHeader/RepoNav above.
11 * - Eyebrow + display headline + 1-line subtitle.
12 * - Each symbol is a card with a kind-specific icon (fn / class / var /
13 * interface / type / const), mono name pill, file:line, signature.
14 * - Search input gets a focus ring + gradient submit.
15 * - Dashed empty-state with orb + CTA when nothing's indexed.
16 *
17 * All query strings + POST handlers preserved verbatim.
818 */
919
1020import { Hono } from "hono";
2030const symbols = new Hono<AuthEnv>();
2131symbols.use("*", softAuth);
2232
33// ─── Scoped CSS (.sym-*) ─────────────────────────────────────────────────
34const symStyles = `
35 .sym-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
36
37 .sym-head {
38 margin-bottom: var(--space-5);
39 display: flex;
40 align-items: flex-end;
41 justify-content: space-between;
42 gap: var(--space-4);
43 flex-wrap: wrap;
44 }
45 .sym-head-text { flex: 1; min-width: 280px; }
46 .sym-eyebrow {
47 display: inline-flex;
48 align-items: center;
49 gap: 8px;
50 text-transform: uppercase;
51 font-family: var(--font-mono);
52 font-size: 11px;
53 letter-spacing: 0.16em;
54 color: var(--text-muted);
55 font-weight: 600;
56 margin-bottom: 10px;
57 }
58 .sym-eyebrow-dot {
59 width: 8px; height: 8px;
60 border-radius: 9999px;
61 background: linear-gradient(135deg, #8c6dff, #36c5d6);
62 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
63 }
64 .sym-title {
65 font-family: var(--font-display);
66 font-size: clamp(24px, 3.4vw, 36px);
67 font-weight: 800;
68 letter-spacing: -0.028em;
69 line-height: 1.1;
70 margin: 0 0 6px;
71 color: var(--text-strong);
72 }
73 .sym-title-grad {
74 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
75 -webkit-background-clip: text;
76 background-clip: text;
77 -webkit-text-fill-color: transparent;
78 color: transparent;
79 }
80 .sym-title-mono {
81 font-family: var(--font-mono);
82 letter-spacing: -0.018em;
83 }
84 .sym-sub {
85 margin: 0;
86 font-size: 14px;
87 color: var(--text-muted);
88 line-height: 1.5;
89 max-width: 720px;
90 }
91
92 .sym-btn {
93 display: inline-flex;
94 align-items: center;
95 justify-content: center;
96 gap: 6px;
97 padding: 9px 16px;
98 border-radius: 10px;
99 font-size: 13px;
100 font-weight: 600;
101 text-decoration: none;
102 border: 1px solid transparent;
103 cursor: pointer;
104 font: inherit;
105 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
106 line-height: 1;
107 white-space: nowrap;
108 }
109 .sym-btn-primary {
110 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
111 color: #ffffff;
112 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
113 }
114 .sym-btn-primary:hover {
115 transform: translateY(-1px);
116 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
117 text-decoration: none;
118 color: #ffffff;
119 }
120 .sym-btn-ghost {
121 background: transparent;
122 color: var(--text);
123 border-color: var(--border-strong);
124 }
125 .sym-btn-ghost:hover {
126 background: rgba(140,109,255,0.06);
127 border-color: rgba(140,109,255,0.45);
128 color: var(--text-strong);
129 text-decoration: none;
130 }
131
132 /* ─── Banners ─── */
133 .sym-banner {
134 margin-bottom: var(--space-4);
135 padding: 10px 14px;
136 border-radius: 10px;
137 font-size: 13.5px;
138 border: 1px solid var(--border);
139 background: rgba(255,255,255,0.025);
140 color: var(--text);
141 display: flex;
142 align-items: center;
143 gap: 10px;
144 }
145 .sym-banner.is-ok { border-color: rgba(52,211,153,0.40); background: rgba(52,211,153,0.08); color: #bbf7d0; }
146 .sym-banner-dot { width: 8px; height: 8px; border-radius: 9999px; background: currentColor; flex-shrink: 0; }
147
148 /* ─── Search bar ─── */
149 .sym-search {
150 display: flex;
151 gap: 10px;
152 align-items: stretch;
153 margin-bottom: var(--space-4);
154 }
155 .sym-search-input-wrap { position: relative; flex: 1; }
156 .sym-search-icon {
157 position: absolute;
158 top: 50%;
159 left: 14px;
160 transform: translateY(-50%);
161 color: var(--text-muted);
162 pointer-events: none;
163 }
164 .sym-search-input {
165 width: 100%;
166 box-sizing: border-box;
167 padding: 11px 14px 11px 40px;
168 font: inherit;
169 font-size: 14px;
170 color: var(--text);
171 background: rgba(255,255,255,0.03);
172 border: 1px solid var(--border-strong);
173 border-radius: 12px;
174 outline: none;
175 font-family: var(--font-mono);
176 transition: border-color 120ms ease, background 120ms ease, box-shadow 120ms ease;
177 }
178 .sym-search-input:focus {
179 border-color: rgba(140,109,255,0.55);
180 background: rgba(255,255,255,0.05);
181 box-shadow: 0 0 0 3px rgba(140,109,255,0.20);
182 }
183
184 /* ─── Summary tiles ─── */
185 .sym-stats {
186 display: grid;
187 grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
188 gap: 10px;
189 margin-bottom: var(--space-5);
190 }
191 .sym-stat {
192 position: relative;
193 padding: 14px 16px;
194 background: var(--bg-elevated);
195 border: 1px solid var(--border);
196 border-radius: 12px;
197 overflow: hidden;
198 }
199 .sym-stat::before {
200 content: '';
201 position: absolute;
202 top: 0; left: 0; right: 0;
203 height: 1.5px;
204 background: linear-gradient(90deg, transparent 0%, #8c6dff 50%, #36c5d6 100%);
205 opacity: 0.40;
206 pointer-events: none;
207 }
208 .sym-stat-num {
209 font-family: var(--font-display);
210 font-size: 22px;
211 font-weight: 800;
212 letter-spacing: -0.02em;
213 color: var(--text-strong);
214 font-variant-numeric: tabular-nums;
215 }
216 .sym-stat-label {
217 font-size: 11px;
218 color: var(--text-muted);
219 text-transform: uppercase;
220 letter-spacing: 0.06em;
221 margin-top: 4px;
222 font-weight: 600;
223 }
224
225 /* ─── Section header ─── */
226 .sym-section-title {
227 margin: 0 0 var(--space-3);
228 font-family: var(--font-display);
229 font-size: 17px;
230 font-weight: 700;
231 letter-spacing: -0.018em;
232 color: var(--text-strong);
233 }
234
235 /* ─── Symbol cards ─── */
236 .sym-list { display: flex; flex-direction: column; gap: 8px; }
237 .sym-card {
238 display: flex;
239 align-items: flex-start;
240 gap: 12px;
241 padding: 12px 14px;
242 background: var(--bg-elevated);
243 border: 1px solid var(--border);
244 border-radius: 12px;
245 transition: border-color 120ms ease, background 120ms ease;
246 }
247 .sym-card:hover {
248 border-color: var(--border-strong);
249 background: rgba(255,255,255,0.025);
250 }
251 .sym-kind {
252 display: inline-flex;
253 align-items: center;
254 justify-content: center;
255 width: 30px; height: 30px;
256 border-radius: 8px;
257 flex-shrink: 0;
258 font-family: var(--font-mono);
259 font-size: 11px;
260 font-weight: 700;
261 }
262 .sym-kind.is-function {
263 background: rgba(140,109,255,0.14);
264 color: #c4b5fd;
265 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.32);
266 }
267 .sym-kind.is-class {
268 background: rgba(54,197,214,0.14);
269 color: #67e8f9;
270 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.32);
271 }
272 .sym-kind.is-interface {
273 background: rgba(34,211,238,0.10);
274 color: #a5f3fc;
275 box-shadow: inset 0 0 0 1px rgba(34,211,238,0.32);
276 }
277 .sym-kind.is-type {
278 background: rgba(251,191,36,0.12);
279 color: #fde68a;
280 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
281 }
282 .sym-kind.is-const,
283 .sym-kind.is-var {
284 background: rgba(52,211,153,0.12);
285 color: #6ee7b7;
286 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
287 }
288 .sym-kind.is-other {
289 background: rgba(148,163,184,0.14);
290 color: #cbd5e1;
291 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
292 }
293
294 .sym-card-body { flex: 1; min-width: 0; }
295 .sym-card-row {
296 display: flex;
297 align-items: center;
298 justify-content: space-between;
299 gap: 10px;
300 flex-wrap: wrap;
301 margin-bottom: 4px;
302 }
303 .sym-card-name {
304 display: inline-flex;
305 align-items: baseline;
306 gap: 8px;
307 flex-wrap: wrap;
308 }
309 .sym-name {
310 font-family: var(--font-mono);
311 font-size: 14px;
312 font-weight: 700;
313 color: var(--text-strong);
314 text-decoration: none;
315 letter-spacing: -0.005em;
316 word-break: break-all;
317 }
318 .sym-name:hover { color: #c4b5fd; text-decoration: none; }
319 .sym-kind-label {
320 font-size: 10.5px;
321 text-transform: uppercase;
322 letter-spacing: 0.06em;
323 color: var(--text-muted);
324 font-weight: 600;
325 }
326 .sym-card-loc {
327 font-family: var(--font-mono);
328 font-size: 11.5px;
329 color: var(--text-muted);
330 text-decoration: none;
331 word-break: break-all;
332 font-variant-numeric: tabular-nums;
333 }
334 .sym-card-loc:hover { color: var(--text-strong); text-decoration: underline; }
335 .sym-sig {
336 margin: 6px 0 0;
337 padding: 8px 10px;
338 background: rgba(0,0,0,0.25);
339 border: 1px solid var(--border);
340 border-radius: 8px;
341 font-family: var(--font-mono);
342 font-size: 11.5px;
343 line-height: 1.5;
344 color: var(--text);
345 overflow-x: auto;
346 white-space: pre;
347 }
348
349 /* ─── Empty state ─── */
350 .sym-empty {
351 position: relative;
352 overflow: hidden;
353 padding: clamp(28px, 5vw, 52px) clamp(20px, 4vw, 40px);
354 text-align: center;
355 background: var(--bg-elevated);
356 border: 1px dashed var(--border-strong);
357 border-radius: 16px;
358 }
359 .sym-empty-orb {
360 position: absolute;
361 inset: -40% 25% auto 25%;
362 height: 300px;
363 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
364 filter: blur(72px);
365 opacity: 0.7;
366 pointer-events: none;
367 z-index: 0;
368 }
369 .sym-empty-inner { position: relative; z-index: 1; }
370 .sym-empty-icon {
371 width: 56px; height: 56px;
372 margin: 0 auto 14px;
373 border-radius: 9999px;
374 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
375 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
376 display: inline-flex;
377 align-items: center;
378 justify-content: center;
379 color: #c4b5fd;
380 }
381 .sym-empty-title {
382 font-family: var(--font-display);
383 font-size: 18px;
384 font-weight: 700;
385 margin: 0 0 6px;
386 color: var(--text-strong);
387 }
388 .sym-empty-sub {
389 margin: 0 auto 16px;
390 font-size: 13.5px;
391 color: var(--text-muted);
392 max-width: 460px;
393 line-height: 1.5;
394 }
395
396 .sym-crumbs {
397 margin-bottom: var(--space-4);
398 font-size: 12.5px;
399 }
400 .sym-crumbs a {
401 display: inline-flex;
402 align-items: center;
403 gap: 5px;
404 padding: 6px 11px;
405 background: rgba(255,255,255,0.025);
406 border: 1px solid var(--border);
407 border-radius: 8px;
408 color: var(--text-muted);
409 text-decoration: none;
410 font-weight: 500;
411 }
412 .sym-crumbs a:hover { border-color: var(--border-strong); color: var(--text-strong); text-decoration: none; }
413`;
414
415function IconRefresh() {
416 return (
417 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
418 <polyline points="23 4 23 10 17 10" />
419 <polyline points="1 20 1 14 7 14" />
420 <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10" />
421 <path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14" />
422 </svg>
423 );
424}
425function IconSearch() {
426 return (
427 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
428 <circle cx="11" cy="11" r="7" />
429 <line x1="21" y1="21" x2="16.65" y2="16.65" />
430 </svg>
431 );
432}
433function IconCode() {
434 return (
435 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
436 <polyline points="16 18 22 12 16 6" />
437 <polyline points="8 6 2 12 8 18" />
438 </svg>
439 );
440}
441
442/** Map a code-symbol kind to a short glyph + className suffix. */
443function kindBadge(kind: string): { cls: string; glyph: string; label: string } {
444 const k = (kind || "").toLowerCase();
445 if (k === "function" || k === "fn" || k === "method")
446 return { cls: "is-function", glyph: "fn", label: "function" };
447 if (k === "class") return { cls: "is-class", glyph: "Cl", label: "class" };
448 if (k === "interface") return { cls: "is-interface", glyph: "If", label: "interface" };
449 if (k === "type") return { cls: "is-type", glyph: "T", label: "type" };
450 if (k === "const") return { cls: "is-const", glyph: "K", label: "const" };
451 if (k === "var" || k === "variable" || k === "let")
452 return { cls: "is-var", glyph: "V", label: "var" };
453 return { cls: "is-other", glyph: "·", label: kind || "symbol" };
454}
455
23456async function loadRepo(ownerName: string, repoName: string) {
24457 const [owner] = await db
25458 .select()
38471 return { owner, repo };
39472}
40473
474function SymbolCard(props: {
475 ownerName: string;
476 repoName: string;
477 name: string;
478 kind: string;
479 path: string;
480 line: number;
481 signature?: string | null;
482}) {
483 const { ownerName, repoName, name, kind, path, line, signature } = props;
484 const badge = kindBadge(kind);
485 return (
486 <div class="sym-card">
487 <span
488 class={"sym-kind " + badge.cls}
489 title={badge.label}
490 aria-label={badge.label}
491 >
492 {badge.glyph}
493 </span>
494 <div class="sym-card-body">
495 <div class="sym-card-row">
496 <div class="sym-card-name">
497 <a
498 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(name)}`}
499 class="sym-name"
500 >
501 {name}
502 </a>
503 <span class="sym-kind-label">{badge.label}</span>
504 </div>
505 <a
506 href={`/${ownerName}/${repoName}/blob/HEAD/${path}#L${line}`}
507 class="sym-card-loc"
508 >
509 {path}:{line}
510 </a>
511 </div>
512 {signature && <pre class="sym-sig">{signature}</pre>}
513 </div>
514 </div>
515 );
516}
517
41518// ---------- Overview ----------
42519
43520symbols.get("/:owner/:repo/symbols", async (c) => {
45522 const { owner: ownerName, repo: repoName } = c.req.param();
46523 const ctx = await loadRepo(ownerName, repoName);
47524 if (!ctx) return c.notFound();
48 const { owner, repo } = ctx;
525 const { repo } = ctx;
49526 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
50527 return c.notFound();
51528 }
84561 <Layout title={`Symbols — ${ownerName}/${repoName}`} user={user}>
85562 <RepoHeader owner={ownerName} repo={repoName} />
86563 <RepoNav owner={ownerName} repo={repoName} active="code" />
87 <div class="settings-container">
88 <div style="display:flex;justify-content:space-between;align-items:center">
89 <h2 style="margin:0">Symbols</h2>
564 <div class="sym-wrap">
565 <header class="sym-head">
566 <div class="sym-head-text">
567 <div class="sym-eyebrow">
568 <span class="sym-eyebrow-dot" aria-hidden="true" />
569 Repository · Symbols
570 </div>
571 <h1 class="sym-title">
572 <span class="sym-title-grad">Top-level definitions, indexed.</span>
573 </h1>
574 <p class="sym-sub">
575 Jump straight to the file and line where each function, class,
576 interface, type, or constant is defined on the default branch.
577 </p>
578 </div>
90579 {isOwner && (
91580 <form method="post" action={`/${ownerName}/${repoName}/symbols/reindex`}>
92 <button type="submit" class="btn btn-primary btn-sm">
581 <button type="submit" class="sym-btn sym-btn-primary">
582 <IconRefresh />
93583 Reindex
94584 </button>
95585 </form>
96586 )}
97 </div>
587 </header>
588
98589 {message && (
99 <div class="auth-success" style="margin-top:12px">
590 <div class="sym-banner is-ok" role="status">
591 <span class="sym-banner-dot" aria-hidden="true" />
100592 {decodeURIComponent(message)}
101593 </div>
102594 )}
103 <p style="color:var(--text-muted);margin-top:8px">
104 Top-level definitions from the default branch. Click a symbol to
105 see all its definitions.
106 </p>
107595
108596 <form
109597 method="get"
110598 action={`/${ownerName}/${repoName}/symbols/search`}
111 style="display:flex;gap:8px;margin:16px 0"
599 class="sym-search"
112600 >
113 <input
114 type="text"
115 name="q"
116 placeholder="Search symbol name..."
117 required
118 aria-label="Search symbol name"
119 style="flex:1"
120 />
121 <button type="submit" class="btn">
601 <div class="sym-search-input-wrap">
602 <span class="sym-search-icon" aria-hidden="true">
603 <IconSearch />
604 </span>
605 <input
606 type="text"
607 name="q"
608 placeholder="Search symbol name…"
609 required
610 aria-label="Search symbol name"
611 class="sym-search-input"
612 />
613 </div>
614 <button type="submit" class="sym-btn sym-btn-primary">
122615 Search
123616 </button>
124617 </form>
125618
126 <div
127 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:16px"
128 >
129 <div class="panel" style="padding:12px;text-align:center">
130 <div style="font-size:20px;font-weight:700">{total}</div>
131 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
132 Symbols
133 </div>
619 <div class="sym-stats">
620 <div class="sym-stat">
621 <div class="sym-stat-num">{total}</div>
622 <div class="sym-stat-label">Symbols</div>
134623 </div>
135624 {byKindRaw.map((r) => (
136 <div class="panel" style="padding:12px;text-align:center">
137 <div style="font-size:20px;font-weight:700">{Number(r.n)}</div>
138 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
139 {r.kind}
140 </div>
625 <div class="sym-stat">
626 <div class="sym-stat-num">{Number(r.n)}</div>
627 <div class="sym-stat-label">{r.kind}</div>
141628 </div>
142629 ))}
143630 </div>
144631
145 <h3>A–Z</h3>
632 <h3 class="sym-section-title">A–Z</h3>
146633 {total === 0 ? (
147 <div class="panel-empty" style="padding:24px">
148 No symbols indexed yet.
149 {isOwner && " Click Reindex to scan the repository."}
634 <div class="sym-empty">
635 <div class="sym-empty-orb" aria-hidden="true" />
636 <div class="sym-empty-inner">
637 <div class="sym-empty-icon" aria-hidden="true">
638 <IconCode />
639 </div>
640 <h3 class="sym-empty-title">No symbols indexed yet</h3>
641 <p class="sym-empty-sub">
642 {isOwner
643 ? "Click Reindex to scan the default branch for top-level functions, classes, interfaces, types, and constants."
644 : "The owner hasn't indexed this repository's symbols yet."}
645 </p>
646 {isOwner && (
647 <form method="post" action={`/${ownerName}/${repoName}/symbols/reindex`}>
648 <button type="submit" class="sym-btn sym-btn-primary">
649 <IconRefresh />
650 Reindex now
651 </button>
652 </form>
653 )}
654 </div>
150655 </div>
151656 ) : (
152 <div class="panel">
657 <div class="sym-list">
153658 {latest.map((s) => (
154 <div class="panel-item" style="justify-content:space-between">
155 <div>
156 <a
157 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(s.name)}`}
158 style="font-weight:600;font-family:var(--font-mono)"
159 >
160 {s.name}
161 </a>{" "}
162 <span
163 style="font-size:11px;color:var(--text-muted);text-transform:uppercase;margin-left:6px"
164 >
165 {s.kind}
166 </span>
167 </div>
168 <a
169 href={`/${ownerName}/${repoName}/blob/HEAD/${s.path}#L${s.line}`}
170 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
171 >
172 {s.path}:{s.line}
173 </a>
174 </div>
659 <SymbolCard
660 ownerName={ownerName}
661 repoName={repoName}
662 name={s.name}
663 kind={s.kind}
664 path={s.path}
665 line={s.line}
666 />
175667 ))}
176668 </div>
177669 )}
178670 </div>
671 <style dangerouslySetInnerHTML={{ __html: symStyles }} />
179672 </Layout>
180673 );
181674});
216709 <Layout title={`Symbol search — ${ownerName}/${repoName}`} user={user}>
217710 <RepoHeader owner={ownerName} repo={repoName} />
218711 <RepoNav owner={ownerName} repo={repoName} active="code" />
219 <div class="settings-container">
220 <h2>Symbol search</h2>
712 <div class="sym-wrap">
713 <div class="sym-crumbs">
714 <a href={`/${ownerName}/${repoName}/symbols`}>← Back to symbols</a>
715 </div>
716 <header class="sym-head">
717 <div class="sym-head-text">
718 <div class="sym-eyebrow">
719 <span class="sym-eyebrow-dot" aria-hidden="true" />
720 Repository · Symbol search
721 </div>
722 <h1 class="sym-title">
723 <span class="sym-title-grad">Find a symbol by name.</span>
724 </h1>
725 <p class="sym-sub">
726 Prefix match across every indexed symbol — capped at 200
727 results.
728 </p>
729 </div>
730 </header>
731
221732 <form
222733 method="get"
223734 action={`/${ownerName}/${repoName}/symbols/search`}
224 style="display:flex;gap:8px;margin:12px 0"
735 class="sym-search"
225736 >
226 <input
227 type="text"
228 name="q"
229 value={q}
230 placeholder="Search symbol name..."
231 required
232 aria-label="Search symbol name"
233 style="flex:1"
234 />
235 <button type="submit" class="btn">
737 <div class="sym-search-input-wrap">
738 <span class="sym-search-icon" aria-hidden="true">
739 <IconSearch />
740 </span>
741 <input
742 type="text"
743 name="q"
744 value={q}
745 placeholder="Search symbol name…"
746 required
747 aria-label="Search symbol name"
748 class="sym-search-input"
749 />
750 </div>
751 <button type="submit" class="sym-btn sym-btn-primary">
236752 Search
237753 </button>
238 <a href={`/${ownerName}/${repoName}/symbols`} class="btn">
239 Back
240 </a>
241754 </form>
755
242756 {q === "" ? (
243 <p style="color:var(--text-muted)">Enter a prefix to search.</p>
757 <div class="sym-empty">
758 <div class="sym-empty-orb" aria-hidden="true" />
759 <div class="sym-empty-inner">
760 <div class="sym-empty-icon" aria-hidden="true">
761 <IconSearch />
762 </div>
763 <h3 class="sym-empty-title">Type a prefix to search</h3>
764 <p class="sym-empty-sub">
765 Start typing a symbol name — matches appear as you submit.
766 </p>
767 </div>
768 </div>
244769 ) : results.length === 0 ? (
245 <p style="color:var(--text-muted)">No symbols match "{q}".</p>
770 <div class="sym-empty">
771 <div class="sym-empty-orb" aria-hidden="true" />
772 <div class="sym-empty-inner">
773 <div class="sym-empty-icon" aria-hidden="true">
774 <IconSearch />
775 </div>
776 <h3 class="sym-empty-title">No symbols match "{q}"</h3>
777 <p class="sym-empty-sub">
778 Try a shorter prefix, or reindex if you've made changes.
779 </p>
780 </div>
781 </div>
246782 ) : (
247 <div class="panel">
783 <div class="sym-list">
248784 {results.map((s) => (
249 <div class="panel-item" style="justify-content:space-between">
250 <div>
251 <a
252 href={`/${ownerName}/${repoName}/symbols/${encodeURIComponent(s.name)}`}
253 style="font-weight:600;font-family:var(--font-mono)"
254 >
255 {s.name}
256 </a>{" "}
257 <span
258 style="font-size:11px;color:var(--text-muted);text-transform:uppercase;margin-left:6px"
259 >
260 {s.kind}
261 </span>
262 </div>
263 <a
264 href={`/${ownerName}/${repoName}/blob/HEAD/${s.path}#L${s.line}`}
265 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
266 >
267 {s.path}:{s.line}
268 </a>
269 </div>
785 <SymbolCard
786 ownerName={ownerName}
787 repoName={repoName}
788 name={s.name}
789 kind={s.kind}
790 path={s.path}
791 line={s.line}
792 />
270793 ))}
271794 </div>
272795 )}
273796 </div>
797 <style dangerouslySetInnerHTML={{ __html: symStyles }} />
274798 </Layout>
275799 );
276800});
287811 return c.notFound();
288812 }
289813
290 const defs = await findDefinitions(repo.id, decodeURIComponent(name));
814 const decodedName = decodeURIComponent(name);
815 const defs = await findDefinitions(repo.id, decodedName);
291816
292817 return c.html(
293818 <Layout title={`${name} — ${ownerName}/${repoName}`} user={user}>
294819 <RepoHeader owner={ownerName} repo={repoName} />
295820 <RepoNav owner={ownerName} repo={repoName} active="code" />
296 <div class="settings-container">
297 <h2 style="font-family:var(--font-mono)">{decodeURIComponent(name)}</h2>
298 <p style="color:var(--text-muted)">
299 {defs.length} definition{defs.length === 1 ? "" : "s"}
300 </p>
821 <div class="sym-wrap">
822 <div class="sym-crumbs">
823 <a href={`/${ownerName}/${repoName}/symbols`}>← Back to symbols</a>
824 </div>
825 <header class="sym-head">
826 <div class="sym-head-text">
827 <div class="sym-eyebrow">
828 <span class="sym-eyebrow-dot" aria-hidden="true" />
829 Repository · Symbol
830 </div>
831 <h1 class="sym-title sym-title-mono">
832 <span class="sym-title-grad">{decodedName}</span>
833 </h1>
834 <p class="sym-sub">
835 {defs.length} definition{defs.length === 1 ? "" : "s"} found
836 across the indexed code.
837 </p>
838 </div>
839 </header>
840
301841 {defs.length === 0 ? (
302 <div class="panel-empty" style="padding:24px">
303 No definitions found.{" "}
304 <a href={`/${ownerName}/${repoName}/symbols`}>Back to symbols</a>
842 <div class="sym-empty">
843 <div class="sym-empty-orb" aria-hidden="true" />
844 <div class="sym-empty-inner">
845 <div class="sym-empty-icon" aria-hidden="true">
846 <IconCode />
847 </div>
848 <h3 class="sym-empty-title">No definitions found</h3>
849 <p class="sym-empty-sub">
850 This symbol isn't currently indexed. It may have been removed,
851 or you may need to reindex.
852 </p>
853 <a
854 href={`/${ownerName}/${repoName}/symbols`}
855 class="sym-btn sym-btn-ghost"
856 >
857 Back to symbols
858 </a>
859 </div>
305860 </div>
306861 ) : (
307 <div class="panel">
862 <div class="sym-list">
308863 {defs.map((d) => (
309 <div class="panel-item" style="flex-direction:column;align-items:stretch;gap:4px">
310 <div style="display:flex;justify-content:space-between;align-items:center">
311 <span
312 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
313 >
314 {d.kind}
315 </span>
316 <a
317 href={`/${ownerName}/${repoName}/blob/HEAD/${d.path}#L${d.line}`}
318 style="font-size:12px;font-family:var(--font-mono)"
319 >
320 {d.path}:{d.line}
321 </a>
322 </div>
323 {d.signature && (
324 <pre
325 style="margin:0;padding:8px;background:var(--bg-subtle);border-radius:4px;font-size:12px;overflow-x:auto"
326 >
327 {d.signature}
328 </pre>
329 )}
330 </div>
864 <SymbolCard
865 ownerName={ownerName}
866 repoName={repoName}
867 name={decodedName}
868 kind={d.kind}
869 path={d.path}
870 line={d.line}
871 signature={d.signature}
872 />
331873 ))}
332874 </div>
333875 )}
334 <p style="margin-top:20px">
335 <a href={`/${ownerName}/${repoName}/symbols`}>← Back to symbols</a>
336 </p>
337876 </div>
877 <style dangerouslySetInnerHTML={{ __html: symStyles }} />
338878 </Layout>
339879 );
340880});
341881