logs stub

This commit is contained in:
2026-03-27 21:10:49 +01:00
parent 9393dc2f21
commit c2062f3a8a
12 changed files with 133 additions and 4 deletions

View File

@@ -10,6 +10,7 @@ use crate::{
quotes::QuoteError, quotes::QuoteError,
tags::TagError, tags::TagError,
users::{UserError, auth::AuthError, sessions::SessionError}, users::{UserError, auth::AuthError, sessions::SessionError},
web::RedirectViaError,
}; };
mod auth; mod auth;
@@ -83,5 +84,6 @@ composite_from!(
TagError, TagError,
PersonError, PersonError,
QuoteError, QuoteError,
DatabaseError DatabaseError,
RedirectViaError
); );

26
src/logs.rs Normal file
View File

@@ -0,0 +1,26 @@
use std::fmt::Display;
use uuid::Uuid;
use crate::users::User;
pub struct LogEntry {
pub id: Uuid,
pub actor: User,
pub data: LogAction,
}
#[derive(Debug)]
pub enum LogAction {
Initialize,
RegenInfradmin,
}
impl Display for LogAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogAction::Initialize => write!(f, "Initialized Mnemosyne."),
LogAction::RegenInfradmin => write!(f, "Regenerated the Infradmin account."),
}
}
}

View File

@@ -6,6 +6,7 @@ use tokio::net::TcpListener;
mod api; mod api;
mod config; mod config;
mod database; mod database;
mod logs;
mod persons; mod persons;
mod quotes; mod quotes;
mod tags; mod tags;

View File

