Skip to content

Commit eac5e22

Browse files
committed
feat(logging): compact coloured timestamp + fix UI compile error
Adds src/logging.rs with CompactUtcTime (FormatTime impl, time crate). Replaces the default ISO 8601 timestamp with [2026-05-22]-[12:56:28.9]. ANSI-gated colouring on terminals: orange date digits, light blue time digits, darker shades on internal punctuation, gray brackets/separator, green separator dash. Plain text fallback for the UI log panel and Android Logcat. Wired into all three subscriber setups (main, UI, JNI). Also fixes a compile error in the UI binary: stream_timeout_secs was added to Config in b3c51e0 but never plumbed through FormState. Added to the struct, both init paths (load-from-config and fresh-default), to_config() output, and ConfigWire so Save doesn't drop a non-default value.
1 parent 99ab0c3 commit eac5e22

5 files changed

Lines changed: 208 additions & 1 deletion

File tree

src/android_jni.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ fn install_logging_once() {
132132
.with_target(false)
133133
.with_ansi(false)
134134
.with_writer(LogcatWriter)
135+
.with_timer(crate::logging::CompactUtcTime)
135136
.try_init();
136137

137138
let _ = rustls::crypto::ring::default_provider().install_default();

src/bin/ui.rs

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ struct FormState {
294294
auto_blacklist_window_secs: u64,
295295
auto_blacklist_cooldown_secs: u64,
296296
request_timeout_secs: u64,
297+
stream_timeout_secs: u64,
297298
/// Optional second-hop exit node for CF-anti-bot bypass (chatgpt.com /
298299
/// claude.ai / grok.com / x.com). Config-only — no UI editor yet.
299300
/// See `assets/exit_node/` for the generic exit-node handler.
@@ -391,6 +392,7 @@ fn load_form() -> (FormState, Option<String>) {
391392
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
392393
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
393394
request_timeout_secs: c.request_timeout_secs,
395+
stream_timeout_secs: c.stream_timeout_secs,
394396
exit_node: c.exit_node.clone(),
395397
}
396398
} else {
@@ -433,6 +435,7 @@ fn load_form() -> (FormState, Option<String>) {
433435
auto_blacklist_window_secs: 30,
434436
auto_blacklist_cooldown_secs: 120,
435437
request_timeout_secs: 30,
438+
stream_timeout_secs: 300,
436439
exit_node: mhrv_rs::config::ExitNodeConfig::default(),
437440
}
438441
};
@@ -618,6 +621,7 @@ impl FormState {
618621
auto_blacklist_window_secs: self.auto_blacklist_window_secs,
619622
auto_blacklist_cooldown_secs: self.auto_blacklist_cooldown_secs,
620623
request_timeout_secs: self.request_timeout_secs,
624+
stream_timeout_secs: self.stream_timeout_secs,
621625
// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai
622626
// / grok.com / x.com). Round-trip through FormState — config-only
623627
// editing for now, UI editor planned for v1.9.x desktop UI batch.
@@ -637,6 +641,166 @@ fn save_config(cfg: &Config) -> Result<PathBuf, String> {
637641
Ok(path)
638642
}
639643

644+
#[derive(serde::Serialize)]
645+
struct ConfigWire<'a> {
646+
mode: &'a str,
647+
google_ip: &'a str,
648+
front_domain: &'a str,
649+
#[serde(skip_serializing_if = "Option::is_none")]
650+
script_id: Option<ScriptIdWire<'a>>,
651+
auth_key: &'a str,
652+
listen_host: &'a str,
653+
listen_port: u16,
654+
#[serde(skip_serializing_if = "Option::is_none")]
655+
socks5_port: Option<u16>,
656+
log_level: &'a str,
657+
verify_ssl: bool,
658+
#[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
659+
hosts: &'a std::collections::HashMap<String, String>,
660+
#[serde(skip_serializing_if = "Option::is_none")]
661+
upstream_socks5: Option<&'a str>,
662+
#[serde(skip_serializing_if = "is_zero_u8")]
663+
parallel_relay: u8,
664+
#[serde(skip_serializing_if = "Option::is_none")]
665+
sni_hosts: Option<Vec<&'a str>>,
666+
#[serde(skip_serializing_if = "is_false")]
667+
normalize_x_graphql: bool,
668+
#[serde(skip_serializing_if = "is_false")]
669+
youtube_via_relay: bool,
670+
#[serde(skip_serializing_if = "Vec::is_empty")]
671+
passthrough_hosts: &'a Vec<String>,
672+
// IP-scan knobs. These used to be missing from the wire struct, so
673+
// every Save-config silently dropped them — the user would toggle
674+
// "fetch from API" on, save, reopen, and find it off again. Add
675+
// them here and keep them in sync if Config ever grows more.
676+
#[serde(skip_serializing_if = "is_false")]
677+
fetch_ips_from_api: bool,
678+
max_ips_to_scan: usize,
679+
scan_batch_size: usize,
680+
google_ip_validation: bool,
681+
/// Default false (= bypass DoH). Only emitted when explicitly true
682+
/// so unchanged configs stay clean.
683+
#[serde(skip_serializing_if = "is_false")]
684+
tunnel_doh: bool,
685+
#[serde(skip_serializing_if = "Vec::is_empty")]
686+
bypass_doh_hosts: &'a Vec<String>,
687+
/// PR #763: default true (= browser DoH rejected, system DNS used).
688+
/// Skip when matching default to keep unchanged configs clean —
689+
/// emit only when the user has explicitly disabled the block.
690+
#[serde(skip_serializing_if = "is_true")]
691+
block_doh: bool,
692+
/// Default false. Emit only when the user enables STUN/TURN blocking.
693+
#[serde(skip_serializing_if = "is_false")]
694+
block_stun: bool,
695+
#[serde(skip_serializing_if = "Vec::is_empty")]
696+
fronting_groups: &'a Vec<FrontingGroup>,
697+
/// Auto-blacklist tuning + batch timeout (#391, #444, #430). Skip
698+
/// serialization when matching the historical defaults so unchanged
699+
/// configs stay clean — only emitted when the user has explicitly
700+
/// tuned them.
701+
#[serde(skip_serializing_if = "is_default_strikes")]
702+
auto_blacklist_strikes: u32,
703+
#[serde(skip_serializing_if = "is_default_window_secs")]
704+
auto_blacklist_window_secs: u64,
705+
#[serde(skip_serializing_if = "is_default_cooldown_secs")]
706+
auto_blacklist_cooldown_secs: u64,
707+
#[serde(skip_serializing_if = "is_default_timeout_secs")]
708+
request_timeout_secs: u64,
709+
#[serde(skip_serializing_if = "is_default_stream_timeout_secs")]
710+
stream_timeout_secs: u64,
711+
/// HTTP/2 multiplexing kill switch. Default false (h2 active); only
712+
/// emitted on save when the user has explicitly disabled h2, so
713+
/// unchanged configs stay clean.
714+
#[serde(skip_serializing_if = "is_false")]
715+
force_http1: bool,
716+
/// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai /
717+
/// grok.com / x.com via exit-node second-hop relay). Skip when fully
718+
/// default (disabled with no URL/PSK/hosts) so configs without
719+
/// exit-node setup stay clean. Round-tripped through FormState so
720+
/// Save preserves user-edited values.
721+
#[serde(skip_serializing_if = "is_default_exit_node")]
722+
exit_node: &'a mhrv_rs::config::ExitNodeConfig,
723+
}
724+
725+
fn is_default_strikes(v: &u32) -> bool { *v == 3 }
726+
fn is_default_window_secs(v: &u64) -> bool { *v == 30 }
727+
fn is_default_cooldown_secs(v: &u64) -> bool { *v == 120 }
728+
fn is_default_timeout_secs(v: &u64) -> bool { *v == 30 }
729+
fn is_default_stream_timeout_secs(v: &u64) -> bool { *v == 300 }
730+
fn is_default_exit_node(en: &&mhrv_rs::config::ExitNodeConfig) -> bool {
731+
!en.enabled
732+
&& en.relay_url.is_empty()
733+
&& en.psk.is_empty()
734+
&& en.hosts.is_empty()
735+
&& (en.mode.is_empty() || en.mode == "selective")
736+
}
737+
738+
fn is_false(b: &bool) -> bool {
739+
!*b
740+
}
741+
742+
fn is_true(b: &bool) -> bool {
743+
*b
744+
}
745+
746+
fn is_zero_u8(v: &u8) -> bool {
747+
*v == 0
748+
}
749+
750+
#[derive(serde::Serialize)]
751+
#[serde(untagged)]
752+
enum ScriptIdWire<'a> {
753+
One(&'a str),
754+
Many(Vec<&'a str>),
755+
}
756+
757+
impl<'a> From<&'a Config> for ConfigWire<'a> {
758+
fn from(c: &'a Config) -> Self {
759+
let script_id = c.script_id.as_ref().map(|s| match s {
760+
ScriptId::One(v) => ScriptIdWire::One(v.as_str()),
761+
ScriptId::Many(v) => ScriptIdWire::Many(v.iter().map(String::as_str).collect()),
762+
});
763+
ConfigWire {
764+
mode: c.mode.as_str(),
765+
google_ip: c.google_ip.as_str(),
766+
front_domain: c.front_domain.as_str(),
767+
script_id,
768+
auth_key: c.auth_key.as_str(),
769+
listen_host: c.listen_host.as_str(),
770+
listen_port: c.listen_port,
771+
socks5_port: c.socks5_port,
772+
log_level: c.log_level.as_str(),
773+
verify_ssl: c.verify_ssl,
774+
hosts: &c.hosts,
775+
upstream_socks5: c.upstream_socks5.as_deref(),
776+
parallel_relay: c.parallel_relay,
777+
sni_hosts: c
778+
.sni_hosts
779+
.as_ref()
780+
.map(|v| v.iter().map(String::as_str).collect()),
781+
normalize_x_graphql: c.normalize_x_graphql,
782+
youtube_via_relay: c.youtube_via_relay,
783+
passthrough_hosts: &c.passthrough_hosts,
784+
fetch_ips_from_api: c.fetch_ips_from_api,
785+
max_ips_to_scan: c.max_ips_to_scan,
786+
scan_batch_size: c.scan_batch_size,
787+
google_ip_validation: c.google_ip_validation,
788+
tunnel_doh: c.tunnel_doh,
789+
bypass_doh_hosts: &c.bypass_doh_hosts,
790+
block_doh: c.block_doh,
791+
block_stun: c.block_stun,
792+
fronting_groups: &c.fronting_groups,
793+
auto_blacklist_strikes: c.auto_blacklist_strikes,
794+
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
795+
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
796+
request_timeout_secs: c.request_timeout_secs,
797+
stream_timeout_secs: c.stream_timeout_secs,
798+
force_http1: c.force_http1,
799+
exit_node: &c.exit_node,
800+
}
801+
}
802+
}
803+
640804
/// Accent color — same blue used throughout the UI for primary actions.
641805
const ACCENT: egui::Color32 = egui::Color32::from_rgb(70, 120, 180);
642806
const ACCENT_HOVER: egui::Color32 = egui::Color32::from_rgb(90, 145, 205);
@@ -2480,7 +2644,8 @@ fn install_ui_tracing(shared: Arc<Shared>, config_level: &str) {
24802644
let fmt_layer = tracing_subscriber::fmt::layer()
24812645
.with_target(false)
24822646
.with_ansi(false)
2483-
.with_writer(writer);
2647+
.with_writer(writer)
2648+
.with_timer(mhrv_rs::logging::CompactUtcTime);
24842649

24852650
let _ = tracing_subscriber::registry()
24862651
.with(filter_layer)

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub mod tunnel_client;
1313
pub mod scan_ips;
1414
pub mod scan_sni;
1515
pub mod test_cmd;
16+
pub mod logging;
1617
pub mod update_check;
1718

1819
#[cfg(target_os = "android")]

src/logging.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use time::OffsetDateTime;
2+
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
3+
4+
pub struct CompactUtcTime;
5+
6+
impl FormatTime for CompactUtcTime {
7+
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
8+
let now = OffsetDateTime::now_utc();
9+
if w.has_ansi_escapes() {
10+
write!(
11+
w,
12+
"{g}[{bo}{year:04}{odo}-{bo}{mo:02}{odo}-{bo}{day:02}{g}]{sg}-{g}[{sb}{h:02}{db}:{sb}{min:02}{db}:{sb}{s:02}{db}.{sb}{t}{g}]{r}",
13+
g = "\x1b[38;5;250m", // light gray — brackets
14+
bo = "\x1b[38;5;215m", // light orange — date digits
15+
odo = "\x1b[38;5;166m", // dark orange — dashes inside date
16+
sg = "\x1b[38;5;120m", // light green — separator dash
17+
sb = "\x1b[38;5;159m", // light blue — time digits
18+
db = "\x1b[38;5;74m", // dark blue — colons + dot inside time
19+
r = "\x1b[0m",
20+
year = now.year(),
21+
mo = now.month() as u8,
22+
day = now.day(),
23+
h = now.hour(),
24+
min = now.minute(),
25+
s = now.second(),
26+
t = now.millisecond() / 100,
27+
)
28+
} else {
29+
write!(
30+
w,
31+
"[{:04}-{:02}-{:02}]-[{:02}:{:02}:{:02}.{}]",
32+
now.year(), now.month() as u8, now.day(),
33+
now.hour(), now.minute(), now.second(),
34+
now.millisecond() / 100,
35+
)
36+
}
37+
}
38+
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::process::ExitCode;
55
use std::sync::Arc;
66

77
use tokio::sync::Mutex;
8+
use mhrv_rs::logging::CompactUtcTime;
89
use tracing_subscriber::EnvFilter;
910

1011
use mhrv_rs::cert_installer::{install_ca, is_ca_trusted, reconcile_sudo_environment, remove_ca};
@@ -131,6 +132,7 @@ fn init_logging(level: &str) {
131132
let _ = tracing_subscriber::fmt()
132133
.with_env_filter(filter)
133134
.with_target(false)
135+
.with_timer(CompactUtcTime)
134136
.try_init();
135137
}
136138

0 commit comments

Comments
 (0)