清理无用代码

This commit is contained in:
imengying committed 2026-09-04 14:02:27 +08:00
1 parent 7b1ad8584d
commit b7f54cddac
26 files changed
+167 -225

No files matched your search

+29 -34
View File
@@ -300,6 +300,7 @@ impl PublicIpProbe {
let mut state = state.lock().unwrap();
state.v4.observe(v4, observed_at);
state.v6.observe(v6, observed_at);
drop(state);
probing.store(false, Ordering::SeqCst);
});
}
@@ -753,8 +754,10 @@ fn os_name() -> String {
text("/etc/os-release")
.lines()
.find_map(|line| line.strip_prefix("PRETTY_NAME="))
.map(|value| value.trim_matches('"').to_string())
.unwrap_or_else(|| command("uname", &["-s"]))
.map_or_else(
|| command("uname", &["-s"]),
|value| value.trim_matches('"').to_string(),
)
}
#[cfg(target_os = "linux")]
@@ -1143,7 +1146,7 @@ fn sysfs_gpu_names() -> Vec<String> {
return Vec::new();
};
entries
.filter_map(|entry| entry.ok())
.filter_map(std::result::Result::ok)
.filter(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
@@ -1208,10 +1211,7 @@ fn parse_pciconf_gpu_names(output: &str) -> Vec<String> {
let mut block = Vec::new();
for line in output.lines() {
if !line.is_empty()
&& !line
.chars()
.next()
.is_some_and(|character| character.is_whitespace())
&& !line.chars().next().is_some_and(char::is_whitespace)
&& !block.is_empty()
{
if let Some(name) = block_name(&block) {
@@ -1345,9 +1345,7 @@ impl Collector {
fn refresh_basic(&mut self) {
let gpus = gpu_info();
self.basic = BasicMetrics {
cpu_cores: thread::available_parallelism()
.map(|value| value.get() as i64)
.unwrap_or(1),
cpu_cores: thread::available_parallelism().map_or(1, |value| value.get() as i64),
cpu_model: cpu_model(),
os: os_name(),
kernel: command("uname", &["-r"]),
@@ -1365,19 +1363,17 @@ impl Collector {
}
fn refresh_slow(&mut self) {
let processes = fs::read_dir("/proc")
.map(|items| {
items
.filter_map(|item| item.ok())
.filter(|item| {
item.file_name()
.to_string_lossy()
.chars()
.all(|ch| ch.is_ascii_digit())
})
.count() as i64
})
.unwrap_or(0);
let processes = fs::read_dir("/proc").map_or(0, |items| {
items
.filter_map(std::result::Result::ok)
.filter(|item| {
item.file_name()
.to_string_lossy()
.chars()
.all(|ch| ch.is_ascii_digit())
})
.count() as i64
});
self.slow = SlowMetrics {
disks: disk_usage(),
processes,
@@ -1891,7 +1887,7 @@ fn receive_remote_output(
}
}
fn remote_result_text(stdout: CapturedOutput, stderr: CapturedOutput) -> String {
fn remote_result_text(stdout: &CapturedOutput, stderr: &CapturedOutput) -> String {
let truncated = stdout.truncated || stderr.truncated;
let stdout = String::from_utf8_lossy(&stdout.bytes);
let stderr = String::from_utf8_lossy(&stderr.bytes);
@@ -1989,7 +1985,7 @@ fn execute_remote_task_with_timeout(
terminate_remote_process_tree(&mut child);
let stdout = receive_remote_output(&stdout, &mut child);
let stderr = receive_remote_output(&stderr, &mut child);
let output = remote_result_text(stdout, stderr);
let output = remote_result_text(&stdout, &stderr);
return TaskResultMessage {
message_type: "task_result".to_string(),
task_id,
@@ -2005,7 +2001,7 @@ fn execute_remote_task_with_timeout(
};
let stdout = receive_remote_output(&stdout, &mut child);
let stderr = receive_remote_output(&stderr, &mut child);
let output = remote_result_text(stdout, stderr);
let output = remote_result_text(&stdout, &stderr);
let Some(status) = status else {
let timeout_seconds = timeout.as_secs().max(1);
@@ -2279,8 +2275,7 @@ fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWorker) {
let mut socket: Option<LiveSocket> = None;
let mut wss_interval = configured_interval
.lock()
.map(|interval| *interval)
.unwrap_or(Duration::from_secs(1));
.map_or(Duration::from_secs(1), |interval| *interval);
let mut accepted_through = persisted_through.load(Ordering::Acquire);
let mut next_send_at = Instant::now();
let mut next_probe_at = Instant::now();
@@ -2326,9 +2321,8 @@ fn live_sender_loop(endpoint: &str, token: &str, worker: LiveSenderWorker) {
}
let (queue_lock, ready) = &*pending;
let mut queue = match queue_lock.lock() {
Ok(queue) => queue,
Err(_) => return,
let Ok(mut queue) = queue_lock.lock() else {
return;
};
let batch = loop {
let now = Instant::now();
@@ -2684,7 +2678,8 @@ fn rewrite_pending_spool(path: &Path, samples: &[Report]) -> Result<()> {
}
fn normalized_version(value: &str) -> &str {
value.trim().strip_prefix('v').unwrap_or(value.trim())
let value = value.trim();
value.strip_prefix('v').unwrap_or(value)
}
fn version_triplet(value: &str) -> Option<(u64, u64, u64)> {
@@ -3343,11 +3338,11 @@ mod tests {
#[test]
fn remote_command_output_is_bounded() {
let result = remote_result_text(
CapturedOutput {
&CapturedOutput {
bytes: vec![b'x'; REMOTE_RESULT_OUTPUT_BYTES + 100],
truncated: true,
},
CapturedOutput::default(),
&CapturedOutput::default(),
);
assert!(result.len() < REMOTE_RESULT_OUTPUT_BYTES + 64);
assert!(result.ends_with("[输出已截断]"));
+1 -1
View File
@@ -98,7 +98,7 @@ CREATE TABLE remote_tasks (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
command TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'running', 'success', 'failed')),
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'success', 'failed')),
requested_by TEXT NOT NULL,
requested_at BIGINT NOT NULL,
started_at BIGINT,
+1 -1
View File
@@ -100,7 +100,7 @@ CREATE TABLE remote_tasks (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
command TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'running', 'success', 'failed')),
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'sent', 'success', 'failed')),
requested_by TEXT NOT NULL,
requested_at INTEGER NOT NULL,
started_at INTEGER,
+7 -13
View File
@@ -168,20 +168,16 @@ fn row_values(row: &AnyRow, columns: &[BackupColumn]) -> Result<Vec<Value>> {
Ok(match column.kind {
BackupKind::Bool => row
.try_get::<Option<bool>, _>(index)?
.map(Value::Bool)
.unwrap_or(Value::Null),
.map_or(Value::Null, Value::Bool),
BackupKind::SmallInt => row
.try_get::<Option<i16>, _>(index)?
.map(|value| Value::Number(i64::from(value).into()))
.unwrap_or(Value::Null),
.map_or(Value::Null, |value| Value::Number(i64::from(value).into())),
BackupKind::Integer => row
.try_get::<Option<i32>, _>(index)?
.map(|value| Value::Number(i64::from(value).into()))
.unwrap_or(Value::Null),
.map_or(Value::Null, |value| Value::Number(i64::from(value).into())),
BackupKind::BigInt => row
.try_get::<Option<i64>, _>(index)?
.map(|value| Value::Number(value.into()))
.unwrap_or(Value::Null),
.map_or(Value::Null, |value| Value::Number(value.into())),
BackupKind::Real => match row.try_get::<Option<f32>, _>(index)? {
Some(value) => finite_json_number(f64::from(value))?,
None => Value::Null,
@@ -192,12 +188,10 @@ fn row_values(row: &AnyRow, columns: &[BackupColumn]) -> Result<Vec<Value>> {
},
BackupKind::Text => row
.try_get::<Option<String>, _>(index)?
.map(Value::String)
.unwrap_or(Value::Null),
.map_or(Value::Null, Value::String),
BackupKind::Blob => row
.try_get::<Option<Vec<u8>>, _>(index)?
.map(|value| Value::String(hex::encode(value)))
.unwrap_or(Value::Null),
.map_or(Value::Null, |value| Value::String(hex::encode(value))),
})
})
.collect()
@@ -518,7 +512,7 @@ pub async fn restore_archive(db: &Database, archive: &[u8]) -> Result<usize> {
sqlx::query(db.sql(
"UPDATE remote_tasks SET status='failed', completed_at=?, \
result=CASE WHEN result='' THEN '数据库恢复后已取消未完成任务' ELSE result END \
WHERE status IN ('pending','sent','running')",
WHERE status IN ('pending','sent')",
))
.bind(crate::db::now())
.execute(&mut *transaction)
+2 -2
View File
@@ -10,7 +10,7 @@ pub const VERSION: &str = match option_env!("NODEFLARE_VERSION") {
};
#[derive(Debug, Parser)]
#[command(version = VERSION, about = "NodeFlare standalone monitoring server")]
#[command(version = VERSION, about = "NodeFlare monitoring server")]
pub struct Args {
/// Path to the TOML configuration file.
#[arg(short, long, default_value = "/etc/nodeflare/config.toml")]
@@ -81,7 +81,7 @@ impl Config {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read configuration {}", path.display()))?;
let mut config: Config = toml::from_str(&content)
let mut config: Self = toml::from_str(&content)
.with_context(|| format!("failed to parse configuration {}", path.display()))?;
let base = path
.parent()
+1 -1
View File
@@ -359,7 +359,7 @@ pub async fn initialize(pool: &Database, config: &Config) -> Result<()> {
sqlx::query("DELETE FROM settings WHERE key LIKE 'session_%'")
.execute(&mut *transaction)
.await?;
tracing::info!(timestamp = now, "initialized standalone password scheme");
tracing::info!(timestamp = now, "initialized password scheme");
}
transaction.commit().await?;
Ok(())
+7 -6
View File
@@ -570,7 +570,7 @@ async fn load_traffic_state(
})
})
.transpose()
.map(|state| state.unwrap_or_default())
.map(Option::unwrap_or_default)
.map_err(Into::into)
}
@@ -870,7 +870,7 @@ pub async fn cleanup_database(db: &Database, retention_days: i64) -> Result<()>
sqlx::query(db.sql(
"UPDATE remote_tasks SET status='failed', completed_at=?, \
result=CASE WHEN result='' THEN '任务等待超过 24 小时,已自动取消' ELSE result END \
WHERE status IN ('pending','sent','running') AND requested_at<?",
WHERE status IN ('pending','sent') AND requested_at<?",
))
.bind(current)
.bind(active_task_cutoff)
@@ -1530,7 +1530,8 @@ pub async fn remote_task(db: &Database, id: &str) -> Result<Option<RemoteTaskInf
.bind(id)
.fetch_optional(db.pool())
.await?;
row.map(remote_task_from_row)
row.as_ref()
.map(remote_task_from_row)
.transpose()
.map_err(Into::into)
}
@@ -1558,14 +1559,14 @@ pub async fn pending_remote_tasks(db: &Database, server_id: &str) -> Result<Vec<
.fetch_all(&mut *transaction)
.await?;
transaction.commit().await?;
rows.into_iter()
rows.iter()
.map(remote_task_from_row)
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
}
fn remote_task_from_row(
row: sqlx::any::AnyRow,
row: &sqlx::any::AnyRow,
) -> std::result::Result<RemoteTaskInfo, sqlx::Error> {
Ok(RemoteTaskInfo {
id: row.try_get("id")?,
@@ -1604,7 +1605,7 @@ pub async fn update_remote_task_result(
) -> Result<bool> {
let result = sqlx::query(db.sql(
"UPDATE remote_tasks SET status=?, result=?, exit_code=?, completed_at=? \
WHERE id=? AND server_id=? AND status IN ('pending','sent','running')",
WHERE id=? AND server_id=? AND status IN ('pending','sent')",
))
.bind(status)
.bind(result)
-1
View File
@@ -75,7 +75,6 @@ async fn fetch_json(client: &reqwest::Client, url: &str) -> Result<Value> {
let response = client
.get(url)
.header("Accept", "application/json")
.header("User-Agent", "NodeFlare-Standalone")
.timeout(Duration::from_secs(8))
.send()
.await?
+4 -2
View File
@@ -4,6 +4,7 @@ mod config;
mod db;
mod exchange;
mod middleware;
mod mime;
mod models;
mod notify;
mod routes;
@@ -64,7 +65,8 @@ impl AppState {
}
pub async fn disconnect_agent(&self, server_id: &str) {
if let Some(connection) = self.agents.write().await.remove(server_id) {
let connection = self.agents.write().await.remove(server_id);
if let Some(connection) = connection {
let _ = connection.sender.try_send(websocket::AgentCommand::Close);
}
}
@@ -136,7 +138,7 @@ async fn main() -> Result<()> {
}
let http = reqwest::Client::builder()
.user_agent("NodeFlare-Standalone")
.user_agent(format!("NodeFlare/{}", config::VERSION))
.connect_timeout(std::time::Duration::from_secs(5))
.build()?;
let (dashboard_tx, _) = broadcast::channel(2048);
+27
View File
@@ -0,0 +1,27 @@
use std::path::Path;
pub fn content_type(path: &Path) -> &'static str {
match path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
{
"html" => "text/html; charset=utf-8",
"js" | "mjs" => "application/javascript; charset=utf-8",
"css" => "text/css; charset=utf-8",
"json" | "map" => "application/json; charset=utf-8",
"svg" => "image/svg+xml",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"gif" => "image/gif",
"ico" => "image/x-icon",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"wasm" => "application/wasm",
"sh" => "text/x-shellscript; charset=utf-8",
"ps1" => "text/plain; charset=utf-8",
_ => "application/octet-stream",
}
}
+1 -2
View File
@@ -161,8 +161,7 @@ pub async fn settings_patch(
let next_username = input
.admin_username
.as_deref()
.map(str::trim)
.unwrap_or(&current.admin_username)
.map_or(current.admin_username.as_str(), str::trim)
.to_string();
let username_changed = next_username != user.username;
if !username_changed {
+10 -8
View File
@@ -294,14 +294,16 @@ pub(crate) fn session_device(headers: &HeaderMap, peer: SocketAddr) -> crate::db
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| {
value
.chars()
.filter(|character| !character.is_control())
.take(512)
.collect()
})
.unwrap_or_else(|| "未知客户端".to_string());
.map_or_else(
|| "未知客户端".to_string(),
|value| {
value
.chars()
.filter(|character| !character.is_control())
.take(512)
.collect()
},
);
crate::db::SessionDevice {
ip_address,
user_agent,
+5 -32
View File
@@ -44,14 +44,10 @@ pub async fn handle(State(state): State<Arc<AppState>>, OriginalUri(uri): Origin
Ok(settings) => settings,
Err(error) => return ApiResponse::internal(error).into_response(),
};
let base = match crate::db::queries::theme_resolved_url(
&state.db,
&settings.active_theme_id,
)
.await
{
Ok(Some(base)) => base,
_ => return StatusCode::NOT_FOUND.into_response(),
let Ok(Some(base)) =
crate::db::queries::theme_resolved_url(&state.db, &settings.active_theme_id).await
else {
return StatusCode::NOT_FOUND.into_response();
};
return theme_file(&state, &base, relative, "/__theme-active").await;
}
@@ -97,7 +93,7 @@ async fn local_file(root: &Path, relative: &str, cache: bool) -> Response {
return StatusCode::NOT_FOUND.into_response();
};
match tokio::fs::read(&path).await {
Ok(body) => response(body, content_type(&path), cache),
Ok(body) => response(body, crate::mime::content_type(&path), cache),
Err(_) => StatusCode::NOT_FOUND.into_response(),
}
}
@@ -149,26 +145,3 @@ fn response(body: Vec<u8>, content_type: &str, cache: bool) -> Response {
}
response
}
fn content_type(path: &Path) -> &'static str {
match path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
{
"html" => "text/html; charset=utf-8",
"js" | "mjs" => "application/javascript; charset=utf-8",
"css" => "text/css; charset=utf-8",
"json" => "application/json; charset=utf-8",
"svg" => "image/svg+xml",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"ico" => "image/x-icon",
"woff2" => "font/woff2",
"wasm" => "application/wasm",
"sh" => "text/x-shellscript; charset=utf-8",
"ps1" => "text/plain; charset=utf-8",
_ => "application/octet-stream",
}
}
+4 -28
View File
@@ -141,7 +141,7 @@ fn validate_release_asset_url(value: &str, owner: &str, repository: &str) -> Res
let parsed = Url::parse(value).context("Release ZIP 下载地址无效")?;
let parts = parsed
.path_segments()
.map(|parts| parts.collect::<Vec<_>>())
.map(Iterator::collect::<Vec<_>>)
.unwrap_or_default();
if parsed.scheme() != "https"
|| parsed.host_str() != Some("github.com")
@@ -227,7 +227,7 @@ pub async fn fetch_theme_path(
};
let root = local_root(theme_dir, base)?;
let body = read_local(&root, target, limit).await?;
let content_type = content_type(Path::new(target)).to_string();
let content_type = crate::mime::content_type(Path::new(target)).to_string();
if target == "index.html" {
let html = String::from_utf8(body)?;
return Ok((rewrite_index(&html, prefix).into_bytes(), content_type));
@@ -276,7 +276,8 @@ fn empty_settings_schema() -> Value {
}
fn sanitize_version(value: &str) -> Option<String> {
let version = value.trim().strip_prefix('v').unwrap_or(value.trim());
let value = value.trim();
let version = value.strip_prefix('v').unwrap_or(value);
(!version.is_empty()
&& version.len() <= 40
&& version
@@ -334,31 +335,6 @@ async fn read_local_optional(root: &Path, relative: &str, limit: usize) -> Resul
Ok(Some(body))
}
fn content_type(path: &Path) -> &'static str {
match path
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
{
"html" => "text/html; charset=utf-8",
"js" | "mjs" => "application/javascript; charset=utf-8",
"css" => "text/css; charset=utf-8",
"json" => "application/json; charset=utf-8",
"svg" => "image/svg+xml",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"gif" => "image/gif",
"ico" => "image/x-icon",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"wasm" => "application/wasm",
"map" => "application/json; charset=utf-8",
_ => "application/octet-stream",
}
}
fn install_archive_blocking(theme_dir: &Path, id: &str, archive: Vec<u8>) -> Result<()> {
fs::create_dir_all(theme_dir)
.with_context(|| format!("无法创建主题目录 {}", theme_dir.display()))?;
+7 -5
View File
@@ -180,12 +180,14 @@ async fn run(
}
}
writer.abort();
let mut agents = state.agents.write().await;
if agents
.get(&identity.server_id)
.is_some_and(|connection| connection.connection_id == connection_id)
{
agents.remove(&identity.server_id);
let mut agents = state.agents.write().await;
if agents
.get(&identity.server_id)
.is_some_and(|connection| connection.connection_id == connection_id)
{
agents.remove(&identity.server_id);
}
}
tracing::info!(server_id = %identity.server_id, "Agent disconnected");
}
+1 -2
View File
@@ -55,8 +55,7 @@ async fn run(socket: WebSocket, state: Arc<AppState>, server_id: Option<String>)
break;
}
}
Ok(_) => {}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
},
incoming = receiver.next() => match incoming {
+4 -24
View File
@@ -10,6 +10,7 @@ import {
type PlaybackBuffer,
} from "./live";
import { ui } from "./locale";
import { useFavicon, useStoredAppearance, useSystemDark } from "./hooks/useBrowserAppearance";
import { resolveBackground, themeToggle } from "./theme";
import {
BOOTSTRAP_POLL_INTERVAL_MS,
@@ -82,11 +83,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
const [error, setError] = useState("");
const [access, setAccess] = useState<Access>("ok");
const [selectedId, setSelectedId] = useState<string | null>(routeServerId);
const [appearance, setAppearance] = useState<"light" | "dark" | null>(() => {
const stored = localStorage.getItem("nodeflare-theme");
return stored === "light" || stored === "dark" ? stored : null;
});
const [systemDark, setSystemDark] = useState(() => matchMedia("(prefers-color-scheme: dark)").matches);
const [appearance, setAppearance] = useStoredAppearance("nodeflare-theme");
const systemDark = useSystemDark();
const liveConnectedRef = useRef(false);
const playbackRef = useRef<PlaybackBuffer>(new Map());
const serversRef = useRef<Server[]>([]);
@@ -99,6 +97,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
const background = resolveBackground(config.background_url, dark);
const blur = themeToggle(config, "enableBlur");
const carrierLatency = config.show_latency && themeToggle(config, "showCarrierLatency", false);
useFavicon(config.logo_url);
if (!reloadQueueRef.current) {
reloadQueueRef.current = createRefreshQueue(async (quiet) => {
@@ -266,24 +265,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
document.documentElement.lang = config.locale;
}, [blur, config.locale, dark]);
useEffect(() => {
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (!link) {
link = document.createElement("link");
link.rel = "icon";
document.head.append(link);
}
link.removeAttribute("type");
link.href = config.logo_url || "/logo.svg";
}, [config.logo_url]);
useEffect(() => {
const media = matchMedia("(prefers-color-scheme: dark)");
const update = () => setSystemDark(media.matches);
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
useEffect(() => {
const onPop = () => setSelectedId(routeServerId());
window.addEventListener("popstate", onPop);
@@ -317,7 +298,6 @@ export function AppProvider({ children }: { children: ReactNode }) {
setAppearance((current) => {
const resolved = current ?? (dark ? "dark" : "light");
const next = resolved === "dark" ? "light" : "dark";
localStorage.setItem("nodeflare-theme", next);
return next;
});
}, [dark]);
+4 -26
View File
@@ -2,22 +2,17 @@ import { StrictMode, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { api } from "./api";
import { AdminPanel } from "./components/AdminPanel";
import { useFavicon, useStoredAppearance, useSystemDark } from "./hooks/useBrowserAppearance";
import type { Config } from "./types";
import "./styles/admin.css";
const THEME_KEY = "nodeflare-admin-theme";
function initialAppearance(): "light" | "dark" | null {
const stored = localStorage.getItem(THEME_KEY);
return stored === "light" || stored === "dark" ? stored : null;
}
function AdminApp() {
const [config, setConfig] = useState<Config | null>(null);
const [error, setError] = useState("");
const [appearance, setAppearance] = useState<"light" | "dark" | null>(initialAppearance);
const [systemDark, setSystemDark] = useState(() => matchMedia("(prefers-color-scheme: dark)").matches);
const [appearance, setAppearance] = useStoredAppearance("nodeflare-admin-theme");
const systemDark = useSystemDark();
const dark = appearance ? appearance === "dark" : config?.default_theme === "system" || !config ? systemDark : config.default_theme === "dark";
useFavicon(config?.logo_url);
async function loadConfig() {
try {
@@ -34,31 +29,14 @@ function AdminApp() {
document.documentElement.classList.toggle("dark", dark);
document.documentElement.style.colorScheme = dark ? "dark" : "light";
}, [dark]);
useEffect(() => {
const media = matchMedia("(prefers-color-scheme: dark)");
const update = () => setSystemDark(media.matches);
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
useEffect(() => {
document.title = config ? `管理面板 · ${config.site_name}` : "管理面板";
}, [config]);
useEffect(() => {
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (!link) {
link = document.createElement("link");
link.rel = "icon";
document.head.append(link);
}
link.removeAttribute("type");
link.href = config?.logo_url || "/logo.svg";
}, [config?.logo_url]);
if (error) return <div className={`admin-loading ${dark ? "admin-dark" : ""}`}><span>{error}</span><button className="secondary-btn" onClick={() => void loadConfig()}>重试</button></div>;
if (!config) return <div className={`admin-loading ${dark ? "admin-dark" : ""}`}>正在加载管理面板</div>;
return <AdminPanel config={config} dark={dark} onToggleTheme={() => {
const next = dark ? "light" : "dark";
localStorage.setItem(THEME_KEY, next);
setAppearance(next);
}} onChanged={() => void loadConfig()} />;
}
+1 -1
View File
@@ -15,7 +15,7 @@ export interface FormattedBytes {
unit: string;
}
export function formatBytesParts(value: number | null | undefined, decimals = 1): FormattedBytes {
function formatBytesParts(value: number | null | undefined, decimals = 1): FormattedBytes {
const size = Math.max(0, number(value));
if (size === 0) return { value: "0", unit: "B" };
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
type Appearance = "light" | "dark";
export function useStoredAppearance(key: string) {
const [appearance, setAppearance] = useState<Appearance | null>(() => {
const stored = localStorage.getItem(key);
return stored === "light" || stored === "dark" ? stored : null;
});
useEffect(() => {
if (appearance) localStorage.setItem(key, appearance);
}, [appearance, key]);
return [appearance, setAppearance] as const;
}
export function useSystemDark() {
const [systemDark, setSystemDark] = useState(
() => matchMedia("(prefers-color-scheme: dark)").matches,
);
useEffect(() => {
const media = matchMedia("(prefers-color-scheme: dark)");
const update = () => setSystemDark(media.matches);
media.addEventListener("change", update);
return () => media.removeEventListener("change", update);
}, []);
return systemDark;
}
export function useFavicon(logoUrl: string | null | undefined) {
useEffect(() => {
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (!link) {
link = document.createElement("link");
link.rel = "icon";
document.head.append(link);
}
link.removeAttribute("type");
link.href = logoUrl || "/logo.svg";
}, [logoUrl]);
}
+3 -3
View File
@@ -2,7 +2,7 @@ import { clockLabel } from "./chart";
import { ui, type UiLocale } from "./locale";
import type { LatencySample } from "./types";
export type LatencyTone = "good" | "fair" | "warning" | "poor" | "danger" | "empty";
type LatencyTone = "good" | "fair" | "warning" | "poor" | "danger" | "empty";
export interface LatencyBar {
key: string;
@@ -31,7 +31,7 @@ export const CARRIER_SLOTS = [
export type CarrierSlot = typeof CARRIER_SLOTS[number];
export function latencyTone(value: number): LatencyTone {
function latencyTone(value: number): LatencyTone {
if (value <= 60) return "good";
if (value <= 100) return "fair";
if (value <= 160) return "warning";
@@ -39,7 +39,7 @@ export function latencyTone(value: number): LatencyTone {
return "danger";
}
export function lossTone(value: number): LatencyTone {
function lossTone(value: number): LatencyTone {
if (value <= 1) return "good";
if (value <= 3) return "fair";
if (value <= 6) return "warning";
+2 -2
View File
@@ -1,7 +1,7 @@
import type { LatencySample, LiveLatencyResult, Server } from "./types";
/** A single Agent report as replayed by the NodeFlare server. */
export interface LiveSample {
interface LiveSample {
ts: number;
data: Partial<Server> & { latency_results?: LiveLatencyResult[] };
}
@@ -30,7 +30,7 @@ export interface BatchUpdate {
reportAgeMs?: number;
}
export const MAX_PLAYBACK_SAMPLES_PER_SERVER = 600;
const MAX_PLAYBACK_SAMPLES_PER_SERVER = 600;
const MAX_LIVE_LATENCY_RESULTS = 4096;
export function mergeLiveResults(
+1 -1
View File
@@ -17,7 +17,7 @@ export function themeToggle(config: Config, key: string, fallback = true) {
return typeof value === "boolean" ? value : fallback;
}
export function themeText(config: Config, key: string) {
function themeText(config: Config, key: string) {
const value = option(config, key);
return typeof value === "string" ? value.trim() : "";
}
-4
View File
@@ -10,7 +10,3 @@ cd "$root_dir"
[ -d node_modules ] || bun install --frozen-lockfile
bun run --cwd frontend build
if build_key=$(sh scripts/build-key.sh frontend 2>/dev/null); then
printf '%s\n' "$build_key" > frontend/dist/.nodeflare-revision
fi
-25
View File
@@ -1,25 +0,0 @@
#!/bin/sh
set -eu
case "${1:-}" in
frontend)
paths="frontend package.json bun.lock scripts/build-frontend.sh scripts/build-key.sh scripts/resolve-version.sh"
;;
backend)
paths="backend/Cargo.toml backend/Cargo.lock backend/src backend/migrations scripts/build-backend.sh scripts/build-key.sh scripts/resolve-version.sh"
;;
*)
echo "usage: $0 frontend|backend" >&2
exit 2
;;
esac
{
sh scripts/resolve-version.sh
git rev-parse HEAD
git diff --binary -- $paths
git ls-files --others --exclude-standard -- $paths | while IFS= read -r file; do
printf '%s ' "$file"
git hash-object "$file"
done
} | cksum | awk '{ print $1 "-" $2 }'
+1 -1
View File
@@ -10,7 +10,7 @@ if [ -z "$config_path" ]; then
config_path=/etc/nodeflare/config.toml
elif [ -f backend/config.toml ]; then
config_path=backend/config.toml
echo "正在使用兼容的本地配置 backend/config.toml"
echo "正在使用本地开发配置 backend/config.toml"
else
echo "未找到 /etc/nodeflare/config.toml" >&2
echo "请先运行 sudo ./install.sh,安装时会询问管理员用户名和密码。" >&2