diff --git a/server/templates/dashboard.html b/server/templates/dashboard.html
index 38b9b12..eb6defe 100644
--- a/server/templates/dashboard.html
+++ b/server/templates/dashboard.html
@@ -1112,6 +1112,90 @@
}
}
+ function parseViewBox(svgEl) {
+ const raw = String(svgEl.getAttribute("viewBox") || "").trim();
+ const parts = raw.split(/\s+/).map((item) => Number(item));
+ if (parts.length !== 4 || parts.some((v) => Number.isNaN(v))) {
+ return { minX: 0, minY: 0, width: 100, height: 100 };
+ }
+ return { minX: parts[0], minY: parts[1], width: parts[2], height: parts[3] };
+ }
+
+ function groupLooksOffCanvas(groupEl, viewBox) {
+ const attrNames = new Set(["x", "y", "cx", "cy", "x1", "y1", "x2", "y2", "d", "points"]);
+ const allElements = [groupEl, ...groupEl.querySelectorAll("*")];
+ let farOutsideCount = 0;
+ let numericCount = 0;
+ const minAllowedX = viewBox.minX - Math.max(40, viewBox.width * 0.8);
+ const minAllowedY = viewBox.minY - Math.max(40, viewBox.height * 0.8);
+ const maxAllowedX = viewBox.minX + viewBox.width + Math.max(40, viewBox.width * 0.8);
+ const maxAllowedY = viewBox.minY + viewBox.height + Math.max(40, viewBox.height * 0.8);
+
+ for (const node of allElements) {
+ for (const attr of node.getAttributeNames()) {
+ if (!attrNames.has(attr)) continue;
+ const value = node.getAttribute(attr);
+ if (!value) continue;
+ const matches = value.match(/-?\d*\.?\d+/g);
+ if (!matches) continue;
+
+ for (let idx = 0; idx < matches.length; idx += 1) {
+ const num = Number(matches[idx]);
+ if (Number.isNaN(num)) continue;
+ numericCount += 1;
+ const isXCoord = idx % 2 === 0;
+ if (isXCoord) {
+ if (num < minAllowedX || num > maxAllowedX) farOutsideCount += 1;
+ } else {
+ if (num < minAllowedY || num > maxAllowedY) farOutsideCount += 1;
+ }
+ }
+ }
+ }
+
+ if (numericCount < 10) return false;
+ return farOutsideCount / numericCount > 0.55;
+ }
+
+ function maxNestedGroupDepth(groupEl) {
+ let maxDepth = 1;
+ const stack = [{ node: groupEl, depth: 1 }];
+ while (stack.length > 0) {
+ const entry = stack.pop();
+ if (!entry) continue;
+ maxDepth = Math.max(maxDepth, entry.depth);
+ for (const child of Array.from(entry.node.children)) {
+ if (!child.tagName || child.tagName.toLowerCase() !== "g") continue;
+ stack.push({ node: child, depth: entry.depth + 1 });
+ }
+ }
+ return maxDepth;
+ }
+
+ function normalizeHeadSvgMarkup(svgMarkup) {
+ if (!svgMarkup) return null;
+ try {
+ const parser = new DOMParser();
+ const parsed = parser.parseFromString(svgMarkup, "image/svg+xml");
+ const svgEl = parsed.querySelector("svg");
+ if (!svgEl) return svgMarkup;
+
+ const topLevelGroups = Array.from(svgEl.children).filter((el) => el.tagName && el.tagName.toLowerCase() === "g");
+ if (topLevelGroups.length > 1) {
+ const viewBox = parseViewBox(svgEl);
+ const firstGroup = topLevelGroups[0];
+ const deeplyNestedGroup = maxNestedGroupDepth(firstGroup) >= 3;
+ if (groupLooksOffCanvas(firstGroup, viewBox) || deeplyNestedGroup) {
+ firstGroup.remove();
+ }
+ }
+
+ return new XMLSerializer().serializeToString(svgEl);
+ } catch {
+ return svgMarkup;
+ }
+ }
+
async function preloadReplaySvgs() {
if (!replay || !Array.isArray(replay.turns)) return;
const urls = new Set();
@@ -1135,7 +1219,7 @@
if (type === "head") {
const svgMarkup = svgCache.get(iconUrl);
if (svgMarkup) {
- layer.innerHTML = svgMarkup;
+ layer.innerHTML = normalizeHeadSvgMarkup(svgMarkup);
const svgEl = layer.querySelector("svg");
if (svgEl) {
svgEl.style.width = "100%";