From 861692c534f9e1411309fda16595255d5c687f3d Mon Sep 17 00:00:00 2001 From: Astra Date: Mon, 28 Sep 2026 11:36:05 +0100 Subject: [PATCH] Staff avatars from Steam, cached in memory The public staff page, the staff panel's staff list and the signed-in badge show each member's Steam avatar over their initial. The panel fetches it from the profile XML (no Web API key), keeps it in memory for a day (failures for an hour, one shared fetch per player) and serves it from /avatar/ with a day's browser caching, so the CSP stays img-src 'self' and visitors never contact Steam. Only SimpleAdmin admins are served; anything else falls back to the initial. --- internal/web/assets/admin.js | 4 +- internal/web/assets/common.js | 11 +++ internal/web/assets/public.js | 2 +- internal/web/assets/styles.css | 2 + internal/web/avatar.go | 159 +++++++++++++++++++++++++++++++++ internal/web/server.go | 39 ++++---- 6 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 internal/web/avatar.go diff --git a/internal/web/assets/admin.js b/internal/web/assets/admin.js index 6befcc7..320a3f8 100644 --- a/internal/web/assets/admin.js +++ b/internal/web/assets/admin.js @@ -282,7 +282,7 @@ async function staff() { const supporters = d.groups.filter((x) => x.supporter); const memberRow = (p, groupName) => ` -
${initial(p.name)}
${esc(p.name)}${esc(p.steamid)}
+
${avatar(p.steamid, p.name)}
${esc(p.name)}${esc(p.steamid)}
${p.online ? 'Online' : p.lastSeen ? `Seen ${fmtAgo(p.lastSeen)}` : 'Never joined'} ${p.ends ? `Until ${fmtDate(p.ends)}` : "No expiry"}
${manage && p.steamid !== d.me ? `` : ""}
@@ -832,7 +832,7 @@ document.addEventListener("keydown", (e) => { document.querySelectorAll(".site-name").forEach((el) => { el.innerHTML = i > 0 ? `${esc(name.slice(0, i))}.${esc(name.slice(i + 1))}` : esc(name); }); - $("#me").innerHTML = `${initial(state.me.name)} + $("#me").innerHTML = `${avatar(state.me.steamid, state.me.name)}
${esc(state.me.name)}${esc(state.me.rank?.label || "Staff")}
`; // Fill the sidebar counts once, even when starting on another page. diff --git a/internal/web/assets/common.js b/internal/web/assets/common.js index 130ee86..36b044f 100644 --- a/internal/web/assets/common.js +++ b/internal/web/assets/common.js @@ -82,6 +82,17 @@ function rankTag(rank) { return `${esc(rank.label)}`; } +// A player's Steam avatar over their initial. /avatar only serves staff; for anyone else, or if +// Steam has none, the image fails and the initial shows (see the error listener below). +function avatar(sid, name, cls = "") { + return `${initial(name)}`; +} + +// Error events don't bubble, so catch them on the way down; the CSP rules out inline onerror. +document.addEventListener("error", (e) => { + if (e.target instanceof HTMLImageElement && e.target.parentElement?.classList.contains("avatar")) e.target.remove(); +}, true); + function initial(name) { return esc(String(name || "").replace(/[^a-z0-9]/gi, "").charAt(0).toUpperCase() || "?"); } // Time served against the full term: the bar under each length. diff --git a/internal/web/assets/public.js b/internal/web/assets/public.js index dd0adf6..bb6562b 100644 --- a/internal/web/assets/public.js +++ b/internal/web/assets/public.js @@ -161,7 +161,7 @@ async function staff() {