Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

badge.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

badge.tsBlame90 lines · 1 contributor
a7361c0Claude1/**
2 * Health badge endpoint — public, no auth required.
3 *
4 * GET /badge/:owner/:repo
5 * Returns a shields.io-style SVG badge showing the repo's health grade.
6 * If no score has been computed yet, triggers a background computation
7 * and returns a grey "computing" badge.
8 *
9 * Cache-Control headers allow CDNs to cache the badge for up to 1 hour.
10 * The badge is suitable for embedding in GitHub READMEs and other
11 * Markdown-rendered surfaces.
12 */
13
14import { Hono } from "hono";
15import {
16 getLatestHealthScore,
17 computeAndStoreHealthScore,
18 getBadgeColor,
19} from "../lib/health-score";
20import { loadRepoByPath } from "../lib/namespace";
21
22const badge = new Hono();
23
24/**
25 * Generate a shields.io-style SVG badge.
26 *
27 * Left panel: "health" label on dark grey (#555), 62px wide.
28 * Right panel: grade letter (A–F or "?") coloured by getBadgeColor, 28px wide.
29 * Total: 90×20px, rounded corners rx=3.
30 * Font: DejaVu Sans 11px with 1px drop shadow.
31 */
32function makeBadgeSvg(grade: string, color: string): string {
33 return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20">
34 <linearGradient id="s" x2="0" y2="100%">
35 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
36 <stop offset="1" stop-opacity=".1"/>
37 </linearGradient>
38 <clipPath id="r"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath>
39 <g clip-path="url(#r)">
40 <rect width="62" height="20" fill="#555"/>
41 <rect x="62" width="28" height="20" fill="${color}"/>
42 <rect width="90" height="20" fill="url(#s)"/>
43 </g>
44 <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
45 <text x="32" y="15" fill="#010101" fill-opacity=".3" lengthAdjust="spacing">health</text>
46 <text x="32" y="14" lengthAdjust="spacing">health</text>
47 <text x="76" y="15" fill="#010101" fill-opacity=".3" font-weight="bold">${grade}</text>
48 <text x="76" y="14" font-weight="bold">${grade}</text>
49 </g>
50</svg>`;
51}
52
53badge.get("/badge/:owner/:repo", async (c) => {
54 const { owner, repo } = c.req.param();
55
56 // Set SVG content type and cache headers up front — we always return SVG.
57 c.header("Content-Type", "image/svg+xml");
58 c.header("Cache-Control", "max-age=3600, s-maxage=3600");
59 c.header("X-Content-Type-Options", "nosniff");
60
61 // Load the repo — for public repos no auth is required.
62 const repoRow = await loadRepoByPath(owner, repo);
63
64 if (!repoRow) {
65 // Unknown repo — return a grey "unknown" badge rather than 404 so the
66 // image tag doesn't break in READMEs.
67 return c.body(makeBadgeSvg("?", "#9f9f9f"), 200);
68 }
69
70 // Private repos don't expose their health score publicly.
71 if (repoRow.isPrivate) {
72 return c.body(makeBadgeSvg("?", "#9f9f9f"), 200);
73 }
74
75 // Try to load an existing score.
76 const scoreRow = await getLatestHealthScore(repoRow.id);
77
78 if (!scoreRow) {
79 // No score yet — fire a background computation and show a computing badge.
80 computeAndStoreHealthScore(repoRow.id, owner, repo).catch((err) => {
81 console.error("[badge] background compute failed:", err);
82 });
83 return c.body(makeBadgeSvg("?", "#9f9f9f"), 200);
84 }
85
86 const color = getBadgeColor(scoreRow.grade);
87 return c.body(makeBadgeSvg(scoreRow.grade, color), 200);
88});
89
90export default badge;