清理无用代码
This commit is contained in:
1 parent
7b1ad8584d
commit
b7f54cddac
26 files changed
+167
-225
No files matched your search
+29
-34
@@ -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("[输出已截断]"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -161,8 +161,7 @@ pub async fn settings_patch(
|
||||
let next_username = input
|
||||
.admin_username
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.unwrap_or(¤t.admin_username)
|
||||
.map_or(current.admin_username.as_str(), str::trim)
|
||||
.to_string();
|
||||
let username_changed = next_username != user.username;
|
||||
if !username_changed {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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()))?;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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()} />;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() : "";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 }'
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user