diff options
Diffstat (limited to 'server/src/routes/ui/admin/mod.rs')
-rw-r--r-- | server/src/routes/ui/admin/mod.rs | 290 |
1 files changed, 0 insertions, 290 deletions
diff --git a/server/src/routes/ui/admin/mod.rs b/server/src/routes/ui/admin/mod.rs deleted file mode 100644 index f44b36c..0000000 --- a/server/src/routes/ui/admin/mod.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - 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> -*/ -pub mod log; -pub mod user; - -use super::{ - account::session::AdminSession, - assets::{resolve_asset, AVIF_QUALITY, AVIF_SPEED}, -}; -use crate::{ - database::Database, - routes::ui::{ - admin::log::rocket_uri_macro_r_admin_log, - error::MyResult, - layout::{DynLayoutPage, FlashDisplay, LayoutPage}, - }, - uri, -}; -use anyhow::{anyhow, Context}; -use jellybase::{assetfed::AssetInner, federation::Federation, CONF}; -use jellyimport::{import_wrap, is_importing, IMPORT_ERRORS}; -use markup::DynRender; -use rand::Rng; -use rocket::{form::Form, get, post, FromForm, State}; -use std::time::Instant; -use tokio::{sync::Semaphore, task::spawn_blocking}; -use user::rocket_uri_macro_r_admin_users; - -#[get("/admin/dashboard")] -pub async fn r_admin_dashboard( - _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>> { - let invites = database.list_invites()?; - let flash = flash.map(|f| f.map_err(|e| format!("{e:?}"))); - - 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"]; - } - } - }} - - h2 { "Database" } - @match db_stats(&database) { - Ok(s) => { @s } - Err(e) => { pre.error { @format!("{e:?}") } } - } - }, - ..Default::default() - }) -} - -#[post("/admin/generate_invite")] -pub async fn r_admin_invite( - _session: AdminSession, - database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { - let i = format!("{}", rand::rng().random::<u128>()); - database.create_invite(&i)?; - admin_dashboard(database, Some(Ok(format!("Invite: {}", i)))).await -} - -#[derive(FromForm)] -pub struct DeleteInvite { - invite: String, -} - -#[post("/admin/remove_invite", data = "<form>")] -pub async fn r_admin_remove_invite( - session: AdminSession, - database: &State<Database>, - form: Form<DeleteInvite>, -) -> MyResult<DynLayoutPage<'static>> { - drop(session); - if !database.delete_invite(&form.invite)? { - Err(anyhow!("invite does not exist"))?; - }; - admin_dashboard(database, Some(Ok("Invite invalidated".into()))).await -} - -#[post("/admin/import?<incremental>")] -pub async fn r_admin_import( - session: AdminSession, - database: &State<Database>, - _federation: &State<Federation>, - incremental: bool, -) -> MyResult<DynLayoutPage<'static>> { - 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 -} - -#[post("/admin/update_search")] -pub async fn r_admin_update_search( - _session: AdminSession, - database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { - 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 -} - -#[post("/admin/delete_cache")] -pub async fn r_admin_delete_cache( - session: AdminSession, - database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { - 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 -} - -static SEM_TRANSCODING: Semaphore = Semaphore::const_new(1); -fn is_transcoding() -> bool { - SEM_TRANSCODING.available_permits() == 0 -} - -#[post("/admin/transcode_posters")] -pub async fn r_admin_transcode_posters( - session: AdminSession, - database: &State<Database>, -) -> MyResult<DynLayoutPage<'static>> { - drop(session); - let _permit = SEM_TRANSCODING - .try_acquire() - .context("transcoding in progress")?; - - let t = Instant::now(); - - { - let nodes = database.list_nodes_with_udata("")?; - for (node, _) in nodes { - if let Some(poster) = &node.poster { - let asset = AssetInner::deser(&poster.0)?; - if asset.is_federated() { - continue; - } - let source = resolve_asset(asset).await.context("resolving asset")?; - jellytranscoder::image::transcode(&source, AVIF_QUALITY, AVIF_SPEED, 1024) - .await - .context("transcoding asset")?; - } - } - } - drop(_permit); - - admin_dashboard( - database, - Some(Ok(format!( - "All posters pre-transcoded; took {:?}", - t.elapsed() - ))), - ) - .await -} - -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()?; - - 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 } - // } - }) -} |