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

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.

health.tsxBlame288 lines · 1 contributor
2c34075Claude1/**
2 * Repository Health Dashboard — the page GitHub doesn't have.
3 *
4 * Shows a live health score, security scan results, test coverage estimate,
5 * dependency freshness, complexity analysis, and actionable insights.
6 *
7 * This runs EVERY TIME someone views the repo. No config needed.
8 * No yaml. No CI setup. Just push code and gluecron tells you what's wrong.
9 */
10
11import { Hono } from "hono";
12import { Layout } from "../views/layout";
13import { RepoHeader, RepoNav } from "../views/components";
14import {
15 computeHealthScore,
16 detectCIConfig,
17 type RepoHealthReport,
18 type SecurityIssue,
19} from "../lib/intelligence";
20import { repoExists, getDefaultBranch } from "../git/repository";
21import { softAuth } from "../middleware/auth";
22import type { AuthEnv } from "../middleware/auth";
23
24const health = new Hono<AuthEnv>();
25
26health.use("*", softAuth);
27
28health.get("/:owner/:repo/health", async (c) => {
29 const { owner, repo } = c.req.param();
30 const user = c.get("user");
31
32 if (!(await repoExists(owner, repo))) return c.notFound();
33
34 const ref = (await getDefaultBranch(owner, repo)) || "main";
35
36 // Run analysis in parallel
37 const [report, ciConfig] = await Promise.all([
38 computeHealthScore(owner, repo),
39 detectCIConfig(owner, repo, ref),
40 ]);
41
42 const gradeColor =
43 report.grade === "A+" || report.grade === "A"
44 ? "var(--green)"
45 : report.grade === "B"
46 ? "#58a6ff"
47 : report.grade === "C"
48 ? "var(--yellow)"
49 : "var(--red)";
50
51 return c.html(
52 <Layout title={`Health — ${owner}/${repo}`} user={user}>
53 <RepoHeader owner={owner} repo={repo} />
54 <HealthNav owner={owner} repo={repo} active="health" />
55
56 <div style="display: flex; gap: 24px; flex-wrap: wrap; margin-bottom: 32px">
57 <div
58 style={`text-align: center; padding: 24px 40px; background: var(--bg-secondary); border: 2px solid ${gradeColor}; border-radius: var(--radius);`}
59 >
60 <div style={`font-size: 48px; font-weight: 800; color: ${gradeColor}`}>
61 {report.grade}
62 </div>
63 <div style="font-size: 32px; font-weight: 600; color: var(--text)">
64 {report.score}/100
65 </div>
66 <div style="font-size: 13px; color: var(--text-muted); margin-top: 4px">
67 Health Score
68 </div>
69 </div>
70
71 <div style="flex: 1; min-width: 300px">
72 <h3 style="margin-bottom: 12px">Insights</h3>
73 {report.insights.map((insight) => (
74 <div style="padding: 8px 0; font-size: 14px; border-bottom: 1px solid var(--border); display: flex; gap: 8px; align-items: start">
75 <span style="color: var(--text-link); flex-shrink: 0">*</span>
76 <span>{insight}</span>
77 </div>
78 ))}
79 </div>
80 </div>
81
82 <div class="card-grid" style="grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))">
83 <ScoreCard
84 title="Security"
85 score={report.breakdown.security.score}
86 details={[
87 `${report.breakdown.security.issues.length} issue${report.breakdown.security.issues.length !== 1 ? "s" : ""} found`,
88 `${report.breakdown.security.issues.filter((i) => i.severity === "critical").length} critical`,
89 `${report.breakdown.security.issues.filter((i) => i.severity === "high").length} high`,
90 ]}
91 />
92 <ScoreCard
93 title="Testing"
94 score={report.breakdown.testing.score}
95 details={[
96 report.breakdown.testing.hasTests ? `${report.breakdown.testing.testFileCount} test files` : "No tests found",
97 `Coverage estimate: ${report.breakdown.testing.estimatedCoverage}`,
98 ]}
99 />
100 <ScoreCard
101 title="Complexity"
102 score={report.breakdown.complexity.score}
103 details={[
104 `${report.breakdown.complexity.totalFiles} source files`,
105 `Avg file size: ${report.breakdown.complexity.avgFileSize} bytes`,
106 ]}
107 />
108 <ScoreCard
109 title="Dependencies"
110 score={report.breakdown.dependencies.score}
111 details={[
112 `${report.breakdown.dependencies.total} dependencies`,
113 report.breakdown.dependencies.lockfileExists ? "Lockfile present" : "No lockfile",
114 ]}
115 />
116 <ScoreCard
117 title="Documentation"
118 score={report.breakdown.documentation.score}
119 details={[
120 report.breakdown.documentation.hasReadme ? "README found" : "No README",
121 report.breakdown.documentation.hasLicense ? "License present" : "No license",
122 `${report.breakdown.documentation.docFileCount} doc files`,
123 ]}
124 />
125 <ScoreCard
126 title="Activity"
127 score={report.breakdown.activity.score}
128 details={[
129 `${report.breakdown.activity.recentCommits} commits (30d)`,
130 `${report.breakdown.activity.uniqueContributors} contributors`,
131 `Last push: ${report.breakdown.activity.lastPushDaysAgo}d ago`,
132 ]}
133 />
134 </div>
135
136 {report.breakdown.security.issues.length > 0 && (
137 <div style="margin-top: 32px">
138 <h3 style="margin-bottom: 12px">Security Issues</h3>
139 <div class="issue-list">
140 {report.breakdown.security.issues.map((issue) => (
141 <div class="issue-item">
142 <div style="display: flex; gap: 8px; align-items: center">
143 <SeverityBadge severity={issue.severity} />
144 <div>
145 <div style="font-size: 14px; font-weight: 500">
146 {issue.message}
147 </div>
148 <div style="font-size: 12px; color: var(--text-muted); font-family: var(--font-mono)">
149 {issue.file}
150 {issue.line ? `:${issue.line}` : ""} — {issue.rule}
151 </div>
152 </div>
153 </div>
154 </div>
155 ))}
156 </div>
157 </div>
158 )}
159
160 {ciConfig.commands.length > 0 && (
161 <div style="margin-top: 32px">
162 <h3 style="margin-bottom: 12px">
163 Zero-Config CI
164 <span style="font-size: 13px; color: var(--text-muted); font-weight: 400; margin-left: 8px">
165 Auto-detected
166 </span>
167 </h3>
168 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px">
169 <div style="margin-bottom: 12px; font-size: 13px; color: var(--text-muted)">
170 {ciConfig.detected.join(" | ")}
171 </div>
172 {ciConfig.commands.map((cmd) => (
173 <div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border); align-items: center">
174 <span style="font-size: 14px; font-weight: 500">
175 {cmd.name}
176 </span>
177 <code style="font-size: 12px; background: var(--bg-tertiary); padding: 4px 8px; border-radius: 3px">
178 {cmd.command}
179 </code>
180 </div>
181 ))}
182 </div>
183 </div>
184 )}
185 </Layout>
186 );
187});
188
189const HealthNav = ({
190 owner,
191 repo,
192 active,
193}: {
194 owner: string;
195 repo: string;
196 active: string;
197}) => (
198 <div class="repo-nav">
199 <a href={`/${owner}/${repo}`} class={active === "code" ? "active" : ""}>
200 Code
201 </a>
202 <a
203 href={`/${owner}/${repo}/issues`}
204 class={active === "issues" ? "active" : ""}
205 >
206 Issues
207 </a>
208 <a
209 href={`/${owner}/${repo}/pulls`}
210 class={active === "pulls" ? "active" : ""}
211 >
212 Pull Requests
213 </a>
214 <a
215 href={`/${owner}/${repo}/health`}
216 class={active === "health" ? "active" : ""}
217 >
218 Health
219 </a>
220 <a
221 href={`/${owner}/${repo}/commits`}
222 class={active === "commits" ? "active" : ""}
223 >
224 Commits
225 </a>
226 </div>
227);
228
229const ScoreCard = ({
230 title,
231 score,
232 details,
233}: {
234 title: string;
235 score: number;
236 details: string[];
237}) => {
238 const color =
239 score >= 80 ? "var(--green)" : score >= 50 ? "var(--yellow)" : "var(--red)";
240 return (
241 <div class="card">
242 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px">
243 <h3 style="font-size: 15px">{title}</h3>
244 <span
245 style={`font-size: 18px; font-weight: 700; color: ${color}`}
246 >
247 {score}
248 </span>
249 </div>
250 <div
251 style="height: 4px; background: var(--bg-tertiary); border-radius: 2px; margin-bottom: 8px; overflow: hidden"
252 >
253 <div
254 style={`height: 100%; width: ${score}%; background: ${color}; border-radius: 2px; transition: width 0.3s;`}
255 />
256 </div>
257 {details.map((d) => (
258 <div style="font-size: 12px; color: var(--text-muted); margin-top: 2px">
259 {d}
260 </div>
261 ))}
262 </div>
263 );
264};
265
266const SeverityBadge = ({
267 severity,
268}: {
269 severity: SecurityIssue["severity"];
270}) => {
271 const colors: Record<string, string> = {
272 critical: "var(--red)",
273 high: "#ff7b72",
274 medium: "var(--yellow)",
275 low: "var(--text-muted)",
276 info: "var(--text-link)",
277 };
278 return (
279 <span
280 class="badge"
281 style={`color: ${colors[severity]}; border-color: ${colors[severity]}; font-size: 11px; text-transform: uppercase`}
282 >
283 {severity}
284 </span>
285 );
286};
287
288export default health;