diff options
Diffstat (limited to 'ui/src/admin')
-rw-r--r-- | ui/src/admin/log.rs | 117 | ||||
-rw-r--r-- | ui/src/admin/mod.rs | 74 | ||||
-rw-r--r-- | ui/src/admin/user.rs | 31 |
3 files changed, 220 insertions, 2 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!(), + }); + } + _ => (), + }, + _ => (), + } + } +} diff --git a/ui/src/admin/mod.rs b/ui/src/admin/mod.rs index 292e445..74a5e1a 100644 --- a/ui/src/admin/mod.rs +++ b/ui/src/admin/mod.rs @@ -5,3 +5,77 @@ */ pub mod user; +pub mod log; + +use crate::{Page, locale::Language, scaffold::FlashDisplay}; +use jellycommon::routes::{ + u_admin_import, u_admin_invite_create, u_admin_invite_remove, u_admin_log, + u_admin_transcode_posters, u_admin_update_search, u_admin_users, +}; + +impl Page for AdminDashboardPage<'_> { + fn title(&self) -> String { + "Admin Dashboard".to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} + +markup::define!( + AdminDashboardPage<'a>(lang: &'a Language, busy: Option<&'static str>, last_import_err: &'a [String], flash: Option<Result<String, String>>, invites: &'a [String]) { + h1 { "Admin Panel" } + @FlashDisplay { flash: flash.clone() } + @if !last_import_err.is_empty() { + section.message.error { + details { + summary { p.error { @format!("The last import resulted in {} errors:", last_import_err.len()) } } + ol { @for e in *last_import_err { + li.error { pre.error { @e } } + }} + } + } + } + ul { + li{a[href=u_admin_log(true)] { "Server Log (Warnings only)" }} + li{a[href=u_admin_log(false)] { "Server Log (Full) " }} + } + h2 { "Library" } + @if let Some(text) = busy { + section.message { p.warn { @text } } + } + form[method="POST", action=u_admin_import(true)] { + input[type="submit", disabled=busy.is_some(), value="Start incremental import"]; + } + form[method="POST", action=u_admin_import(false)] { + input[type="submit", disabled=busy.is_some(), value="Start full import"]; + } + form[method="POST", action=u_admin_transcode_posters()] { + input[type="submit", disabled=busy.is_some(), value="Transcode all posters with low resolution"]; + } + form[method="POST", action=u_admin_update_search()] { + input[type="submit", value="Regenerate full-text search index"]; + } + h2 { "Users" } + p { a[href=u_admin_users()] "Manage Users" } + h2 { "Invitations" } + form[method="POST", action=u_admin_invite_create()] { + input[type="submit", value="Generate new invite code"]; + } + ul { @for t in *invites { + li { + form[method="POST", action=u_admin_invite_remove()] { + span { @t } + input[type="text", name="invite", value=&t, hidden]; + input[type="submit", value="Invalidate"]; + } + } + }} + + // h2 { "Database" } + // @match db_stats(&database) { + // Ok(s) => { @s } + // Err(e) => { pre.error { @format!("{e:?}") } } + // } + } +); diff --git a/ui/src/admin/user.rs b/ui/src/admin/user.rs index 9878803..613fc08 100644 --- a/ui/src/admin/user.rs +++ b/ui/src/admin/user.rs @@ -4,13 +4,40 @@ Copyright (C) 2025 metamuffin <metamuffin.org> */ -use crate::{locale::Language, scaffold::FlashDisplay}; +use crate::{Page, locale::Language, scaffold::FlashDisplay}; use jellycommon::{ - routes::{u_admin_user_permission, u_admin_user_remove, u_admin_users}, + routes::{u_admin_user, u_admin_user_permission, u_admin_user_remove, u_admin_users}, user::{PermissionSet, User, UserPermission}, }; +impl Page for AdminUserPage<'_> { + fn title(&self) -> String { + "User Management".to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} +impl Page for AdminUsersPage<'_> { + fn title(&self) -> String { + "User Management".to_string() + } + fn to_render(&self) -> markup::DynRender { + markup::new!(@self) + } +} + markup::define! { + AdminUsersPage<'a>(lang: &'a Language, users: &'a [User], flash: Option<Result<String, String>>) { + h1 { "User Management" } + @FlashDisplay { flash: flash.clone() } + h2 { "All Users" } + ul { @for u in *users { + li { + a[href=u_admin_user(&u.name)] { @format!("{:?}", u.display_name) " (" @u.name ")" } + } + }} + } AdminUserPage<'a>(lang: &'a Language, user: &'a User, flash: Option<Result<String, String>>) { h1 { @format!("{:?}", user.display_name) " (" @user.name ")" } a[href=u_admin_users()] "Back to the User List" |