spb/earth-now Public License
earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.
TypeScript 93%
Shell 2.3%
SQL 1.4%
JavaScript 1.3%
Dockerfile 1.2%
CSS 0.8%
1/**2 * earth-now.co3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: apps/api/src/badge.ts6 * Purpose: Server-side SVG badge rendering using the SAME counterValue/formatValue as every client runtime7 */89import { type CounterModel, counterValue, formatValue } from "@earth-now/counter";10import type { MetricEntry } from "@earth-now/registry";1112const FONT = "Verdana,Geneva,DejaVu Sans,sans-serif";13const ATTRIBUTION = "earth-now.co";14const HEIGHT = 28;15const PAD = 12;16const GAP = 12;1718const XML_ESCAPES: Record<string, string> = {19 "&": "&",20 "<": "<",21 ">": ">",22 '"': """,23 "'": "'",24};2526function escapeXml(text: string): string {27 return text.replace(/[&<>"']/g, (c) => XML_ESCAPES[c] ?? c);28}2930/**31 * Dark pill badge. The value is evaluated with the shared runtime at render32 * time; badges are static contexts so the sigFigs honesty cap applies strictly.33 * A metric without a deployable model renders "data pending" (never NaN).34 */35export function renderBadgeSvg(36 entry: MetricEntry,37 model: CounterModel | undefined,38 nowMs: number,39): string {40 const name = entry.name.en;41 const valueText = model42 ? `${formatValue(counterValue(model, nowMs), model.displayHints, { applySigFigs: true })} ${model.displayHints.unit}`.trim()43 : "data pending";4445 // Approximate text advance widths (no font metrics server-side by design).46 const nameW = Math.ceil(name.length * 6.4);47 const valueW = Math.ceil(valueText.length * 7.4);48 const attrW = Math.ceil(ATTRIBUTION.length * 4.8);49 const width = PAD + nameW + GAP + valueW + GAP + attrW + PAD;50 const label = `${name}: ${valueText}`;5152 return [53 `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${HEIGHT}" role="img" aria-label="${escapeXml(label)}">`,54 `<title>${escapeXml(label)} — ${ATTRIBUTION}</title>`,55 `<rect width="${width}" height="${HEIGHT}" rx="${HEIGHT / 2}" fill="#0f172a"/>`,56 `<text x="${PAD}" y="18" font-family="${FONT}" font-size="11" fill="#94a3b8">${escapeXml(name)}</text>`,57 `<text x="${PAD + nameW + GAP}" y="18" font-family="${FONT}" font-size="12" font-weight="bold" fill="${model ? "#f1f5f9" : "#fbbf24"}">${escapeXml(valueText)}</text>`,58 `<text x="${width - PAD}" y="18" text-anchor="end" font-family="${FONT}" font-size="8" fill="#64748b">${ATTRIBUTION}</text>`,59 `</svg>`,60 ].join("");61}62