aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--import/src/lib.rs7
-rw-r--r--server/src/routes/mod.rs3
-rw-r--r--server/src/routes/ui/admin/log.rs66
-rw-r--r--web/script/log_stream.ts43
-rw-r--r--web/script/main.ts1
5 files changed, 106 insertions, 14 deletions
diff --git a/import/src/lib.rs b/import/src/lib.rs
index a4f3668..36014ea 100644
--- a/import/src/lib.rs
+++ b/import/src/lib.rs
@@ -15,7 +15,7 @@ use jellybase::{
CONF, SECRETS,
};
use jellyclient::{Appearance, PeopleGroup, TmdbKind, TraktKind, Visibility};
-use log::warn;
+use log::{info, warn};
use matroska::matroska_metadata;
use rayon::iter::{ParallelBridge, ParallelIterator};
use std::{
@@ -178,18 +178,21 @@ fn import_file(
let filename = path.file_name().unwrap().to_string_lossy();
match filename.as_ref() {
"poster.jpeg" | "poster.webp" | "poster.png" => {
+ info!("import poster at {path:?}");
db.update_node_init(parent, |node| {
node.poster = Some(AssetInner::Media(path.to_owned()).ser());
Ok(())
})?;
}
"backdrop.jpeg" | "backdrop.webp" | "backdrop.png" => {
+ info!("import backdrop at {path:?}");
db.update_node_init(parent, |node| {
node.backdrop = Some(AssetInner::Media(path.to_owned()).ser());
Ok(())
})?;
}
"node.yaml" => {
+ info!("import node info at {path:?}");
let data = serde_yaml::from_str::<Node>(&read_to_string(path)?)?;
db.update_node_init(parent, |node| {
fn merge_option<T>(a: &mut Option<T>, b: Option<T>) {
@@ -211,6 +214,7 @@ fn import_file(
})?;
}
"channel.info.json" => {
+ info!("import channel info.json at {path:?}");
let data = serde_json::from_reader::<_, YVideo>(BufReader::new(File::open(path)?))?;
db.update_node_init(parent, |node| {
node.kind = NodeKind::Channel;
@@ -252,6 +256,7 @@ fn import_media_file(
parent: NodeID,
visibility: Visibility,
) -> Result<()> {
+ info!("media file {path:?}");
let Some(m) = (*matroska_metadata(path)?).to_owned() else {
return Ok(());
};
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index b7d63da..e0b955a 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -30,7 +30,7 @@ use ui::{
settings::{r_account_settings, r_account_settings_post},
},
admin::{
- log::r_admin_log,
+ log::{r_admin_log, r_admin_log_stream},
r_admin_dashboard, r_admin_delete_cache, r_admin_import, r_admin_invite,
r_admin_remove_invite, r_admin_transcode_posters, r_admin_update_search,
user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users},
@@ -135,6 +135,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket<Build>
r_admin_delete_cache,
r_admin_transcode_posters,
r_admin_log,
+ r_admin_log_stream,
r_admin_import,
r_admin_update_search,
r_account_settings,
diff --git a/server/src/routes/ui/admin/log.rs b/server/src/routes/ui/admin/log.rs
index cc34dbc..f962138 100644
--- a/server/src/routes/ui/admin/log.rs
+++ b/server/src/routes/ui/admin/log.rs
@@ -13,12 +13,16 @@ use crate::{
};
use chrono::{DateTime, Utc};
use log::Level;
+use markup::Render;
use rocket::get;
+use rocket_ws::{Message, Stream, WebSocket};
+use serde_json::json;
use std::{
collections::VecDeque,
fmt::Write,
- sync::{LazyLock, RwLock},
+ sync::{Arc, LazyLock, RwLock},
};
+use tokio::sync::broadcast;
const MAX_LOG_LEN: usize = 4096;
@@ -31,7 +35,11 @@ pub fn enable_logging() {
pub struct Log {
inner: env_logger::Logger,
- log: RwLock<(VecDeque<LogLine>, VecDeque<LogLine>)>,
+ stream: (
+ broadcast::Sender<Arc<LogLine>>,
+ broadcast::Sender<Arc<LogLine>>,
+ ),
+ log: RwLock<(VecDeque<Arc<LogLine>>, VecDeque<Arc<LogLine>>)>,
}
pub struct LogLine {
@@ -41,14 +49,15 @@ pub struct LogLine {
message: String,
}
-#[get("/admin/log?<warnonly>")]
+#[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 {
+ 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()] {
@@ -63,6 +72,32 @@ pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<DynLa
..Default::default()
})
}
+
+#[get("/admin/log?stream&<warnonly>", rank = 1)]
+pub fn r_admin_log_stream(
+ _session: AdminSession,
+ ws: WebSocket,
+ warnonly: bool,
+) -> Stream!['static] {
+ let mut stream = if warnonly {
+ LOGGER.stream.1.subscribe()
+ } else {
+ LOGGER.stream.0.subscribe()
+ };
+ 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 {
@@ -70,6 +105,10 @@ impl Default for Log {
.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(),
}
}
@@ -85,24 +124,22 @@ impl Log {
}
}
fn do_log(&self, record: &log::Record) {
- let mut w = self.log.write().unwrap();
let time = Utc::now();
- w.0.push_back(LogLine {
+ 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 {
- w.1.push_back(LogLine {
- time,
- module: record.module_path_static(),
- level: record.level(),
- message: record.args().to_string(),
- });
+ let _ = self.stream.1.send(line.clone());
+ w.1.push_back(line);
while w.1.len() > MAX_LOG_LEN {
w.1.pop_front();
}
@@ -150,6 +187,11 @@ fn format_level(level: Level) -> impl markup::Render {
};
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 {
diff --git a/web/script/log_stream.ts b/web/script/log_stream.ts
new file mode 100644
index 0000000..5a6a3ce
--- /dev/null
+++ b/web/script/log_stream.ts
@@ -0,0 +1,43 @@
+/*
+ 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>
+*/
+/// <reference lib="dom" />
+globalThis.addEventListener("DOMContentLoaded", () => {
+ if (!document.body.classList.contains("admin_log")) return
+ const log = document.getElementById("log")!
+
+ const warnonly = new URL(globalThis.location.href).searchParams.get("warnonly") == "true"
+ const ws = new WebSocket(`/admin/log?stream&warnonly=${warnonly}`)
+ ws.onopen = () => console.log("live log connected");
+ ws.onclose = () => console.log("live log disconnected");
+ ws.onerror = e => console.log(`live log ws error: ${e}`);
+
+ ws.onmessage = msg => {
+ const line = JSON.parse(msg.data)
+
+ const td_time = document.createElement("td")
+ td_time.classList.add("time")
+ td_time.textContent = line.time
+
+ const td_level = document.createElement("td")
+ td_level.classList.add("level")
+ td_level.innerHTML = line.level_html
+
+ const td_module = document.createElement("td")
+ td_module.classList.add("module")
+ td_module.textContent = line.module
+
+ const td_message = document.createElement("td")
+ td_message.innerHTML = line.message
+
+ const tr = document.createElement("tr");
+ tr.classList.add(line.level_class)
+ tr.append(td_time, td_level, td_module, td_message)
+
+ log.children[0].children[0].append(tr)
+ while (log.children[0].children[0].children.length > 1024)
+ log.children[0].children[0].children[0].remove()
+ }
+})
diff --git a/web/script/main.ts b/web/script/main.ts
index 6d335ee..d7a36cb 100644
--- a/web/script/main.ts
+++ b/web/script/main.ts
@@ -8,3 +8,4 @@ import "./player/mod.ts"
import "./transition.ts"
import "./backbutton.ts"
import "./dangerbutton.ts"
+import "./log_stream.ts"