Skip to content

Commit e59f999

Browse files
committed
feat: bring back the chart and add line type chart at the top called system status and update the color of chart
1 parent c51d7a4 commit e59f999

8 files changed

Lines changed: 248 additions & 83 deletions

File tree

client/status-icon-fix.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
(function () {
2+
"use strict";
3+
4+
var ICON_MARKUP =
5+
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="vertical-align:-3px;margin-right:6px;flex-shrink:0">' +
6+
'<path d="M15 2.5H12C7.52166 2.5 5.28249 2.5 3.89124 3.89124C2.5 5.28249 2.5 7.52166 2.5 12C2.5 16.4783 2.5 18.7175 3.89124 20.1088C5.28249 21.5 7.52166 21.5 12 21.5C16.4783 21.5 18.7175 21.5 20.1088 20.1088C21.5 18.7175 21.5 16.4783 21.5 12V10" stroke="#22C55E" stroke-width="2" stroke-linecap="round"/>' +
7+
'<path d="M8.5 10L12 13.5L21.0002 3.5" stroke="#22C55E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
8+
"</svg>";
9+
10+
function createIcon() {
11+
var span = document.createElement("span");
12+
span.innerHTML = ICON_MARKUP;
13+
return span.firstChild;
14+
}
15+
16+
function fix() {
17+
var article = document.querySelector("article.up");
18+
if (!article) return;
19+
var walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT, null);
20+
var node;
21+
while ((node = walker.nextNode())) {
22+
if (node.nodeValue.trim() === "✅") {
23+
node.parentNode.replaceChild(createIcon(), node);
24+
break;
25+
}
26+
}
27+
}
28+
29+
fix();
30+
document.addEventListener("DOMContentLoaded", fix);
31+
window.addEventListener("load", fix);
32+
33+
// The Sapper bundle re-renders this section on hydration and on each
34+
// data poll, reinstating the emoji text node — watch for that and
35+
// swap it back to the SVG.
36+
var target = document.querySelector("main.container");
37+
if (target && window.MutationObserver) {
38+
new MutationObserver(fix).observe(target, {
39+
childList: true,
40+
subtree: true,
41+
characterData: true,
42+
});
43+
}
44+
})();

client/system-status.js

Lines changed: 156 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,75 @@
99
var DAY_MS = 24 * 60 * 60 * 1000;
1010
var MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1111

