+
);
-};
+});
diff --git a/client/ui/frontend/src/modules/auto-update/UpdatingOverlay.tsx b/client/ui/frontend/src/modules/auto-update/UpdatingOverlay.tsx
new file mode 100644
index 000000000..aa1cef7de
--- /dev/null
+++ b/client/ui/frontend/src/modules/auto-update/UpdatingOverlay.tsx
@@ -0,0 +1,110 @@
+import { Loader2, XCircle } from "lucide-react";
+import { Button } from "@/components/Button";
+
+type Props = {
+ version: string | null;
+ error: string | null;
+ onDismiss: () => void;
+};
+
+type Variant = {
+ title: string;
+ description: string;
+ message?: string;
+};
+
+function classifyError(msg: string, version: string | null): Variant {
+ const lower = msg.toLowerCase();
+ const target = version ? `v${version}` : "the new version";
+ if (lower.includes("timeout") || lower.includes("timed out")) {
+ return {
+ title: "Update Is Taking Too Long",
+ description: `Installing ${target} took too long and didn't finish.`,
+ };
+ }
+ if (lower.includes("cancel")) {
+ return {
+ title: "Update Was Stopped",
+ description: `The update to ${target} was canceled before it finished.`,
+ };
+ }
+ return {
+ title: "Couldn't Install the Update",
+ description: `${target} couldn't be installed.`,
+ message: msg || "unknown error",
+ };
+}
+
+export const UpdatingOverlay = ({ version, error, onDismiss }: Props) => {
+ const isError = Boolean(error);
+ const errorInfo = error ? classifyError(error, version) : null;
+
+ return (
+
{
+ if (isError) return;
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ onKeyDown={(e) => {
+ if (isError) return;
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+ {isError ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {isError
+ ? errorInfo!.title
+ : version
+ ? `Updating NetBird to v${version}`
+ : "Updating NetBird"}
+
+
+ {isError ? (
+ <>
+ {errorInfo!.description}
+ {errorInfo!.message && (
+ <>
+
+
+ {errorInfo!.message}
+
+ >
+ )}
+ >
+ ) : (
+ "A newer version is available and is being installed. NetBird will restart automatically once the update is finished."
+ )}
+
+
+
+ {isError && (
+
+
+ Close
+
+
+ )}
+
+
+ );
+};
diff --git a/client/ui/frontend/src/modules/settings/Settings.tsx b/client/ui/frontend/src/modules/settings/Settings.tsx
index 841aebfa5..176625119 100644
--- a/client/ui/frontend/src/modules/settings/Settings.tsx
+++ b/client/ui/frontend/src/modules/settings/Settings.tsx
@@ -7,6 +7,7 @@ import { VerticalTabs } from "@/components/VerticalTabs.tsx";
import { SettingsNavigationTriggers } from "@/modules/settings/SettingsNavigationTriggers.tsx";
import { SettingsProvider } from "@/modules/settings/SettingsContext.tsx";
import { SettingsGeneral } from "@/modules/settings/SettingsGeneral.tsx";
+import { SettingsAppearance } from "@/modules/settings/SettingsAppearance.tsx";
import { SettingsNetwork } from "@/modules/settings/SettingsNetwork.tsx";
import { SettingsSecurity } from "@/modules/settings/SettingsSecurity.tsx";
import { SettingsSSH } from "@/modules/settings/SettingsSSH.tsx";
@@ -14,15 +15,35 @@ import { SettingsAdvanced } from "@/modules/settings/SettingsAdvanced.tsx";
import { SettingsTroubleshooting } from "@/modules/settings/SettingsTroubleshooting.tsx";
import { SettingsAbout } from "@/modules/settings/SettingsAbout.tsx";
+const LAST_TAB_KEY = "netbird:settings:lastTab";
+
+const readLastTab = () => {
+ try {
+ return localStorage.getItem(LAST_TAB_KEY);
+ } catch {
+ return null;
+ }
+};
+
export const Settings = () => {
const location = useLocation();
const navState = location.state as { tab?: string } | null;
- const [active, setActive] = useState(navState?.tab ?? "general");
+ const [active, setActive] = useState(
+ () => navState?.tab ?? readLastTab() ?? "general",
+ );
useEffect(() => {
if (navState?.tab) setActive(navState.tab);
}, [navState?.tab, location.key]);
+ useEffect(() => {
+ try {
+ localStorage.setItem(LAST_TAB_KEY, active);
+ } catch {
+ // ignore quota / unavailable storage
+ }
+ }, [active]);
+
return (
@@ -37,6 +58,9 @@ export const Settings = () => {
+
+
+
diff --git a/client/ui/frontend/src/modules/settings/SettingsAppearance.tsx b/client/ui/frontend/src/modules/settings/SettingsAppearance.tsx
new file mode 100644
index 000000000..e740bf424
--- /dev/null
+++ b/client/ui/frontend/src/modules/settings/SettingsAppearance.tsx
@@ -0,0 +1,88 @@
+import FancyToggleSwitch from "@/components/FancyToggleSwitch";
+import { CardSelect } from "@/components/CardSelect.tsx";
+import { SectionGroup } from "@/modules/settings/SettingsSection.tsx";
+import {
+ useAppearance,
+ type AppearanceView,
+} from "@/modules/appearance/AppearanceContext.tsx";
+import simpleScreen from "@/assets/screens/simple.png";
+import advancedScreen from "@/assets/screens/advanced.png";
+
+const ScreenPreview = ({ src, alt }: { src: string; alt: string }) => (
+
+);
+
+export function SettingsAppearance() {
+ const {
+ view,
+ setView,
+ showPeersNav,
+ showResourcesNav,
+ showExitNodeNav,
+ showProfileSelector,
+ showSettingsButton,
+ setField,
+ } = useAppearance();
+
+ return (
+ <>
+
+ setView(v as AppearanceView)}
+ >
+ }
+ />
+ }
+ />
+
+
+
+
+ setField("showPeersNav", v)}
+ label={"Peers"}
+ helpText={"Show the Peers item in the side navigation."}
+ />
+ setField("showResourcesNav", v)}
+ label={"Resources"}
+ helpText={"Show the Resources item in the side navigation."}
+ />
+ setField("showExitNodeNav", v)}
+ label={"Exit Node"}
+ helpText={"Show the active exit node in the side navigation."}
+ />
+ setField("showProfileSelector", v)}
+ label={"Profile Selector"}
+ helpText={"Show the profile selector in the header."}
+ />
+ setField("showSettingsButton", v)}
+ label={"Settings Button"}
+ helpText={"Show the settings button in the header."}
+ />
+
+ >
+ );
+}
diff --git a/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx b/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx
index 3a751beb1..97e160fa1 100644
--- a/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx
+++ b/client/ui/frontend/src/modules/settings/SettingsNavigationTriggers.tsx
@@ -10,6 +10,7 @@ import {
ShieldIcon,
SlidersHorizontalIcon,
SquareTerminalIcon,
+ SwatchBookIcon,
} from "lucide-react";
export const SettingsNavigationTriggers = () => {
@@ -29,6 +30,11 @@ export const SettingsNavigationTriggers = () => {
icon={SlidersHorizontalIcon}
title={"General"}
/>
+
+
+
+
+ Session expired
+
+ Your NetBird session has expired. Sign in again to keep your devices connected.
+
+
+
+ Later
+
+
+ Sign in
+
+
+
+ );
+}
diff --git a/client/ui/main.go b/client/ui/main.go
index 21d53dd9c..2912eafae 100644
--- a/client/ui/main.go
+++ b/client/ui/main.go
@@ -127,9 +127,7 @@ func main() {
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "NetBird",
Width: 925,
- MinWidth: 925,
Height: 615,
- MinHeight: 615,
Hidden: true,
BackgroundColour: application.NewRGB(24, 26, 29),
URL: "/",