my own status page
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add for the badge options

+91 -25
+2 -2
src/index.ts
··· 26 26 } 27 27 28 28 if (path === "/badge") { 29 - return handleOverallBadge(env); 29 + return handleOverallBadge(env, url); 30 30 } 31 31 32 32 const badgeMatch = path.match(/^\/badge\/(.+)$/); 33 33 if (badgeMatch) { 34 - return handleBadge(env, badgeMatch[1]); 34 + return handleBadge(env, badgeMatch[1], url); 35 35 } 36 36 37 37 return new Response("Not Found", { status: 404 });
+89 -23
src/routes/badge.ts
··· 17 17 }; 18 18 19 19 // Verdana character width table at 11px (from shields.io) 20 - const WIDTHS: Record<string, number> = { 20 + const WIDTHS_11: Record<string, number> = { 21 21 " ": 3.3, "!": 4.2, '"': 5.2, "#": 7.8, $: 6.3, "%": 9.5, "&": 7.6, 22 22 "'": 2.8, "(": 4.2, ")": 4.2, "*": 6.3, "+": 7.8, ",": 3.5, "-": 4.4, 23 23 ".": 3.5, "/": 4.8, "0": 6.3, "1": 6.3, "2": 6.3, "3": 6.3, "4": 6.3, ··· 32 32 y: 5.8, z: 5.0, "|": 4.2, 33 33 }; 34 34 35 - function textWidth(s: string): number { 35 + function textWidth(s: string, scale: number = 1): number { 36 36 let w = 0; 37 - for (const c of s) w += WIDTHS[c] ?? 6.5; 37 + for (const c of s) w += (WIDTHS_11[c] ?? 6.5) * scale; 38 38 return w; 39 39 } 40 40 41 - function makeBadge(label: string, status: string, uptime: number): string { 42 - const color = COLORS[status] ?? COLORS.unknown; 43 - const statusLabel = STATUS_LABELS[status] ?? "unknown"; 44 - const value = `${statusLabel} ${uptime}%`; 41 + type BadgeStyle = "flat" | "for-the-badge"; 42 + 43 + interface BadgeOptions { 44 + label: string; 45 + status: string; 46 + uptime: number; 47 + style?: BadgeStyle; 48 + colorA?: string; 49 + colorB?: string; 50 + } 51 + 52 + function makeBadge(opts: BadgeOptions): string { 53 + const style = opts.style ?? "flat"; 54 + return style === "for-the-badge" ? makeForTheBadge(opts) : makeFlat(opts); 55 + } 56 + 57 + function makeFlat(opts: BadgeOptions): string { 58 + const color = opts.colorB ?? COLORS[opts.status] ?? COLORS.unknown; 59 + const labelColor = opts.colorA ?? "#555"; 60 + const statusLabel = STATUS_LABELS[opts.status] ?? "unknown"; 61 + const value = `${statusLabel} ${opts.uptime}%`; 45 62 46 63 const pad = 20; 47 - const labelW = Math.round(textWidth(label) + pad); 64 + const labelW = Math.round(textWidth(opts.label) + pad); 48 65 const valueW = Math.round(textWidth(value) + pad); 49 66 const total = labelW + valueW; 50 67 const labelX = labelW / 2; 51 68 const valueX = labelW + valueW / 2; 52 69 53 - return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${total}" height="20" role="img"> 54 - <title>${label}: ${value}</title> 70 + return `<svg xmlns="http://www.w3.org/2000/svg" width="${total}" height="20" role="img"> 71 + <title>${esc(opts.label)}: ${esc(value)}</title> 55 72 <linearGradient id="s" x2="0" y2="100%"> 56 73 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> 57 74 <stop offset="1" stop-opacity=".1"/> 58 75 </linearGradient> 59 76 <clipPath id="r"><rect width="${total}" height="20" rx="3" fill="#fff"/></clipPath> 60 77 <g clip-path="url(#r)"> 61 - <rect width="${labelW}" height="20" fill="#555"/> 78 + <rect width="${labelW}" height="20" fill="${labelColor}"/> 62 79 <rect x="${labelW}" width="${valueW}" height="20" fill="${color}"/> 63 80 <rect width="${total}" height="20" fill="url(#s)"/> 64 81 </g> 65 - <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"> 66 - <text aria-hidden="true" x="${labelX}" y="15" fill="#010101" fill-opacity=".3">${esc(label)}</text> 67 - <text x="${labelX}" y="14">${esc(label)}</text> 68 - <text aria-hidden="true" x="${valueX}" y="15" fill="#010101" fill-opacity=".3">${esc(value)}</text> 69 - <text x="${valueX}" y="14">${esc(value)}</text> 82 + <g text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11"> 83 + <text x="${labelX}" y="14" fill="${textColorForBg(labelColor)}">${esc(opts.label)}</text> 84 + <text x="${valueX}" y="14" fill="${textColorForBg(color)}">${esc(value)}</text> 70 85 </g> 71 86 </svg>`; 72 87 } 73 88 89 + function makeForTheBadge(opts: BadgeOptions): string { 90 + const color = opts.colorB ?? COLORS[opts.status] ?? COLORS.unknown; 91 + const labelColor = opts.colorA ?? "#555"; 92 + const statusLabel = STATUS_LABELS[opts.status] ?? "unknown"; 93 + const value = `${statusLabel} ${opts.uptime}%`.toUpperCase(); 94 + const label = opts.label.toUpperCase(); 95 + 96 + // shields.io uses 10x scale trick: font-size 100 + scale(.1) 97 + // textLength controls letter spacing, ~75 per char for label, ~75 per char for value 98 + const charW = 75; 99 + const pad = 240; // padding in 10x space (24px real) 100 + const labelTL = label.length * charW; 101 + const valueTL = value.length * charW; 102 + const labelW = (labelTL + pad) / 10; 103 + const valueW = (valueTL + pad) / 10; 104 + const total = labelW + valueW; 105 + const labelX = labelW * 5; // center in 10x space 106 + const valueX = (labelW + valueW / 2) * 10; 107 + const h = 28; 108 + 109 + return `<svg xmlns="http://www.w3.org/2000/svg" width="${total}" height="${h}" role="img" aria-label="${esc(opts.label)}: ${esc(value)}"><title>${esc(opts.label)}: ${esc(value)}</title><g shape-rendering="crispEdges"><rect width="${labelW}" height="${h}" fill="${labelColor}"/><rect x="${labelW}" width="${valueW}" height="${h}" fill="${color}"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="100"><text transform="scale(.1)" x="${labelX}" y="175" textLength="${labelTL}" fill="${textColorForBg(labelColor)}">${esc(label)}</text><text transform="scale(.1)" x="${valueX}" y="175" textLength="${valueTL}" fill="${textColorForBg(color)}" font-weight="bold">${esc(value)}</text></g></svg>`; 110 + } 111 + 112 + function textColorForBg(hex: string): string { 113 + const c = hex.replace("#", ""); 114 + const r = parseInt(c.slice(0, 2), 16); 115 + const g = parseInt(c.slice(2, 4), 16); 116 + const b = parseInt(c.slice(4, 6), 16); 117 + // W3C relative luminance 118 + const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; 119 + return lum > 0.5 ? "#333" : "#fff"; 120 + } 121 + 74 122 function esc(s: string): string { 75 123 return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 76 124 } 77 125 126 + function parseColor(s: string | null): string | undefined { 127 + if (!s) return undefined; 128 + return s.startsWith("#") ? s : `#${s}`; 129 + } 130 + 131 + function parseOpts(url: URL): Partial<BadgeOptions> { 132 + return { 133 + style: (url.searchParams.get("style") as BadgeStyle) ?? undefined, 134 + colorA: parseColor(url.searchParams.get("colorA")), 135 + colorB: parseColor(url.searchParams.get("colorB")), 136 + }; 137 + } 138 + 78 139 const BADGE_HEADERS = { 79 140 "Content-Type": "image/svg+xml", 80 141 "Cache-Control": "no-cache, no-store, must-revalidate", ··· 84 145 export async function handleBadge( 85 146 env: Env, 86 147 serviceId: string, 148 + url: URL, 87 149 ): Promise<Response> { 88 150 const ping = await getLatestPing(env.DB, serviceId); 89 151 const uptime = await getUptime7d(env.DB, serviceId); 90 152 const status = (ping?.status as string) ?? "unknown"; 153 + const opts = parseOpts(url); 91 154 92 - return new Response(makeBadge(serviceId, status, uptime), { 93 - headers: BADGE_HEADERS, 94 - }); 155 + return new Response( 156 + makeBadge({ label: serviceId, status, uptime, ...opts }), 157 + { headers: BADGE_HEADERS }, 158 + ); 95 159 } 96 160 97 - export async function handleOverallBadge(env: Env): Promise<Response> { 161 + export async function handleOverallBadge(env: Env, url: URL): Promise<Response> { 98 162 const manifest = await getManifest(env); 99 163 const monitored = manifest.filter((s) => s.health_url !== null); 100 164 ··· 115 179 monitored.length > 0 116 180 ? Math.round((totalUptime / monitored.length) * 100) / 100 117 181 : 100; 182 + const opts = parseOpts(url); 118 183 119 - return new Response(makeBadge("infra", worst, avgUptime), { 120 - headers: BADGE_HEADERS, 121 - }); 184 + return new Response( 185 + makeBadge({ label: "infra", status: worst, uptime: avgUptime, ...opts }), 186 + { headers: BADGE_HEADERS }, 187 + ); 122 188 }