Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _data/site.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ github_repo: https://github.com/ReduxStation/website
# Set to "" to disable the poller and show "server status unavailable".
status_endpoint: "/serverinfo.json"

poll_interval_ms: 4000
poll_interval_ms: 1000
11 changes: 11 additions & 0 deletions eleventy.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ module.exports = function (eleventyConfig) {
eleventyConfig.addNunjucksAsyncShortcode("webpack", shortcodes.webpack);
eleventyConfig.addShortcode("currentBuildDate", () => new Date().toISOString());

// Cache-bust query string baked at build time. Webpack's manifest plugin
// does not add [contenthash] to the copied JS bundles (they go through
// CopyWebpackPlugin, not webpack's regular pipeline), so the
// /assets/js/*.js URLs are stable. Caddy serves /assets/* with
// Cache-Control: max-age=31536000, so without this query string a
// browser that fetched the JS once would never refetch after a content
// change. Pinning to one buildId per `npm run build` flushes caches
// exactly when assets actually change.
const buildId = Date.now().toString(36);
eleventyConfig.addShortcode("buildId", () => buildId);

eleventyConfig.addWatchTarget(path.join(__dirname, "_site/assets/manifest.json"));
eleventyConfig.addWatchTarget(path.join(__dirname, "_data/*.yaml"));

Expand Down
10 changes: 5 additions & 5 deletions index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<title>{{ site.brand }} - {{ site.tagline }}</title>
<meta name="description" content="{{ site.brand }} - {{ site.tagline }}. Join us on {{ site.byond_url }}.">
<link rel="icon" type="image/svg+xml" href='data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" fill="%23000080"/><text x="8" y="12" font-family="MS Sans Serif,Tahoma,sans-serif" font-size="11" font-weight="bold" fill="white" text-anchor="middle">R</text></svg>'>
<link rel="stylesheet" href="{% webpack 'main.css' %}">
<link rel="stylesheet" href="{% webpack 'main.css' %}?v={% buildId %}">
</head>
<body>

Expand Down Expand Up @@ -128,9 +128,9 @@
<span>Built {% currentBuildDate %}</span>
</footer>

<script defer src="{% webpack 'js/main.js' %}"></script>
<script defer src="{% webpack 'js/parallax.js' %}"></script>
<script defer src="{% webpack 'js/gamebanners.js' %}"></script>
<script defer src="{% webpack 'js/dismiss.js' %}"></script>
<script defer src="{% webpack 'js/main.js' %}?v={% buildId %}"></script>
<script defer src="{% webpack 'js/parallax.js' %}?v={% buildId %}"></script>
<script defer src="{% webpack 'js/gamebanners.js' %}?v={% buildId %}"></script>
<script defer src="{% webpack 'js/dismiss.js' %}?v={% buildId %}"></script>
</body>
</html>
67 changes: 10 additions & 57 deletions js/gamebanners.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
container.querySelectorAll(".gamebanner").forEach((banner) => {
banner.classList.remove("loading", "lobby", "underway", "end");
banner.classList.add("error");
banner._statusData = null;
const v = banner.querySelector(".version");
if (v) v.textContent = "server status unavailable";
const m = banner.querySelector(".map");
Expand All @@ -40,7 +39,6 @@
container.querySelectorAll(".gamebanner").forEach((banner) => {
banner.classList.remove("loading", "lobby", "underway", "end");
banner.classList.add("error");
banner._statusData = null;
const v = banner.querySelector(".version");
if (v) v.textContent = "connection error";
});
Expand Down Expand Up @@ -100,54 +98,17 @@
}
}

// Build the contents of the .status line. extraSec is the number of
// seconds elapsed since the data was fetched; the local 1Hz ticker
// passes a growing value so round_duration reads as a live clock
// instead of stepping every server poll. shuttle_timer is left at
// the server value because we don't know whether it counts up or
// down for the current mode — it just resyncs on the next fetch.
function buildStatusString(d, extraSec) {
let s = String(d.players);
const cap = popcapString(d);
if (cap) s += "/" + cap;
if (Number(d.round_duration)) s += " " + secondsToTime(Number(d.round_duration) + extraSec);
if (d.shuttle_mode && Number(d.shuttle_timer)) s += " " + shuttleTime(d.shuttle_mode, Number(d.shuttle_timer));
return s;
}

// Pick the timestamp the data was generated at, not the timestamp the
// browser received the response. The serverinfo-updater stamps
// payload.last_update at the moment it polls DD; using that as the
// ticker anchor lets the displayed clock stay aligned with real time
// even though the JSON file is up to one poll-interval stale by the
// time the browser fetches it (3-4s offset otherwise).
//
// Falls back to Date.now() when last_update is missing or when the
// browser clock disagrees with the server clock by > 5 minutes
// (NTP outage, user clock wrong). Without the guard, a multi-minute
// skew would surface as a multi-minute jump in the displayed timer.
function pickBaselineTime(lastUpdateIso) {
if (!lastUpdateIso) return Date.now();
const t = new Date(lastUpdateIso).getTime();
if (!isFinite(t)) return Date.now();
if (Math.abs(Date.now() - t) > 5 * 60 * 1000) return Date.now();
return t;
}

function renderBanner(server, lastUpdateIso) {
function renderBanner(server) {
const banner = document.getElementById(server.identifier);
if (!banner) return 0;
if (!server.data || server.data.ERROR || !server.data.players || !server.data.version) {
banner.classList.remove("loading", "lobby", "underway", "end");
banner.classList.add("error");
banner._statusData = null;
const v = banner.querySelector(".version");
if (v) v.textContent = (server.data && server.data.errortext) || "connection error";
return 0;
}
const d = server.data;
banner._statusData = d;
banner._fetchedAt = pickBaselineTime(lastUpdateIso);
applyState(banner, d.gamestate);
const v = banner.querySelector(".version");
if (v) {
Expand All @@ -159,7 +120,14 @@
const m = banner.querySelector(".map");
if (m) m.textContent = "Map: " + (d.map_name || "?");
const status = banner.querySelector(".status");
if (status) status.textContent = buildStatusString(d, 0);
if (status) {
let s = String(d.players);
const cap = popcapString(d);
if (cap) s += "/" + cap;
if (Number(d.round_duration)) s += " " + secondsToTime(Number(d.round_duration));
if (d.shuttle_mode && Number(d.shuttle_timer)) s += " " + shuttleTime(d.shuttle_mode, Number(d.shuttle_timer));
status.textContent = s;
}
const r = banner.querySelector(".revision");
if (r && d.revision) r.textContent = "rev " + String(d.revision).substr(0, 7);
return Number(d.players) || 0;
Expand All @@ -175,7 +143,7 @@
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
let total = 0;
(data.servers || []).forEach((s) => { total += renderBanner(s, data.last_update); });
(data.servers || []).forEach((s) => { total += renderBanner(s); });
if (playercountEl) playercountEl.textContent = total + " online";
if (statusDot) {
statusDot.classList.remove("offline");
Expand All @@ -186,21 +154,6 @@
}
}

// Local 1Hz ticker. Recomputes only the .status line so the round
// timer increments smoothly between server polls. No-op on banners
// whose last fetch errored (_statusData cleared) or hasn't returned
// yet, so the page never shows phantom data.
setInterval(() => {
container.querySelectorAll(".gamebanner").forEach((banner) => {
const d = banner._statusData;
if (!d) return;
const status = banner.querySelector(".status");
if (!status) return;
const extraSec = Math.floor((Date.now() - banner._fetchedAt) / 1000);
status.textContent = buildStatusString(d, extraSec);
});
}, 1000);

poll();
setInterval(poll, interval);
})();
Loading