Blog/Components/Pages/Rvrb.razor.js 3.1 K · 106 lines · raw · history

1 // The page itself is server-rendered and static. This adds the two things a status page wants that
2 // a snapshot can't carry: times that stay relative to now, and a track counter that keeps running
3 // between refreshes.
4
5 const REFRESH_KEY = "rvrb-auto-refresh";
6 const REFRESH_MS = 30_000;
7
8 let timer;
9
10 export function onLoad() {
11 wireAutoRefresh();
12 startTicking();
13 }
14
15 export function onUpdate() {
16 // Enhanced navigation swaps the DOM without a page load, so whatever was ticking is now
17 // pointing at elements that are gone.
18 stopTicking();
19 wireAutoRefresh();
20 startTicking();
21 }
22
23 export function onDispose() {
24 stopTicking();
25 }
26
27 function wireAutoRefresh() {
28 const checkbox = document.getElementById("autoRefresh");
29 if (!checkbox) return;
30
31 // Read back from storage, since the setting has to survive the very reload it causes.
32 checkbox.checked = localStorage.getItem(REFRESH_KEY) === "on";
33 checkbox.addEventListener("change", () => {
34 localStorage.setItem(REFRESH_KEY, checkbox.checked ? "on" : "off");
35 });
36 }
37
38 function startTicking() {
39 const started = performance.now();
40 const progress = document.getElementById("trackProgress");
41 const elapsedLabel = document.getElementById("trackElapsed");
42 const times = document.querySelectorAll("time[data-relative]");
43 const checkbox = document.getElementById("autoRefresh");
44
45 const tick = () => {
46 const sinceRender = performance.now() - started;
47
48 if (progress) advanceTrack(progress, elapsedLabel, sinceRender);
49 times.forEach(relabel);
50
51 if (checkbox?.checked && sinceRender >= REFRESH_MS) location.reload();
52 };
53
54 tick();
55 timer = setInterval(tick, 1000);
56 }
57
58 function stopTicking() {
59 clearInterval(timer);
60 timer = undefined;
61 }
62
63 // The snapshot says how far into the track the bot was when it was taken, and how long ago that
64 // was; everything after that is just wall clock.
65 function advanceTrack(progress, label, sinceRender) {
66 const duration = Number(progress.dataset.durationMs);
67 if (!duration) return;
68
69 const elapsed = Math.min(
70 Number(progress.dataset.elapsedMs) + Number(progress.dataset.ageMs) + sinceRender,
71 duration);
72
73 progress.value = Math.round((elapsed / duration) * Number(progress.max));
74 if (label) label.textContent = formatDuration(elapsed);
75 }
76
77 function relabel(time) {
78 const at = Date.parse(time.dateTime);
79 if (Number.isNaN(at)) return;
80
81 time.title ||= time.textContent;
82 time.textContent = formatAgo(Date.now() - at);
83 }
84
85 function formatAgo(ms) {
86 if (ms < 0) return "just now";
87
88 const seconds = Math.floor(ms / 1000);
89 if (seconds < 60) return `${seconds}s ago`;
90
91 const minutes = Math.floor(seconds / 60);
92 if (minutes < 60) return `${minutes}m ago`;
93
94 const hours = Math.floor(minutes / 60);
95 if (hours < 24) return `${hours}h ago`;
96
97 const days = Math.floor(hours / 24);
98 return days < 30 ? `${days}d ago` : `${Math.floor(days / 30)}mo ago`;
99 }
100
101 function formatDuration(ms) {
102 const seconds = Math.max(Math.floor(ms / 1000), 0);
103 const minutes = Math.floor(seconds / 60);
104
105 return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
106 }