+
+ {!updated && (
+
+ )}
+ {/* A plain anchor: the root boundary renders outside the router. */}
+
+
+ {!updated && (
+
+
+ {t("crash_details")}
+
+
+
+ {detail}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/panel/src/components/FleetGrid.tsx b/panel/src/components/FleetGrid.tsx
new file mode 100644
index 0000000..7db8559
--- /dev/null
+++ b/panel/src/components/FleetGrid.tsx
@@ -0,0 +1,67 @@
+import { Link } from "react-router-dom";
+import { Info } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { PHASE_KEY, phaseColor } from "@/components/PhaseBadge";
+import { cn } from "@/lib/utils";
+import type { Phase, ServerInfo } from "@/lib/types";
+
+// FleetGrid is the flat stand-in for VoxelFleet when WebGL is unavailable or the
+// 3D chunk failed: the same square layout and phase palette seen from above, so
+// the legend under the card still reads correctly. It lives outside the three.js
+// chunk so it renders even when that chunk cannot load.
+
+const TRANSIENT: ReadonlySet = new Set(["Starting", "Stopping"]);
+
+export type FleetGridReason = "webgl" | "error";
+
+export function FleetGrid({
+ servers,
+ reason,
+}: {
+ servers: Pick[];
+ reason: FleetGridReason;
+}) {
+ const { t } = useTranslation(["dashboard", "servers"]);
+ const cols = Math.max(1, Math.ceil(Math.sqrt(servers.length)));
+ const n = servers.length;
+ const tile = n <= 9 ? "3rem" : n <= 36 ? "1.75rem" : n <= 100 ? "1.25rem" : "0.875rem";
+
+ return (
+