diff --git a/CHANGELOG.md b/CHANGELOG.md index 3455e33..a89c162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to Setu. Release notes are cut from the section matching the tag. +## 0.1.6 — 2026-10-23 + +### Fixed + +- **Site logs are plain text.** The agent strips terminal escape sequences + (colours, cursor moves, window titles) from each journal line, so an app + that colours its logs under systemd no longer fills the Logs tab with + `ESC[32m` noise. +- **Apps page on phones.** A followed manifest URL no longer widens the + app card past the screen, which made the whole page scroll sideways. + +### Security + +- tokio 1.54.1 and ctutils 0.4.5 (RUSTSEC-2026-0385, RUSTSEC-2026-0386). + ## 0.1.5 — 2026-10-21 Spec: `docs/specs/2026-10-21-setu-0.1.5-design.md`. diff --git a/Cargo.lock b/Cargo.lock index 3cfb77a..e466d11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,9 +589,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", ] @@ -707,7 +707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -977,7 +977,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hello" -version = "0.1.5" +version = "0.1.6" dependencies = [ "axum", "reqwest", @@ -1714,7 +1714,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1972,7 +1972,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2170,7 +2170,7 @@ dependencies = [ [[package]] name = "setu-agent" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "async-trait", @@ -2204,7 +2204,7 @@ dependencies = [ [[package]] name = "setu-api" -version = "0.1.5" +version = "0.1.6" dependencies = [ "argon2", "async-trait", @@ -2248,7 +2248,7 @@ dependencies = [ [[package]] name = "setu-common" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "axum", @@ -2274,7 +2274,7 @@ dependencies = [ [[package]] name = "setu-db" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "base64 0.22.1", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "setu-jobs" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "chrono", @@ -2317,7 +2317,7 @@ dependencies = [ [[package]] name = "setu-manifest" -version = "0.1.5" +version = "0.1.6" dependencies = [ "hex", "rand 0.9.5", @@ -2331,7 +2331,7 @@ dependencies = [ [[package]] name = "setu-proto" -version = "0.1.5" +version = "0.1.6" dependencies = [ "chrono", "serde", @@ -2341,7 +2341,7 @@ dependencies = [ [[package]] name = "setu-testkit" -version = "0.1.5" +version = "0.1.6" dependencies = [ "sqlx", "tokio", @@ -2721,7 +2721,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2810,9 +2810,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.2" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -3370,7 +3370,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5305e19..0eb4bb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ ] [workspace.package] -version = "0.1.5" +version = "0.1.6" edition = "2021" license = "AGPL-3.0-only" # Never published to crates.io (single binary + private repo); also diff --git a/crates/agent/src/journal.rs b/crates/agent/src/journal.rs index f660c19..4e1ad89 100644 --- a/crates/agent/src/journal.rs +++ b/crates/agent/src/journal.rs @@ -40,14 +40,14 @@ pub fn parse_journal_json(text: &str) -> LogStream { next = Some(c.to_string()); } match v.get("MESSAGE") { - Some(serde_json::Value::String(s)) => lines.push(s.clone()), + Some(serde_json::Value::String(s)) => lines.push(strip_ansi(s)), // Non-UTF-8 messages arrive as a byte array. Some(serde_json::Value::Array(bytes)) => { let raw: Vec = bytes .iter() .filter_map(|b| b.as_u64().and_then(|b| u8::try_from(b).ok())) .collect(); - lines.push(String::from_utf8_lossy(&raw).into_owned()); + lines.push(strip_ansi(&String::from_utf8_lossy(&raw))); } _ => {} } @@ -55,6 +55,49 @@ pub fn parse_journal_json(text: &str) -> LogStream { LogStream { lines, next } } +/// Drops terminal escape sequences (colours, cursor moves, titles) from a +/// log line. Apps colour their logs whether or not a terminal is reading, +/// and the panel shows plain text, so `ESC[32mINFO` would arrive as +/// garbage. Handles CSI (`ESC [ … final`), OSC (`ESC ] … BEL` or +/// `ESC \\`) and two-byte `ESC x` escapes; any other text is kept. +pub fn strip_ansi(line: &str) -> String { + if !line.contains('\u{1b}') { + return line.to_owned(); + } + let mut out = String::with_capacity(line.len()); + let mut chars = line.chars().peekable(); + while let Some(c) = chars.next() { + if c != '\u{1b}' { + out.push(c); + continue; + } + match chars.next() { + // CSI: parameters and intermediates up to a final byte in @..~. + Some('[') => { + for c in chars.by_ref() { + if ('@'..='~').contains(&c) { + break; + } + } + } + // OSC: up to BEL or the string terminator ESC \. + Some(']') => { + while let Some(c) = chars.next() { + if c == '\u{7}' { + break; + } + if c == '\u{1b}' && chars.peek() == Some(&'\\') { + chars.next(); + break; + } + } + } + _ => {} + } + } + out +} + /// Read up to `lines` entries after `cursor`. /// `program` is `journalctl` in production (a fake script in tests). pub async fn read( @@ -109,4 +152,36 @@ mod tests { assert_eq!(s.lines, ["hello", "hi"]); assert_eq!(s.next.as_deref(), Some("c2")); } + + #[test] + fn colour_codes_are_stripped_from_messages() { + // What a tracing subscriber writes when it thinks a terminal is + // reading: dim timestamp, green level, dim target. + let coloured = "\u{1b}[2m2026-10-22T06:37:20Z\u{1b}[0m \u{1b}[32m INFO\u{1b}[0m \u{1b}[2mvyasa::host\u{1b}[0m\u{1b}[2m:\u{1b}[0m docs sync finished"; + let text = format!( + "{}\n{}\n", + serde_json::json!({"__CURSOR": "c1", "MESSAGE": coloured}), + // journald hands control characters over as a byte array. + serde_json::json!({"__CURSOR": "c2", "MESSAGE": "\u{1b}[31mfail\u{1b}[0m".bytes().collect::>()}), + ); + let s = parse_journal_json(&text); + assert_eq!( + s.lines, + [ + "2026-10-22T06:37:20Z INFO vyasa::host: docs sync finished", + "fail" + ] + ); + } + + #[test] + fn strip_ansi_keeps_plain_text_and_drops_osc_titles() { + assert_eq!(strip_ansi("plain [text] ok"), "plain [text] ok"); + assert_eq!(strip_ansi("\u{1b}]0;title\u{7}after"), "after"); + assert_eq!( + strip_ansi("a\u{1b}]8;;http://x\u{1b}\\link\u{1b}]8;;\u{1b}\\b"), + "alinkb" + ); + assert_eq!(strip_ansi("bold \u{1b}[1;31mred\u{1b}[0m"), "bold red"); + } } diff --git a/ui/package.json b/ui/package.json index 339c2bc..78d8f51 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { "name": "setu-ui", "private": true, - "version": "0.1.5", + "version": "0.1.6", "type": "module", "scripts": { "gen:api": "tsx scripts/gen-api.ts", diff --git a/ui/src/components/AppsCatalogue.tsx b/ui/src/components/AppsCatalogue.tsx index c2572ca..d58cc74 100644 --- a/ui/src/components/AppsCatalogue.tsx +++ b/ui/src/components/AppsCatalogue.tsx @@ -84,7 +84,7 @@ function AppCard({ app }: { app: AppInfo }) { const list = versions.data ?? []; const [latest, ...older] = list; return ( -
  • +