aboutsummaryrefslogtreecommitdiff
path: root/ui/src/admin/log.rs
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/admin/log.rs')
-rw-r--r--ui/src/admin/log.rs117
1 files changed, 117 insertions, 0 deletions
diff --git a/ui/src/admin/log.rs b/ui/src/admin/log.rs
new file mode 100644
index 0000000..a69bdfa
--- /dev/null
+++ b/ui/src/admin/log.rs
@@ -0,0 +1,117 @@
+/*
+ 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 <metamuffin.org>
+*/
+
+use jellycommon::{
+ api::{LogLevel, LogLine},
+ routes::u_admin_log,
+};
+use markup::raw;
+use std::fmt::Write;
+
+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, "<span style=color:#{:02x}{:02x}{:02x}>", r, g, b).unwrap()
+ }
+ pub fn reset_color(&mut self) {
+ if self.color {
+ write!(self.s, "</span>").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!(),
+ });
+ }
+ _ => (),
+ },
+ _ => (),
+ }
+ }
+}