@@ -15,6 +15,7 @@ pub enum Permission {
RenameTags, RenameTags,
DeleteTags, DeleteTags,
ChangePersonPrimaryName, ChangePersonPrimaryName,
BrowseServerLogs,
} }
impl User { impl User {

View File

@@ -10,7 +10,7 @@ const LINKS: &[(&str, &str, &str, bool)] = &[
("Persons", "/persons", icons::CONTACT, false), ("Persons", "/persons", icons::CONTACT, false),
("Tags", "/tags", icons::TAG, false), ("Tags", "/tags", icons::TAG, false),
("Users", "/users", icons::USERS, true), ("Users", "/users", icons::USERS, true),
("Logs", "#logs", icons::CLIPBOARD_CLOCK, true), ("Logs", "/logs", icons::CLIPBOARD_CLOCK, true),
]; ];
pub fn nav(user: Option<&User>, uri: &str) -> Markup { pub fn nav(user: Option<&User>, uri: &str) -> Markup {

1
src/web/icons/clock.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock-icon lucide-clock"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -2,6 +2,7 @@
pub const ARROW_RIGHT: &str = include_str!("arrow-right.svg"); pub const ARROW_RIGHT: &str = include_str!("arrow-right.svg");
pub const CALENDAR_1: &str = include_str!("calendar-1.svg"); pub const CALENDAR_1: &str = include_str!("calendar-1.svg");
pub const CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg"); pub const CLIPBOARD_CLOCK: &str = include_str!("clipboard-clock.svg");
pub const CLOCK: &str = include_str!("clock.svg");
pub const CONTACT: &str = include_str!("contact.svg"); pub const CONTACT: &str = include_str!("contact.svg");
pub const EYE: &str = include_str!("eye.svg"); pub const EYE: &str = include_str!("eye.svg");
pub const FILE_IMAGE: &str = include_str!("file-image.svg"); pub const FILE_IMAGE: &str = include_str!("file-image.svg");
@@ -9,6 +10,7 @@ pub const INFO: &str = include_str!("info.svg");
pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg"); pub const LAYOUT_DASHBOARD: &str = include_str!("layout-dashboard.svg");
pub const LOG_OUT: &str = include_str!("log-out.svg"); pub const LOG_OUT: &str = include_str!("log-out.svg");
pub const MAP_PIN: &str = include_str!("map-pin.svg"); pub const MAP_PIN: &str = include_str!("map-pin.svg");
pub const PEN: &str = include_str!("pen.svg");
pub const QUOTE: &str = include_str!("quote.svg"); pub const QUOTE: &str = include_str!("quote.svg");
pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg"); pub const SCROLL_TEXT: &str = include_str!("scroll-text.svg");
pub const SERVER: &str = include_str!("server.svg"); pub const SERVER: &str = include_str!("server.svg");

1
src/web/icons/pen.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pen-icon lucide-pen"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@@ -1,4 +1,7 @@
use axum::Router; use axum::{
Router,
response::{IntoResponse, Redirect, Response},
};
use tower_http::services::ServeFile; use tower_http::services::ServeFile;
mod components; mod components;
@@ -10,3 +13,10 @@ pub fn web_router() -> Router {
.route_service("/styles.css", ServeFile::new("src/web/styles.css")) .route_service("/styles.css", ServeFile::new("src/web/styles.css"))
.merge(pages::pages()) .merge(pages::pages())
} }
pub struct RedirectViaError(Redirect);
impl IntoResponse for RedirectViaError {
fn into_response(self) -> Response {
self.0.into_response()
}
}

83
src/web/pages/logs.rs Normal file
View File

@@ -0,0 +1,83 @@
use axum::{
extract::Request,
response::{IntoResponse, Redirect, Response},
};
use maud::{PreEscaped, html};
use uuid::Uuid;
use crate::{
api::CompositeError,
logs::{LogAction, LogEntry},
users::{User, auth::UserAuthenticate, permissions::Permission},
web::{RedirectViaError, components::nav::nav, icons, pages::base},
};
pub async fn page(req: Request) -> Result<Response, CompositeError> {
let u = User::authenticate(req.headers())?
.ok_or(RedirectViaError(Redirect::to("/login?re=/logs")))?;
let logs: Vec<LogEntry> = vec![
LogEntry {
id: Uuid::now_v7(),
actor: User::get_by_id(Uuid::nil()).unwrap(),
data: LogAction::Initialize,
},
LogEntry {
id: Uuid::now_v7(),
actor: User::get_by_id(Uuid::nil()).unwrap(),
data: LogAction::RegenInfradmin,
},
]
.into_iter()
.rev()
.collect();
Ok(base(
"Persons | Mnemosyne",
html!(
(nav(Some(&u), req.uri().path()))
@if let Ok(true) = u.has_permission(Permission::BrowseServerLogs) {
div class="max-w-4xl mx-auto px-2" {
div class="my-4" {
p class="flex items-center gap-2" {
span class="text-neutral-500" {(PreEscaped(icons::CLIPBOARD_CLOCK))}
span class="text-2xl font-semibold font-lora" {"Logs"}
}
p class="text-neutral-500 text-sm font-light" {
"Work in progress."
}
}
div class="w-full border border-neutral-200/25 rounded grid grid-cols-[auto_auto_1fr]" {
@for (txt, ico) in [("Timestamp", icons::CLOCK), ("Actor", icons::USER), ("Action", icons::PEN)] {
div class="p-2 flex gap-1 font-semibold border-b border-neutral-200/25" {
span class="text-neutral-500 scale-[.8]" {(PreEscaped(ico))}
(txt)
}
}
@for (idx, log) in logs.iter().enumerate() {
@let s = if idx % 2 == 0 {"background-color: #e5e5e50b"} else {""};
div class="p-2 font-light" style=(s) {
(log.id.get_timestamp()
.map(|ts| {
let (secs, nanos) = ts.to_unix();
chrono::DateTime::from_timestamp(secs as i64, nanos)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
.unwrap_or_else(|| "invalid date".to_string())
})
.unwrap_or_else(|| "no timestamp".to_string()))
}
div class="p-2 font-light" style=(s) {(log.actor.handle)}
div class="p-2 font-light" style=(s) {(log.data)}
}
@if true {
div class="p-2 col-span-3 text-center font-light text-neutral-400" {"You've reached the end of all logs."}
}
}
}
} @else {
p class="text-center p-2" {"You must have permission to view this page."}
}
),
)
.into_response())
}

View File

@@ -7,6 +7,7 @@ use maud::{DOCTYPE, Markup, html};
pub mod dashboard; pub mod dashboard;
pub mod index; pub mod index;
pub mod login; pub mod login;
pub mod logs;
pub mod persons; pub mod persons;
pub mod tags; pub mod tags;
pub mod users; pub mod users;
@@ -28,6 +29,7 @@ pub fn pages() -> Router {
.route("/tags/create", post(tags::create)) .route("/tags/create", post(tags::create))
.route("/persons", get(persons::page)) .route("/persons", get(persons::page))
.route("/persons/create", post(persons::create)) .route("/persons/create", post(persons::create))
.route("/logs", get(logs::page))
} }
pub fn base(title: &str, inner: Markup) -> Markup { pub fn base(title: &str, inner: Markup) -> Markup {

File diff suppressed because one or more lines are too long