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.tsxBlame221 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.
9 */
10
11import { Hono } from "hono";
12import { and, desc, eq, or, sql } from "drizzle-orm";
13import { db } from "../db";
14import { gateRuns, repositories, users } from "../db/schema";
15import { Layout } from "../views/layout";
16import { RepoHeader, RepoNav } from "../views/components";
17import { softAuth } from "../middleware/auth";
18import type { AuthEnv } from "../middleware/auth";
19
20const codeScanning = new Hono<AuthEnv>();
21codeScanning.use("*", softAuth);
22
23codeScanning.get("/:owner/:repo/security", async (c) => {
24 const { owner: ownerName, repo: repoName } = c.req.param();
25 const user = c.get("user");
26
27 const [ownerUser] = await db
28 .select()
29 .from(users)
30 .where(eq(users.username, ownerName))
31 .limit(1);
32 if (!ownerUser) return c.notFound();
33
34 const [repo] = await db
35 .select()
36 .from(repositories)
37 .where(
38 and(
39 eq(repositories.ownerId, ownerUser.id),
40 eq(repositories.name, repoName)
41 )
42 )
43 .limit(1);
44 if (!repo) return c.notFound();
45 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
46 return c.notFound();
47 }
48
49 // Pull the most recent 100 scan-related gate runs.
50 const runs = await db
51 .select()
52 .from(gateRuns)
53 .where(
54 and(
55 eq(gateRuns.repositoryId, repo.id),
56 or(
57 sql`lower(${gateRuns.gateName}) like '%scan%'`,
58 sql`lower(${gateRuns.gateName}) like '%security%'`
59 )!
60 )
61 )
62 .orderBy(desc(gateRuns.createdAt))
63 .limit(100);
64
65 // Summarize: latest status per gate, total alerts (failed + repaired).
66 const latestByName = new Map<
67 string,
68 { status: string; summary: string | null; sha: string; at: Date }
69 >();
70 for (const r of runs) {
71 if (!latestByName.has(r.gateName)) {
72 latestByName.set(r.gateName, {
73 status: r.status,
74 summary: r.summary,
75 sha: r.commitSha,
76 at: r.createdAt,
77 });
78 }
79 }
80
81 const failed = runs.filter((r) => r.status === "failed").length;
82 const repaired = runs.filter((r) => r.status === "repaired").length;
83
84 return c.html(
85 <Layout title={`Security — ${ownerName}/${repoName}`} user={user}>
86 <RepoHeader
87 owner={ownerName}
88 repo={repoName}
89 currentUser={user?.username}
90 archived={repo.isArchived}
91 isTemplate={repo.isTemplate}
92 />
93 <RepoNav owner={ownerName} repo={repoName} active="gates" />
94
95 <div style="display:flex;gap:12px;margin:20px 0">
96 <div
97 class="panel"
98 style="flex:1;padding:16px;text-align:center"
99 >
100 <div style="font-size:28px;font-weight:700">
101 {latestByName.size}
102 </div>
103 <div style="font-size:12px;color:var(--text-muted)">
104 Configured scanners
105 </div>
106 </div>
107 <div
108 class="panel"
109 style="flex:1;padding:16px;text-align:center"
110 >
111 <div
112 style={`font-size:28px;font-weight:700;color:${failed > 0 ? "var(--red)" : "var(--text)"}`}
113 >
114 {failed}
115 </div>
116 <div style="font-size:12px;color:var(--text-muted)">
117 Failed runs (last 100)
118 </div>
119 </div>
120 <div
121 class="panel"
122 style="flex:1;padding:16px;text-align:center"
123 >
124 <div style="font-size:28px;font-weight:700;color:var(--green)">
125 {repaired}
126 </div>
127 <div style="font-size:12px;color:var(--text-muted)">
128 Auto-repaired
129 </div>
130 </div>
131 </div>
132
133 <h3>Scanner status</h3>
134 <div class="panel" style="margin-bottom:20px">
135 {latestByName.size === 0 ? (
136 <div class="panel-empty">
137 No scan runs yet. Push a commit to trigger scanners.
138 </div>
139 ) : (
140 Array.from(latestByName.entries()).map(([name, info]) => (
141 <div class="panel-item" style="justify-content:space-between">
142 <div>
143 <div style="font-weight:600">{name}</div>
144 <div
145 style="font-size:12px;color:var(--text-muted);margin-top:2px"
146 >
147 {info.summary || "no summary"}
148 </div>
149 </div>
150 <div style="text-align:right">
151 <span
152 style={`font-size:11px;text-transform:uppercase;padding:2px 8px;border-radius:10px;background:${statusColor(info.status)};color:white`}
153 >
154 {info.status}
155 </span>
156 <div
157 style="font-size:11px;color:var(--text-muted);margin-top:4px"
158 >
159 <code>{info.sha.slice(0, 7)}</code> ·{" "}
160 {info.at.toLocaleDateString()}
161 </div>
162 </div>
163 </div>
164 ))
165 )}
166 </div>
167
168 <h3>Recent runs</h3>
169 <div class="panel">
170 {runs.length === 0 ? (
171 <div class="panel-empty">No runs.</div>
172 ) : (
173 runs.slice(0, 50).map((r) => (
174 <div class="panel-item" style="justify-content:space-between">
175 <div style="flex:1;min-width:0">
176 <code style="font-size:12px">{r.commitSha.slice(0, 7)}</code>{" "}
177 <span style="font-size:13px">{r.gateName}</span>
178 {r.summary && (
179 <div
180 style="font-size:12px;color:var(--text-muted);margin-top:2px"
181 >
182 {r.summary}
183 </div>
184 )}
185 </div>
186 <div style="text-align:right;white-space:nowrap">
187 <span
188 style={`font-size:11px;text-transform:uppercase;padding:2px 8px;border-radius:10px;background:${statusColor(r.status)};color:white`}
189 >
190 {r.status}
191 </span>
192 <div
193 style="font-size:11px;color:var(--text-muted);margin-top:2px"
194 >
195 {r.createdAt.toLocaleString()}
196 </div>
197 </div>
198 </div>
199 ))
200 )}
201 </div>
202 </Layout>
203 );
204});
205
206function statusColor(status: string): string {
207 switch (status) {
208 case "passed":
209 return "var(--green)";
210 case "failed":
211 return "var(--red)";
212 case "repaired":
213 return "var(--accent)";
214 case "skipped":
215 return "var(--text-muted)";
216 default:
217 return "var(--text-muted)";
218 }
219}
220
221export default codeScanning;