[management,client] 0.75.0 release with new desktop UI (#6473)

- **Wails v3 application** (`client/ui`) with a React + TypeScript + Tailwind frontend replacing the Fyne UI: main connection view, exit-node switcher, networks/peers browser with detail panels, profile management, settings (general, network, SSH, security, troubleshooting, appearance), debug-bundle creation, and a first-run welcome flow.
- **Internationalization**: go-i18n bundle with 9 locales (en, de, es, fr, hu, it, pt, ru, zh-CN) shared between the tray and the frontend.
- **New system tray** implementation with per-platform theme-aware icons, including a native XEmbed host for Linux (`xembed_tray_linux.c`) and a Linux theme watcher.
- **Session handling**: auth session watcher (`client/internal/auth/sessionwatch`), pending login flow, session-expiration dialog and tray notifications, and `netbird login` improvements.
- **Daemon API extensions** (`daemon.proto`): status stream subscription, event stream, networks/exit-node selection endpoints, and richer full status — with probe throttling on the daemon side to protect against UI-driven request storms.
- **UI preferences store** persisted per profile, autostart management via the daemon (single source of truth in HKCU on Windows).
- **Build system**: Taskfile-based builds per platform (macOS, Linux, Windows), Docker cross-compilation images, MSIX/NSIS/nfpm/AppImage packaging, and a new `frontend-ui` CI workflow.

Co-authored-by: Zoltan Papp <zoltan.pmail@gmail.com>
Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
Co-authored-by: braginini <bangvalo@gmail.com>
Co-authored-by: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com>
Co-authored-by: riccardom <riccardomanfrin@gmail.com>
This commit is contained in:
Maycon Santos
2026-07-06 13:47:16 +02:00
committed by GitHub
co-authored by Zoltan Papp Eduard Gert braginini Pascal Fischer riccardom
parent c9d387bd0d
commit 91acb8147c
389 changed files with 46269 additions and 6684 deletions
@@ -0,0 +1,50 @@
import { useTranslation } from "react-i18next";
import { AlertTriangleIcon, DownloadIcon } from "lucide-react";
import { Browser } from "@wailsio/runtime";
import { Button } from "@/components/buttons/Button";
import { useStatus } from "@/contexts/StatusContext.tsx";
const RELEASES_URL = "https://github.com/netbirdio/netbird/releases/latest";
function openUrl(url: string) {
Browser.OpenURL(url).catch(() => globalThis.open(url, "_blank"));
}
export const DaemonOutdatedOverlay = () => {
const { t } = useTranslation();
const { isDaemonOutdated } = useStatus();
if (!isDaemonOutdated) return null;
return (
<div
className={
"wails-draggable fixed inset-0 z-[100] flex cursor-default select-none items-center justify-center bg-nb-gray-950 backdrop-blur-sm"
}
>
<div className={"flex max-w-lg flex-col items-center gap-5 px-8 text-center"}>
<div
className={
"flex h-11 w-11 items-center justify-center rounded-xl border border-nb-gray-900 bg-nb-gray-920 text-amber-500"
}
>
<AlertTriangleIcon size={20} />
</div>
<div className={"flex flex-col items-center gap-1"}>
<p className={"text-base font-medium text-nb-gray-50"}>
{t("daemon.outdated.title")}
</p>
<p className={"text-sm text-nb-gray-300"}>{t("daemon.outdated.description")}</p>
</div>
<div className={"wails-no-draggable"}>
<Button variant={"primary"} size={"xs"} onClick={() => openUrl(RELEASES_URL)}>
<DownloadIcon size={14} />
{t("update.card.getInstaller")}
</Button>
</div>
</div>
</div>
);
};
@@ -0,0 +1,52 @@
import { useTranslation } from "react-i18next";
import { AlertCircleIcon, BookText } from "lucide-react";
import { Browser } from "@wailsio/runtime";
import { Button } from "@/components/buttons/Button";
import { useStatus } from "@/contexts/StatusContext.tsx";
const DOCS_URL = "https://docs.netbird.io/how-to/installation";
function openUrl(url: string) {
Browser.OpenURL(url).catch(() => globalThis.open(url, "_blank"));
}
export const DaemonUnavailableOverlay = () => {
const { t } = useTranslation();
const { isDaemonUnavailable } = useStatus();
if (!isDaemonUnavailable) return null;
return (
<div
className={
"wails-draggable fixed inset-0 z-[100] flex cursor-default select-none items-center justify-center bg-nb-gray-950 backdrop-blur-sm"
}
>
<div className={"flex max-w-lg flex-col items-center gap-5 px-8 text-center"}>
<div
className={
"flex h-11 w-11 items-center justify-center rounded-xl border border-nb-gray-900 bg-nb-gray-920 text-red-500"
}
>
<AlertCircleIcon size={20} />
</div>
<div className={"flex flex-col items-center gap-1"}>
<p className={"text-base font-medium text-nb-gray-50"}>
{t("daemon.unavailable.title")}
</p>
<p className={"text-sm text-nb-gray-300"}>
{t("daemon.unavailable.description")}
</p>
</div>
<div className={"wails-no-draggable"}>
<Button variant={"secondary"} size={"xs"} onClick={() => openUrl(DOCS_URL)}>
<BookText size={14} />
{t("daemon.unavailable.docsLink")}
</Button>
</div>
</div>
</div>
);
};
@@ -0,0 +1,31 @@
import { type ComponentType } from "react";
import { type LucideProps } from "lucide-react";
import { cn } from "@/lib/cn";
import { SquareIcon } from "@/components/SquareIcon";
import { isMacOS } from "@/lib/platform";
// Knob to shift the centered main-window content up/down together.
export const contentVerticalOffset = (): string => (isMacOS() ? "0.6rem" : "-1.4rem");
export const contentTop = (base: string) => `calc(${base} + ${contentVerticalOffset()})`;
type Props = {
icon: ComponentType<LucideProps>;
title: string;
description?: string;
className?: string;
};
export const EmptyState = ({ icon, title, description, className }: Props) => {
return (
<div className={cn("py-12 text-center", className)}>
<div
className={"relative mx-auto flex max-w-sm flex-col items-center justify-start"}
style={{ top: contentTop("7.8rem") }}
>
<SquareIcon icon={icon} className={"mb-3"} />
<p className={"mb-1 text-[0.95rem] font-medium text-nb-gray-200"}>{title}</p>
{description && <p className={"text-sm text-nb-gray-350"}>{description}</p>}
</div>
</div>
);
};
@@ -0,0 +1,22 @@
import { type ComponentType } from "react";
import { FunnelXIcon, type LucideProps } from "lucide-react";
import { useTranslation } from "react-i18next";
import { EmptyState } from "./EmptyState";
type Props = {
icon?: ComponentType<LucideProps>;
title?: string;
description?: string;
};
export const NoResults = ({ icon = FunnelXIcon, title, description }: Props) => {
const { t } = useTranslation();
return (
<EmptyState
icon={icon}
title={title ?? t("common.noResults.title")}
description={description ?? t("common.noResults.description")}
className={"pointer-events-none relative -top-[3.8rem]"}
/>
);
};
@@ -0,0 +1,16 @@
import { GlobeOffIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { EmptyState } from "./EmptyState";
export const NotConnectedState = () => {
const { t } = useTranslation();
return (
<div className={"relative top-[3rem] w-full"}>
<EmptyState
icon={GlobeOffIcon}
title={t("notConnected.title")}
description={t("notConnected.description")}
/>
</div>
);
};