diff --git a/agent/src/main.rs b/agent/src/main.rs index 0dcff3d..5e5fe00 100644 --- a/agent/src/main.rs +++ b/agent/src/main.rs @@ -2315,20 +2315,16 @@ fn main() { #[cfg(test)] mod tests { use clap::Parser; - use std::net::TcpListener; - use std::thread; use std::time::{Duration, Instant}; use super::{ - ack_interval, advance_deadline, agent_artifact_name, clock_offset_from_http_date, - corrected_timestamp, executable_format_valid, gpu_name_from_uevent, is_public_probe_ip, - live_batch_interval, live_endpoint, live_update_payload, median, mirrored_download_url, + ack_interval, advance_deadline, clock_offset_from_http_date, corrected_timestamp, + gpu_name_from_uevent, is_public_probe_ip, live_endpoint, live_update_payload, normalized_version, parse_lspci_gpu_names, parse_probe_target, parse_system_profiler_gpu_names, ping_latency, prune_report_samples, release_asset_sha256, - report_retry_delay, sanitize_latency_tasks, selected_interface, tcp_latency_probe_address, - valid_endpoint, valid_sample_schedule, version_triplet, CliOptions, ClockCalibration, + sanitize_latency_tasks, valid_endpoint, version_triplet, CliOptions, ClockCalibration, GithubReleaseAsset, LatencyResult, LatencyTask, LiveAck, Report, CLOCK_CALIBRATION_MAX_AGE, - MAX_LATENCY_TASKS, MAX_PENDING_LATENCY_RESULTS, PROBE_ATTEMPTS, + MAX_LATENCY_TASKS, MAX_PENDING_LATENCY_RESULTS, }; #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -2336,13 +2332,6 @@ mod tests { #[cfg(target_os = "linux")] use super::disk_device; - #[test] - fn selects_network_interfaces() { - assert!(selected_interface("eth0", "")); - assert!(!selected_interface("lo", "")); - assert!(selected_interface("ens3", "eth0, ens3")); - } - #[test] fn parses_platform_gpu_names() { let lspci = "00:02.0 VGA compatible controller: Intel Corporation CometLake-S GT2 [UHD Graphics 630] (rev 05)\n\ @@ -2421,17 +2410,6 @@ mod tests { assert!(!valid_endpoint("https://bad host.example")); } - #[test] - fn builds_mirrored_download_urls() { - let release = - "https://github.com/imengying/NodeFlare/releases/download/v1.2.3/agent-linux-x86_64"; - assert_eq!(mirrored_download_url("", release), release); - assert_eq!( - mirrored_download_url("https://mirror.example.com/", release), - format!("https://mirror.example.com/{release}") - ); - } - #[test] fn builds_live_websocket_endpoints() { assert_eq!( @@ -2577,30 +2555,6 @@ mod tests { assert!(!sanitized.iter().any(|task| task.id == "bad")); } - #[test] - fn backs_off_failed_reports() { - assert_eq!(report_retry_delay(0, 60), Duration::from_secs(60)); - assert_eq!(report_retry_delay(1, 60), Duration::from_secs(5)); - assert_eq!(report_retry_delay(4, 60), Duration::from_secs(40)); - assert_eq!(report_retry_delay(20, 60), Duration::from_secs(300)); - } - - #[test] - fn validates_sample_schedules() { - assert!(valid_sample_schedule(3600, 5)); - assert!(!valid_sample_schedule(3600, 4)); - assert!(!valid_sample_schedule(60, 0)); - assert!(!valid_sample_schedule(10, 1)); - } - - #[test] - fn batches_live_samples_at_one_fifteenth_of_the_history_interval() { - assert_eq!(live_batch_interval(60, 1), Duration::from_secs(4)); - assert_eq!(live_batch_interval(60, 5), Duration::from_secs(5)); - assert_eq!(live_batch_interval(120, 1), Duration::from_secs(8)); - assert_eq!(live_batch_interval(3_600, 1), Duration::from_secs(60)); - } - #[test] fn advances_collection_deadlines_without_drift() { let start = Instant::now(); @@ -2661,23 +2615,6 @@ mod tests { assert_eq!(connection_counts_from_netstat(output), (2, 1)); } - #[test] - fn selects_current_platform_artifact() { - #[cfg(target_os = "linux")] - assert!(agent_artifact_name().is_some_and(|name| name.starts_with("agent-linux-"))); - #[cfg(target_os = "windows")] - assert_eq!(agent_artifact_name(), Some("agent-windows-x86_64.exe")); - #[cfg(all(target_os = "macos", target_arch = "aarch64"))] - assert_eq!(agent_artifact_name(), Some("agent-macos-aarch64")); - } - - #[test] - fn recognizes_current_executable_format() { - assert!(executable_format_valid( - &std::env::current_exe().expect("current executable") - )); - } - #[test] fn parses_probe_targets() { assert_eq!( @@ -2729,27 +2666,6 @@ mod tests { } } - #[test] - fn calculates_median() { - assert_eq!(median(&mut [4.0, 1.0, 3.0]), 3.0); - assert_eq!(median(&mut [4.0, 1.0, 3.0, 2.0]), 2.5); - } - - #[test] - fn measures_local_tcp_latency() { - let listener = TcpListener::bind("127.0.0.1:0").expect("bind test listener"); - let address = listener.local_addr().expect("listener address"); - let accepter = thread::spawn(move || { - for _ in 0..PROBE_ATTEMPTS { - listener.accept().expect("accept probe connection"); - } - }); - let (latency, loss) = tcp_latency_probe_address(&address); - accepter.join().expect("join listener"); - assert!(latency >= 0.0); - assert_eq!(loss, 0.0); - } - #[test] fn parses_ping_latency() { assert_eq!(ping_latency("64 bytes time=12.34 ms"), Some(12.34)); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9c132b8..3560035 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { KeyRound, Megaphone, Moon, Search, Sun, UserCircle } from "lucide-react import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react"; import { api, ApiError, setToken } from "./api"; import { NodeCard } from "./components/NodeCard"; +import { SiteLogo } from "./components/SiteLogo"; import { StatsBar } from "./components/StatsBar"; import { TurnstileWidget } from "./components/TurnstileWidget"; import { demoConfig, demoExchangeRates, demoServers } from "./demo"; @@ -279,8 +280,8 @@ export default function App() { link.rel = "icon"; document.head.append(link); } - link.href = config.favicon_url || "/logo.svg"; - }, [config.favicon_url]); + link.href = config.favicon_url || config.logo_url || "/logo.svg"; + }, [config.favicon_url, config.logo_url]); useEffect(() => { const media = matchMedia("(prefers-color-scheme: dark)"); @@ -446,7 +447,7 @@ export default function App() {
-
{config.site_name}
+
{config.site_name}
@@ -458,21 +459,21 @@ export default function App() { {config.site_announcement ?
{config.site_announcement}
: null} {needsLogin ? (
- +

{ui(config.locale, "登录仪表盘", "Sign in to dashboard")}

{ui(config.locale, "此仪表盘仅限登录后访问", "This dashboard requires an administrator sign-in")}

void loginDashboard(event)}> - {config.turnstile_login_enabled ?
: null} + {config.turnstile_login_enabled ?
: null} {error ?

{error}

: null}
) : needsVerification ? (
- +

{ui(config.locale, "访问验证", "Access verification")}

{config.site_name}

- {config.turnstile_site_key ? void verifyDashboard(token)} onError={setError} /> :

{ui(config.locale, "Turnstile 尚未正确配置", "Turnstile is not configured")}

} + {config.turnstile_site_key ? void verifyDashboard(token)} onError={setError} /> :

{ui(config.locale, "Turnstile 尚未正确配置", "Turnstile is not configured")}

} {verificationBusy ? {ui(config.locale, "正在验证", "Verifying")} : null} {error ?

{error}

: null}
diff --git a/frontend/src/admin-main.tsx b/frontend/src/admin-main.tsx index d3bfac4..67338e6 100644 --- a/frontend/src/admin-main.tsx +++ b/frontend/src/admin-main.tsx @@ -43,6 +43,15 @@ function AdminApp() { useEffect(() => { document.title = config ? `管理面板 · ${config.site_name}` : "管理面板"; }, [config]); + useEffect(() => { + let link = document.querySelector('link[rel="icon"]'); + if (!link) { + link = document.createElement("link"); + link.rel = "icon"; + document.head.append(link); + } + link.href = config?.favicon_url || config?.logo_url || "/logo.svg"; + }, [config?.favicon_url, config?.logo_url]); if (error) return
{error}
; if (!config) return
正在加载管理面板
; diff --git a/frontend/src/components/AdminPanel.tsx b/frontend/src/components/AdminPanel.tsx index 2af4775..30ab90f 100644 --- a/frontend/src/components/AdminPanel.tsx +++ b/frontend/src/components/AdminPanel.tsx @@ -37,6 +37,8 @@ import { derivePassword } from "../password"; import { ASSET_CURRENCIES, type AdminServer, type CloudflareUsage, type Config, type DatabaseStats, type ExchangeRates, type ServerInput, type Settings, type Theme, type ThemeSettingField, type ThemeSettingsSchema, type ThemeSettingValue } from "../types"; import { Checkbox } from "./Checkbox"; import { TurnstileWidget } from "./TurnstileWidget"; +import { useDialog } from "./useDialog"; +import { SiteLogo } from "./SiteLogo"; import { LatencyManager } from "./LatencyManager"; import { AlertRuleManager } from "./AlertRuleManager"; import { Flag } from "./Flag"; @@ -499,6 +501,8 @@ export function AdminPanel({ } return `curl -fsSL ${installer}/agent.sh | sudo sh -s -- -e ${shellLiteral(origin)} -t ${shellLiteral(install.agent_token)}${shellMirror}`; }, [install, installPlatform]); + const serverDialog = useDialog(editing !== null, () => setEditing(null)); + const installDialog = useDialog(Boolean(installCommand), () => setInstall(null)); const toggleSelected = (id: string) => setSelectedIds((current) => current.includes(id) ? current.filter((value) => value !== id) : [...current, id]); const allSelected = servers.length > 0 && selectedIds.length === servers.length; @@ -513,32 +517,33 @@ export function AdminPanel({ setNotice(""); setError(""); }; + const siteLogoUrl = settings ? settings.logo_url : config.logo_url; return (
{error ?
: notice ?
: null} {!authenticated ?
- +
- +

管理员登录

- {config.turnstile_login_enabled || config.turnstile_enabled ?
: null} + {config.turnstile_login_enabled || config.turnstile_enabled ?
: null}
:
- + {config.site_name}
- - + +
@@ -605,20 +610,20 @@ export function AdminPanel({
- ) : settings ? ( + ) : settings && tab !== "about" ? (
{tab === "appearance" ? <>
外观与展示