12-
var root = document.getElementById("system-status-rows");
13-
var rangeLabel = document.getElementById("system-status-range");
14-
var prevBtn = document.querySelector('.status-nav-btn[data-dir="prev"]');
15-
var nextBtn = document.querySelector('.status-nav-btn[data-dir="next"]');
16-
if (!root) return;
12+
var SPINNER =
13+
'<div class="loading"><svg height="38" stroke="#aaa" width="38" xmlns="http://www.w3.org/2000/svg">' +
14+
'<g fill="none" fill-rule="evenodd"><g stroke-width="2" transform="translate(1 1)">' +
15+
'<circle cx="18" cy="18" r="18" stroke-opacity=".5"></circle>' +
16+
'<path d="M36 18c0-9.94-8.06-18-18-18"><animateTransform attributeName="transform" dur="1s" ' +
17+
'from="0 18 18" repeatCount="indefinite" to="360 18 18" type="rotate"></animateTransform></path>' +
18+
"</g></g></svg></div>";
19+
20+
var SECTION_HTML =
21+
'<section id="system-status" class="system-status" aria-labelledby="system-status-heading">' +
22+
'<div class="f changed"><h2 id="system-status-heading">System status</h2>' +
23+
'<div class="status-nav">' +
24+
'<button type="button" class="status-nav-btn" data-dir="prev" aria-label="Previous period" disabled>‹</button>' +
25+
'<span class="status-range" id="system-status-range">Loading…</span>' +
26+
'<button type="button" class="status-nav-btn" data-dir="next" aria-label="Next period" disabled>›</button>' +
27+
"</div></div>" +
28+
'<div id="system-status-rows" class="status-rows">' +
29+
SPINNER +
30+
"</div></section>";
31+
32+
// Sapper hydrates over the SSR'd DOM and discards any element it
33+
// doesn't recognize as part of its own component tree — so this
34+
// section can't just be static markup in index.html, it has to be
35+
// built and (re-)inserted from script, and watched in case
36+
// hydration tears it out from under us.
37+
function findAnchor() {
38+
return document.querySelector(".f.changed");
39+
}
40+
41+
function buildSection() {
42+
var wrapper = document.createElement("div");
43+
wrapper.innerHTML = SECTION_HTML;
44+
return wrapper.firstElementChild;
45+
}
46+
47+
var refs = null; // { root, rangeLabel, prevBtn, nextBtn }
48+
49+
function ensureSection() {
50+
var existing = document.getElementById("system-status");
51+
if (existing) return existing;
52+
53+
var anchor = findAnchor();
54+
if (!anchor || !anchor.parentNode) return null;
55+
56+
var section = buildSection();
57+
anchor.parentNode.insertBefore(section, anchor);
58+
59+
refs = {
60+
root: section.querySelector("#system-status-rows"),
61+
rangeLabel: section.querySelector("#system-status-range"),
62+
prevBtn: section.querySelector('.status-nav-btn[data-dir="prev"]'),
63+
nextBtn: section.querySelector('.status-nav-btn[data-dir="next"]'),
64+
};
65+
refs.prevBtn.addEventListener("click", onPrev);
66+
refs.nextBtn.addEventListener("click", onNext);
67+
68+
if (state.ready) render();
69+
else if (state.error) showError();
70+
71+
return section;
72+
}
73+
74+
function watchForRemoval() {
75+
var host = document.querySelector("main.container") || document.body;
76+
if (!host || !window.MutationObserver) return;
77+
new MutationObserver(function () {
78+
if (!document.getElementById("system-status")) ensureSection();
79+
}).observe(host, { childList: true });
80+
}
1781

1882
function toDateOnly(d) {
1983
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
@@ -130,82 +194,102 @@
130194
return row;
131195
}
132196

