aboutsummaryrefslogtreecommitdiff
path: root/server/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/ui')
-rw-r--r--server/src/ui/account/mod.rs19
-rw-r--r--server/src/ui/account/settings.rs42
-rw-r--r--server/src/ui/admin/log.rs253
-rw-r--r--server/src/ui/admin/mod.rs283
-rw-r--r--server/src/ui/admin/user.rs140
5 files changed, 239 insertions, 498 deletions
diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs
index c1e5479..54fa4d0 100644
--- a/server/src/ui/account/mod.rs
+++ b/server/src/ui/account/mod.rs
@@ -9,12 +9,9 @@ use super::error::MyError;
use crate::{
database::Database,
locale::AcceptLanguage,
- logic::session::{self, Session},
ui::{error::MyResult, home::rocket_uri_macro_r_home},
- uri,
};
use anyhow::anyhow;
-use argon2::{password_hash::Salt, Argon2, PasswordHasher};
use chrono::Duration;
use jellycommon::user::{User, UserPermission};
use rocket::{
@@ -44,21 +41,7 @@ pub async fn r_account_register(lang: AcceptLanguage) -> DynLayoutPage<'static>
LayoutPage {
title: tr(lang, "account.register").to_string(),
content: markup::new! {
- form.account[method="POST", action=""] {
- h1 { @trs(&lang, "account.register") }
-
- label[for="inp-invitation"] { @trs(&lang, "account.register.invitation") }
- input[type="text", id="inp-invitation", name="invitation"]; br;
-
- label[for="inp-username"] { @trs(&lang, "account.username") }
- input[type="text", id="inp-username", name="username"]; br;
- label[for="inp-password"] { @trs(&lang, "account.password") }
- input[type="password", id="inp-password", name="password"]; br;
-
- input[type="submit", value=&*tr(lang, "account.register.submit")];
-
- p { @trs(&lang, "account.register.login") " " a[href=uri!(r_account_login())] { @trs(&lang, "account.register.login_here") } }
- }
+
},
..Default::default()
}
diff --git a/server/src/ui/account/settings.rs b/server/src/ui/account/settings.rs
index 3b53bc4..e6d096f 100644
--- a/server/src/ui/account/settings.rs
+++ b/server/src/ui/account/settings.rs
@@ -4,27 +4,18 @@
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
use super::{format_form_error, hash_password};
-use crate::{
- database::Database,
- locale::AcceptLanguage,
- ui::{
- account::{rocket_uri_macro_r_account_login, session::Session},
- error::MyResult,
- layout::{trs, DynLayoutPage, LayoutPage},
- },
- uri,
-};
-use jellybase::{
- locale::{tr, Language},
- permission::PermissionSetExt,
-};
+use crate::{database::Database, locale::AcceptLanguage, ui::error::MyResult};
+use jellybase::permission::PermissionSetExt;
use jellycommon::user::{PlayerKind, Theme, UserPermission};
-use markup::{Render, RenderAttributeValue};
+use jellylogic::session::Session;
+use jellyui::locale::Language;
use rocket::{
form::{self, validate::len, Contextual, Form},
get,
http::uri::fmt::{Query, UriDisplay},
- post, FromForm, State,
+ post,
+ response::content::RawHtml,
+ FromForm, State,
};
use std::ops::Range;
@@ -47,24 +38,11 @@ fn settings_page(
session: Session,
flash: Option<MyResult<String>>,
lang: Language,
-) -> DynLayoutPage<'static> {
- LayoutPage {
- title: "Settings".to_string(),
- class: Some("settings"),
- content:
- }
-}
-
-struct A<T>(pub T);
-impl<T: UriDisplay<Query>> RenderAttributeValue for A<T> {}
-impl<T: UriDisplay<Query>> Render for A<T> {
- fn render(&self, writer: &mut impl std::fmt::Write) -> std::fmt::Result {
- writer.write_fmt(format_args!("{}", &self.0 as &dyn UriDisplay<Query>))
- }
+) -> RawHtml<String> {
}
#[get("/account/settings")]
-pub fn r_account_settings(session: Session, lang: AcceptLanguage) -> DynLayoutPage<'static> {
+pub fn r_account_settings(session: Session, lang: AcceptLanguage) -> RawHtml<String> {
let AcceptLanguage(lang) = lang;
settings_page(session, None, lang)
}
@@ -75,7 +53,7 @@ pub fn r_account_settings_post(
database: &State<Database>,
form: Form<Contextual<SettingsForm>>,
lang: AcceptLanguage,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<RawHtml<String>> {
let AcceptLanguage(lang) = lang;
session
.user
diff --git a/server/src/ui/admin/log.rs b/server/src/ui/admin/log.rs
index dff6d1b..bd6d7af 100644
--- a/server/src/ui/admin/log.rs
+++ b/server/src/ui/admin/log.rs
@@ -3,256 +3,35 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-use crate::{
- logic::session::AdminSession,
- ui::{
- error::MyResult,
- layout::{DynLayoutPage, LayoutPage},
- },
- uri,
-};
-use chrono::{DateTime, Utc};
-use log::Level;
-use markup::Render;
-use rocket::get;
+use crate::ui::error::MyResult;
+use jellylogic::{admin::log::get_log_stream, session::AdminSession};
+use jellyui::admin::log::render_log_line;
+use rocket::{get, response::content::RawHtml};
use rocket_ws::{Message, Stream, WebSocket};
use serde_json::json;
-use std::{
- collections::VecDeque,
- fmt::Write,
- sync::{Arc, LazyLock, RwLock},
-};
-use tokio::sync::broadcast;
-
-const MAX_LOG_LEN: usize = 4096;
-
-static LOGGER: LazyLock<Log> = LazyLock::new(Log::default);
-
-pub fn enable_logging() {
- log::set_logger(&*LOGGER).unwrap();
- log::set_max_level(log::LevelFilter::Debug);
-}
-
-type LogBuffer = VecDeque<Arc<LogLine>>;
-
-pub struct Log {
- inner: env_logger::Logger,
- stream: (
- broadcast::Sender<Arc<LogLine>>,
- broadcast::Sender<Arc<LogLine>>,
- ),
- log: RwLock<(LogBuffer, LogBuffer)>,
-}
-
-pub struct LogLine {
- time: DateTime<Utc>,
- module: Option<&'static str>,
- level: Level,
- message: String,
-}
#[get("/admin/log?<warnonly>", rank = 2)]
-pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<DynLayoutPage<'a>> {
- Ok(LayoutPage {
- title: "Log".into(),
- class: Some("admin_log"),
- content: markup::new! {
- h1 { "Server Log" }
- a[href=uri!(r_admin_log(!warnonly))] { @if warnonly { "Show everything" } else { "Show only warnings" }}
- code.log[id="log"] {
- @let g = LOGGER.log.read().unwrap();
- table { @for e in if warnonly { g.1.iter() } else { g.0.iter() } {
- 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 r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<RawHtml<String>> {}
-#[get("/admin/log?stream&<warnonly>", rank = 1)]
+#[get("/admin/log?stream&<warnonly>&<html>", rank = 1)]
pub fn r_admin_log_stream(
_session: AdminSession,
ws: WebSocket,
warnonly: bool,
+ html: bool,
) -> Stream!['static] {
- let mut stream = if warnonly {
- LOGGER.stream.1.subscribe()
- } else {
- LOGGER.stream.0.subscribe()
- };
+ let mut stream = get_log_stream(warnonly);
Stream! { ws =>
- let _ = ws;
- while let Ok(line) = stream.recv().await {
- yield Message::Text(json!({
- "time": line.time,
- "level_class": format!("level-{:?}", line.level).to_ascii_lowercase(),
- "level_html": format_level_string(line.level),
- "module": line.module,
- "message": vt100_to_html(&line.message),
- }).to_string());
- }
- }
-}
-
-impl Default for Log {
- fn default() -> Self {
- Self {
- inner: env_logger::builder()
- .filter_level(log::LevelFilter::Warn)
- .parse_env("LOG")
- .build(),
- stream: (
- tokio::sync::broadcast::channel(1024).0,
- tokio::sync::broadcast::channel(1024).0,
- ),
- log: Default::default(),
- }
- }
-}
-impl Log {
- fn should_log(&self, metadata: &log::Metadata) -> bool {
- let level = metadata.level();
- level
- <= match metadata.target() {
- x if x.starts_with("jelly") => Level::Debug,
- x if x.starts_with("rocket::") => Level::Info,
- _ => Level::Warn,
+ if html {
+ let _ = ws;
+ while let Ok(line) = stream.recv().await {
+ yield Message::Text(render_log_line(&line));
}
- }
- fn do_log(&self, record: &log::Record) {
- let time = Utc::now();
- let line = Arc::new(LogLine {
- time,
- module: record.module_path_static(),
- level: record.level(),
- message: record.args().to_string(),
- });
- let mut w = self.log.write().unwrap();
- w.0.push_back(line.clone());
- let _ = self.stream.0.send(line.clone());
- while w.0.len() > MAX_LOG_LEN {
- w.0.pop_front();
- }
- if record.level() <= Level::Warn {
- let _ = self.stream.1.send(line.clone());
- w.1.push_back(line);
- while w.1.len() > MAX_LOG_LEN {
- w.1.pop_front();
+ } else {
+ let _ = ws;
+ while let Ok(line) = stream.recv().await {
+ yield Message::Text(json!(line).to_string());
}
}
}
}
-
-impl log::Log for Log {
- fn enabled(&self, metadata: &log::Metadata) -> bool {
- self.inner.enabled(metadata) || self.should_log(metadata)
- }
- fn log(&self, record: &log::Record) {
- match (record.module_path_static(), record.line()) {
- // TODO is there a better way to ignore those?
- (Some("rocket::rocket"), Some(670)) => return,
- (Some("rocket::server"), Some(401)) => return,
- _ => {}
- }
- if self.inner.enabled(record.metadata()) {
- self.inner.log(record);
- }
- if self.should_log(record.metadata()) {
- self.do_log(record)
- }
- }
- fn flush(&self) {
- self.inner.flush();
- }
-}
-
-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: Level) -> impl markup::Render {
- let (s, c) = match level {
- Level::Debug => ("DEBUG", "blue"),
- Level::Error => ("ERROR", "red"),
- Level::Warn => ("WARN", "yellow"),
- Level::Info => ("INFO", "green"),
- Level::Trace => ("TRACE", "lightblue"),
- };
- markup::new! { span[style=format!("color:{c}")] {@s} }
-}
-fn format_level_string(level: Level) -> String {
- let mut w = String::new();
- format_level(level).render(&mut w).unwrap();
- w
-}
-
-#[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/server/src/ui/admin/mod.rs b/server/src/ui/admin/mod.rs
index d380ae2..b155121 100644
--- a/server/src/ui/admin/mod.rs
+++ b/server/src/ui/admin/mod.rs
@@ -10,108 +10,74 @@ use super::{
assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED},
error::MyResult,
};
-use crate::{database::Database, logic::session::AdminSession};
+use crate::{database::Database, locale::AcceptLanguage};
use anyhow::{anyhow, Context};
use jellybase::{assetfed::AssetInner, federation::Federation, CONF};
-use jellyimport::{import_wrap, IMPORT_ERRORS};
+use jellycommon::routes::u_admin_dashboard;
+use jellyimport::{import_wrap, is_importing, IMPORT_ERRORS};
+use jellylogic::session::AdminSession;
+use jellyui::{
+ admin::AdminDashboardPage,
+ render_page,
+ scaffold::{RenderInfo, SessionInfo},
+};
use rand::Rng;
-use rocket::{form::Form, get, post, FromForm, State};
+use rocket::{
+ form::Form,
+ get, post,
+ response::{content::RawHtml, Redirect},
+ FromForm, State,
+};
use std::time::Instant;
use tokio::{sync::Semaphore, task::spawn_blocking};
#[get("/admin/dashboard")]
pub async fn r_admin_dashboard(
- _session: AdminSession,
+ session: AdminSession,
database: &State<Database>,
-) -> MyResult<DynLayoutPage<'static>> {
- admin_dashboard(database, None).await
-}
-
-pub async fn admin_dashboard<'a>(
- database: &Database,
- flash: Option<MyResult<String>>,
-) -> MyResult<DynLayoutPage<'a>> {
+ lang: AcceptLanguage,
+) -> MyResult<RawHtml<String>> {
+ let AcceptLanguage(lang) = lang;
let invites = database.list_invites()?;
- let flash = flash.map(|f| f.map_err(|e| format!("{e:?}")));
+ let flash = None;
let last_import_err = IMPORT_ERRORS.read().await.to_owned();
- let database = database.to_owned();
- Ok(LayoutPage {
- title: "Admin Dashboard".to_string(),
- content: markup::new! {
- 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=uri!(r_admin_log(true))] { "Server Log (Warnings only)" }}
- li{a[href=uri!(r_admin_log(false))] { "Server Log (Full) " }}
- }
- h2 { "Library" }
- @if is_importing() {
- section.message { p.warn { "An import is currently running." } }
- }
- @if is_transcoding() {
- section.message { p.warn { "Currently transcoding posters." } }
- }
- form[method="POST", action=uri!(r_admin_import(true))] {
- input[type="submit", disabled=is_importing(), value="Start incremental import"];
- }
- form[method="POST", action=uri!(r_admin_import(false))] {
- input[type="submit", disabled=is_importing(), value="Start full import"];
- }
- form[method="POST", action=uri!(r_admin_transcode_posters())] {
- input[type="submit", disabled=is_transcoding(), value="Transcode all posters with low resolution"];
- }
- form[method="POST", action=uri!(r_admin_update_search())] {
- input[type="submit", value="Regenerate full-text search index"];
- }
- form[method="POST", action=uri!(r_admin_delete_cache())] {
- input.danger[type="submit", value="Delete Cache"];
- }
- h2 { "Users" }
- p { a[href=uri!(r_admin_users())] "Manage Users" }
- h2 { "Invitations" }
- form[method="POST", action=uri!(r_admin_invite())] {
- input[type="submit", value="Generate new invite code"];
- }
- ul { @for t in &invites {
- li {
- form[method="POST", action=uri!(r_admin_remove_invite())] {
- span { @t }
- input[type="text", name="invite", value=&t, hidden];
- input[type="submit", value="Invalidate"];
- }
- }
- }}
+ let busy = if is_importing() {
+ Some("An import is currently running.")
+ } else if is_transcoding() {
+ Some("Currently transcoding posters.")
+ } else {
+ None
+ };
- h2 { "Database" }
- @match db_stats(&database) {
- Ok(s) => { @s }
- Err(e) => { pre.error { @format!("{e:?}") } }
- }
+ Ok(RawHtml(render_page(
+ &AdminDashboardPage {
+ busy,
+ last_import_err: &last_import_err,
+ invites: &invites,
+ flash,
+ lang: &lang,
},
- ..Default::default()
- })
+ RenderInfo {
+ importing: is_importing(),
+ session: Some(SessionInfo {
+ user: session.0.user,
+ }),
+ },
+ lang,
+ )))
}
#[post("/admin/generate_invite")]
pub async fn r_admin_invite(
_session: AdminSession,
database: &State<Database>,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<Redirect> {
let i = format!("{}", rand::rng().random::<u128>());
database.create_invite(&i)?;
- admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await
+ // admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await
+ Ok(Redirect::temporary(u_admin_dashboard()))
}
#[derive(FromForm)]
@@ -124,12 +90,13 @@ pub async fn r_admin_remove_invite(
session: AdminSession,
database: &State<Database>,
form: Form<DeleteInvite>,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<Redirect> {
drop(session);
if !database.delete_invite(&form.invite)? {
Err(anyhow!("invite does not exist"))?;
};
- admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await
+ // admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await
+ Ok(Redirect::temporary(u_admin_dashboard()))
}
#[post("/admin/import?<incremental>")]
@@ -138,55 +105,58 @@ pub async fn r_admin_import(
database: &State<Database>,
_federation: &State<Federation>,
incremental: bool,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<Redirect> {
drop(session);
let t = Instant::now();
if !incremental {
database.clear_nodes()?;
}
let r = import_wrap((*database).clone(), incremental).await;
- let flash = r
- .map_err(|e| e.into())
- .map(|_| format!("Import successful; took {:?}", t.elapsed()));
- admin_dashboard(database, Some(flash)).await
+ // let flash = r
+ // .map_err(|e| e.into())
+ // .map(|_| format!("Import successful; took {:?}", t.elapsed()));
+ // admin_dashboard(database, Some(flash)).await
+ Ok(Redirect::temporary(u_admin_dashboard()))
}
#[post("/admin/update_search")]
pub async fn r_admin_update_search(
_session: AdminSession,
database: &State<Database>,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<Redirect> {
let db2 = (*database).clone();
let r = spawn_blocking(move || db2.search_create_index())
.await
.unwrap();
- admin_dashboard(
- database,
- Some(
- r.map_err(|e| e.into())
- .map(|_| "Search index updated".to_string()),
- ),
- )
- .await
+ // admin_dashboard(
+ // database,
+ // Some(
+ // r.map_err(|e| e.into())
+ // .map(|_| "Search index updated".to_string()),
+ // ),
+ // )
+ // .await
+ Ok(Redirect::temporary(u_admin_dashboard()))
}
#[post("/admin/delete_cache")]
pub async fn r_admin_delete_cache(
session: AdminSession,
database: &State<Database>,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<Redirect> {
drop(session);
let t = Instant::now();
let r = tokio::fs::remove_dir_all(&CONF.cache_path).await;
tokio::fs::create_dir(&CONF.cache_path).await?;
- admin_dashboard(
- database,
- Some(
- r.map_err(|e| e.into())
- .map(|_| format!("Cache deleted; took {:?}", t.elapsed())),
- ),
- )
- .await
+ // admin_dashboard(
+ // database,
+ // Some(
+ // r.map_err(|e| e.into())
+ // .map(|_| format!("Cache deleted; took {:?}", t.elapsed())),
+ // ),
+ // )
+ // .await
+ Ok(Redirect::temporary(u_admin_dashboard()))
}
static SEM_TRANSCODING: Semaphore = Semaphore::const_new(1);
@@ -198,7 +168,7 @@ fn is_transcoding() -> bool {
pub async fn r_admin_transcode_posters(
session: AdminSession,
database: &State<Database>,
-) -> MyResult<DynLayoutPage<'static>> {
+) -> MyResult<Redirect> {
drop(session);
let _permit = SEM_TRANSCODING
.try_acquire()
@@ -223,58 +193,59 @@ pub async fn r_admin_transcode_posters(
}
drop(_permit);
- admin_dashboard(
- database,
- Some(Ok(format!(
- "All posters pre-transcoded; took {:?}",
- t.elapsed()
- ))),
- )
- .await
+ // admin_dashboard(
+ // database,
+ // Some(Ok(format!(
+ // "All posters pre-transcoded; took {:?}",
+ // t.elapsed()
+ // ))),
+ // )
+ // .await
+ Ok(Redirect::temporary(u_admin_dashboard()))
}
-fn db_stats(_db: &Database) -> anyhow::Result<DynRender> {
- // TODO
- // let txn = db.inner.begin_read()?;
- // let stats = [
- // ("node", txn.open_table(T_NODE)?.stats()?),
- // ("user", txn.open_table(T_USER_NODE)?.stats()?),
- // ("user-node", txn.open_table(T_USER_NODE)?.stats()?),
- // ("invite", txn.open_table(T_INVITE)?.stats()?),
- // ];
+// fn db_stats(_db: &Database) -> anyhow::Result<DynRender> {
+// // TODO
+// // let txn = db.inner.begin_read()?;
+// // let stats = [
+// // ("node", txn.open_table(T_NODE)?.stats()?),
+// // ("user", txn.open_table(T_USER_NODE)?.stats()?),
+// // ("user-node", txn.open_table(T_USER_NODE)?.stats()?),
+// // ("invite", txn.open_table(T_INVITE)?.stats()?),
+// // ];
- // let cache_stats = db.node_index.reader.searcher().doc_store_cache_stats();
- // let ft_total_docs = db.node_index.reader.searcher().total_num_docs()?;
+// // let cache_stats = db.node_index.reader.searcher().doc_store_cache_stats();
+// // let ft_total_docs = db.node_index.reader.searcher().total_num_docs()?;
- Ok(markup::new! {
- // h3 { "Key-Value-Store Statistics" }
- // table.border {
- // tbody {
- // tr {
- // th { "table name" }
- // th { "tree height" }
- // th { "stored bytes" }
- // th { "metadata bytes" }
- // th { "fragmented bytes" }
- // th { "branch pages" }
- // th { "leaf pages" }
- // }
- // @for (name, stats) in &stats { tr {
- // td { @name }
- // td { @stats.tree_height() }
- // td { @format_size(stats.stored_bytes(), DECIMAL) }
- // td { @format_size(stats.metadata_bytes(), DECIMAL) }
- // td { @format_size(stats.fragmented_bytes(), DECIMAL) }
- // td { @stats.branch_pages() }
- // td { @stats.leaf_pages() }
- // }}
- // }
- // }
- // h3 { "Search Engine Statistics" }
- // ul {
- // li { "Total documents: " @ft_total_docs }
- // li { "Cache misses: " @cache_stats.cache_misses }
- // li { "Cache hits: " @cache_stats.cache_hits }
- // }
- })
-}
+// Ok(markup::new! {
+// // h3 { "Key-Value-Store Statistics" }
+// // table.border {
+// // tbody {
+// // tr {
+// // th { "table name" }
+// // th { "tree height" }
+// // th { "stored bytes" }
+// // th { "metadata bytes" }
+// // th { "fragmented bytes" }
+// // th { "branch pages" }
+// // th { "leaf pages" }
+// // }
+// // @for (name, stats) in &stats { tr {
+// // td { @name }
+// // td { @stats.tree_height() }
+// // td { @format_size(stats.stored_bytes(), DECIMAL) }
+// // td { @format_size(stats.metadata_bytes(), DECIMAL) }
+// // td { @format_size(stats.fragmented_bytes(), DECIMAL) }
+// // td { @stats.branch_pages() }
+// // td { @stats.leaf_pages() }
+// // }}
+// // }
+// // }
+// // h3 { "Search Engine Statistics" }
+// // ul {
+// // li { "Total documents: " @ft_total_docs }
+// // li { "Cache misses: " @cache_stats.cache_misses }
+// // li { "Cache hits: " @cache_stats.cache_hits }
+// // }
+// })
+// }
diff --git a/server/src/ui/admin/user.rs b/server/src/ui/admin/user.rs
index 1af83d4..77bcc71 100644
--- a/server/src/ui/admin/user.rs
+++ b/server/src/ui/admin/user.rs
@@ -3,68 +3,68 @@
which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
Copyright (C) 2025 metamuffin <metamuffin.org>
*/
-use crate::{database::Database, ui::error::MyResult};
+use crate::{database::Database, locale::AcceptLanguage, ui::error::MyResult};
use anyhow::{anyhow, Context};
use jellycommon::user::UserPermission;
-use jellylogic::session::AdminSession;
-use rocket::{form::Form, get, post, FromForm, FromFormField, State};
+use jellyimport::is_importing;
+use jellylogic::{admin::user::admin_users, session::AdminSession};
+use jellyui::{
+ admin::user::{AdminUserPage, AdminUsersPage},
+ render_page,
+ scaffold::{RenderInfo, SessionInfo},
+};
+use rocket::{form::Form, get, post, response::content::RawHtml, FromForm, FromFormField, State};
#[get("/admin/users")]
pub fn r_admin_users(
- _session: AdminSession,
+ session: AdminSession,
database: &State<Database>,
-) -> MyResult<DynLayoutPage<'static>> {
- user_management(database, None)
-}
-
-fn user_management<'a>(
- database: &Database,
- flash: Option<MyResult<String>>,
-) -> MyResult<DynLayoutPage<'a>> {
- // TODO this doesnt scale, pagination!
- let users = database.list_users()?;
- let flash = flash.map(|f| f.map_err(|e| format!("{e:?}")));
-
- Ok(LayoutPage {
- title: "User management".to_string(),
- content: markup::new! {
- h1 { "User Management" }
- @FlashDisplay { flash: flash.clone() }
- h2 { "All Users" }
- ul { @for u in &users {
- li {
- a[href=uri!(r_admin_user(&u.name))] { @format!("{:?}", u.display_name) " (" @u.name ")" }
- }
- }}
+ lang: AcceptLanguage,
+) -> MyResult<RawHtml<String>> {
+ let AcceptLanguage(lang) = lang;
+ let r = admin_users(database, &session)?;
+ Ok(RawHtml(render_page(
+ &AdminUsersPage {
+ flash: None,
+ lang: &lang,
+ users: &r.users,
+ },
+ RenderInfo {
+ importing: is_importing(),
+ session: Some(SessionInfo {
+ user: session.0.user,
+ }),
},
- ..Default::default()
- })
+ lang,
+ )))
}
#[get("/admin/user/<name>")]
pub fn r_admin_user<'a>(
- _session: AdminSession,
+ session: AdminSession,
database: &State<Database>,
name: &'a str,
-) -> MyResult<DynLayoutPage<'a>> {
- manage_single_user(database, None, name.to_string())
-}
-
-fn manage_single_user<'a>(
- database: &Database,
- flash: Option<MyResult<String>>,
- name: String,
-) -> MyResult<DynLayoutPage<'a>> {
+ lang: AcceptLanguage,
+) -> MyResult<RawHtml<String>> {
+ let AcceptLanguage(lang) = lang;
let user = database
.get_user(&name)?
.ok_or(anyhow!("user does not exist"))?;
- let flash = flash.map(|f| f.map_err(|e| format!("{e:?}")));
- Ok(LayoutPage {
- title: "User management".to_string(),
- content: markup::new! {},
- ..Default::default()
- })
+ Ok(RawHtml(render_page(
+ &AdminUserPage {
+ flash: None,
+ lang: &lang,
+ user: &user,
+ },
+ RenderInfo {
+ importing: is_importing(),
+ session: Some(SessionInfo {
+ user: session.0.user,
+ }),
+ },
+ lang,
+ )))
}
#[derive(FromForm)]
@@ -86,8 +86,9 @@ pub fn r_admin_user_permission(
database: &State<Database>,
form: Form<UserPermissionForm>,
name: &str,
-) -> MyResult<DynLayoutPage<'static>> {
- drop(session);
+ lang: AcceptLanguage,
+) -> MyResult<RawHtml<String>> {
+ let AcceptLanguage(lang) = lang;
let perm = serde_json::from_str::<UserPermission>(&form.permission)
.context("parsing provided permission")?;
@@ -100,11 +101,24 @@ pub fn r_admin_user_permission(
Ok(())
})?;
- manage_single_user(
- database,
- Some(Ok("Permissions update".into())),
- form.name.clone(),
- )
+ let user = database
+ .get_user(&name)?
+ .ok_or(anyhow!("user does not exist"))?;
+
+ Ok(RawHtml(render_page(
+ &AdminUserPage {
+ flash: Some(Ok("Permissions updated".to_string())),
+ lang: &lang,
+ user: &user,
+ },
+ RenderInfo {
+ importing: is_importing(),
+ session: Some(SessionInfo {
+ user: session.0.user,
+ }),
+ },
+ lang,
+ )))
}
#[post("/admin/<name>/remove")]
@@ -112,10 +126,26 @@ pub fn r_admin_remove_user(
session: AdminSession,
database: &State<Database>,
name: &str,
-) -> MyResult<DynLayoutPage<'static>> {
- drop(session);
+ lang: AcceptLanguage,
+) -> MyResult<RawHtml<String>> {
+ let AcceptLanguage(lang) = lang;
if !database.delete_user(&name)? {
Err(anyhow!("user did not exist"))?;
}
- user_management(database, Some(Ok("User removed".into())))
+ let r = admin_users(database, &session)?;
+
+ Ok(RawHtml(render_page(
+ &AdminUsersPage {
+ flash: Some(Ok("User removed".to_string())),
+ lang: &lang,
+ users: &r.users,
+ },
+ RenderInfo {
+ importing: is_importing(),
+ session: Some(SessionInfo {
+ user: session.0.user,
+ }),
+ },
+ lang,
+ )))
}