/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use crate::Page; use jellycommon::{ api::{LogLevel, LogLine}, routes::u_admin_log, }; use markup::raw; use std::fmt::Write; impl Page for ServerLogPage<'_> { fn title(&self) -> String { "Server Log".to_string() } fn class(&self) -> Option<&'static str> { Some("admin_log") } fn to_render(&self) -> markup::DynRender<'_> { markup::new!(@self) } } markup::define! { ServerLogPage<'a>(warnonly: bool, messages: &'a [String]) { h1 { "Server Log" } a[href=u_admin_log(!warnonly)] { @if *warnonly { "Show everything" } else { "Show only warnings" }} code.log[id="log"] { table { @for e in *messages { @raw(e) }} } } ServerLogLine<'a>(e: &'a LogLine) { tr[class=format!("level-{}", e.level).to_ascii_lowercase()] { td.time { @e.time.to_rfc3339() } td.loglevel { @format_level(e.level) } td.module { @e.module } td { @markup::raw(vt100_to_html(&e.message)) } } } } pub fn render_log_line(line: &LogLine) -> String { ServerLogLine { e: line }.to_string() } fn vt100_to_html(s: &str) -> String { let mut out = HtmlOut::default(); let mut st = vte::Parser::new(); st.advance(&mut out, s.as_bytes()); out.s } fn format_level(level: LogLevel) -> impl markup::Render { let (s, c) = match level { LogLevel::Debug => ("DEBUG", "blue"), LogLevel::Error => ("ERROR", "red"), LogLevel::Warn => ("WARN", "yellow"), LogLevel::Info => ("INFO", "green"), LogLevel::Trace => ("TRACE", "lightblue"), }; markup::new! { span[style=format!("color:{c}")] {@s} } } #[derive(Default)] pub struct HtmlOut { s: String, color: bool, } impl HtmlOut { pub fn set_color(&mut self, [r, g, b]: [u8; 3]) { self.reset_color(); self.color = true; write!(self.s, "", r, g, b).unwrap() } pub fn reset_color(&mut self) { if self.color { write!(self.s, "").unwrap(); self.color = false; } } } impl vte::Perform for HtmlOut { fn print(&mut self, c: char) { match c { 'a'..='z' | 'A'..='Z' | '0'..='9' | ' ' => self.s.push(c), x => write!(self.s, "&#{};", x as u32).unwrap(), } } fn execute(&mut self, _byte: u8) {} fn hook(&mut self, _params: &vte::Params, _i: &[u8], _ignore: bool, _a: char) {} fn put(&mut self, _byte: u8) {} fn unhook(&mut self) {} fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {} fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {} fn csi_dispatch( &mut self, params: &vte::Params, _intermediates: &[u8], _ignore: bool, action: char, ) { let mut k = params.iter(); #[allow(clippy::single_match)] match action { 'm' => match k.next().unwrap_or(&[0]).first().unwrap_or(&0) { c @ (30..=37 | 40..=47) => { let c = if *c >= 40 { *c - 10 } else { *c }; self.set_color(match c { 30 => [0, 0, 0], 31 => [255, 0, 0], 32 => [0, 255, 0], 33 => [255, 255, 0], 34 => [0, 0, 255], 35 => [255, 0, 255], 36 => [0, 255, 255], 37 => [255, 255, 255], _ => unreachable!(), }); } _ => (), }, _ => (), } } }