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

code-scanning.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.

code-scanning.tsxBlame545 lines · 1 contributor
08420cdClaude1/**
2 * Block I5 — Code scanning UI.
3 *
4 * GET /:owner/:repo/security
5 *
6 * Aggregates gate_runs where the gate name contains "scan" (Secret scan,
7 * Security scan, Dependency scan) and presents them as a clean alerts
8 * dashboard. Data already exists — this is a surfacing layer only.
b1f2895Claude9 *
10 * 2026 polish: gradient-hairline hero + radial orb + scoped severity-pill
11 * cards (.sec-*). Mirrors the recipe used in admin-integrations and
12 * admin-diagnose. No data fetch shape, query, or action changed.
08420cdClaude13 */
14
15import { Hono } from "hono";
16import { and, desc, eq, or, sql } from "drizzle-orm";
17import { db } from "../db";
18import { gateRuns, repositories, users } from "../db/schema";
19import { Layout } from "../views/layout";
20import { RepoHeader, RepoNav } from "../views/components";
21import { softAuth } from "../middleware/auth";
22import type { AuthEnv } from "../middleware/auth";
23
24const codeScanning = new Hono<AuthEnv>();
25codeScanning.use("*", softAuth);
26
b1f2895Claude27/* ─────────────────────────────────────────────────────────────────────────
28 * Scoped CSS — every class prefixed `.sec-` so this surface can't bleed
29 * into the wider repo polish. Mirrors the gradient-hairline hero + radial
30 * orb + per-card pattern from `admin-integrations` and `admin-diagnose`.
31 * ───────────────────────────────────────────────────────────────────── */
32const styles = `
eed4684Claude33 .sec-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4); }
b1f2895Claude34
35 .sec-hero {
36 position: relative;
37 margin-bottom: var(--space-5);
38 padding: var(--space-5) var(--space-6);
39 background: var(--bg-elevated);
40 border: 1px solid var(--border);
41 border-radius: 16px;
42 overflow: hidden;
43 }
44 .sec-hero::before {
45 content: '';
46 position: absolute;
47 top: 0; left: 0; right: 0;
48 height: 2px;
49 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
50 opacity: 0.75;
51 pointer-events: none;
52 }
53 .sec-hero-orb {
54 position: absolute;
55 inset: -30% -15% auto auto;
56 width: 460px; height: 460px;
57 background: radial-gradient(circle, rgba(140,109,255,0.22), rgba(54,197,214,0.10) 45%, transparent 70%);
58 filter: blur(80px);
59 opacity: 0.75;
60 pointer-events: none;
61 z-index: 0;
62 }
63 .sec-hero-inner { position: relative; z-index: 1; max-width: 760px; }
64 .sec-eyebrow {
65 display: inline-flex;
66 align-items: center;
67 gap: 8px;
68 text-transform: uppercase;
69 font-family: var(--font-mono);
70 font-size: 11px;
71 letter-spacing: 0.18em;
72 color: var(--text-muted);
73 font-weight: 600;
74 margin-bottom: 14px;
75 }
76 .sec-eyebrow-dot {
77 width: 8px; height: 8px;
78 border-radius: 9999px;
79 background: linear-gradient(135deg, #8c6dff, #36c5d6);
80 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
81 }
82 .sec-title {
83 font-family: var(--font-display);
84 font-size: clamp(28px, 4vw, 40px);
85 font-weight: 800;
86 letter-spacing: -0.028em;
87 line-height: 1.05;
88 margin: 0 0 var(--space-2);
89 color: var(--text-strong);
90 }
91 .sec-title-grad {
92 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
93 -webkit-background-clip: text;
94 background-clip: text;
95 -webkit-text-fill-color: transparent;
96 color: transparent;
97 }
98 .sec-sub {
99 font-size: 15px;
100 color: var(--text-muted);
101 margin: 0;
102 line-height: 1.55;
103 max-width: 640px;
104 }
105
106 /* Healthy banner — green gradient checkmark */
107 .sec-healthy {
108 display: flex;
109 align-items: center;
110 gap: 14px;
111 margin-bottom: var(--space-4);
112 padding: 14px 18px;
113 border-radius: 14px;
114 background: linear-gradient(135deg, rgba(52,211,153,0.10), rgba(54,197,214,0.06));
115 border: 1px solid rgba(52,211,153,0.32);
116 color: #bbf7d0;
117 }
118 .sec-healthy-icon {
119 flex: 0 0 auto;
120 width: 36px; height: 36px;
121 border-radius: 9999px;
122 display: inline-flex;
123 align-items: center;
124 justify-content: center;
125 background: linear-gradient(135deg, #34d399 0%, #36c5d6 100%);
126 color: #04231a;
127 box-shadow: 0 0 0 4px rgba(52,211,153,0.16);
128 }
129 .sec-healthy-text { font-size: 14px; line-height: 1.45; }
130 .sec-healthy-text strong { display: block; color: #d1fae5; font-weight: 700; font-size: 14.5px; margin-bottom: 2px; }
131 .sec-healthy-text span { color: rgba(187,247,208,0.85); font-size: 12.5px; }
132
133 /* Stat grid */
134 .sec-stats {
135 display: grid;
136 grid-template-columns: repeat(3, 1fr);
137 gap: var(--space-3);
138 margin-bottom: var(--space-5);
139 }
140 @media (max-width: 640px) {
141 .sec-stats { grid-template-columns: 1fr; }
142 }
143 .sec-stat {
144 position: relative;
145 background: var(--bg-elevated);
146 border: 1px solid var(--border);
147 border-radius: 14px;
148 padding: var(--space-4);
149 transition: border-color 120ms ease, transform 120ms ease;
150 }
151 .sec-stat:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
152 .sec-stat.is-red { border-color: rgba(248,113,113,0.34); }
153 .sec-stat.is-green { border-color: rgba(52,211,153,0.22); }
154 .sec-stat-label {
155 font-size: 10.5px;
156 letter-spacing: 0.14em;
157 text-transform: uppercase;
158 color: var(--text-muted);
159 font-weight: 700;
160 margin-bottom: 6px;
161 }
162 .sec-stat-value {
163 font-family: var(--font-display);
164 font-size: 32px;
165 font-weight: 800;
166 letter-spacing: -0.022em;
167 color: var(--text-strong);
168 font-variant-numeric: tabular-nums;
169 line-height: 1;
170 }
171 .sec-stat.is-red .sec-stat-value { color: #fca5a5; }
172 .sec-stat.is-green .sec-stat-value { color: #6ee7b7; }
173 .sec-stat-hint {
174 margin-top: 6px;
175 font-size: 12px;
176 color: var(--text-muted);
177 }
178
179 /* Section heading */
180 .sec-section-head {
181 margin: 0 0 var(--space-3);
182 display: flex;
183 align-items: baseline;
184 justify-content: space-between;
185 gap: var(--space-3);
186 flex-wrap: wrap;
187 }
188 .sec-section-title {
189 margin: 0;
190 font-family: var(--font-display);
191 font-size: 18px;
192 font-weight: 700;
193 letter-spacing: -0.018em;
194 color: var(--text-strong);
195 }
196 .sec-section-sub {
197 font-size: 12.5px;
198 color: var(--text-muted);
199 }
200
201 /* Card list */
202 .sec-list {
203 display: flex;
204 flex-direction: column;
205 gap: var(--space-2);
206 margin-bottom: var(--space-5);
207 }
208 .sec-card {
209 position: relative;
210 background: var(--bg-elevated);
211 border: 1px solid var(--border);
212 border-radius: 12px;
213 padding: var(--space-3) var(--space-4);
214 transition: border-color 120ms ease, transform 120ms ease;
215 }
216 .sec-card:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
217 .sec-card.is-passed { border-color: rgba(52,211,153,0.22); }
218 .sec-card.is-failed { border-color: rgba(248,113,113,0.34); }
219 .sec-card.is-repaired { border-color: rgba(140,109,255,0.30); }
220 .sec-card-row {
221 display: flex;
222 justify-content: space-between;
223 align-items: flex-start;
224 gap: 16px;
225 flex-wrap: wrap;
226 }
227 .sec-card-main { min-width: 0; flex: 1; }
228 .sec-card-title {
229 font-family: var(--font-display);
230 font-size: 14.5px;
231 font-weight: 700;
232 color: var(--text-strong);
233 letter-spacing: -0.005em;
234 line-height: 1.3;
235 margin: 0 0 4px;
236 word-break: break-word;
237 }
238 .sec-card-summary {
239 font-size: 12.5px;
240 color: var(--text-muted);
241 line-height: 1.45;
242 margin: 0;
243 word-break: break-word;
244 }
245 .sec-card-meta {
246 margin-top: 6px;
247 display: flex;
248 gap: 10px;
249 align-items: center;
250 flex-wrap: wrap;
251 font-size: 11.5px;
252 color: var(--text-muted);
253 }
254 .sec-card-meta code {
255 font-family: var(--font-mono);
256 font-size: 11.5px;
257 background: rgba(255,255,255,0.04);
258 border: 1px solid var(--border);
259 padding: 1px 6px;
260 border-radius: 5px;
261 color: var(--text);
262 }
263 .sec-card-right {
264 flex: 0 0 auto;
265 display: flex;
266 flex-direction: column;
267 align-items: flex-end;
268 gap: 6px;
269 }
270
271 /* Severity / status pill */
272 .sec-pill {
273 display: inline-flex;
274 align-items: center;
275 gap: 6px;
276 padding: 3px 10px;
277 border-radius: 9999px;
278 font-size: 10.5px;
279 font-weight: 700;
280 letter-spacing: 0.06em;
281 text-transform: uppercase;
282 line-height: 1.4;
283 }
284 .sec-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
285 .sec-pill.is-passed { background: rgba(52,211,153,0.14); color: #6ee7b7; box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32); }
286 .sec-pill.is-failed { background: rgba(248,113,113,0.12); color: #fca5a5; box-shadow: inset 0 0 0 1px rgba(248,113,113,0.34); }
287 .sec-pill.is-repaired { background: rgba(140,109,255,0.14); color: #c4b5fd; box-shadow: inset 0 0 0 1px rgba(140,109,255,0.34); }
288 .sec-pill.is-skipped { background: rgba(148,163,184,0.10); color: #cbd5e1; box-shadow: inset 0 0 0 1px rgba(148,163,184,0.28); }
289 .sec-pill.is-pending,
290 .sec-pill.is-running { background: rgba(251,191,36,0.10); color: #fde68a; box-shadow: inset 0 0 0 1px rgba(251,191,36,0.30); }
291
292 /* Empty state — dashed orb card */
293 .sec-empty {
294 position: relative;
295 overflow: hidden;
296 text-align: center;
297 padding: var(--space-6) var(--space-4);
298 border: 1px dashed var(--border-strong, var(--border));
299 border-radius: 16px;
300 background: rgba(255,255,255,0.012);
301 color: var(--text-muted);
302 }
303 .sec-empty::before {
304 content: '';
305 position: absolute;
306 inset: -40% -20% auto auto;
307 width: 320px; height: 320px;
308 background: radial-gradient(circle, rgba(140,109,255,0.14), rgba(54,197,214,0.06) 45%, transparent 70%);
309 filter: blur(60px);
310 pointer-events: none;
311 }
312 .sec-empty-inner { position: relative; z-index: 1; }
313 .sec-empty strong {
314 display: block;
315 font-family: var(--font-display);
316 font-size: 16px;
317 font-weight: 700;
318 color: var(--text-strong);
319 margin-bottom: 4px;
320 }
321 .sec-empty span { font-size: 13px; }
322`;
323
08420cdClaude324codeScanning.get("/:owner/:repo/security", async (c) => {
325 const { owner: ownerName, repo: repoName } = c.req.param();
326 const user = c.get("user");
327
328 const [ownerUser] = await db
329 .select()
330 .from(users)
331 .where(eq(users.username, ownerName))
332 .limit(1);
333 if (!ownerUser) return c.notFound();
334
335 const [repo] = await db
336 .select()
337 .from(repositories)
338 .where(
339 and(
340 eq(repositories.ownerId, ownerUser.id),
341 eq(repositories.name, repoName)
342 )
343 )
344 .limit(1);
345 if (!repo) return c.notFound();
346 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
347 return c.notFound();
348 }
349
350 // Pull the most recent 100 scan-related gate runs.
351 const runs = await db
352 .select()
353 .from(gateRuns)
354 .where(
355 and(
356 eq(gateRuns.repositoryId, repo.id),
357 or(
358 sql`lower(${gateRuns.gateName}) like '%scan%'`,
359 sql`lower(${gateRuns.gateName}) like '%security%'`
360 )!
361 )
362 )
363 .orderBy(desc(gateRuns.createdAt))
364 .limit(100);
365
366 // Summarize: latest status per gate, total alerts (failed + repaired).
367 const latestByName = new Map<
368 string,
369 { status: string; summary: string | null; sha: string; at: Date }
370 >();
371 for (const r of runs) {
372 if (!latestByName.has(r.gateName)) {
373 latestByName.set(r.gateName, {
374 status: r.status,
375 summary: r.summary,
376 sha: r.commitSha,
377 at: r.createdAt,
378 });
379 }
380 }
381
382 const failed = runs.filter((r) => r.status === "failed").length;
383 const repaired = runs.filter((r) => r.status === "repaired").length;
b1f2895Claude384 const allHealthy =
385 latestByName.size > 0 &&
386 failed === 0 &&
387 Array.from(latestByName.values()).every(
388 (info) => info.status === "passed" || info.status === "repaired"
389 );
08420cdClaude390
391 return c.html(
392 <Layout title={`Security — ${ownerName}/${repoName}`} user={user}>
393 <RepoHeader
394 owner={ownerName}
395 repo={repoName}
396 currentUser={user?.username}
397 archived={repo.isArchived}
398 isTemplate={repo.isTemplate}
399 />
400 <RepoNav owner={ownerName} repo={repoName} active="gates" />
401
b1f2895Claude402 <div class="sec-wrap">
403 <section class="sec-hero">
404 <div class="sec-hero-orb" aria-hidden="true" />
405 <div class="sec-hero-inner">
406 <div class="sec-eyebrow">
407 <span class="sec-eyebrow-dot" aria-hidden="true" />
408 Code scanning · {ownerName}/{repoName}
409 </div>
410 <h2 class="sec-title">
411 <span class="sec-title-grad">Security overview.</span>
412 </h2>
413 <p class="sec-sub">
414 Latest results from every configured scanner — secret detection,
415 dependency audits, and security gates. {latestByName.size}{" "}
416 scanner{latestByName.size === 1 ? "" : "s"} watching this repo.
417 </p>
c6018a5Claude418 <p class="sec-sub" style="margin-top: 8px; font-size: 13px;">
419 <strong>AI patch generator:</strong> when any scanner flags a
420 finding, the autopilot's patch generator can propose a fix
421 PR automatically. See its live status on{" "}
422 <a href="/admin/diagnose">/admin/diagnose</a> (site admins
423 only) or watch for PRs authored by <code>gluecron[bot]</code>.
424 </p>
08420cdClaude425 </div>
b1f2895Claude426 </section>
427
428 {allHealthy && (
429 <div class="sec-healthy" role="status">
430 <span class="sec-healthy-icon" aria-hidden="true">
431 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
432 <polyline points="20 6 9 17 4 12" />
433 </svg>
434 </span>
435 <div class="sec-healthy-text">
436 <strong>Healthy</strong>
437 <span>
438 Every scanner is passing on the latest commit. No action needed.
439 </span>
440 </div>
08420cdClaude441 </div>
b1f2895Claude442 )}
443
444 <div class="sec-stats">
445 <div class="sec-stat">
446 <div class="sec-stat-label">Scanners</div>
447 <div class="sec-stat-value">{latestByName.size}</div>
448 <div class="sec-stat-hint">Distinct gates configured</div>
08420cdClaude449 </div>
b1f2895Claude450 <div class={"sec-stat " + (failed > 0 ? "is-red" : "")}>
451 <div class="sec-stat-label">Failed (last 100)</div>
452 <div class="sec-stat-value">{failed}</div>
453 <div class="sec-stat-hint">Runs that blocked a push</div>
08420cdClaude454 </div>
b1f2895Claude455 <div class={"sec-stat " + (repaired > 0 ? "is-green" : "")}>
456 <div class="sec-stat-label">Auto-repaired</div>
457 <div class="sec-stat-value">{repaired}</div>
458 <div class="sec-stat-hint">Fixed automatically</div>
08420cdClaude459 </div>
460 </div>
461
b1f2895Claude462 <div class="sec-section-head">
463 <h3 class="sec-section-title">Scanner status</h3>
464 <span class="sec-section-sub">Most recent run per scanner</span>
465 </div>
08420cdClaude466 {latestByName.size === 0 ? (
b1f2895Claude467 <div class="sec-empty">
468 <div class="sec-empty-inner">
469 <strong>No scan runs yet</strong>
470 <span>Push a commit to trigger the configured scanners.</span>
471 </div>
08420cdClaude472 </div>
473 ) : (
b1f2895Claude474 <div class="sec-list">
475 {Array.from(latestByName.entries()).map(([name, info]) => (
476 <div class={"sec-card is-" + info.status}>
477 <div class="sec-card-row">
478 <div class="sec-card-main">
479 <h4 class="sec-card-title">{name}</h4>
480 <p class="sec-card-summary">
481 {info.summary || "No summary recorded."}
482 </p>
483 <div class="sec-card-meta">
484 <code>{info.sha.slice(0, 7)}</code>
485 <span>·</span>
486 <span>{info.at.toLocaleDateString()}</span>
487 </div>
488 </div>
489 <div class="sec-card-right">
490 <span class={"sec-pill is-" + info.status}>
491 <span class="dot" aria-hidden="true" />
492 {info.status}
493 </span>
494 </div>
08420cdClaude495 </div>
496 </div>
b1f2895Claude497 ))}
498 </div>
08420cdClaude499 )}
500
b1f2895Claude501 <div class="sec-section-head">
502 <h3 class="sec-section-title">Recent runs</h3>
503 <span class="sec-section-sub">Most recent {Math.min(runs.length, 50)} of {runs.length}</span>
504 </div>
08420cdClaude505 {runs.length === 0 ? (
b1f2895Claude506 <div class="sec-empty">
507 <div class="sec-empty-inner">
508 <strong>No runs yet</strong>
509 <span>Push code to see scanner history here.</span>
510 </div>
511 </div>
08420cdClaude512 ) : (
b1f2895Claude513 <div class="sec-list">
514 {runs.slice(0, 50).map((r) => (
515 <div class={"sec-card is-" + r.status}>
516 <div class="sec-card-row">
517 <div class="sec-card-main">
518 <h4 class="sec-card-title">{r.gateName}</h4>
519 {r.summary && (
520 <p class="sec-card-summary">{r.summary}</p>
521 )}
522 <div class="sec-card-meta">
523 <code>{r.commitSha.slice(0, 7)}</code>
524 <span>·</span>
525 <span>{r.createdAt.toLocaleString()}</span>
526 </div>
527 </div>
528 <div class="sec-card-right">
529 <span class={"sec-pill is-" + r.status}>
530 <span class="dot" aria-hidden="true" />
531 {r.status}
532 </span>
08420cdClaude533 </div>
534 </div>
535 </div>
b1f2895Claude536 ))}
537 </div>
08420cdClaude538 )}
539 </div>
b1f2895Claude540 <style dangerouslySetInnerHTML={{ __html: styles }} />
08420cdClaude541 </Layout>
542 );
543});
544
545export default codeScanning;