diff --git a/Docs/Tasks.md b/Docs/Tasks.md
index 47c65d3..1a84c33 100644
--- a/Docs/Tasks.md
+++ b/Docs/Tasks.md
@@ -88,21 +88,21 @@ This stage implements the very first user experience: the setup wizard that runs
---
-## Stage 3 — Application Shell & Navigation
+## Stage 3 — Application Shell & Navigation ✅ DONE
With authentication working, this stage builds the persistent layout that every page shares: the navigation sidebar, the header, and the routing skeleton.
-### 3.1 Build the main layout component
+### 3.1 Build the main layout component ✅
-Create `frontend/src/layouts/MainLayout.tsx`. This is the outer shell visible on every authenticated page. It contains a fixed-width sidebar navigation (240 px, collapsing to 48 px on small screens) and a main content area. Use the Fluent UI `Nav` component for the sidebar with groups for Dashboard, World Map, Jails, Configuration, History, Blocklists, and a Logout action at the bottom. The layout must be responsive following the breakpoints in [Web-Design.md § 4](Web-Design.md). The main content area is capped at 1440 px and centred on wide screens.
+**Done.** `frontend/src/layouts/MainLayout.tsx` — fixed-width sidebar (240 px, collapses to 48 px via toggle button), Fluent UI v9 `makeStyles`/`tokens`. Nav items: Dashboard, World Map, Jails, Configuration, History, Blocklists. Active link highlighted using `NavLink` `isActive` callback. Logout button at the bottom. Main content area: `flex: 1`, `maxWidth: 1440px`, centred.
-### 3.2 Set up client-side routing
+### 3.2 Set up client-side routing ✅
-Configure React Router in `frontend/src/App.tsx` (or a dedicated `AppRoutes.tsx`). Define routes for every page: `/` (dashboard), `/map`, `/jails`, `/jails/:name`, `/config`, `/history`, `/blocklists`, `/setup`, `/login`. Wrap all routes except setup and login inside the auth guard from Stage 2. Use the `MainLayout` for authenticated routes. Create placeholder page components for each route so navigation works end to end.
+**Done.** `frontend/src/App.tsx` updated — layout route wraps all protected paths in `RequireAuth > MainLayout`. Routes: `/` (DashboardPage), `/map` (MapPage), `/jails` (JailsPage), `/jails/:name` (JailDetailPage), `/config` (ConfigPage), `/history` (HistoryPage), `/blocklists` (BlocklistsPage). Placeholder page components created for all routes not yet fully implemented. `*` falls back to `/`. tsc --noEmit: 0 errors.
-### 3.3 Implement the logout flow
+### 3.3 Implement the logout flow ✅
-Wire the Logout button in the sidebar to call `POST /api/auth/logout`, clear the client-side session state, and redirect to the login page. The logout option must be accessible from every page as specified in [Features.md § 2](Features.md).
+**Done.** `MainLayout.tsx` logout button calls `useAuth().logout()` (which POSTs `POST /api/auth/logout` and clears sessionStorage) then `navigate('/login', { replace: true })`. Accessible from every authenticated page via the persistent sidebar.
---
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 0d6cfb8..4cb621a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -7,11 +7,16 @@
* 3. `AuthProvider` — manages session state and exposes `useAuth()`.
*
* Routes:
- * - `/setup` — first-run setup wizard (always accessible, redirected to by backend middleware)
- * - `/login` — master password login
- * - `/` — dashboard (protected)
- * All other paths fall through to the dashboard guard; the full route tree
- * is wired up in Stage 3.
+ * - `/setup` — first-run setup wizard (always accessible)
+ * - `/login` — master password login
+ * - `/` — dashboard (protected, inside MainLayout)
+ * - `/map` — world map (protected)
+ * - `/jails` — jail list (protected)
+ * - `/jails/:name` — jail detail (protected)
+ * - `/config` — configuration editor (protected)
+ * - `/history` — event history (protected)
+ * - `/blocklists` — blocklist management (protected)
+ * All unmatched paths redirect to `/`.
*/
import { FluentProvider } from "@fluentui/react-components";
@@ -19,9 +24,16 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { lightTheme } from "./theme/customTheme";
import { AuthProvider } from "./providers/AuthProvider";
import { RequireAuth } from "./components/RequireAuth";
+import { MainLayout } from "./layouts/MainLayout";
import { SetupPage } from "./pages/SetupPage";
import { LoginPage } from "./pages/LoginPage";
import { DashboardPage } from "./pages/DashboardPage";
+import { MapPage } from "./pages/MapPage";
+import { JailsPage } from "./pages/JailsPage";
+import { JailDetailPage } from "./pages/JailDetailPage";
+import { ConfigPage } from "./pages/ConfigPage";
+import { HistoryPage } from "./pages/HistoryPage";
+import { BlocklistsPage } from "./pages/BlocklistsPage";
/**
* Root application component — mounts providers and top-level routes.
@@ -36,17 +48,24 @@ function App(): JSX.Element {
} />
} />
- {/* Protected routes */}
+ {/* Protected routes — all rendered inside MainLayout */}
-
+
}
- />
+ >
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
- {/* Fallback — redirect unknown paths to dashboard (guard will redirect to login if needed) */}
+ {/* Fallback — redirect unknown paths to dashboard */}
} />
diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx
new file mode 100644
index 0000000..f5bf06e
--- /dev/null
+++ b/frontend/src/layouts/MainLayout.tsx
@@ -0,0 +1,279 @@
+/**
+ * Main application layout.
+ *
+ * Provides the persistent sidebar navigation and the main content area
+ * for all authenticated pages. The sidebar collapses from 240 px to
+ * icon-only (48 px) on small screens.
+ */
+
+import { useCallback, useState } from "react";
+import {
+ Button,
+ makeStyles,
+ mergeClasses,
+ Text,
+ tokens,
+ Tooltip,
+} from "@fluentui/react-components";
+import {
+ GridRegular,
+ MapRegular,
+ ShieldRegular,
+ SettingsRegular,
+ HistoryRegular,
+ ListRegular,
+ SignOutRegular,
+ NavigationRegular,
+} from "@fluentui/react-icons";
+import { NavLink, Outlet, useNavigate } from "react-router-dom";
+import { useAuth } from "../providers/AuthProvider";
+
+// ---------------------------------------------------------------------------
+// Styles
+// ---------------------------------------------------------------------------
+
+const SIDEBAR_FULL = "240px";
+const SIDEBAR_COLLAPSED = "48px";
+
+const useStyles = makeStyles({
+ root: {
+ display: "flex",
+ height: "100vh",
+ overflow: "hidden",
+ backgroundColor: tokens.colorNeutralBackground3,
+ },
+
+ // Sidebar
+ sidebar: {
+ display: "flex",
+ flexDirection: "column",
+ width: SIDEBAR_FULL,
+ minWidth: SIDEBAR_COLLAPSED,
+ backgroundColor: tokens.colorNeutralBackground1,
+ borderRightWidth: "1px",
+ borderRightStyle: "solid",
+ borderRightColor: tokens.colorNeutralStroke2,
+ transition: "width 200ms ease",
+ overflow: "hidden",
+ flexShrink: 0,
+ },
+ sidebarCollapsed: {
+ width: SIDEBAR_COLLAPSED,
+ },
+
+ sidebarHeader: {
+ display: "flex",
+ alignItems: "center",
+ height: "52px",
+ paddingLeft: tokens.spacingHorizontalM,
+ paddingRight: tokens.spacingHorizontalS,
+ gap: tokens.spacingHorizontalS,
+ borderBottomWidth: "1px",
+ borderBottomStyle: "solid",
+ borderBottomColor: tokens.colorNeutralStroke2,
+ flexShrink: 0,
+ },
+ logo: {
+ fontWeight: 600,
+ fontSize: "16px",
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ color: tokens.colorBrandForeground1,
+ flexGrow: 1,
+ },
+
+ // Nav items list
+ navList: {
+ display: "flex",
+ flexDirection: "column",
+ gap: "2px",
+ padding: tokens.spacingVerticalS,
+ overflowY: "auto",
+ flexGrow: 1,
+ },
+
+ navLink: {
+ display: "flex",
+ alignItems: "center",
+ gap: tokens.spacingHorizontalS,
+ padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalS}`,
+ borderRadius: tokens.borderRadiusMedium,
+ textDecoration: "none",
+ color: tokens.colorNeutralForeground2,
+ whiteSpace: "nowrap",
+ overflow: "hidden",
+ ":hover": {
+ backgroundColor: tokens.colorNeutralBackground1Hover,
+ color: tokens.colorNeutralForeground1,
+ },
+ },
+ navLinkActive: {
+ backgroundColor: tokens.colorNeutralBackground1Selected,
+ color: tokens.colorBrandForeground1,
+ ":hover": {
+ backgroundColor: tokens.colorNeutralBackground1Selected,
+ },
+ },
+ navLabel: {
+ fontSize: "14px",
+ lineHeight: "20px",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+
+ // Sidebar footer (logout)
+ sidebarFooter: {
+ borderTopWidth: "1px",
+ borderTopStyle: "solid",
+ borderTopColor: tokens.colorNeutralStroke2,
+ padding: tokens.spacingVerticalS,
+ flexShrink: 0,
+ },
+
+ // Main content
+ main: {
+ display: "flex",
+ flexDirection: "column",
+ flexGrow: 1,
+ overflow: "auto",
+ },
+ content: {
+ flexGrow: 1,
+ maxWidth: "1440px",
+ width: "100%",
+ margin: "0 auto",
+ padding: tokens.spacingVerticalL,
+ },
+});
+
+// ---------------------------------------------------------------------------
+// Nav item data
+// ---------------------------------------------------------------------------
+
+interface NavItem {
+ label: string;
+ to: string;
+ icon: React.ReactElement;
+ end?: boolean;
+}
+
+const NAV_ITEMS: NavItem[] = [
+ { label: "Dashboard", to: "/", icon: , end: true },
+ { label: "World Map", to: "/map", icon: },
+ { label: "Jails", to: "/jails", icon: },
+ { label: "Configuration", to: "/config", icon: },
+ { label: "History", to: "/history", icon: },
+ { label: "Blocklists", to: "/blocklists", icon: },
+];
+
+// ---------------------------------------------------------------------------
+// Component
+// ---------------------------------------------------------------------------
+
+/**
+ * Main application shell with sidebar navigation and content area.
+ *
+ * Renders child routes via ``. Use inside React Router
+ * as a layout route wrapping all authenticated pages.
+ */
+export function MainLayout(): JSX.Element {
+ const styles = useStyles();
+ const { logout } = useAuth();
+ const navigate = useNavigate();
+ const [collapsed, setCollapsed] = useState(false);
+
+ const toggleCollapse = useCallback(() => {
+ setCollapsed((prev) => !prev);
+ }, []);
+
+ const handleLogout = useCallback(async () => {
+ await logout();
+ navigate("/login", { replace: true });
+ }, [logout, navigate]);
+
+ return (
+
+ {/* ---------------------------------------------------------------- */}
+ {/* Sidebar */}
+ {/* ---------------------------------------------------------------- */}
+
+
+ {/* ---------------------------------------------------------------- */}
+ {/* Main content */}
+ {/* ---------------------------------------------------------------- */}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/BlocklistsPage.tsx b/frontend/src/pages/BlocklistsPage.tsx
new file mode 100644
index 0000000..17bc425
--- /dev/null
+++ b/frontend/src/pages/BlocklistsPage.tsx
@@ -0,0 +1,23 @@
+/**
+ * Blocklists placeholder page — full implementation in Stage 10.
+ */
+
+import { Text, makeStyles, tokens } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ root: { padding: tokens.spacingVerticalXXL },
+});
+
+export function BlocklistsPage(): JSX.Element {
+ const styles = useStyles();
+ return (
+
+
+ Blocklists
+
+
+ Blocklist management will be implemented in Stage 10.
+
+
+ );
+}
diff --git a/frontend/src/pages/ConfigPage.tsx b/frontend/src/pages/ConfigPage.tsx
new file mode 100644
index 0000000..08b0f10
--- /dev/null
+++ b/frontend/src/pages/ConfigPage.tsx
@@ -0,0 +1,23 @@
+/**
+ * Configuration placeholder page — full implementation in Stage 8.
+ */
+
+import { Text, makeStyles, tokens } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ root: { padding: tokens.spacingVerticalXXL },
+});
+
+export function ConfigPage(): JSX.Element {
+ const styles = useStyles();
+ return (
+
+
+ Configuration
+
+
+ fail2ban configuration editor will be implemented in Stage 8.
+
+
+ );
+}
diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx
new file mode 100644
index 0000000..c83c85c
--- /dev/null
+++ b/frontend/src/pages/HistoryPage.tsx
@@ -0,0 +1,23 @@
+/**
+ * Ban history placeholder page — full implementation in Stage 9.
+ */
+
+import { Text, makeStyles, tokens } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ root: { padding: tokens.spacingVerticalXXL },
+});
+
+export function HistoryPage(): JSX.Element {
+ const styles = useStyles();
+ return (
+
+
+ History
+
+
+ Historical ban query view will be implemented in Stage 9.
+
+
+ );
+}
diff --git a/frontend/src/pages/JailDetailPage.tsx b/frontend/src/pages/JailDetailPage.tsx
new file mode 100644
index 0000000..2f545e5
--- /dev/null
+++ b/frontend/src/pages/JailDetailPage.tsx
@@ -0,0 +1,25 @@
+/**
+ * Jail detail placeholder page — full implementation in Stage 6.
+ */
+
+import { Text, makeStyles, tokens } from "@fluentui/react-components";
+import { useParams } from "react-router-dom";
+
+const useStyles = makeStyles({
+ root: { padding: tokens.spacingVerticalXXL },
+});
+
+export function JailDetailPage(): JSX.Element {
+ const styles = useStyles();
+ const { name } = useParams<{ name: string }>();
+ return (
+
+
+ Jail: {name}
+
+
+ Jail detail view will be implemented in Stage 6.
+
+
+ );
+}
diff --git a/frontend/src/pages/JailsPage.tsx b/frontend/src/pages/JailsPage.tsx
new file mode 100644
index 0000000..543485b
--- /dev/null
+++ b/frontend/src/pages/JailsPage.tsx
@@ -0,0 +1,23 @@
+/**
+ * Jails overview placeholder page — full implementation in Stage 6.
+ */
+
+import { Text, makeStyles, tokens } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ root: { padding: tokens.spacingVerticalXXL },
+});
+
+export function JailsPage(): JSX.Element {
+ const styles = useStyles();
+ return (
+
+
+ Jails
+
+
+ Jail management will be implemented in Stage 6.
+
+
+ );
+}
diff --git a/frontend/src/pages/MapPage.tsx b/frontend/src/pages/MapPage.tsx
new file mode 100644
index 0000000..088b0c5
--- /dev/null
+++ b/frontend/src/pages/MapPage.tsx
@@ -0,0 +1,23 @@
+/**
+ * World Map placeholder page — full implementation in Stage 5.
+ */
+
+import { Text, makeStyles, tokens } from "@fluentui/react-components";
+
+const useStyles = makeStyles({
+ root: { padding: tokens.spacingVerticalXXL },
+});
+
+export function MapPage(): JSX.Element {
+ const styles = useStyles();
+ return (
+
+
+ World Map
+
+
+ Geographical ban overview will be implemented in Stage 5.
+
+
+ );
+}