Self-hostable uptime monitor and status page on Django: HTTP checks, Lighthouse audits, SEO crawls, and email and Discord alerts.
djangodockerhandcodedpythonself-hostedsqlitestatus-pageuptime-monitoringvite
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"));