repos
/ status-django master

status-django

mirror archived upstream

Self-hostable uptime monitor and status page on Django: HTTP checks, Lighthouse audits, SEO crawls, and email and Discord alerts.

djangodockerhandcodedpythonself-hostedsqlitestatus-pageuptime-monitoringvite

5.5 KB · 186 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  const gradient = ctx.createLinearGradient(0, 0, 0, 300);
 61  gradient.addColorStop(0, "rgba(107, 158, 120, 0.35)");
 62  gradient.addColorStop(1, "rgba(107, 158, 120, 0)");
 63  const chart = new Chart(ctx, {
 64    type: "line",
 65    data: {
 66      labels: data.map((d) => {
 67        const date = new Date(d.label);
 68        return `${date.getHours() % 12 || 12}:${date.getMinutes() < 10 ? "0" : ""}${date.getMinutes()} ${date.getHours() >= 12 ? "PM" : "AM"}`;
 69      }),
 70      datasets: [
 71        {
 72          label: "Response time (ms)",
 73          data: data.map((d) => d.count),
 74          backgroundColor: gradient,
 75          borderColor: accent.green,
 76          borderWidth: 2,
 77          pointRadius: 0,
 78          pointHoverRadius: 4,
 79          pointHoverBackgroundColor: accent.greenBright,
 80          tension: 0.25,
 81          fill: true,
 82        },
 83      ],
 84    },
 85    options: {
 86      responsive: true,
 87      maintainAspectRatio: false,
 88      animation: { duration: 0 },
 89      plugins: {
 90        tooltip: {
 91          mode: "index",
 92          intersect: false,
 93          backgroundColor: "rgba(9, 8, 6, 0.95)",
 94          titleColor: "#ede8e0",
 95          bodyColor: "#ddd7cd",
 96          borderColor: "rgba(107, 158, 120, 0.2)",
 97          borderWidth: 1,
 98          padding: 10,
 99          titleFont: tickFont,
100          bodyFont: tickFont,
101        },
102        legend: { position: "top", labels: legendLabel },
103      },
104      scales: {
105        x: {
106          grid: { color: accent.grid, drawBorder: false },
107          ticks: { autoSkip: true, maxRotation: 25, font: tickFont, color: accent.ticks },
108        },
109        y: {
110          grid: { color: accent.grid, drawBorder: false },
111          ticks: {
112            beginAtZero: true,
113            font: tickFont,
114            color: accent.ticks,
115            callback: (value) => `${value} ms`,
116          },
117        },
118      },
119    },
120  });
121  chart.canvas.parentNode.style.width = "100%";
122  chart.canvas.parentNode.style.height = "300px";
123});
124
125function buildDoughnut(canvasId, dataId) {
126  const canvas = document.getElementById(canvasId);
127  if (!canvas) return;
128  const data = JSON.parse(document.getElementById(dataId).innerHTML);
129  const ctx = canvas.getContext("2d");
130
131  // Color rule: status 200 / Uptime = moss green, non-200 / Downtime =
132  // terracotta, everything else pulls from the earthy palette in order.
133  const paint = (label) => {
134    const name = String(label || "").toLowerCase();
135    if (name === "uptime" || name === "200") return [accent.greenFill, "rgba(107, 158, 120, 0.95)"];
136    if (name === "downtime") return [accent.terracottaFill, "rgba(196, 112, 85, 0.95)"];
137    return null;
138  };
139
140  const bg = data.map((d, i) => {
141    const p = paint(d.label);
142    return p ? p[0] : backgroundColors[i % backgroundColors.length];
143  });
144  const bd = data.map((d, i) => {
145    const p = paint(d.label);
146    return p ? p[1] : borderColors[i % borderColors.length];
147  });
148
149  new Chart(ctx, {
150    type: "doughnut",
151    data: {
152      labels: data.map((d) => d.label),
153      datasets: [
154        {
155          data: data.map((d) => d.count),
156          backgroundColor: bg,
157          borderColor: bd,
158          borderWidth: 1.5,
159        },
160      ],
161    },
162    options: {
163      responsive: true,
164      aspectRatio: 2,
165      animation: { animateRotate: false },
166      cutout: "62%",
167      plugins: {
168        legend: { position: "right", labels: legendLabel },
169        tooltip: {
170          backgroundColor: "rgba(9, 8, 6, 0.95)",
171          titleColor: "#ede8e0",
172          bodyColor: "#ddd7cd",
173          borderColor: "rgba(107, 158, 120, 0.2)",
174          borderWidth: 1,
175          padding: 10,
176          titleFont: tickFont,
177          bodyFont: tickFont,
178        },
179      },
180    },
181  });
182}
183
184document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-status-codes", "chart-status-codes-data"));
185document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-uptime", "chart-uptime-data"));