Apply DISPLAY_TIMEZONE to GC and integrity history initial render so the system dashboard table doesn't switch zones after refresh
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2729,6 +2729,7 @@ dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"cookie",
|
||||
"crc32fast",
|
||||
|
||||
@@ -41,6 +41,7 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
thiserror = "2"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono-tz = "0.9"
|
||||
base64 = "0.22"
|
||||
tokio-util = { version = "0.7", features = ["io", "io-util"] }
|
||||
tokio-stream = "0.1"
|
||||
|
||||
@@ -25,6 +25,7 @@ tracing-subscriber = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
http-body = "1"
|
||||
|
||||
@@ -84,6 +84,7 @@ pub struct ServerConfig {
|
||||
pub cors_expose_headers: Vec<String>,
|
||||
pub session_lifetime_days: u64,
|
||||
pub log_level: String,
|
||||
pub display_timezone: String,
|
||||
pub multipart_min_part_size: u64,
|
||||
pub bulk_delete_max_keys: usize,
|
||||
pub stream_chunk_size: usize,
|
||||
@@ -240,6 +241,19 @@ impl ServerConfig {
|
||||
let cors_expose_headers = parse_list_env("CORS_EXPOSE_HEADERS", "*");
|
||||
let session_lifetime_days = parse_u64_env("SESSION_LIFETIME_DAYS", 1);
|
||||
let log_level = std::env::var("LOG_LEVEL").unwrap_or_else(|_| "INFO".to_string());
|
||||
let display_timezone = {
|
||||
let raw = std::env::var("DISPLAY_TIMEZONE").unwrap_or_else(|_| "UTC".to_string());
|
||||
match raw.parse::<chrono_tz::Tz>() {
|
||||
Ok(_) => raw,
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
"Invalid DISPLAY_TIMEZONE '{}', falling back to UTC",
|
||||
raw
|
||||
);
|
||||
"UTC".to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
let multipart_min_part_size = parse_u64_env("MULTIPART_MIN_PART_SIZE", 5_242_880);
|
||||
let bulk_delete_max_keys = parse_usize_env("BULK_DELETE_MAX_KEYS", 1000);
|
||||
let stream_chunk_size = parse_usize_env("STREAM_CHUNK_SIZE", 1_048_576);
|
||||
@@ -331,6 +345,7 @@ impl ServerConfig {
|
||||
cors_expose_headers,
|
||||
session_lifetime_days,
|
||||
log_level,
|
||||
display_timezone,
|
||||
multipart_min_part_size,
|
||||
bulk_delete_max_keys,
|
||||
stream_chunk_size,
|
||||
@@ -425,6 +440,7 @@ impl Default for ServerConfig {
|
||||
cors_expose_headers: vec!["*".to_string()],
|
||||
session_lifetime_days: 1,
|
||||
log_level: "INFO".to_string(),
|
||||
display_timezone: "UTC".to_string(),
|
||||
multipart_min_part_size: 5_242_880,
|
||||
bulk_delete_max_keys: 1000,
|
||||
stream_chunk_size: 1_048_576,
|
||||
|
||||
@@ -1951,13 +1951,17 @@ pub async fn metrics_dashboard(
|
||||
render(&state, "metrics.html", &ctx)
|
||||
}
|
||||
|
||||
fn format_history_timestamp(timestamp: Option<f64>) -> String {
|
||||
fn format_history_timestamp(timestamp: Option<f64>, tz: chrono_tz::Tz) -> String {
|
||||
let Some(timestamp) = timestamp else {
|
||||
return "-".to_string();
|
||||
};
|
||||
let millis = (timestamp * 1000.0).round() as i64;
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp_millis(millis)
|
||||
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
|
||||
.map(|dt| {
|
||||
dt.with_timezone(&tz)
|
||||
.format("%Y-%m-%d %H:%M:%S %Z")
|
||||
.to_string()
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
}
|
||||
|
||||
@@ -1976,7 +1980,7 @@ fn format_byte_count(bytes: u64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn decorate_gc_history(executions: &[Value]) -> Vec<Value> {
|
||||
fn decorate_gc_history(executions: &[Value], tz: chrono_tz::Tz) -> Vec<Value> {
|
||||
executions
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -1990,7 +1994,7 @@ fn decorate_gc_history(executions: &[Value]) -> Vec<Value> {
|
||||
if let Some(obj) = execution.as_object_mut() {
|
||||
obj.insert(
|
||||
"timestamp_display".to_string(),
|
||||
Value::String(format_history_timestamp(timestamp)),
|
||||
Value::String(format_history_timestamp(timestamp, tz)),
|
||||
);
|
||||
obj.insert(
|
||||
"bytes_freed_display".to_string(),
|
||||
@@ -2002,7 +2006,7 @@ fn decorate_gc_history(executions: &[Value]) -> Vec<Value> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn decorate_integrity_history(executions: &[Value]) -> Vec<Value> {
|
||||
fn decorate_integrity_history(executions: &[Value], tz: chrono_tz::Tz) -> Vec<Value> {
|
||||
executions
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -2011,7 +2015,7 @@ fn decorate_integrity_history(executions: &[Value]) -> Vec<Value> {
|
||||
if let Some(obj) = execution.as_object_mut() {
|
||||
obj.insert(
|
||||
"timestamp_display".to_string(),
|
||||
Value::String(format_history_timestamp(timestamp)),
|
||||
Value::String(format_history_timestamp(timestamp, tz)),
|
||||
);
|
||||
}
|
||||
execution
|
||||
@@ -2024,6 +2028,11 @@ pub async fn system_dashboard(
|
||||
Extension(session): Extension<SessionHandle>,
|
||||
) -> Response {
|
||||
let mut ctx = page_context(&state, &session, "ui.system_dashboard");
|
||||
let display_tz: chrono_tz::Tz = state
|
||||
.config
|
||||
.display_timezone
|
||||
.parse()
|
||||
.unwrap_or(chrono_tz::UTC);
|
||||
|
||||
let gc_status = match &state.gc {
|
||||
Some(gc) => gc.status().await,
|
||||
@@ -2045,7 +2054,7 @@ pub async fn system_dashboard(
|
||||
.await
|
||||
.get("executions")
|
||||
.and_then(|value| value.as_array())
|
||||
.map(|values| decorate_gc_history(values))
|
||||
.map(|values| decorate_gc_history(values, display_tz))
|
||||
.unwrap_or_default(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
@@ -2069,7 +2078,7 @@ pub async fn system_dashboard(
|
||||
.await
|
||||
.get("executions")
|
||||
.and_then(|value| value.as_array())
|
||||
.map(|values| decorate_integrity_history(values))
|
||||
.map(|values| decorate_integrity_history(values, display_tz))
|
||||
.unwrap_or_default(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
@@ -2081,7 +2090,7 @@ pub async fn system_dashboard(
|
||||
ctx.insert("gc_status", &gc_status);
|
||||
ctx.insert("integrity_status", &integrity_status);
|
||||
ctx.insert("app_version", &env!("CARGO_PKG_VERSION"));
|
||||
ctx.insert("display_timezone", &"UTC");
|
||||
ctx.insert("display_timezone", &state.config.display_timezone);
|
||||
ctx.insert("platform", &std::env::consts::OS);
|
||||
ctx.insert(
|
||||
"storage_root",
|
||||
|
||||
@@ -192,7 +192,7 @@ impl AppState {
|
||||
None
|
||||
};
|
||||
|
||||
let templates = init_templates(&config.templates_dir);
|
||||
let templates = init_templates(&config.templates_dir, &config.display_timezone);
|
||||
let access_logging = Arc::new(AccessLoggingService::new(&config.storage_root));
|
||||
let session_ttl = Duration::from_secs(config.session_lifetime_days.saturating_mul(86_400));
|
||||
Self {
|
||||
@@ -259,15 +259,18 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
fn init_templates(templates_dir: &std::path::Path) -> Option<Arc<TemplateEngine>> {
|
||||
fn init_templates(
|
||||
templates_dir: &std::path::Path,
|
||||
display_timezone: &str,
|
||||
) -> Option<Arc<TemplateEngine>> {
|
||||
let use_disk = std::env::var("TEMPLATES_DIR").is_ok() && templates_dir.is_dir();
|
||||
let result = if use_disk {
|
||||
let glob = format!("{}/*.html", templates_dir.display()).replace('\\', "/");
|
||||
tracing::info!("Loading templates from disk: {}", templates_dir.display());
|
||||
TemplateEngine::new(&glob)
|
||||
TemplateEngine::new(&glob, display_timezone)
|
||||
} else {
|
||||
tracing::info!("Loading templates from embedded assets");
|
||||
TemplateEngine::from_embedded()
|
||||
TemplateEngine::from_embedded(display_timezone)
|
||||
};
|
||||
match result {
|
||||
Ok(engine) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use parking_lot::RwLock;
|
||||
use serde_json::Value;
|
||||
use tera::{Context, Error as TeraError, Tera};
|
||||
@@ -16,10 +17,10 @@ pub struct TemplateEngine {
|
||||
}
|
||||
|
||||
impl TemplateEngine {
|
||||
pub fn new(template_glob: &str) -> Result<Self, TeraError> {
|
||||
pub fn new(template_glob: &str, display_timezone: &str) -> Result<Self, TeraError> {
|
||||
let mut tera = Tera::new(template_glob)?;
|
||||
tera.set_escape_fn(html_escape);
|
||||
register_filters(&mut tera);
|
||||
register_filters(&mut tera, display_timezone);
|
||||
|
||||
let endpoints: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
@@ -31,10 +32,10 @@ impl TemplateEngine {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_embedded() -> Result<Self, TeraError> {
|
||||
pub fn from_embedded(display_timezone: &str) -> Result<Self, TeraError> {
|
||||
let mut tera = Tera::default();
|
||||
tera.set_escape_fn(html_escape);
|
||||
register_filters(&mut tera);
|
||||
register_filters(&mut tera, display_timezone);
|
||||
|
||||
let names = crate::embedded::template_names();
|
||||
let mut entries: Vec<(String, String)> = Vec::with_capacity(names.len());
|
||||
@@ -95,8 +96,14 @@ fn html_escape(input: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn register_filters(tera: &mut Tera) {
|
||||
tera.register_filter("format_datetime", format_datetime_filter);
|
||||
fn register_filters(tera: &mut Tera, display_timezone: &str) {
|
||||
let tz: Tz = display_timezone.parse().unwrap_or(chrono_tz::UTC);
|
||||
tera.register_filter(
|
||||
"format_datetime",
|
||||
move |value: &Value, args: &HashMap<String, Value>| -> tera::Result<Value> {
|
||||
format_datetime_filter(value, args, tz)
|
||||
},
|
||||
);
|
||||
tera.register_filter("filesizeformat", filesizeformat_filter);
|
||||
tera.register_filter("slice", slice_filter);
|
||||
}
|
||||
@@ -186,11 +193,15 @@ fn urlencode_query(s: &str) -> String {
|
||||
percent_encoding::utf8_percent_encode(s, UNRESERVED).to_string()
|
||||
}
|
||||
|
||||
fn format_datetime_filter(value: &Value, args: &HashMap<String, Value>) -> tera::Result<Value> {
|
||||
fn format_datetime_filter(
|
||||
value: &Value,
|
||||
args: &HashMap<String, Value>,
|
||||
tz: Tz,
|
||||
) -> tera::Result<Value> {
|
||||
let format = args
|
||||
.get("format")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("%Y-%m-%d %H:%M:%S UTC");
|
||||
.unwrap_or("%Y-%m-%d %H:%M:%S %Z");
|
||||
|
||||
let dt: Option<DateTime<Utc>> = match value {
|
||||
Value::String(s) => DateTime::parse_from_rfc3339(s)
|
||||
@@ -210,7 +221,7 @@ fn format_datetime_filter(value: &Value, args: &HashMap<String, Value>) -> tera:
|
||||
};
|
||||
|
||||
match dt {
|
||||
Some(d) => Ok(Value::String(d.format(format).to_string())),
|
||||
Some(d) => Ok(Value::String(d.with_timezone(&tz).format(format).to_string())),
|
||||
None => Ok(value.clone()),
|
||||
}
|
||||
}
|
||||
@@ -291,7 +302,7 @@ mod tests {
|
||||
let tpl = tmp.path().join("t.html");
|
||||
std::fs::write(&tpl, "").unwrap();
|
||||
let glob = format!("{}/*.html", tmp.path().display());
|
||||
let engine = TemplateEngine::new(&glob).unwrap();
|
||||
let engine = TemplateEngine::new(&glob, "UTC").unwrap();
|
||||
engine.register_endpoints(&[
|
||||
("ui.buckets_overview", "/ui/buckets"),
|
||||
("ui.bucket_detail", "/ui/buckets/{bucket_name}"),
|
||||
@@ -356,7 +367,7 @@ mod tests {
|
||||
path.push("templates");
|
||||
path.push("*.html");
|
||||
let glob = path.to_string_lossy().replace('\\', "/");
|
||||
let engine = TemplateEngine::new(&glob).expect("Tera parse failed");
|
||||
let engine = TemplateEngine::new(&glob, "UTC").expect("Tera parse failed");
|
||||
let names: Vec<String> = engine
|
||||
.tera
|
||||
.read()
|
||||
@@ -372,7 +383,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn embedded_templates_parse() {
|
||||
let engine = TemplateEngine::from_embedded().expect("Embedded Tera parse failed");
|
||||
let engine = TemplateEngine::from_embedded("UTC").expect("Embedded Tera parse failed");
|
||||
let names: Vec<String> = engine
|
||||
.tera
|
||||
.read()
|
||||
@@ -393,8 +404,21 @@ mod tests {
|
||||
let v = format_datetime_filter(
|
||||
&Value::String("2024-06-15T12:34:56Z".into()),
|
||||
&HashMap::new(),
|
||||
chrono_tz::UTC,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v, Value::String("2024-06-15 12:34:56 UTC".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_datetime_custom_timezone() {
|
||||
let tz: Tz = "America/New_York".parse().unwrap();
|
||||
let v = format_datetime_filter(
|
||||
&Value::String("2024-06-15T12:34:56Z".into()),
|
||||
&HashMap::new(),
|
||||
tz,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v, Value::String("2024-06-15 08:34:56 EDT".into()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ fn engine() -> TemplateEngine {
|
||||
path.push("templates");
|
||||
path.push("*.html");
|
||||
let glob = path.to_string_lossy().replace('\\', "/");
|
||||
let engine = TemplateEngine::new(&glob).expect("template parse");
|
||||
let engine = TemplateEngine::new(&glob, "UTC").expect("template parse");
|
||||
myfsio_server::handlers::ui_pages::register_ui_endpoints(&engine);
|
||||
engine
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user