repos
/ status-rust master

status-rust

mirror archived upstream

Single-binary self-hosted uptime monitoring and status pages on Rust axum: HTTP probes, Lighthouse audits, SEO crawler, and PDF reports.

axumdockerrustself-hostedsqlitestatus-pageuptime-monitoringvite

6.4 KB · 203 lines · JavaScript Raw History
  1import Chart from "chart.js/auto";
  2
  3const accent = {
  4  green: "#6b9e78",
  5  greenBright: "#7db88c",
  6  greenFill: "rgba(107, 158, 120, 0.35)",
  7  amber: "#c9a84c",
  8  amberFill: "rgba(201, 168, 76, 0.35)",
  9  terracotta: "#c47055",
 10  terracottaFill: "rgba(196, 112, 85, 0.35)",
 11  slate: "#7eaab8",
 12  slateFill: "rgba(126, 170, 184, 0.3)",
 13  grid: "rgba(107, 158, 120, 0.08)",
 14  ticks: "#847c72",
 15};
 16
 17const backgroundColors = [
 18  accent.greenFill,
 19  accent.terracottaFill,
 20  accent.amberFill,
 21  accent.slateFill,
 22  "rgba(221, 215, 205, 0.18)",
 23  "rgba(160, 152, 144, 0.3)",
 24  "rgba(107, 158, 120, 0.55)",
 25  "rgba(196, 112, 85, 0.55)",
 26  "rgba(201, 168, 76, 0.55)",
 27  "rgba(126, 170, 184, 0.55)",
 28];
 29
 30const borderColors = [
 31  "rgba(107, 158, 120, 0.9)",
 32  "rgba(196, 112, 85, 0.9)",
 33  "rgba(201, 168, 76, 0.9)",
 34  "rgba(126, 170, 184, 0.9)",
 35  "rgba(221, 215, 205, 0.5)",
 36  "rgba(160, 152, 144, 0.7)",
 37  "rgba(107, 158, 120, 1)",
 38  "rgba(196, 112, 85, 1)",
 39  "rgba(201, 168, 76, 1)",
 40  "rgba(126, 170, 184, 1)",
 41];
 42
 43const fontStack = '"Monaspace Argon", Consolas, "Liberation Mono", Monaco, "Courier New", monospace';
 44
 45Chart.defaults.color = accent.ticks;
 46Chart.defaults.borderColor = accent.grid;
 47Chart.defaults.font.family = fontStack;
 48Chart.defaults.font.size = 11;
 49
 50const tickFont = { size: 11, family: fontStack };
 51const legendLabel = { boxWidth: 10, boxHeight: 10, font: tickFont, color: "#d1d8d4" };
 52
 53document.addEventListener("DOMContentLoaded", function () {
 54  const canvas = document.getElementById("chart-response-times");
 55  if (!canvas) return;
 56  const data = JSON.parse(
 57    document.getElementById("chart-status-response-times-data").innerHTML
 58  );
 59  const ctx = canvas.getContext("2d");
 60
 61  // Total stays the loud green line so old-eye muscle memory still works.
 62  // The four phase lines are thinner and more muted so they read as
 63  // breakdown, not as five equal-weight series.
 64  const series = [
 65    { key: "total", label: "Total",   color: accent.green,      width: 2,   tension: 0.25 },
 66    { key: "dns",   label: "DNS",     color: accent.terracotta, width: 1.5, tension: 0.2  },
 67    { key: "tcp",   label: "TCP",     color: accent.amber,      width: 1.5, tension: 0.2  },
 68    { key: "tls",   label: "TLS",     color: accent.slate,      width: 1.5, tension: 0.2  },
 69    { key: "ttfb",  label: "TTFB",    color: "#a09890",         width: 1.5, tension: 0.2  },
 70  ];
 71
 72  const chart = new Chart(ctx, {
 73    type: "line",
 74    data: {
 75      labels: data.map((d) => {
 76        const date = new Date(d.label);
 77        return `${date.getHours() % 12 || 12}:${date.getMinutes() < 10 ? "0" : ""}${date.getMinutes()} ${date.getHours() >= 12 ? "PM" : "AM"}`;
 78      }),
 79      datasets: series.map((s) => ({
 80        label: s.label,
 81        // Older rows (pre-migration-0002) have null phase timings — chart.js
 82        // treats null as a gap in the line, which is exactly what we want.
 83        data: data.map((d) => (d[s.key] == null ? null : d[s.key])),
 84        borderColor: s.color,
 85        backgroundColor: s.color,
 86        borderWidth: s.width,
 87        pointRadius: 0,
 88        pointHoverRadius: 4,
 89        pointHoverBackgroundColor: s.color,
 90        tension: s.tension,
 91        fill: false,
 92        spanGaps: false,
 93      })),
 94    },
 95    options: {
 96      responsive: true,
 97      maintainAspectRatio: false,
 98      animation: { duration: 0 },
 99      interaction: { mode: "index", intersect: false },
100      plugins: {
101        tooltip: {
102          mode: "index",
103          intersect: false,
104          backgroundColor: "rgba(9, 8, 6, 0.95)",
105          titleColor: "#ede8e0",
106          bodyColor: "#ddd7cd",
107          borderColor: "rgba(107, 158, 120, 0.2)",
108          borderWidth: 1,
109          padding: 10,
110          titleFont: tickFont,
111          bodyFont: tickFont,
112          callbacks: {
113            label: (item) =>
114              ` ${item.dataset.label}: ${item.parsed.y == null ? "–" : item.parsed.y + " ms"}`,
115          },
116        },
117        legend: { position: "top", labels: legendLabel },
118      },
119      scales: {
120        x: {
121          grid: { color: accent.grid },
122          border: { display: false },
123          ticks: { autoSkip: true, maxRotation: 25, font: tickFont, color: accent.ticks },
124        },
125        y: {
126          grid: { color: accent.grid },
127          border: { display: false },
128          ticks: {
129            beginAtZero: true,
130            font: tickFont,
131            color: accent.ticks,
132            callback: (value) => `${value} ms`,
133          },
134        },
135      },
136    },
137  });
138  chart.canvas.parentNode.style.width = "100%";
139  chart.canvas.parentNode.style.height = "300px";
140});
141
142function buildDoughnut(canvasId, dataId) {
143  const canvas = document.getElementById(canvasId);
144  if (!canvas) return;
145  const data = JSON.parse(document.getElementById(dataId).innerHTML);
146  const ctx = canvas.getContext("2d");
147
148  // Color rule: status 200 / Uptime = moss green, non-200 / Downtime =
149  // terracotta, everything else pulls from the earthy palette in order.
150  const paint = (label) => {
151    const name = String(label || "").toLowerCase();
152    if (name === "uptime" || name === "200") return [accent.greenFill, "rgba(107, 158, 120, 0.95)"];
153    if (name === "downtime") return [accent.terracottaFill, "rgba(196, 112, 85, 0.95)"];
154    return null;
155  };
156
157  const bg = data.map((d, i) => {
158    const p = paint(d.label);
159    return p ? p[0] : backgroundColors[i % backgroundColors.length];
160  });
161  const bd = data.map((d, i) => {
162    const p = paint(d.label);
163    return p ? p[1] : borderColors[i % borderColors.length];
164  });
165
166  new Chart(ctx, {
167    type: "doughnut",
168    data: {
169      labels: data.map((d) => String(d.label)),
170      datasets: [
171        {
172          data: data.map((d) => d.count),
173          backgroundColor: bg,
174          borderColor: bd,
175          borderWidth: 1.5,
176        },
177      ],
178    },
179    options: {
180      responsive: true,
181      aspectRatio: 2,
182      animation: { animateRotate: false },
183      cutout: "62%",
184      plugins: {
185        legend: { position: "right", labels: legendLabel },
186        tooltip: {
187          backgroundColor: "rgba(9, 8, 6, 0.95)",
188          titleColor: "#ede8e0",
189          bodyColor: "#ddd7cd",
190          borderColor: "rgba(107, 158, 120, 0.2)",
191          borderWidth: 1,
192          padding: 10,
193          titleFont: tickFont,
194          bodyFont: tickFont,
195        },
196      },
197    },
198  });
199}
200
201document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-status-codes", "chart-status-codes-data"));
202document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-uptime", "chart-uptime-data"));