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.
| 2c34075 | 1 | /** |
| 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 | ||
| 11 | import { Hono } from "hono"; | |
| 12 | import { Layout } from "../views/layout"; | |
| 13 | import { RepoHeader, RepoNav } from "../views/components"; | |
| 14 | import { | |
| 15 | computeHealthScore, | |
| 16 | detectCIConfig, | |
| 17 | type RepoHealthReport, | |
| 18 | type SecurityIssue, | |
| 19 | } from "../lib/intelligence"; | |
| 20 | import { repoExists, getDefaultBranch } from "../git/repository"; | |
| 21 | import { softAuth } from "../middleware/auth"; | |
| 22 | import type { AuthEnv } from "../middleware/auth"; | |
| 23 | ||
| 24 | const health = new Hono<AuthEnv>(); | |
| 25 | ||
| 26 | health.use("*", softAuth); | |
| 27 | ||
| 28 | health.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 | ||
| 189 | const 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 | ||
| 229 | const 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 | ||
| 266 | const 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 | ||
| 288 | export default health; |