my own status page
0
fork

Configure Feed

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

feat: add tailscale clients

+26 -92
-1
.env
··· 1 - TAILSCALE_API_KEY=tskey-api-kd7rfk8Po221CNTRL-pkN2FbZ24bYhbVYx3JuGaYW2cFeTUZjUA
+1
.gitignore
··· 3 3 .wrangler/ 4 4 .dev.vars 5 5 src/version.ts 6 + .env
+2 -2
src/index.ts
··· 27 27 } 28 28 29 29 if (path === "/badge") { 30 - return handleOverallBadge(env, url); 30 + return handleOverallBadge(env); 31 31 } 32 32 33 33 const badgeMatch = path.match(/^\/badge\/(.+)$/); 34 34 if (badgeMatch) { 35 - return handleBadge(env, badgeMatch[1], url); 35 + return handleBadge(env, badgeMatch[1]); 36 36 } 37 37 38 38 return new Response("Not Found", { status: 404 });
+23 -89
src/routes/badge.ts
··· 17 17 }; 18 18 19 19 // Verdana character width table at 11px (from shields.io) 20 - const WIDTHS_11: Record<string, number> = { 20 + const WIDTHS: 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, scale: number = 1): number { 35 + function textWidth(s: string): number { 36 36 let w = 0; 37 - for (const c of s) w += (WIDTHS_11[c] ?? 6.5) * scale; 37 + for (const c of s) w += WIDTHS[c] ?? 6.5; 38 38 return w; 39 39 } 40 40 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}%`; 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}%`; 62 45 63 46 const pad = 20; 64 - const labelW = Math.round(textWidth(opts.label) + pad); 47 + const labelW = Math.round(textWidth(label) + pad); 65 48 const valueW = Math.round(textWidth(value) + pad); 66 49 const total = labelW + valueW; 67 50 const labelX = labelW / 2; 68 51 const valueX = labelW + valueW / 2; 69 52 70 - return `<svg xmlns="http://www.w3.org/2000/svg" width="${total}" height="20" role="img"> 71 - <title>${esc(opts.label)}: ${esc(value)}</title> 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> 72 55 <linearGradient id="s" x2="0" y2="100%"> 73 56 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> 74 57 <stop offset="1" stop-opacity=".1"/> 75 58 </linearGradient> 76 59 <clipPath id="r"><rect width="${total}" height="20" rx="3" fill="#fff"/></clipPath> 77 60 <g clip-path="url(#r)"> 78 - <rect width="${labelW}" height="20" fill="${labelColor}"/> 61 + <rect width="${labelW}" height="20" fill="#555"/> 79 62 <rect x="${labelW}" width="${valueW}" height="20" fill="${color}"/> 80 63 <rect width="${total}" height="20" fill="url(#s)"/> 81 64 </g> 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> 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> 85 70 </g> 86 71 </svg>`; 87 72 } 88 73 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 - 122 74 function esc(s: string): string { 123 75 return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); 124 76 } 125 77 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 - 139 78 const BADGE_HEADERS = { 140 79 "Content-Type": "image/svg+xml", 141 80 "Cache-Control": "no-cache, no-store, must-revalidate", ··· 145 84 export async function handleBadge( 146 85 env: Env, 147 86 serviceId: string, 148 - url: URL, 149 87 ): Promise<Response> { 150 88 const ping = await getLatestPing(env.DB, serviceId); 151 89 const uptime = await getUptime7d(env.DB, serviceId); 152 90 const status = (ping?.status as string) ?? "unknown"; 153 - const opts = parseOpts(url); 154 91 155 - return new Response( 156 - makeBadge({ label: serviceId, status, uptime, ...opts }), 157 - { headers: BADGE_HEADERS }, 158 - ); 92 + return new Response(makeBadge(serviceId, status, uptime), { 93 + headers: BADGE_HEADERS, 94 + }); 159 95 } 160 96 161 - export async function handleOverallBadge(env: Env, url: URL): Promise<Response> { 97 + export async function handleOverallBadge(env: Env): Promise<Response> { 162 98 const manifest = await getManifest(env); 163 99 const allServices = Object.values(manifest).flatMap((m) => m.services); 164 100 const monitored = allServices.filter((s) => s.health_url !== null); ··· 180 116 monitored.length > 0 181 117 ? Math.round((totalUptime / monitored.length) * 100) / 100 182 118 : 100; 183 - const opts = parseOpts(url); 184 119 185 - return new Response( 186 - makeBadge({ label: "infra", status: worst, uptime: avgUptime, ...opts }), 187 - { headers: BADGE_HEADERS }, 188 - ); 120 + return new Response(makeBadge("infra", worst, avgUptime), { 121 + headers: BADGE_HEADERS, 122 + }); 189 123 }