diff --git a/web/app/error.tsx b/web/app/error.tsx
new file mode 100644
index 0000000..eb67727
--- /dev/null
+++ b/web/app/error.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import { useEffect } from "react";
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ console.error("Top-level error boundary caught:", error);
+ }, [error]);
+
+ return (
+
+
+
+
+ !
+
+
+ api unreachable
+
+
+
+
+
+ Couldn't reach the API
+
+
+ The dashboard fetches data from{" "}
+
+ api-server:3000
+ {" "}
+ on the internal docker network. The container may be down or
+ still starting. Wait a few seconds and retry.
+
+
+
+ Retry →
+
+
+
+
+ error
+
+
+ {error.message}
+ {error.digest ? `\n\ndigest: ${error.digest}` : ""}
+
+
+
+
+
+ );
+}
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index 1a4acb1..e6058d7 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -1,7 +1,21 @@
import "./globals.css";
+import type { Metadata, Viewport } from "next";
+import Nav from "@/components/nav";
+import AutoRefresh from "@/components/auto-refresh";
-export const metadata = {
+export const metadata: Metadata = {
title: "CM Bot V2",
+ description: "CM Bot account and user dashboard",
+};
+
+export const viewport: Viewport = {
+ themeColor: "#facc15",
+};
+
+const workbenchGrid = {
+ backgroundImage:
+ "radial-gradient(circle at 1px 1px, rgba(24,24,27,0.07) 1px, transparent 0)",
+ backgroundSize: "24px 24px",
};
export default function RootLayout({
@@ -11,7 +25,14 @@ export default function RootLayout({
}) {
return (
- {children}
+
+
+ {children}
+
+
);
}
diff --git a/web/components/nav.tsx b/web/components/nav.tsx
new file mode 100644
index 0000000..abf8721
--- /dev/null
+++ b/web/components/nav.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+export default function Nav() {
+ const pathname = usePathname() ?? "/";
+ const isUsers = pathname.startsWith("/users");
+ const isAccounts = !isUsers;
+
+ return (
+
+ );
+}
+
+function NavLink({
+ href,
+ active,
+ children,
+}: {
+ href: string;
+ active: boolean;
+ children: React.ReactNode;
+}) {
+ const base =
+ "inline-flex items-center border-2 px-3 py-1.5 font-mono text-[11px] font-bold uppercase tracking-[0.2em] transition-colors";
+ const activeCls = "border-zinc-900 bg-yellow-300 text-zinc-900";
+ const inactiveCls =
+ "border-transparent text-zinc-700 hover:border-zinc-900 hover:bg-yellow-50 hover:text-zinc-900";
+ return (
+
+ {children}
+
+ );
+}