133-
function init(sites, issuesBySlug, startBySlug) {
197+
var state = {
198+
ready: false,
199+
error: false,
200+
sites: null,
201+
issuesBySlug: {},
202+
startBySlug: {},
203+
windowEnd: toDateOnly(new Date()),
204+
};
205+
206+
function render() {
207+
if (!refs || !refs.root) return;
134208
var today = toDateOnly(new Date());
135-
var windowEnd = today;
136-
137-
function render() {
138-
var windowStart = addDays(windowEnd, -(WINDOW_DAYS - 1));
139-
root.innerHTML = "";
140-
sites.forEach(function (site) {
141-
var events = issuesBySlug[site.slug] || [];
142-
var siteStart = startBySlug[site.slug] || null;
143-
root.appendChild(renderRow(site, events, siteStart, windowStart, windowEnd));
144-
});
145-
rangeLabel.textContent = formatRange(windowStart, windowEnd);
146-
147-
var earliestStart = sites.reduce(function (min, site) {
148-
var s = startBySlug[site.slug];
149-
if (!s) return min;
150-
return !min || s < min ? s : min;
151-
}, null);
152-
prevBtn.disabled = !!earliestStart && windowStart <= earliestStart;
153-
nextBtn.disabled = windowEnd >= today;
154-
}
209+
var windowStart = addDays(state.windowEnd, -(WINDOW_DAYS - 1));
155210

156-
prevBtn.addEventListener("click", function () {
157-
windowEnd = addDays(windowEnd, -WINDOW_DAYS);
158-
render();
159-
});
160-
nextBtn.addEventListener("click", function () {
161-
windowEnd = addDays(windowEnd, WINDOW_DAYS);
162-
if (windowEnd > today) windowEnd = today;
163-
render();
211+
refs.root.innerHTML = "";
212+
state.sites.forEach(function (site) {
213+
var events = state.issuesBySlug[site.slug] || [];
214+
var siteStart = state.startBySlug[site.slug] || null;
215+
refs.root.appendChild(renderRow(site, events, siteStart, windowStart, state.windowEnd));
164216
});
217+
refs.rangeLabel.textContent = formatRange(windowStart, state.windowEnd);
165218

219+
var earliestStart = state.sites.reduce(function (min, site) {
220+
var s = state.startBySlug[site.slug];
221+
if (!s) return min;
222+
return !min || s < min ? s : min;
223+
}, null);
224+
refs.prevBtn.disabled = !!earliestStart && windowStart <= earliestStart;
225+
refs.nextBtn.disabled = state.windowEnd >= today;
226+
}
227+
228+
function showError() {
229+
if (!refs || !refs.root) return;
230+
refs.root.innerHTML = "";
231+
refs.root.appendChild(el("p", "status-error", "Status data is unavailable right now."));
232+
refs.rangeLabel.textContent = "";
233+
refs.prevBtn.disabled = true;
234+
refs.nextBtn.disabled = true;
235+
}
236+
237+
function onPrev() {
238+
state.windowEnd = addDays(state.windowEnd, -WINDOW_DAYS);
166239
render();
167240
}
168241

169-
fetchJson(RAW_BASE + "summary.json")
170-
.then(function (sites) {
171-
var startBySlug = {};
172-
var startFetches = sites.map(function (site) {
173-
return fetch(RAW_BASE + site.slug + ".yml")
174-
.then(function (res) {
175-
return res.ok ? res.text() : "";
176-
})
177-
.then(function (text) {
178-
var m = text.match(/startTime:\s*([^\n]+)/);
179-
if (m) startBySlug[site.slug] = toDateOnly(new Date(m[1].trim()));
180-
})
181-
.catch(function () {});
182-
});
242+
function onNext() {
243+
var today = toDateOnly(new Date());
244+
state.windowEnd = addDays(state.windowEnd, WINDOW_DAYS);
245+
if (state.windowEnd > today) state.windowEnd = today;
246+
render();
247+
}
183248

184-
var issuesBySlug = {};
185-
var issuesFetch = fetchJson(ISSUES_URL)
186-
.then(function (issues) {
187-
var events = issues.map(classifyIssue).filter(Boolean);
188-
// Single-monitor setup: attribute all events to every site.
189-
sites.forEach(function (site) {
190-
issuesBySlug[site.slug] = events;
191-
});
192-
})
193-
.catch(function () {
194-
// Rate-limited or offline — fall back to an all-green bar.
195-
sites.forEach(function (site) {
196-
issuesBySlug[site.slug] = [];
197-
});
249+
function loadData() {
250+
fetchJson(RAW_BASE + "summary.json")
251+
.then(function (sites) {
252+
state.sites = sites;
253+
254+
var startFetches = sites.map(function (site) {
255+
return fetch(RAW_BASE + site.slug + ".yml")
256+
.then(function (res) {
257+
return res.ok ? res.text() : "";
258+
})
259+
.then(function (text) {
260+
var m = text.match(/startTime:\s*([^\n]+)/);
261+
if (m) state.startBySlug[site.slug] = toDateOnly(new Date(m[1].trim()));
262+
})
263+
.catch(function () {});
198264
});
199265

200-
return Promise.all(startFetches.concat([issuesFetch])).then(function () {
201-
init(sites, issuesBySlug, startBySlug);
266+
var issuesFetch = fetchJson(ISSUES_URL)
267+
.then(function (issues) {
268+
var events = issues.map(classifyIssue).filter(Boolean);
269+
sites.forEach(function (site) {
270+
state.issuesBySlug[site.slug] = events;
271+
});
272+
})
273+
.catch(function () {
274+
// Rate-limited or offline — fall back to an all-green bar.
275+
sites.forEach(function (site) {
276+
state.issuesBySlug[site.slug] = [];
277+
});
278+
});
279+
280+
return Promise.all(startFetches.concat([issuesFetch]));
281+
})
282+
.then(function () {
283+
state.ready = true;
284+
render();
285+
})
286+
.catch(function () {
287+
state.error = true;
288+
showError();
202289
});
203-
})
204-
.catch(function () {
205-
root.innerHTML = "";
206-
root.appendChild(el("p", "status-error", "Status data is unavailable right now."));
207-
rangeLabel.textContent = "";
208-
prevBtn.disabled = true;
209-
nextBtn.disabled = true;
210-
});
290+
}
291+
292+
ensureSection();
293+
watchForRemoval();
294+
loadData();
211295
})();

global.css

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,38 @@ article.up::after {
354354
background: var(--up-border-left-color);
355355
}
356356

357-
/* The top summary banner already carries its own status emoji —
358-
the corner dot is redundant there, so drop it. */
357+
/* The top summary banner carries its own status icon (see
358+
status-icon-fix.js) — the corner dot is redundant there. */
359359
article.up:not(.link)::after,
360360
article.degraded:not(.link)::after,
361361
article.down:not(.link)::after {
362362
display: none;
363363
}
364364

365+
/* Real SVG icon replacing the bundle's hardcoded emoji. The icon
366+
itself is a self-contained outline glyph, colored via
367+
currentColor to match its status. */
368+
.status-banner-icon {
369+
display: inline-flex;
370+
align-items: center;
371+
justify-content: center;
372+
flex-shrink: 0;
373+
margin-right: 0.5rem;
374+
vertical-align: -4px;
375+
}
376+
377+
.status-banner-icon.up {
378+
color: var(--up-border-left-color);
379+
}
380+
381+
.status-banner-icon.degraded {
382+
color: var(--degraded-border-left-color);
383+
}
384+
385+
.status-banner-icon.down {
386+
color: var(--down-border-left-color);
387+
}
388+
365389
/* Pulse ring on live/healthy status dots — signals "actively
366390
monitored", not just a static state. Square to match the dot. */
367391
article.up.link::before {
@@ -515,12 +539,26 @@ article.degraded-active {
515539
.live-status article.svelte-fqsq6s.svelte-fqsq6s.svelte-fqsq6s,
516540
.live-status article.graph.svelte-fqsq6s.svelte-fqsq6s.svelte-fqsq6s {
517541
position: relative;
518-
padding-right: 1.25rem;
542+
padding-right: 200px;
543+
min-height: 96px;
519544
}
520545

521-
/* Sparkline background-image removed — card shows status text only */
522546
.live-status article.graph.svelte-fqsq6s.svelte-fqsq6s.svelte-fqsq6s {
523-
background-image: none !important;
547+
background-image: var(--background);
548+
background-size: auto 48px;
549+
background-repeat: no-repeat;
550+
background-position: right 1.25rem bottom 1rem;
551+
opacity: var(--graph-opacity);
552+
filter: var(--graph-filter);
553+
}
554+
555+
/* The response-time chart on a monitor's detail page is drawn on a
556+
<canvas> with its teal line color baked into the compiled bundle
557+
(not a CSS variable) — hue-shift it to the theme's green instead
558+
of hand-patching the minified JS. Same rotation as the sparkline
559+
above, so both read as the same base green. */
560+
main canvas {
561+
filter: var(--graph-filter);
524562
}
525563

526564
.live-status .icon,
@@ -653,7 +691,6 @@ article.degraded-active {
653691
min-width: 4px;
654692
height: 28px;
655693
background: var(--up-border-left-color);
656-
opacity: 0.85;
657694
}
658695

659696
.status-cell.degraded {

0 commit comments

Comments
 (0)