mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-23 08:51:29 +02:00
- **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>
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { UILog } from "@bindings/services";
|
|
|
|
type Level = "trace" | "debug" | "info" | "warn" | "error";
|
|
|
|
const METHOD_LEVELS: Record<string, Level> = {
|
|
trace: "trace",
|
|
debug: "debug",
|
|
log: "info",
|
|
info: "info",
|
|
warn: "warn",
|
|
error: "error",
|
|
};
|
|
|
|
const IGNORED_SOURCES = new Set(["welcome.ts"]);
|
|
|
|
const RATE_LIMIT = 50;
|
|
const RATE_WINDOW_MS = 1000;
|
|
|
|
let installed = false;
|
|
let inForward = false;
|
|
let windowStart = 0;
|
|
let windowCount = 0;
|
|
|
|
function describeCause(rawCause: unknown): string {
|
|
if (rawCause instanceof Error) return `${rawCause.name}: ${rawCause.message}`;
|
|
if (typeof rawCause === "object" && rawCause !== null) {
|
|
try {
|
|
return JSON.stringify(rawCause);
|
|
} catch {
|
|
// Circular ref — fall through to a tag instead of "[object Object]".
|
|
return `<${rawCause.constructor?.name ?? "object"}>`;
|
|
}
|
|
}
|
|
return String(rawCause);
|
|
}
|
|
|
|
function formatCause(rawCause: unknown): string {
|
|
if (rawCause === undefined) return "";
|
|
return `\ncaused by ${describeCause(rawCause)}`;
|
|
}
|
|
|
|
// WebKit (macOS WKWebView) omits the "Name: message" header from Error.stack,
|
|
// so a bare stack hides the real cause. Prepend name+message, then the stack.
|
|
function formatError(e: Error): string {
|
|
const head = `${e.name}: ${e.message}`;
|
|
const cause = formatCause((e as { cause?: unknown }).cause);
|
|
if (!e.stack) return `${head}${cause}`;
|
|
if (e.stack.startsWith(head)) return `${head}${cause}`;
|
|
return `${head}${cause}\n${e.stack}`;
|
|
}
|
|
|
|
function format(args: unknown[]): string {
|
|
return args
|
|
.map((a) => {
|
|
if (typeof a === "string") return a;
|
|
if (a instanceof Error) return formatError(a);
|
|
try {
|
|
return JSON.stringify(a);
|
|
} catch {
|
|
return String(a);
|
|
}
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
function parseStackLine(line: string): string {
|
|
// Find the file:line:col tail at the end of the path.
|
|
const colonCol = line.lastIndexOf(":");
|
|
if (colonCol <= 0) return "";
|
|
const colonLine = line.lastIndexOf(":", colonCol - 1);
|
|
if (colonLine <= 0) return "";
|
|
const col = line.slice(colonCol + 1);
|
|
const lineNo = line.slice(colonLine + 1, colonCol);
|
|
if (!/^\d+$/.test(col) || !/^\d+$/.test(lineNo)) return "";
|
|
const before = line.slice(0, colonLine);
|
|
const sep = Math.max(
|
|
before.lastIndexOf("/"),
|
|
before.lastIndexOf("\\"),
|
|
before.lastIndexOf("("),
|
|
before.lastIndexOf(" "),
|
|
);
|
|
const file = before.slice(sep + 1);
|
|
if (!file.includes(".")) return "";
|
|
return `${file}:${lineNo}`;
|
|
}
|
|
|
|
function callerSource(): string {
|
|
const stack = new Error().stack;
|
|
if (!stack) return "";
|
|
for (const line of stack.split("\n").slice(1)) {
|
|
if (line.includes("/logs.ts")) continue;
|
|
const parsed = parseStackLine(line);
|
|
if (parsed) return parsed;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function forward(level: Level, args: unknown[]) {
|
|
if (inForward) return;
|
|
inForward = true;
|
|
try {
|
|
const now = Date.now();
|
|
if (now - windowStart >= RATE_WINDOW_MS) {
|
|
windowStart = now;
|
|
windowCount = 0;
|
|
}
|
|
if (++windowCount > RATE_LIMIT) return;
|
|
|
|
const source = callerSource();
|
|
if (IGNORED_SOURCES.has(source.split(":")[0])) return;
|
|
// Don't touch console here — it would recurse back into forward().
|
|
UILog.Log(level, source, format(args)).catch(() => {});
|
|
} catch {
|
|
// Swallow — log forwarding must never throw back into the caller.
|
|
} finally {
|
|
inForward = false;
|
|
}
|
|
}
|
|
|
|
export function initLogForwarding() {
|
|
if (installed) return;
|
|
installed = true;
|
|
|
|
const c = console as unknown as Record<string, (...a: unknown[]) => void>;
|
|
for (const [method, level] of Object.entries(METHOD_LEVELS)) {
|
|
const original = c[method]?.bind(console);
|
|
c[method] = (...args: unknown[]) => {
|
|
original?.(...args);
|
|
forward(level, args);
|
|
};
|
|
}
|
|
|
|
globalThis.addEventListener("error", (e) => {
|
|
forward("error", [`uncaught error: ${e.message}`, e.error ?? ""]);
|
|
});
|
|
globalThis.addEventListener("unhandledrejection", (e) => {
|
|
forward("error", ["unhandled promise rejection:", e.reason]);
|
|
});
|
|
}
|