refactor, lint, cleanup

This commit is contained in:
Eduard Gert
2026-06-09 16:31:52 +02:00
parent bada2b5b78
commit f8e3ac6d92
79 changed files with 1441 additions and 2463 deletions
@@ -8,8 +8,7 @@ import {
import { SetConfigParams } from "@bindings/services/models.js";
import { ConfirmDialog } from "@/components/dialog/ConfirmDialog";
import { useAutoSizeWindow } from "@/hooks/useAutoSizeWindow";
import { errorDialog } from "@/lib/dialogs";
import { formatErrorMessage } from "@/lib/errors";
import { errorDialog, formatErrorMessage } from "@/lib/errors";
import i18next from "@/lib/i18n";
import { isCloudManagementUrl } from "@/hooks/useManagementUrl";
import { WelcomeStepTray } from "./WelcomeStepTray";
@@ -17,18 +16,8 @@ import { WelcomeStepManagement } from "./WelcomeStepManagement";
const WINDOW_WIDTH = 360;
// WelcomeStep is the orchestrator's state machine. The transitions:
// tray → management (if eligible) → finish
// tray → finish (otherwise)
// Login itself is no longer part of onboarding — once the welcome window
// closes the user lands in the main window and clicks Connect there.
type WelcomeStep = "tray" | "management";
// shouldShowManagementStep asks the user about Cloud vs self-hosted only
// on a pristine setup — default profile, no email recorded (no successful
// login yet), and the management URL is either unset or already the cloud
// default. Any other state means the user (or a previous run) already
// made a deliberate choice and we shouldn't second-guess it.
function shouldShowManagementStep(
activeProfile: string,
email: string,
@@ -39,10 +28,6 @@ function shouldShowManagementStep(
return isCloudManagementUrl(managementUrl);
}
// initial flow snapshot resolved at mount. Held in component state so the
// step-2 management input can hydrate from initialUrl, and so the
// "should we even show step 2" check is computed once (the user can't
// change profile / URL from inside the welcome window).
type InitialState = {
profileName: string;
username: string;
@@ -54,24 +39,12 @@ export default function WelcomeDialog() {
const [step, setStep] = useState<WelcomeStep>("tray");
const [initial, setInitial] = useState<InitialState | null>(null);
const [closing, setClosing] = useState(false);
// ready=false until the daemon probe resolves — keeps the window
// Hidden so neither the empty padding-only frame (Linux/GNOME paints
// through) nor a placeholder div leaks onto screen.
const contentRef = useAutoSizeWindow<HTMLDivElement>(WINDOW_WIDTH, initial !== null);
// Probe daemon state on mount: who's the active profile, do they
// have an email recorded, and what management URL is configured?
// Errors fall through to "skip the management step" so a daemon
// hiccup never blocks onboarding entirely.
useEffect(() => {
let cancelled = false;
(async () => {
try {
// Resolve username + active profile first so GetConfig + List
// can target the actual profile (passing empty strings would
// work today since the daemon falls back to the default
// profile, but being explicit shields us from future
// changes to that fallback).
const [username, active] = await Promise.all([
ProfilesSvc.Username(),
ProfilesSvc.GetActive(),
@@ -97,8 +70,6 @@ export default function WelcomeDialog() {
} catch (e) {
console.error("welcome: initial probe failed", e);
if (cancelled) return;
// Conservative fallback: skip the management step rather
// than block onboarding behind a daemon hiccup.
setInitial({
profileName: "default",
username: "",
@@ -112,10 +83,6 @@ export default function WelcomeDialog() {
};
}, []);
// finish persists the onboarding flag, opens the main window so the
// user has somewhere to land, and closes the welcome window. Called
// at the end of every successful flow (tray-only and tray→management
// alike). The Connect button in the main window picks up from here.
const finish = useCallback(async () => {
if (closing) return;
setClosing(true);
@@ -148,10 +115,7 @@ export default function WelcomeDialog() {
async (url: string) => {
if (!initial) return;
try {
// SetConfig is a partial update — pointer fields left
// undefined are preserved (services/settings.go). We only
// touch managementUrl; adminUrl stays empty here because
// the daemon already has its own value loaded.
// SetConfig is a partial update — undefined fields are preserved Go-side.
await SettingsSvc.SetConfig(
new SetConfigParams({
profileName: initial.profileName,
@@ -18,16 +18,14 @@ import { cn } from "@/lib/cn.ts";
import { isMacOS } from "@/lib/platform.ts";
type WelcomeStepManagementProps = {
// initialUrl is the management URL the daemon is already configured
// with (empty / cloud-default both render as Cloud selected).
initialUrl: string;
// onContinue is invoked with the URL the user wants to persist. The
// parent owns the actual Settings.SetConfig call so the dialog stays
// free of context dependencies.
onContinue: (url: string) => Promise<void>;
};
export function WelcomeStepManagement({ initialUrl, onContinue }: WelcomeStepManagementProps) {
export function WelcomeStepManagement({
initialUrl,
onContinue,
}: Readonly<WelcomeStepManagementProps>) {
const { t } = useTranslation();
const startsCloud = isCloudManagementUrl(initialUrl);
const [mode, setMode] = useState<ManagementMode>(
@@ -35,21 +33,13 @@ export function WelcomeStepManagement({ initialUrl, onContinue }: WelcomeStepMan
);
const [url, setUrl] = useState(startsCloud ? "" : initialUrl);
const [syntaxError, setSyntaxError] = useState<string | null>(null);
// unreachable: soft warning. Continue stays enabled — user can confirm
// they typed it right and proceed (matches self-hosted-behind-internal-
// DNS / VPN scenarios where the in-app fetch would false-negative).
const [unreachable, setUnreachable] = useState(false);
const [checking, setChecking] = useState(false);
const trimmedUrl = url.trim();
const syntaxValid = mode === ManagementMode.Cloud || isValidManagementUrl(trimmedUrl);
// Continue is no longer disabled for an empty / invalid self-hosted
// URL; a Continue click in that state focuses the input and renders
// an inline error so the user actively notices what's missing.
const inputRef = useRef<HTMLInputElement | null>(null);
// Reset inline error/warning whenever the user edits the URL or flips
// mode — otherwise the warning lingers next to a just-corrected value.
useEffect(() => {
setSyntaxError(null);
setUnreachable(false);
@@ -58,9 +48,6 @@ export function WelcomeStepManagement({ initialUrl, onContinue }: WelcomeStepMan
const handleContinue = useCallback(async () => {
if (checking) return;
if (mode === ManagementMode.SelfHosted && (!trimmedUrl || !syntaxValid)) {
// Empty or syntactically invalid URL — Continue stays enabled
// so the click registers; surface the error inline and focus
// the input so the user has somewhere to fix it.
setSyntaxError(t("welcome.management.urlInvalid"));
inputRef.current?.focus();
return;
@@ -69,14 +56,11 @@ export function WelcomeStepManagement({ initialUrl, onContinue }: WelcomeStepMan
mode === ManagementMode.Cloud
? CLOUD_MANAGEMENT_URL
: normalizeManagementUrl(trimmedUrl);
if (mode === ManagementMode.SelfHosted) {
if (mode === ManagementMode.SelfHosted && !unreachable) {
setChecking(true);
const reachable = await checkManagementUrlReachable(target);
setChecking(false);
// First failed check: show soft warning + bail. A second click
// with the same URL skips the check (unreachable still true)
// so the user can proceed if they're sure.
if (!reachable && !unreachable) {
if (!reachable) {
setUnreachable(true);
return;
}
@@ -84,14 +68,10 @@ export function WelcomeStepManagement({ initialUrl, onContinue }: WelcomeStepMan
try {
await onContinue(target);
} catch (e) {
// Parent surfaces save errors via errorDialog; keep a console
// breadcrumb but don't double-render.
console.error("save management url:", e);
}
}, [checking, mode, syntaxValid, trimmedUrl, unreachable, onContinue, t]);
// Syntax problems are hard errors (red); an unreachable-but-valid URL is
// a soft, non-blocking caveat (orange).
const inputError = syntaxError ?? undefined;
const inputWarning = useMemo(
() => (!syntaxError && unreachable ? t("welcome.management.urlUnreachable") : undefined),
@@ -8,12 +8,7 @@ import trayScreenshotDarwin from "@/assets/img/tray-darwin.png";
import trayScreenshotWindows from "@/assets/img/tray-windows.png";
import trayScreenshotLinux from "@/assets/img/tray-linux.png";
// trayScreenshotForOS picks the marketing screenshot that shows the
// NetBird tray icon in its native menu/task bar — so the onboarding pitch
// matches the chrome the user will actually be hunting for. Evaluated
// inside the component so initPlatform() has finished by the time
// isMacOS/isWindows run (the static imports above only load the bytes,
// no platform check).
// Call at render time, not module scope: initPlatform() must run before isMacOS/isWindows.
function trayScreenshotForOS(): string {
if (isMacOS()) return trayScreenshotDarwin;
if (isWindows()) return trayScreenshotWindows;
@@ -24,7 +19,7 @@ type WelcomeStepTrayProps = {
onContinue: () => void;
};
export function WelcomeStepTray({ onContinue }: WelcomeStepTrayProps) {
export function WelcomeStepTray({ onContinue }: Readonly<WelcomeStepTrayProps>) {
const { t } = useTranslation();
const trayScreenshot = trayScreenshotForOS();