diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx
index 58d9a75..1673980 100644
--- a/src/app/(frontend)/layout.tsx
+++ b/src/app/(frontend)/layout.tsx
@@ -11,7 +11,7 @@ import { Metadata } from "next";
import { type ResolvedLevel, resolveLevel } from "@/utils/xp/resolveLevel";
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
-import { canAccessAdminPanel } from "@/utils/access-control/hasPermission";
+import { canAccessAdminPanel } from "@/utils/access-control/divisionAccess";
import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts";
import { MissionReminderToasts } from "@/components/frontend/intelligence/MissionReminderToasts";
import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime";
@@ -29,6 +29,7 @@ import { cookies } from "next/headers";
import { IMPERSONATION_ACTIVE_COOKIE } from "@/lib/impersonation";
import { ImpersonationBanner } from "@/components/frontend/impersonation/ImpersonationBanner";
import { ShimLoader } from "@/components/frontend/shims/ShimLoader";
+import { DevDashboardGate } from "@/components/frontend/dev/DevDashboardGate";
// Skip static prerendering of this layout (and all pages under (frontend)/).
// The layout calls `getPayload()` at render time to resolve the current user,
@@ -184,6 +185,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
)}
{user && }
{user && }
+ {process.env.NODE_ENV === "development" && user && }
{user && }
{isLogistics && }
diff --git a/src/app/(frontend)/styles.css b/src/app/(frontend)/styles.css
index 4329337..77f52ce 100644
--- a/src/app/(frontend)/styles.css
+++ b/src/app/(frontend)/styles.css
@@ -3576,3 +3576,4 @@
[data-radix-popper-content-wrapper] {
z-index: 1300 !important;
}
+
diff --git a/src/components/frontend/dev/DevDashboardGate.tsx b/src/components/frontend/dev/DevDashboardGate.tsx
new file mode 100644
index 0000000..ec33498
--- /dev/null
+++ b/src/components/frontend/dev/DevDashboardGate.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+
+/**
+ * Dev-only guard for routes whose server render is pathologically slow under
+ * the Next.js 16 dev runtime (see payloadcms/payload#14487): the dashboard
+ * takes roughly 100s per render in dev while the browser gives up around 60s,
+ * so clicking any link to it looks like a hang. This component intercepts
+ * plain left-clicks on anchors pointing at those routes, shows a confirm
+ * dialog, and only navigates when the user insists.
+ *
+ * Gaps by design: direct URL entry, page refresh, modifier-clicks (Ctrl/Cmd +
+ * click), and programmatic router.push calls (e.g. the command palette) are
+ * not gated. Anchors inside a [data-dev-gate-bypass] ancestor are also
+ * exempt, so a specific link can opt out.
+ */
+
+const GATED_PATHS = new Set(["/"]);
+
+function resolveTarget(href: string): string | null {
+ try {
+ const url = new URL(href, window.location.origin);
+ if (url.origin !== window.location.origin) return null;
+ if (!GATED_PATHS.has(url.pathname)) return null;
+ return url.pathname + url.search + url.hash;
+ } catch {
+ return null;
+ }
+}
+
+export function DevDashboardGate() {
+ const router = useRouter();
+ const [open, setOpen] = useState(false);
+ const [target, setTarget] = useState("/");
+ const proceedRef = useRef<() => void>(() => undefined);
+
+ useEffect(() => {
+ if (process.env.NODE_ENV !== "development") return undefined;
+
+ function onClick(event: MouseEvent) {
+ if (event.defaultPrevented) return;
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
+ return;
+ }
+ const anchor = (event.target as Element | null)?.closest?.("a");
+ if (!anchor) return;
+ if (anchor.hasAttribute("download")) return;
+ const targetAttr = anchor.getAttribute("target");
+ if (targetAttr && targetAttr !== "_self") return;
+ if (anchor.closest("[data-dev-gate-bypass]")) return;
+ const next = resolveTarget(anchor.getAttribute("href") ?? "");
+ if (!next) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ proceedRef.current = () => {
+ setOpen(false);
+ router.push(next);
+ };
+ setTarget(next);
+ setOpen(true);
+ }
+
+ document.addEventListener("click", onClick, { capture: true });
+ return () => document.removeEventListener("click", onClick, { capture: true });
+ }, [router]);
+
+ if (process.env.NODE_ENV !== "development") return null;
+
+ return (
+ {
+ if (!next) setOpen(false);
+ }}
+ >
+
+
+ Open the dashboard in dev mode?
+
+ In dev, {target} renders the dashboard
+ server-side in roughly 100 seconds (a known Next.js 16 dev-runtime regression) and the
+ browser usually gives up around 60s with an empty response. Production serves the same
+ page in a few seconds. Stay here, or open anyway and wait.
+
+
+
+ Stay here
+ proceedRef.current()}>Open anyway
+
+
+
+ );
+}