Unify logging to tracing: file appender + unified log viewer
Replace scattered println!/eprintln! with structured tracing macros throughout supervisor, scheduler, and web modules. Add LogConfig (file + level) to Config and a new logging module that initialises a stderr + optional non-blocking file appender on `warpgate run`. Remove the in-memory LogBuffer/LogEntry from AppState; the web /api/logs endpoint now reads the log file directly with from_line/lines pagination. `warpgate log` replaces journalctl with `tail`, and the Logs tab Alpine.js is updated to match the new API response shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
74b0e72549
commit
64d6171ec9
146
Cargo.lock
generated
146
Cargo.lock
generated
@ -8,6 +8,15 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.21"
|
version = "0.6.21"
|
||||||
@ -316,6 +325,21 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctrlc"
|
name = "ctrlc"
|
||||||
version = "3.5.2"
|
version = "3.5.2"
|
||||||
@ -666,6 +690,12 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.180"
|
version = "0.2.180"
|
||||||
@ -690,6 +720,15 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@ -741,6 +780,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -825,6 +873,23 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@ -961,6 +1026,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@ -1061,6 +1135,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.44"
|
version = "0.3.44"
|
||||||
@ -1241,9 +1324,33 @@ checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
@ -1251,6 +1358,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1327,6 +1464,12 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
@ -1350,6 +1493,9 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml",
|
"toml",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
|
"tracing-subscriber",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -18,3 +18,6 @@ tokio-stream = { version = "0.1", features = ["sync"] }
|
|||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
askama = "0.15"
|
askama = "0.15"
|
||||||
tower-http = { version = "0.6", features = ["cors"] }
|
tower-http = { version = "0.6", features = ["cors"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
tracing-appender = "0.2"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
//! `warpgate log` — stream service logs in real time.
|
//! `warpgate log` — stream service logs from the configured log file.
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@ -6,27 +6,24 @@ use anyhow::{Context, Result};
|
|||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
pub fn run(_config: &Config, lines: u32, follow: bool) -> Result<()> {
|
pub fn run(config: &Config, lines: u32, follow: bool) -> Result<()> {
|
||||||
let mut cmd = Command::new("journalctl");
|
let log_file = &config.log.file;
|
||||||
cmd.arg("-u")
|
if log_file.is_empty() {
|
||||||
.arg("warpgate-mount")
|
anyhow::bail!(
|
||||||
.arg("-n")
|
"No log file configured. Set [log] file = \"/var/log/warpgate/warpgate.log\" in config."
|
||||||
.arg(lines.to_string());
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if follow {
|
if follow {
|
||||||
// Stream directly to stdout with -f (like tail -f)
|
Command::new("tail")
|
||||||
cmd.arg("-f");
|
.args(["-f", "-n", &lines.to_string(), log_file])
|
||||||
let status = cmd.status().context("Failed to run journalctl")?;
|
.status()
|
||||||
if !status.success() {
|
.context("Failed to run tail -f")?;
|
||||||
anyhow::bail!("journalctl exited with status {}", status);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let output = cmd.output().context("Failed to run journalctl")?;
|
Command::new("tail")
|
||||||
if !output.status.success() {
|
.args(["-n", &lines.to_string(), log_file])
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
.status()
|
||||||
anyhow::bail!("journalctl failed: {}", stderr.trim());
|
.context("Failed to run tail")?;
|
||||||
}
|
|
||||||
print!("{}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -31,9 +31,39 @@ pub struct Config {
|
|||||||
pub smb_auth: SmbAuthConfig,
|
pub smb_auth: SmbAuthConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dir_refresh: DirRefreshConfig,
|
pub dir_refresh: DirRefreshConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub log: LogConfig,
|
||||||
pub shares: Vec<ShareConfig>,
|
pub shares: Vec<ShareConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logging configuration.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct LogConfig {
|
||||||
|
/// Log file path. Empty string = no file logging.
|
||||||
|
#[serde(default = "default_log_file")]
|
||||||
|
pub file: String,
|
||||||
|
/// Minimum log level: error / warn / info / debug / trace.
|
||||||
|
#[serde(default = "default_log_level")]
|
||||||
|
pub level: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
file: default_log_file(),
|
||||||
|
level: default_log_level(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_log_file() -> String {
|
||||||
|
"/var/log/warpgate/warpgate.log".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_log_level() -> String {
|
||||||
|
"info".into()
|
||||||
|
}
|
||||||
|
|
||||||
/// SFTP connection to a remote NAS.
|
/// SFTP connection to a remote NAS.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct ConnectionConfig {
|
pub struct ConnectionConfig {
|
||||||
@ -457,6 +487,13 @@ impl Config {
|
|||||||
writeln!(out).unwrap();
|
writeln!(out).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Log ---
|
||||||
|
writeln!(out, "# --- Log ---").unwrap();
|
||||||
|
writeln!(out, "[log]").unwrap();
|
||||||
|
writeln!(out, "file = {:?}", self.log.file).unwrap();
|
||||||
|
writeln!(out, "level = {:?}", self.log.level).unwrap();
|
||||||
|
writeln!(out).unwrap();
|
||||||
|
|
||||||
// --- Warmup ---
|
// --- Warmup ---
|
||||||
writeln!(out, "# --- Warmup (change = no restart) ---").unwrap();
|
writeln!(out, "# --- Warmup (change = no restart) ---").unwrap();
|
||||||
writeln!(out, "[warmup]").unwrap();
|
writeln!(out, "[warmup]").unwrap();
|
||||||
|
|||||||
@ -3,12 +3,12 @@
|
|||||||
//! The supervisor owns all mutable state. The web server gets read-only access
|
//! The supervisor owns all mutable state. The web server gets read-only access
|
||||||
//! to status via `Arc<RwLock<DaemonStatus>>` and sends commands via an mpsc channel.
|
//! to status via `Arc<RwLock<DaemonStatus>>` and sends commands via an mpsc channel.
|
||||||
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Instant, SystemTime};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
@ -28,67 +28,6 @@ pub struct AppState {
|
|||||||
/// SSE broadcast: supervisor sends `()` after each status update;
|
/// SSE broadcast: supervisor sends `()` after each status update;
|
||||||
/// web server subscribers render partials and push to connected clients.
|
/// web server subscribers render partials and push to connected clients.
|
||||||
pub sse_tx: tokio::sync::broadcast::Sender<()>,
|
pub sse_tx: tokio::sync::broadcast::Sender<()>,
|
||||||
/// Ring buffer of log entries for the web UI.
|
|
||||||
pub logs: Arc<RwLock<LogBuffer>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ring buffer of timestamped log entries for the web log viewer.
|
|
||||||
pub struct LogBuffer {
|
|
||||||
entries: VecDeque<LogEntry>,
|
|
||||||
/// Monotonically increasing ID for the next entry.
|
|
||||||
next_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single log entry with unix timestamp and message.
|
|
||||||
#[derive(Clone, serde::Serialize)]
|
|
||||||
pub struct LogEntry {
|
|
||||||
pub id: u64,
|
|
||||||
pub ts: u64,
|
|
||||||
pub msg: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOG_BUFFER_MAX: usize = 500;
|
|
||||||
|
|
||||||
impl LogBuffer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
entries: VecDeque::new(),
|
|
||||||
next_id: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a new log message. Timestamps are added automatically.
|
|
||||||
pub fn push(&mut self, msg: impl Into<String>) {
|
|
||||||
let ts = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
self.entries.push_back(LogEntry {
|
|
||||||
id: self.next_id,
|
|
||||||
ts,
|
|
||||||
msg: msg.into(),
|
|
||||||
});
|
|
||||||
self.next_id += 1;
|
|
||||||
if self.entries.len() > LOG_BUFFER_MAX {
|
|
||||||
self.entries.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get entries with ID >= `since_id`.
|
|
||||||
pub fn since(&self, since_id: u64) -> Vec<LogEntry> {
|
|
||||||
let start_id = self.next_id.saturating_sub(self.entries.len() as u64);
|
|
||||||
let skip = if since_id > start_id {
|
|
||||||
(since_id - start_id) as usize
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
self.entries.iter().skip(skip).cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The ID that the next pushed entry will have.
|
|
||||||
pub fn next_id(&self) -> u64 {
|
|
||||||
self.next_id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overall daemon status, updated by the supervisor loop.
|
/// Overall daemon status, updated by the supervisor loop.
|
||||||
|
|||||||
66
src/logging.rs
Normal file
66
src/logging.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! Unified logging initializer.
|
||||||
|
//!
|
||||||
|
//! Configures `tracing` with both a stderr console layer and an optional
|
||||||
|
//! non-blocking file appender. The returned `WorkerGuard` must be kept alive
|
||||||
|
//! for the duration of the process — dropping it flushes and closes the file.
|
||||||
|
|
||||||
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
|
|
||||||
|
/// Initialize tracing for the `run` command (console + optional file).
|
||||||
|
///
|
||||||
|
/// Returns a `WorkerGuard` when file logging is active. The caller must hold
|
||||||
|
/// this value until the process exits so the background writer thread can flush.
|
||||||
|
pub fn init(log_config: &crate::config::LogConfig) -> Option<WorkerGuard> {
|
||||||
|
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
|
let filter = EnvFilter::try_new(&log_config.level).unwrap_or_else(|_| EnvFilter::new("info"));
|
||||||
|
|
||||||
|
let console_layer = fmt::layer().with_target(false).compact();
|
||||||
|
|
||||||
|
if log_config.file.is_empty() {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(console_layer)
|
||||||
|
.init();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure log directory exists.
|
||||||
|
let log_path = std::path::Path::new(&log_config.file);
|
||||||
|
if let Some(dir) = log_path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_appender = tracing_appender::rolling::never(
|
||||||
|
log_path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or(std::path::Path::new(".")),
|
||||||
|
log_path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or(std::ffi::OsStr::new("warpgate.log")),
|
||||||
|
);
|
||||||
|
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
let file_layer = fmt::layer()
|
||||||
|
.with_writer(non_blocking)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_target(false)
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(console_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
Some(guard)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize tracing for CLI sub-commands (console only, no file).
|
||||||
|
pub fn init_console() {
|
||||||
|
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
let filter = EnvFilter::try_new("info").unwrap();
|
||||||
|
let _ = tracing_subscriber::registry()
|
||||||
|
.with(filter)
|
||||||
|
.with(fmt::layer().with_target(false).compact())
|
||||||
|
.try_init();
|
||||||
|
}
|
||||||
13
src/main.rs
13
src/main.rs
@ -3,6 +3,7 @@ mod config;
|
|||||||
mod config_diff;
|
mod config_diff;
|
||||||
mod daemon;
|
mod daemon;
|
||||||
mod deploy;
|
mod deploy;
|
||||||
|
mod logging;
|
||||||
mod rclone;
|
mod rclone;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod services;
|
mod services;
|
||||||
@ -100,6 +101,13 @@ fn main() -> Result<()> {
|
|||||||
cmd => {
|
cmd => {
|
||||||
let config = Config::load(&cli.config)?;
|
let config = Config::load(&cli.config)?;
|
||||||
match cmd {
|
match cmd {
|
||||||
|
Commands::Run => {
|
||||||
|
let _guard = logging::init(&config.log);
|
||||||
|
supervisor::run(&config, cli.config.clone())
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
logging::init_console();
|
||||||
|
match other {
|
||||||
Commands::Status => cli::status::run(&config),
|
Commands::Status => cli::status::run(&config),
|
||||||
Commands::CacheList => cli::cache::list(&config),
|
Commands::CacheList => cli::cache::list(&config),
|
||||||
Commands::CacheClean { all } => cli::cache::clean(&config, all),
|
Commands::CacheClean { all } => cli::cache::clean(&config, all),
|
||||||
@ -111,9 +119,10 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Commands::Log { lines, follow } => cli::log::run(&config, lines, follow),
|
Commands::Log { lines, follow } => cli::log::run(&config, lines, follow),
|
||||||
Commands::SpeedTest => cli::speed_test::run(&config),
|
Commands::SpeedTest => cli::speed_test::run(&config),
|
||||||
Commands::Run => supervisor::run(&config, cli.config.clone()),
|
|
||||||
// already handled above
|
// already handled above
|
||||||
Commands::ConfigInit { .. } | Commands::Deploy => unreachable!(),
|
Commands::Run | Commands::ConfigInit { .. } | Commands::Deploy => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ use std::sync::Arc;
|
|||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
/// A named periodic task.
|
/// A named periodic task.
|
||||||
pub struct ScheduledTask {
|
pub struct ScheduledTask {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
@ -57,7 +59,7 @@ impl ScheduledTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = work() {
|
if let Err(e) = work() {
|
||||||
eprintln!("[{}] error: {e}", self.name);
|
warn!("[{}] error: {e}", self.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ use std::thread;
|
|||||||
use std::time::{Duration, Instant, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::config_diff::{self, ChangeTier};
|
use crate::config_diff::{self, ChangeTier};
|
||||||
@ -100,7 +101,7 @@ pub fn run(config: &Config, config_path: PathBuf) -> Result<()> {
|
|||||||
// Install signal handler (SIGTERM + SIGINT)
|
// Install signal handler (SIGTERM + SIGINT)
|
||||||
let shutdown_flag = Arc::clone(&shutdown);
|
let shutdown_flag = Arc::clone(&shutdown);
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
eprintln!("Signal received, shutting down...");
|
info!("Signal received, shutting down...");
|
||||||
shutdown_flag.store(true, Ordering::SeqCst);
|
shutdown_flag.store(true, Ordering::SeqCst);
|
||||||
})
|
})
|
||||||
.context("Failed to set signal handler")?;
|
.context("Failed to set signal handler")?;
|
||||||
@ -138,11 +139,11 @@ pub fn run(config: &Config, config_path: PathBuf) -> Result<()> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Phase 1: Preflight — create dirs, write rclone.conf
|
// Phase 1: Preflight — create dirs, write rclone.conf
|
||||||
println!("Preflight checks...");
|
info!("Preflight checks...");
|
||||||
preflight(config)?;
|
preflight(config)?;
|
||||||
|
|
||||||
// Phase 1.5: Probe remote paths in parallel
|
// Phase 1.5: Probe remote paths in parallel
|
||||||
println!("Probing remote paths...");
|
info!("Probing remote paths...");
|
||||||
let healthy_names = probe_all_shares(config, &shared_status, &shutdown)?;
|
let healthy_names = probe_all_shares(config, &shared_status, &shutdown)?;
|
||||||
|
|
||||||
if healthy_names.is_empty() {
|
if healthy_names.is_empty() {
|
||||||
@ -159,10 +160,10 @@ pub fn run(config: &Config, config_path: PathBuf) -> Result<()> {
|
|||||||
write_protocol_configs(&healthy_config)?;
|
write_protocol_configs(&healthy_config)?;
|
||||||
|
|
||||||
// Phase 2: Start rclone mounts only for healthy shares
|
// Phase 2: Start rclone mounts only for healthy shares
|
||||||
println!("Starting rclone mounts...");
|
info!("Starting rclone mounts...");
|
||||||
let mut mount_children = start_and_wait_mounts(&healthy_config, &shutdown)?;
|
let mut mount_children = start_and_wait_mounts(&healthy_config, &shutdown)?;
|
||||||
for share in &healthy_config.shares {
|
for share in &healthy_config.shares {
|
||||||
println!(" Mount ready at {}", share.mount_point.display());
|
info!(" Mount ready at {}", share.mount_point.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update status: mounts are ready (match by name, not index)
|
// Update status: mounts are ready (match by name, not index)
|
||||||
@ -178,14 +179,14 @@ pub fn run(config: &Config, config_path: PathBuf) -> Result<()> {
|
|||||||
|
|
||||||
// Phase 3: Start protocol services
|
// Phase 3: Start protocol services
|
||||||
if shutdown.load(Ordering::SeqCst) {
|
if shutdown.load(Ordering::SeqCst) {
|
||||||
println!("Shutdown signal received during mount.");
|
info!("Shutdown signal received during mount.");
|
||||||
for mc in &mut mount_children {
|
for mc in &mut mount_children {
|
||||||
let _ = mc.child.kill();
|
let _ = mc.child.kill();
|
||||||
let _ = mc.child.wait();
|
let _ = mc.child.wait();
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
println!("Starting protocol services...");
|
info!("Starting protocol services...");
|
||||||
let mut protocols = start_protocols(&healthy_config)?;
|
let mut protocols = start_protocols(&healthy_config)?;
|
||||||
|
|
||||||
// Update status: protocols running
|
// Update status: protocols running
|
||||||
@ -202,7 +203,7 @@ pub fn run(config: &Config, config_path: PathBuf) -> Result<()> {
|
|||||||
spawn_dir_refresh(config, &shared_status, &shutdown);
|
spawn_dir_refresh(config, &shared_status, &shutdown);
|
||||||
|
|
||||||
// Phase 4: Supervision loop with command channel
|
// Phase 4: Supervision loop with command channel
|
||||||
println!("Supervision active. Web UI at http://localhost:8090. Press Ctrl+C to stop.");
|
info!("Supervision active. Web UI at http://localhost:8090. Press Ctrl+C to stop.");
|
||||||
let result = supervise(
|
let result = supervise(
|
||||||
&shared_config,
|
&shared_config,
|
||||||
&shared_status,
|
&shared_status,
|
||||||
@ -214,7 +215,7 @@ pub fn run(config: &Config, config_path: PathBuf) -> Result<()> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Phase 5: Teardown (always runs)
|
// Phase 5: Teardown (always runs)
|
||||||
println!("Shutting down...");
|
info!("Shutting down...");
|
||||||
let config = shared_config.read().unwrap().clone();
|
let config = shared_config.read().unwrap().clone();
|
||||||
shutdown_services(&config, &mut mount_children, &mut protocols);
|
shutdown_services(&config, &mut mount_children, &mut protocols);
|
||||||
|
|
||||||
@ -265,17 +266,17 @@ fn spawn_warmup(
|
|||||||
let warmup_shutdown = Arc::clone(shutdown);
|
let warmup_shutdown = Arc::clone(shutdown);
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
println!("Auto-warmup started (background, generation {generation})...");
|
info!("Auto-warmup started (background, generation {generation})...");
|
||||||
for (i, rule) in warmup_config.warmup.rules.iter().enumerate() {
|
for (i, rule) in warmup_config.warmup.rules.iter().enumerate() {
|
||||||
if warmup_shutdown.load(Ordering::SeqCst) {
|
if warmup_shutdown.load(Ordering::SeqCst) {
|
||||||
println!("Auto-warmup interrupted by shutdown.");
|
info!("Auto-warmup interrupted by shutdown.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Check if our generation is still current
|
// Check if our generation is still current
|
||||||
{
|
{
|
||||||
let status = warmup_status.read().unwrap();
|
let status = warmup_status.read().unwrap();
|
||||||
if status.warmup_generation != generation {
|
if status.warmup_generation != generation {
|
||||||
println!("Auto-warmup superseded by newer generation.");
|
info!("Auto-warmup superseded by newer generation.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,10 +290,10 @@ fn spawn_warmup(
|
|||||||
generation,
|
generation,
|
||||||
&warmup_shutdown,
|
&warmup_shutdown,
|
||||||
) {
|
) {
|
||||||
eprintln!("Warmup warning: {e}");
|
warn!("Warmup warning: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Auto-warmup complete.");
|
info!("Auto-warmup complete.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +341,7 @@ fn spawn_dir_refresh(
|
|||||||
let gen_arc2 = Arc::clone(&gen_arc);
|
let gen_arc2 = Arc::clone(&gen_arc);
|
||||||
let sd = Arc::clone(shutdown);
|
let sd = Arc::clone(shutdown);
|
||||||
|
|
||||||
println!(
|
info!(
|
||||||
" dir-refresh: scheduling '{}' every {}s",
|
" dir-refresh: scheduling '{}' every {}s",
|
||||||
share_name,
|
share_name,
|
||||||
interval.as_secs()
|
interval.as_secs()
|
||||||
@ -353,7 +354,7 @@ fn spawn_dir_refresh(
|
|||||||
.spawn(generation, gen_arc2, sd, move || {
|
.spawn(generation, gen_arc2, sd, move || {
|
||||||
rc::vfs_refresh(rc_port, "/", recursive)
|
rc::vfs_refresh(rc_port, "/", recursive)
|
||||||
.with_context(|| format!("dir-refresh for '{share_name}'"))?;
|
.with_context(|| format!("dir-refresh for '{share_name}'"))?;
|
||||||
println!(" dir-refresh OK: {share_name}");
|
info!(" dir-refresh OK: {share_name}");
|
||||||
let mut s = status.write().unwrap();
|
let mut s = status.write().unwrap();
|
||||||
s.last_dir_refresh.insert(share_name.clone(), SystemTime::now());
|
s.last_dir_refresh.insert(share_name.clone(), SystemTime::now());
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -445,7 +446,7 @@ fn probe_all_shares(
|
|||||||
}
|
}
|
||||||
match handle.join() {
|
match handle.join() {
|
||||||
Ok((name, Ok(()))) => {
|
Ok((name, Ok(()))) => {
|
||||||
println!(" Probe OK: {name}");
|
info!(" Probe OK: {name}");
|
||||||
let mut status = shared_status.write().unwrap();
|
let mut status = shared_status.write().unwrap();
|
||||||
if let Some(ss) = status.shares.iter_mut().find(|s| s.name == name) {
|
if let Some(ss) = status.shares.iter_mut().find(|s| s.name == name) {
|
||||||
ss.health = ShareHealth::Healthy;
|
ss.health = ShareHealth::Healthy;
|
||||||
@ -454,14 +455,14 @@ fn probe_all_shares(
|
|||||||
}
|
}
|
||||||
Ok((name, Err(e))) => {
|
Ok((name, Err(e))) => {
|
||||||
let msg = format!("{e}");
|
let msg = format!("{e}");
|
||||||
eprintln!(" Probe FAILED: {name} — {msg}");
|
error!(" Probe FAILED: {name} — {msg}");
|
||||||
let mut status = shared_status.write().unwrap();
|
let mut status = shared_status.write().unwrap();
|
||||||
if let Some(ss) = status.shares.iter_mut().find(|s| s.name == name) {
|
if let Some(ss) = status.shares.iter_mut().find(|s| s.name == name) {
|
||||||
ss.health = ShareHealth::Failed(msg);
|
ss.health = ShareHealth::Failed(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!(" Probe thread panicked");
|
error!(" Probe thread panicked");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -549,7 +550,7 @@ fn start_and_wait_mounts(config: &Config, shutdown: &AtomicBool) -> Result<Vec<M
|
|||||||
Ok(true) => ready[i] = true,
|
Ok(true) => ready[i] = true,
|
||||||
Ok(false) => all_ready = false,
|
Ok(false) => all_ready = false,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Warning: mount check failed for '{}': {e}", share.name);
|
warn!("Warning: mount check failed for '{}': {e}", share.name);
|
||||||
all_ready = false;
|
all_ready = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -579,7 +580,7 @@ fn spawn_smbd() -> Result<Child> {
|
|||||||
fn start_protocols(config: &Config) -> Result<ProtocolChildren> {
|
fn start_protocols(config: &Config) -> Result<ProtocolChildren> {
|
||||||
let smbd = if config.protocols.enable_smb {
|
let smbd = if config.protocols.enable_smb {
|
||||||
let child = spawn_smbd()?;
|
let child = spawn_smbd()?;
|
||||||
println!(" SMB: started");
|
info!(" SMB: started");
|
||||||
Some(child)
|
Some(child)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -593,12 +594,12 @@ fn start_protocols(config: &Config) -> Result<ProtocolChildren> {
|
|||||||
if !status.success() {
|
if !status.success() {
|
||||||
anyhow::bail!("exportfs -ra failed: {status}");
|
anyhow::bail!("exportfs -ra failed: {status}");
|
||||||
}
|
}
|
||||||
println!(" NFS: exported");
|
info!(" NFS: exported");
|
||||||
}
|
}
|
||||||
|
|
||||||
let webdav = if config.protocols.enable_webdav {
|
let webdav = if config.protocols.enable_webdav {
|
||||||
let child = spawn_webdav(config)?;
|
let child = spawn_webdav(config)?;
|
||||||
println!(" WebDAV: started");
|
info!(" WebDAV: started");
|
||||||
Some(child)
|
Some(child)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -640,15 +641,15 @@ fn supervise(
|
|||||||
// Check for commands (non-blocking with timeout = POLL_INTERVAL)
|
// Check for commands (non-blocking with timeout = POLL_INTERVAL)
|
||||||
match cmd_rx.recv_timeout(POLL_INTERVAL) {
|
match cmd_rx.recv_timeout(POLL_INTERVAL) {
|
||||||
Ok(SupervisorCmd::Shutdown) => {
|
Ok(SupervisorCmd::Shutdown) => {
|
||||||
println!("Shutdown command received.");
|
info!("Shutdown command received.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Ok(SupervisorCmd::BwLimit { up, down }) => {
|
Ok(SupervisorCmd::BwLimit { up, down }) => {
|
||||||
println!("Applying bandwidth limit: up={up}, down={down}");
|
info!("Applying bandwidth limit: up={up}, down={down}");
|
||||||
apply_bwlimit(mounts, &up, &down);
|
apply_bwlimit(mounts, &up, &down);
|
||||||
}
|
}
|
||||||
Ok(SupervisorCmd::Reload(new_config)) => {
|
Ok(SupervisorCmd::Reload(new_config)) => {
|
||||||
println!("Config reload requested...");
|
info!("Config reload requested...");
|
||||||
handle_reload(
|
handle_reload(
|
||||||
shared_config,
|
shared_config,
|
||||||
shared_status,
|
shared_status,
|
||||||
@ -659,18 +660,18 @@ fn supervise(
|
|||||||
new_config,
|
new_config,
|
||||||
&shutdown,
|
&shutdown,
|
||||||
)?;
|
)?;
|
||||||
println!("Config reload complete.");
|
info!("Config reload complete.");
|
||||||
}
|
}
|
||||||
Err(RecvTimeoutError::Timeout) => {} // normal poll cycle
|
Err(RecvTimeoutError::Timeout) => {} // normal poll cycle
|
||||||
Err(RecvTimeoutError::Disconnected) => {
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
println!("Command channel disconnected, shutting down.");
|
info!("Command channel disconnected, shutting down.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for shutdown signal
|
// Check for shutdown signal
|
||||||
if shutdown.load(Ordering::SeqCst) {
|
if shutdown.load(Ordering::SeqCst) {
|
||||||
println!("Shutdown signal received.");
|
info!("Shutdown signal received.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,11 +696,11 @@ fn supervise(
|
|||||||
if let Some(child) = &mut protocols.smbd {
|
if let Some(child) = &mut protocols.smbd {
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
Ok(Some(status)) => {
|
Ok(Some(status)) => {
|
||||||
eprintln!("smbd exited ({status}).");
|
warn!("smbd exited ({status}).");
|
||||||
if smbd_tracker.can_restart() {
|
if smbd_tracker.can_restart() {
|
||||||
smbd_tracker.record_restart();
|
smbd_tracker.record_restart();
|
||||||
let delay = smbd_tracker.count * 2;
|
let delay = smbd_tracker.count * 2;
|
||||||
eprintln!(
|
warn!(
|
||||||
"Restarting smbd in {delay}s ({}/{MAX_RESTARTS})...",
|
"Restarting smbd in {delay}s ({}/{MAX_RESTARTS})...",
|
||||||
smbd_tracker.count,
|
smbd_tracker.count,
|
||||||
);
|
);
|
||||||
@ -707,19 +708,19 @@ fn supervise(
|
|||||||
match spawn_smbd() {
|
match spawn_smbd() {
|
||||||
Ok(new_child) => *child = new_child,
|
Ok(new_child) => *child = new_child,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to restart smbd: {e}");
|
error!("Failed to restart smbd: {e}");
|
||||||
protocols.smbd = None;
|
protocols.smbd = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
error!(
|
||||||
"smbd exceeded max restarts ({MAX_RESTARTS}), giving up."
|
"smbd exceeded max restarts ({MAX_RESTARTS}), giving up."
|
||||||
);
|
);
|
||||||
protocols.smbd = None;
|
protocols.smbd = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => eprintln!("Warning: failed to check smbd status: {e}"),
|
Err(e) => warn!("Warning: failed to check smbd status: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -728,11 +729,11 @@ fn supervise(
|
|||||||
if let Some(child) = &mut protocols.webdav {
|
if let Some(child) = &mut protocols.webdav {
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
Ok(Some(status)) => {
|
Ok(Some(status)) => {
|
||||||
eprintln!("WebDAV exited ({status}).");
|
warn!("WebDAV exited ({status}).");
|
||||||
if webdav_tracker.can_restart() {
|
if webdav_tracker.can_restart() {
|
||||||
webdav_tracker.record_restart();
|
webdav_tracker.record_restart();
|
||||||
let delay = webdav_tracker.count * 2;
|
let delay = webdav_tracker.count * 2;
|
||||||
eprintln!(
|
warn!(
|
||||||
"Restarting WebDAV in {delay}s ({}/{MAX_RESTARTS})...",
|
"Restarting WebDAV in {delay}s ({}/{MAX_RESTARTS})...",
|
||||||
webdav_tracker.count,
|
webdav_tracker.count,
|
||||||
);
|
);
|
||||||
@ -740,19 +741,19 @@ fn supervise(
|
|||||||
match spawn_webdav(&config) {
|
match spawn_webdav(&config) {
|
||||||
Ok(new_child) => *child = new_child,
|
Ok(new_child) => *child = new_child,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to restart WebDAV: {e}");
|
error!("Failed to restart WebDAV: {e}");
|
||||||
protocols.webdav = None;
|
protocols.webdav = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
error!(
|
||||||
"WebDAV exceeded max restarts ({MAX_RESTARTS}), giving up."
|
"WebDAV exceeded max restarts ({MAX_RESTARTS}), giving up."
|
||||||
);
|
);
|
||||||
protocols.webdav = None;
|
protocols.webdav = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => eprintln!("Warning: failed to check WebDAV status: {e}"),
|
Err(e) => warn!("Warning: failed to check WebDAV status: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -818,8 +819,8 @@ fn update_status(
|
|||||||
fn apply_bwlimit(mounts: &[MountChild], up: &str, down: &str) {
|
fn apply_bwlimit(mounts: &[MountChild], up: &str, down: &str) {
|
||||||
for mc in mounts {
|
for mc in mounts {
|
||||||
match rc::bwlimit(mc.rc_port, Some(up), Some(down)) {
|
match rc::bwlimit(mc.rc_port, Some(up), Some(down)) {
|
||||||
Ok(_) => println!(" bwlimit applied to '{}'", mc.name),
|
Ok(_) => info!(" bwlimit applied to '{}'", mc.name),
|
||||||
Err(e) => eprintln!(" bwlimit failed for '{}': {e}", mc.name),
|
Err(e) => warn!(" bwlimit failed for '{}': {e}", mc.name),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -839,18 +840,18 @@ fn handle_reload(
|
|||||||
let diff = config_diff::diff(&old_config, &new_config);
|
let diff = config_diff::diff(&old_config, &new_config);
|
||||||
|
|
||||||
if diff.is_empty() {
|
if diff.is_empty() {
|
||||||
println!(" No changes detected.");
|
info!(" No changes detected.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" Changes: {}", diff.summary());
|
info!(" Changes: {}", diff.summary());
|
||||||
|
|
||||||
match diff.highest_tier() {
|
match diff.highest_tier() {
|
||||||
ChangeTier::None => {}
|
ChangeTier::None => {}
|
||||||
|
|
||||||
ChangeTier::Live => {
|
ChangeTier::Live => {
|
||||||
// Tier A: bandwidth only — RC API call, no restart
|
// Tier A: bandwidth only — RC API call, no restart
|
||||||
println!(" Tier A: applying bandwidth limits via RC API...");
|
info!(" Tier A: applying bandwidth limits via RC API...");
|
||||||
apply_bwlimit(mounts, &new_config.bandwidth.limit_up, &new_config.bandwidth.limit_down);
|
apply_bwlimit(mounts, &new_config.bandwidth.limit_up, &new_config.bandwidth.limit_down);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -860,7 +861,7 @@ fn handle_reload(
|
|||||||
if diff.bandwidth_changed {
|
if diff.bandwidth_changed {
|
||||||
apply_bwlimit(mounts, &new_config.bandwidth.limit_up, &new_config.bandwidth.limit_down);
|
apply_bwlimit(mounts, &new_config.bandwidth.limit_up, &new_config.bandwidth.limit_down);
|
||||||
}
|
}
|
||||||
println!(" Tier B: restarting protocol services...");
|
info!(" Tier B: restarting protocol services...");
|
||||||
restart_protocols(protocols, smbd_tracker, webdav_tracker, &new_config)?;
|
restart_protocols(protocols, smbd_tracker, webdav_tracker, &new_config)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -875,13 +876,13 @@ fn handle_reload(
|
|||||||
|| !diff.connections_removed.is_empty()
|
|| !diff.connections_removed.is_empty()
|
||||||
|| !diff.connections_modified.is_empty()
|
|| !diff.connections_modified.is_empty()
|
||||||
{
|
{
|
||||||
println!(" Regenerating rclone.conf (connections changed)...");
|
info!(" Regenerating rclone.conf (connections changed)...");
|
||||||
crate::rclone::config::write_config(&new_config)?;
|
crate::rclone::config::write_config(&new_config)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle removed shares: drain → unmount → kill
|
// Handle removed shares: drain → unmount → kill
|
||||||
for name in &diff.shares_removed {
|
for name in &diff.shares_removed {
|
||||||
println!(" Removing share '{name}'...");
|
info!(" Removing share '{name}'...");
|
||||||
if let Some(idx) = mounts.iter().position(|mc| mc.name == *name) {
|
if let Some(idx) = mounts.iter().position(|mc| mc.name == *name) {
|
||||||
let mc = &mounts[idx];
|
let mc = &mounts[idx];
|
||||||
wait_writeback_drain(mc.rc_port);
|
wait_writeback_drain(mc.rc_port);
|
||||||
@ -893,7 +894,7 @@ fn handle_reload(
|
|||||||
|
|
||||||
// Handle modified shares: treat as remove + add
|
// Handle modified shares: treat as remove + add
|
||||||
for name in &diff.shares_modified {
|
for name in &diff.shares_modified {
|
||||||
println!(" Restarting modified share '{name}'...");
|
info!(" Restarting modified share '{name}'...");
|
||||||
// Remove old
|
// Remove old
|
||||||
if let Some(idx) = mounts.iter().position(|mc| mc.name == *name) {
|
if let Some(idx) = mounts.iter().position(|mc| mc.name == *name) {
|
||||||
let mc = &mounts[idx];
|
let mc = &mounts[idx];
|
||||||
@ -913,7 +914,7 @@ fn handle_reload(
|
|||||||
|
|
||||||
// Handle added shares: spawn new mount
|
// Handle added shares: spawn new mount
|
||||||
for name in &diff.shares_added {
|
for name in &diff.shares_added {
|
||||||
println!(" Adding share '{name}'...");
|
info!(" Adding share '{name}'...");
|
||||||
if let Some((i, share)) = new_config.shares.iter().enumerate().find(|(_, s)| s.name == *name) {
|
if let Some((i, share)) = new_config.shares.iter().enumerate().find(|(_, s)| s.name == *name) {
|
||||||
let rc_port = new_config.rc_port(i);
|
let rc_port = new_config.rc_port(i);
|
||||||
std::fs::create_dir_all(&share.mount_point).ok();
|
std::fs::create_dir_all(&share.mount_point).ok();
|
||||||
@ -935,7 +936,7 @@ fn handle_reload(
|
|||||||
|
|
||||||
ChangeTier::Global => {
|
ChangeTier::Global => {
|
||||||
// Tier D: global restart — drain all → stop everything → restart
|
// Tier D: global restart — drain all → stop everything → restart
|
||||||
println!(" Tier D: full restart (global settings changed)...");
|
info!(" Tier D: full restart (global settings changed)...");
|
||||||
|
|
||||||
// Drain all write-back queues
|
// Drain all write-back queues
|
||||||
for mc in mounts.iter() {
|
for mc in mounts.iter() {
|
||||||
@ -1027,13 +1028,13 @@ fn handle_reload(
|
|||||||
|
|
||||||
// Re-trigger warmup if settings changed
|
// Re-trigger warmup if settings changed
|
||||||
if diff.warmup_changed {
|
if diff.warmup_changed {
|
||||||
println!(" Warmup settings changed, re-triggering...");
|
info!(" Warmup settings changed, re-triggering...");
|
||||||
spawn_warmup(&new_config, shared_status, shutdown);
|
spawn_warmup(&new_config, shared_status, shutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-trigger dir-refresh if settings changed
|
// Re-trigger dir-refresh if settings changed
|
||||||
if diff.dir_refresh_changed {
|
if diff.dir_refresh_changed {
|
||||||
println!(" Dir-refresh settings changed, re-triggering...");
|
info!(" Dir-refresh settings changed, re-triggering...");
|
||||||
spawn_dir_refresh(&new_config, shared_status, shutdown);
|
spawn_dir_refresh(&new_config, shared_status, shutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1061,7 +1062,7 @@ fn spawn_mount(config: &Config, share: &crate::config::ShareConfig, rc_port: u16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(" Mount ready: {} at {}", share.name, share.mount_point.display());
|
info!(" Mount ready: {} at {}", share.name, share.mount_point.display());
|
||||||
Ok(MountChild {
|
Ok(MountChild {
|
||||||
name: share.name.clone(),
|
name: share.name.clone(),
|
||||||
child,
|
child,
|
||||||
@ -1092,18 +1093,18 @@ fn unmount_share(config: &Config, share_name: &str) {
|
|||||||
fn stop_protocols(protocols: &mut ProtocolChildren, config: &Config) {
|
fn stop_protocols(protocols: &mut ProtocolChildren, config: &Config) {
|
||||||
if let Some(child) = &mut protocols.smbd {
|
if let Some(child) = &mut protocols.smbd {
|
||||||
graceful_kill(child);
|
graceful_kill(child);
|
||||||
println!(" SMB: stopped");
|
info!(" SMB: stopped");
|
||||||
}
|
}
|
||||||
protocols.smbd = None;
|
protocols.smbd = None;
|
||||||
|
|
||||||
if config.protocols.enable_nfs {
|
if config.protocols.enable_nfs {
|
||||||
let _ = Command::new("exportfs").arg("-ua").status();
|
let _ = Command::new("exportfs").arg("-ua").status();
|
||||||
println!(" NFS: unexported");
|
info!(" NFS: unexported");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(child) = &mut protocols.webdav {
|
if let Some(child) = &mut protocols.webdav {
|
||||||
graceful_kill(child);
|
graceful_kill(child);
|
||||||
println!(" WebDAV: stopped");
|
info!(" WebDAV: stopped");
|
||||||
}
|
}
|
||||||
protocols.webdav = None;
|
protocols.webdav = None;
|
||||||
}
|
}
|
||||||
@ -1123,13 +1124,13 @@ fn reload_protocol_configs(protocols: &ProtocolChildren, config: &Config) -> Res
|
|||||||
let pid = child.id() as i32;
|
let pid = child.id() as i32;
|
||||||
// SAFETY: sending SIGHUP to a known child PID is safe.
|
// SAFETY: sending SIGHUP to a known child PID is safe.
|
||||||
unsafe { libc::kill(pid, libc::SIGHUP) };
|
unsafe { libc::kill(pid, libc::SIGHUP) };
|
||||||
println!(" SMB: config reloaded (SIGHUP)");
|
info!(" SMB: config reloaded (SIGHUP)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config.protocols.enable_nfs {
|
if config.protocols.enable_nfs {
|
||||||
nfs::write_config(config)?;
|
nfs::write_config(config)?;
|
||||||
let _ = Command::new("exportfs").arg("-ra").status();
|
let _ = Command::new("exportfs").arg("-ra").status();
|
||||||
println!(" NFS: re-exported");
|
info!(" NFS: re-exported");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -1206,17 +1207,17 @@ fn wait_writeback_drain(port: u16) {
|
|||||||
let pending = dc.uploads_in_progress + dc.uploads_queued;
|
let pending = dc.uploads_in_progress + dc.uploads_queued;
|
||||||
if pending == 0 {
|
if pending == 0 {
|
||||||
if !first {
|
if !first {
|
||||||
println!(" Write-back queue drained.");
|
info!(" Write-back queue drained.");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if first {
|
if first {
|
||||||
println!(
|
info!(
|
||||||
" Waiting for write-back queue ({pending} files pending)..."
|
" Waiting for write-back queue ({pending} files pending)..."
|
||||||
);
|
);
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
eprint!("\r Write-back: {pending} files remaining... ");
|
info!(" Write-back: {pending} files remaining...");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
@ -1226,8 +1227,7 @@ fn wait_writeback_drain(port: u16) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if Instant::now() > deadline {
|
if Instant::now() > deadline {
|
||||||
eprintln!();
|
warn!(
|
||||||
eprintln!(
|
|
||||||
" Warning: write-back drain timed out after {}s, proceeding.",
|
" Warning: write-back drain timed out after {}s, proceeding.",
|
||||||
WRITEBACK_DRAIN_TIMEOUT.as_secs()
|
WRITEBACK_DRAIN_TIMEOUT.as_secs()
|
||||||
);
|
);
|
||||||
@ -1264,13 +1264,13 @@ fn shutdown_services(config: &Config, mounts: &mut Vec<MountChild>, protocols: &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!(" FUSE: unmounted");
|
info!(" FUSE: unmounted");
|
||||||
|
|
||||||
// Gracefully stop all rclone mount processes
|
// Gracefully stop all rclone mount processes
|
||||||
for mc in mounts.iter_mut() {
|
for mc in mounts.iter_mut() {
|
||||||
graceful_kill(&mut mc.child);
|
graceful_kill(&mut mc.child);
|
||||||
}
|
}
|
||||||
println!(" rclone: stopped");
|
info!(" rclone: stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use axum::Router;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::daemon::{LogEntry, SupervisorCmd};
|
use crate::daemon::SupervisorCmd;
|
||||||
use crate::web::SharedState;
|
use crate::web::SharedState;
|
||||||
|
|
||||||
pub fn routes() -> Router<SharedState> {
|
pub fn routes() -> Router<SharedState> {
|
||||||
@ -305,27 +305,64 @@ async fn post_bwlimit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/logs?since=0 — recent log entries.
|
/// GET /api/logs?lines=200&from_line=0 — recent log file entries.
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct LogsQuery {
|
struct LogsQuery {
|
||||||
|
#[serde(default = "default_lines")]
|
||||||
|
lines: usize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
since: u64,
|
from_line: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_lines() -> usize {
|
||||||
|
200
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct LogsResponse {
|
struct LogsResponse {
|
||||||
next_id: u64,
|
total_lines: usize,
|
||||||
entries: Vec<LogEntry>,
|
entries: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_logs(
|
async fn get_logs(
|
||||||
State(state): State<SharedState>,
|
State(state): State<SharedState>,
|
||||||
Query(params): Query<LogsQuery>,
|
Query(params): Query<LogsQuery>,
|
||||||
) -> Json<LogsResponse> {
|
) -> Json<LogsResponse> {
|
||||||
let logs = state.logs.read().unwrap();
|
let log_file = {
|
||||||
let entries = logs.since(params.since);
|
let config = state.config.read().unwrap();
|
||||||
|
config.log.file.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if log_file.is_empty() {
|
||||||
|
return Json(LogsResponse {
|
||||||
|
total_lines: 0,
|
||||||
|
entries: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = match std::fs::read_to_string(&log_file) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => {
|
||||||
|
return Json(LogsResponse {
|
||||||
|
total_lines: 0,
|
||||||
|
entries: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let all_lines: Vec<String> = content
|
||||||
|
.lines()
|
||||||
|
.map(|l| l.to_string())
|
||||||
|
.collect();
|
||||||
|
let total_lines = all_lines.len();
|
||||||
|
let entries: Vec<String> = all_lines
|
||||||
|
.into_iter()
|
||||||
|
.skip(params.from_line)
|
||||||
|
.take(params.lines)
|
||||||
|
.collect();
|
||||||
|
|
||||||
Json(LogsResponse {
|
Json(LogsResponse {
|
||||||
next_id: logs.next_id(),
|
total_lines,
|
||||||
entries,
|
entries,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ use axum::routing::get;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::daemon::{AppState, DaemonStatus, LogBuffer, SupervisorCmd, DEFAULT_WEB_PORT};
|
use crate::daemon::{AppState, DaemonStatus, SupervisorCmd, DEFAULT_WEB_PORT};
|
||||||
|
|
||||||
/// Axum-compatible shared state (wraps AppState in an Arc for axum).
|
/// Axum-compatible shared state (wraps AppState in an Arc for axum).
|
||||||
pub type SharedState = Arc<AppState>;
|
pub type SharedState = Arc<AppState>;
|
||||||
@ -50,18 +50,12 @@ pub fn spawn_web_server(
|
|||||||
sse_tx: tokio::sync::broadcast::Sender<()>,
|
sse_tx: tokio::sync::broadcast::Sender<()>,
|
||||||
) -> thread::JoinHandle<()> {
|
) -> thread::JoinHandle<()> {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let logs = Arc::new(RwLock::new(LogBuffer::new()));
|
|
||||||
{
|
|
||||||
let mut lb = logs.write().unwrap();
|
|
||||||
lb.push("Web UI started");
|
|
||||||
}
|
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
config,
|
config,
|
||||||
status,
|
status,
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
config_path,
|
config_path,
|
||||||
sse_tx,
|
sse_tx,
|
||||||
logs,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
@ -76,9 +70,9 @@ pub fn spawn_web_server(
|
|||||||
let listener = tokio::net::TcpListener::bind(&addr)
|
let listener = tokio::net::TcpListener::bind(&addr)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| panic!("Failed to bind web server to {addr}: {e}"));
|
.unwrap_or_else(|e| panic!("Failed to bind web server to {addr}: {e}"));
|
||||||
println!(" Web UI: http://localhost:{DEFAULT_WEB_PORT}");
|
tracing::info!(" Web UI: http://localhost:{DEFAULT_WEB_PORT}");
|
||||||
if let Err(e) = axum::serve(listener, app).await {
|
if let Err(e) = axum::serve(listener, app).await {
|
||||||
eprintln!("Web server error: {e}");
|
tracing::error!("Web server error: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@ -571,10 +571,7 @@ async fn config_apply(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
tracing::info!("Config applied: {diff_summary}");
|
||||||
let mut logs = state.logs.write().unwrap();
|
|
||||||
logs.push(format!("Config applied: {diff_summary}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Json(ConfigApplyResponse {
|
Json(ConfigApplyResponse {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
function logViewerFn() {
|
function logViewerFn() {
|
||||||
return {
|
return {
|
||||||
entries: [],
|
entries: [],
|
||||||
nextId: 0,
|
fromLine: 0,
|
||||||
polling: null,
|
polling: null,
|
||||||
autoScroll: true,
|
autoScroll: true,
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ function logViewerFn() {
|
|||||||
|
|
||||||
async fetchLogs() {
|
async fetchLogs() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/logs?since=' + this.nextId);
|
const resp = await fetch('/api/logs?lines=200&from_line=' + this.fromLine);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.entries.length > 0) {
|
if (data.entries.length > 0) {
|
||||||
this.entries = this.entries.concat(data.entries);
|
this.entries = this.entries.concat(data.entries);
|
||||||
@ -25,7 +25,7 @@ function logViewerFn() {
|
|||||||
if (this.entries.length > 1000) {
|
if (this.entries.length > 1000) {
|
||||||
this.entries = this.entries.slice(-500);
|
this.entries = this.entries.slice(-500);
|
||||||
}
|
}
|
||||||
this.nextId = data.next_id;
|
this.fromLine = data.total_lines;
|
||||||
if (this.autoScroll) {
|
if (this.autoScroll) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const el = this.$refs.logBox;
|
const el = this.$refs.logBox;
|
||||||
@ -36,11 +36,6 @@ function logViewerFn() {
|
|||||||
} catch(e) { /* ignore fetch errors */ }
|
} catch(e) { /* ignore fetch errors */ }
|
||||||
},
|
},
|
||||||
|
|
||||||
formatTime(ts) {
|
|
||||||
const d = new Date(ts * 1000);
|
|
||||||
return d.toLocaleTimeString('en-GB', { hour12: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
}
|
}
|
||||||
@ -74,10 +69,9 @@ if (window.Alpine) {
|
|||||||
<template x-if="entries.length === 0">
|
<template x-if="entries.length === 0">
|
||||||
<div style="color:var(--text-muted);padding:24px;text-align:center">No log entries yet. Events will appear here as they occur.</div>
|
<div style="color:var(--text-muted);padding:24px;text-align:center">No log entries yet. Events will appear here as they occur.</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-for="entry in entries" :key="entry.id">
|
<template x-for="(line, idx) in entries" :key="idx">
|
||||||
<div class="log-line">
|
<div class="log-line">
|
||||||
<span class="log-ts" x-text="formatTime(entry.ts)"></span>
|
<span class="log-msg" x-text="line"></span>
|
||||||
<span class="log-msg" x-text="entry.msg"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user