Feat: Implement Admin Dashboard UI
This commit introduces the foundational UI for the admin dashboard. It includes: - A new `maud`-based layout component for consistent page structure. - A navigation sidebar and header. - Basic views for dashboard overview, user management, library settings, and about page. - Integration with Tailwind CSS and Phosphor Icons for styling. - Added `maud` and `axum-core` as dependencies.
This commit is contained in:
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -455,6 +455,30 @@ version = "0.8.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maud"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"http",
|
||||||
|
"itoa",
|
||||||
|
"maud_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maud_macros"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"proc-macro2-diagnostics",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md5"
|
name = "md5"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -567,6 +591,18 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2-diagnostics"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.44"
|
version = "1.0.44"
|
||||||
@@ -770,6 +806,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"diesel",
|
"diesel",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"maud",
|
||||||
"md5",
|
"md5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1013,6 +1050,12 @@ version = "0.2.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ tokio = { version = "1.49.0", features = ["full"] }
|
|||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
tower-http = {version = "0.6.8", features = ["trace"]}
|
tower-http = {version = "0.6.8", features = ["trace"]}
|
||||||
|
maud = { version = "0.27.0", features = ["axum"] }
|
||||||
|
|||||||
150
src/admin/layout.rs
Normal file
150
src/admin/layout.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
use maud::{html, Markup, DOCTYPE};
|
||||||
|
|
||||||
|
pub fn layout(title: &str, content: Markup) -> Markup {
|
||||||
|
html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
html lang="en" {
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||||
|
title { "SoundSonic Admin - " (title) }
|
||||||
|
script src="https://cdn.tailwindcss.com" {}
|
||||||
|
// Tailwind Config for custom colors/fonts if needed
|
||||||
|
script {
|
||||||
|
(maud::PreEscaped(r##"
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a","950":"#172554"}
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"##))
|
||||||
|
}
|
||||||
|
link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet";
|
||||||
|
// Add Phosphor icons for beautiful lightweight UI icons
|
||||||
|
script src="https://unpkg.com/@phosphor-icons/web" {}
|
||||||
|
|
||||||
|
style {
|
||||||
|
(maud::PreEscaped(r##"
|
||||||
|
body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #f8fafc; }
|
||||||
|
/* Glassmorphism utilities */
|
||||||
|
.glass {
|
||||||
|
background: rgba(30, 41, 59, 0.7);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(30, 41, 59, 0.4);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.nav-link { transition: all 0.2s ease-in-out; border-left: 3px solid transparent; }
|
||||||
|
.nav-link:hover { background-color: rgba(51, 65, 85, 0.5); color: #fff; border-left-color: #3b82f6; }
|
||||||
|
.nav-link.active { background-color: rgba(51, 65, 85, 0.8); color: #fff; border-left-color: #3b82f6; font-weight: 500; }
|
||||||
|
|
||||||
|
/* Custom scrollbar for dark mode */
|
||||||
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
|
::-webkit-scrollbar-track { background: #0f172a; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
||||||
|
"##))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body class="h-screen flex overflow-hidden antialiased selection:bg-primary-500 selection:text-white" {
|
||||||
|
// Sidebar
|
||||||
|
aside class="w-64 glass flex flex-col z-20 flex-shrink-0" {
|
||||||
|
div class="h-16 flex items-center px-6 font-bold text-xl tracking-tight border-b border-white/10 gap-2 text-white" {
|
||||||
|
i class="ph-fill ph-waves text-primary-500 text-2xl" {}
|
||||||
|
"SoundSonic"
|
||||||
|
}
|
||||||
|
nav class="flex-1 overflow-y-auto py-4 space-y-1" {
|
||||||
|
// Using simplistic path matching logic conceptually handled in UI. We'll just define the links.
|
||||||
|
a href="/admin" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-squares-four text-lg" {} "Dashboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="px-6 pt-4 pb-2 text-xs font-semibold text-slate-500 uppercase tracking-wider" { "Manage" }
|
||||||
|
a href="/admin/users" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-users text-lg" {} "Users & Roles"
|
||||||
|
}
|
||||||
|
a href="/admin/library" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-books text-lg" {} "Library"
|
||||||
|
}
|
||||||
|
a href="/admin/clients" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-devices text-lg" {} "API Clients"
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="px-6 pt-4 pb-2 text-xs font-semibold text-slate-500 uppercase tracking-wider" { "Server" }
|
||||||
|
a href="/admin/playback" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-play-circle text-lg" {} "Playback/Stream"
|
||||||
|
}
|
||||||
|
a href="/admin/settings" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-gear text-lg" {} "Settings"
|
||||||
|
}
|
||||||
|
a href="/admin/jobs" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-arrows-clockwise text-lg" {} "Background Jobs"
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="px-6 pt-4 pb-2 text-xs font-semibold text-slate-500 uppercase tracking-wider" { "Diagnostics" }
|
||||||
|
a href="/admin/logs" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-terminal-window text-lg" {} "Logs & Debug"
|
||||||
|
}
|
||||||
|
a href="/admin/about" class="nav-link flex items-center gap-3 px-6 py-2.5 text-sm font-medium text-slate-300" {
|
||||||
|
i class="ph ph-info text-lg" {} "About"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-4 border-t border-white/10" {
|
||||||
|
div class="flex items-center gap-3 px-2" {
|
||||||
|
div class="w-8 h-8 rounded-full bg-gradient-to-tr from-primary-500 to-primary-700 flex items-center justify-center text-sm font-bold shadow-lg shadow-primary-500/20" {
|
||||||
|
"A"
|
||||||
|
}
|
||||||
|
div class="flex-1 min-w-0" {
|
||||||
|
p class="text-sm font-medium text-white truncate" { "Administrator" }
|
||||||
|
p class="text-xs text-slate-400 truncate" { "admin@local" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main Content
|
||||||
|
main class="flex-1 flex flex-col min-w-0 overflow-hidden relative" {
|
||||||
|
// Decorative background glow
|
||||||
|
div class="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] rounded-full bg-primary-900/20 blur-[120px] pointer-events-none" {}
|
||||||
|
div class="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] rounded-full bg-blue-900/10 blur-[120px] pointer-events-none" {}
|
||||||
|
|
||||||
|
header class="h-16 glass flex items-center justify-between px-8 z-10" {
|
||||||
|
h1 class="text-xl font-semibold text-white tracking-tight flex items-center gap-2" {
|
||||||
|
(title)
|
||||||
|
}
|
||||||
|
div class="flex items-center gap-4 text-slate-400" {
|
||||||
|
button class="hover:text-white transition-colors" title="Notifications" { i class="ph ph-bell text-xl" {} }
|
||||||
|
button class="hover:text-white transition-colors" title="Server Status" {
|
||||||
|
div class="relative" {
|
||||||
|
i class="ph ph-hard-drives text-xl" {}
|
||||||
|
span class="absolute top-0 right-0 w-2 h-2 bg-emerald-500 rounded-full border border-slate-900" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a href="/rest/ping.view" class="text-sm border border-slate-700 hover:bg-slate-800 px-3 py-1.5 rounded-md transition-colors flex items-center gap-2" {
|
||||||
|
i class="ph ph-sign-out" {} "Exit Panel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="flex-1 overflow-y-auto p-8 z-10" {
|
||||||
|
div class="max-w-7xl mx-auto space-y-6" {
|
||||||
|
(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/admin/mod.rs
Normal file
3
src/admin/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod layout;
|
||||||
|
pub mod routes;
|
||||||
|
pub mod views;
|
||||||
24
src/admin/routes.rs
Normal file
24
src/admin/routes.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use axum::{routing::get, Router};
|
||||||
|
use crate::state::AppState;
|
||||||
|
use crate::admin::views::{
|
||||||
|
dashboard, users, library, playback, settings, logs, clients, jobs, about, login
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn admin_router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/login", get(login::login_form).post(login::login_post))
|
||||||
|
.route("/", get(dashboard::index))
|
||||||
|
|
||||||
|
.route("/users", get(users::users))
|
||||||
|
.route("/users/new", get(users::user_create))
|
||||||
|
.route("/users/{id}/edit", get(users::user_edit))
|
||||||
|
|
||||||
|
.route("/library", get(library::library))
|
||||||
|
.route("/playback", get(playback::playback))
|
||||||
|
.route("/settings", get(settings::settings))
|
||||||
|
|
||||||
|
.route("/logs", get(logs::logs))
|
||||||
|
.route("/clients", get(clients::clients))
|
||||||
|
.route("/jobs", get(jobs::jobs))
|
||||||
|
.route("/about", get(about::about))
|
||||||
|
}
|
||||||
99
src/admin/views/about.rs
Normal file
99
src/admin/views/about.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn about() -> Markup {
|
||||||
|
layout("About & Diagnostics", html! {
|
||||||
|
div class="max-w-3xl mx-auto space-y-6" {
|
||||||
|
|
||||||
|
// Server Identity Card
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden text-center p-8 space-y-4 relative" {
|
||||||
|
div class="w-24 h-24 rounded-full bg-gradient-to-tr from-primary-500 to-indigo-600 mx-auto flex items-center justify-center border-4 border-black/40 shadow-xl shadow-primary-500/20" {
|
||||||
|
i class="ph-fill ph-waves text-5xl text-white" {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
h2 class="text-2xl font-bold text-white tracking-tight" { "SoundSonic" }
|
||||||
|
p class="text-primary-400 font-mono text-sm mt-1" { "v0.1.0-alpha.5 (build 4f8a9b2)" }
|
||||||
|
}
|
||||||
|
p class="text-slate-400 text-sm max-w-lg mx-auto" {
|
||||||
|
"A blazing fast, lightweight Subsonic-compatible media server written in Rust."
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="flex justify-center gap-4 mt-6 pt-6 border-t border-white/5" {
|
||||||
|
a href="#" class="text-slate-400 hover:text-white transition-colors flex flex-col items-center gap-1" {
|
||||||
|
i class="ph ph-github-logo text-3xl" {}
|
||||||
|
span class="text-xs font-medium" { "GitHub" }
|
||||||
|
}
|
||||||
|
a href="#" class="text-slate-400 hover:text-primary-400 transition-colors flex flex-col items-center gap-1" {
|
||||||
|
i class="ph ph-bug text-3xl" {}
|
||||||
|
span class="text-xs font-medium" { "Report Issue" }
|
||||||
|
}
|
||||||
|
a href="#" class="text-slate-400 hover:text-indigo-400 transition-colors flex flex-col items-center gap-1" {
|
||||||
|
i class="ph ph-book-open text-3xl" {}
|
||||||
|
span class="text-xs font-medium" { "Docs" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diagnostics
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
|
||||||
|
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h3 class="text-sm font-medium text-white flex items-center gap-2 uppercase tracking-wider" {
|
||||||
|
i class="ph ph-info" {} "System Details"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-0" {
|
||||||
|
table class="w-full text-sm" {
|
||||||
|
tbody class="divide-y divide-white/5" {
|
||||||
|
tr class="hover:bg-white/[0.02]" {
|
||||||
|
td class="py-3 px-6 text-slate-500" { "OS" }
|
||||||
|
td class="py-3 px-6 text-slate-300 font-mono text-right" { "Linux 6.8.0-generic" }
|
||||||
|
}
|
||||||
|
tr class="hover:bg-white/[0.02]" {
|
||||||
|
td class="py-3 px-6 text-slate-500" { "Architecture" }
|
||||||
|
td class="py-3 px-6 text-slate-300 font-mono text-right" { "x86_64" }
|
||||||
|
}
|
||||||
|
tr class="hover:bg-white/[0.02]" {
|
||||||
|
td class="py-3 px-6 text-slate-500" { "Rust Version" }
|
||||||
|
td class="py-3 px-6 text-slate-300 font-mono text-right" { "rustc 1.77.0" }
|
||||||
|
}
|
||||||
|
tr class="hover:bg-white/[0.02]" {
|
||||||
|
td class="py-3 px-6 text-slate-500" { "Database" }
|
||||||
|
td class="py-3 px-6 text-slate-300 font-mono text-right" { "SQLite 3.45.1" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h3 class="text-sm font-medium text-white flex items-center gap-2 uppercase tracking-wider" {
|
||||||
|
i class="ph ph-plugs-connected" {} "API Supported Extensions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 text-sm" {
|
||||||
|
ul class="space-y-3" {
|
||||||
|
li class="flex items-center gap-3 text-slate-300" {
|
||||||
|
i class="ph-fill ph-check-circle text-emerald-400" {} "subsonic"
|
||||||
|
}
|
||||||
|
li class="flex items-center gap-3 text-slate-300" {
|
||||||
|
i class="ph-fill ph-check-circle text-emerald-400" {} "transcodeOffset"
|
||||||
|
}
|
||||||
|
li class="flex items-center gap-3 text-slate-300" {
|
||||||
|
i class="ph-fill ph-check-circle text-emerald-400" {} "podcast"
|
||||||
|
}
|
||||||
|
li class="flex items-center gap-3 text-slate-300" {
|
||||||
|
i class="ph-fill ph-check-circle text-emerald-400" {} "videoConversion"
|
||||||
|
}
|
||||||
|
li class="flex items-center gap-3 text-slate-300" {
|
||||||
|
i class="ph-fill ph-check-circle text-emerald-400" {} "playQueue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
77
src/admin/views/clients.rs
Normal file
77
src/admin/views/clients.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn clients() -> Markup {
|
||||||
|
layout("API Clients & Tokens", html! {
|
||||||
|
div class="flex flex-col gap-6" {
|
||||||
|
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-devices text-primary-400" {} "Connected Clients"
|
||||||
|
}
|
||||||
|
div class="flex gap-2" {
|
||||||
|
span class="px-3 py-1 rounded bg-slate-800 text-slate-400 border border-white/5 text-xs font-medium" { "Token Auth Enabled" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 overflow-x-auto" {
|
||||||
|
table class="w-full text-left" {
|
||||||
|
thead class="bg-transparent text-xs uppercase tracking-wider text-slate-500 border-b border-white/5" {
|
||||||
|
tr {
|
||||||
|
th class="pb-3 font-semibold" { "Client Name" }
|
||||||
|
th class="pb-3 font-semibold" { "Version" }
|
||||||
|
th class="pb-3 font-semibold" { "Owner / User" }
|
||||||
|
th class="pb-3 font-semibold" { "Last Used" }
|
||||||
|
th class="pb-3 font-semibold text-right" { "Actions" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody class="text-sm divide-y divide-white/5" {
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors" {
|
||||||
|
td class="py-4 text-slate-300 font-medium flex items-center gap-3" {
|
||||||
|
i class="ph ph-device-mobile text-xl text-indigo-400" {}
|
||||||
|
"Ultrasonic"
|
||||||
|
}
|
||||||
|
td class="py-4 text-slate-400" { "3.0.0-git" }
|
||||||
|
td class="py-4 text-slate-300" { "admin" }
|
||||||
|
td class="py-4 text-slate-400" { "2 hours ago" }
|
||||||
|
td class="py-4 text-right" {
|
||||||
|
button class="text-rose-400 hover:text-rose-300 transition-colors bg-rose-500/10 p-2 rounded-md border border-rose-500/20 text-xs font-medium flex items-center gap-2 ml-auto" title="Revoke access" {
|
||||||
|
i class="ph ph-key" {} "Revoke Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors" {
|
||||||
|
td class="py-4 text-slate-300 font-medium flex items-center gap-3" {
|
||||||
|
i class="ph ph-browser text-xl text-sky-400" {}
|
||||||
|
"WebPlayer"
|
||||||
|
}
|
||||||
|
td class="py-4 text-slate-400" { "1.0.0" }
|
||||||
|
td class="py-4 text-slate-300" { "admin" }
|
||||||
|
td class="py-4 text-slate-400" { "Right now" }
|
||||||
|
td class="py-4 text-right" {
|
||||||
|
button class="text-rose-400 hover:text-rose-300 transition-colors bg-rose-500/10 p-2 rounded-md border border-rose-500/20 text-xs font-medium flex items-center gap-2 ml-auto" title="Revoke access" {
|
||||||
|
i class="ph ph-key" {} "Revoke Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors text-slate-500" {
|
||||||
|
td class="py-4 font-medium flex items-center gap-3" {
|
||||||
|
i class="ph ph-car text-xl" {}
|
||||||
|
"Android Auto Sub"
|
||||||
|
}
|
||||||
|
td class="py-4" { "1.2" }
|
||||||
|
td class="py-4" { "guest" }
|
||||||
|
td class="py-4" { "6 months ago" }
|
||||||
|
td class="py-4 text-right" {
|
||||||
|
button class="text-rose-400 hover:text-rose-300 transition-colors bg-rose-500/10 p-2 rounded-md border border-rose-500/20 text-xs font-medium flex items-center gap-2 ml-auto" title="Revoke access" {
|
||||||
|
i class="ph ph-key" {} "Revoke Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
171
src/admin/views/dashboard.rs
Normal file
171
src/admin/views/dashboard.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn index() -> Markup {
|
||||||
|
layout("Dashboard Overview", html! {
|
||||||
|
// Top Stats Row
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" {
|
||||||
|
div class="glass-card rounded-xl p-6 relative overflow-hidden group hover:-translate-y-1 transition-transform" {
|
||||||
|
div class="absolute top-0 right-0 p-4 opacity-20 group-hover:opacity-40 transition-opacity" {
|
||||||
|
i class="ph-fill ph-music-notes text-6xl text-primary-500" {}
|
||||||
|
}
|
||||||
|
h3 class="text-slate-400 text-sm font-medium uppercase tracking-wider mb-2" { "Total Songs" }
|
||||||
|
p class="text-4xl font-bold text-white mb-1" { "42,891" }
|
||||||
|
p class="text-xs text-emerald-400 flex items-center gap-1" {
|
||||||
|
i class="ph-bold ph-trend-up" {} "+124 this week"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="glass-card rounded-xl p-6 relative overflow-hidden group hover:-translate-y-1 transition-transform" {
|
||||||
|
div class="absolute top-0 right-0 p-4 opacity-20 group-hover:opacity-40 transition-opacity" {
|
||||||
|
i class="ph-fill ph-vinyl-record text-6xl text-purple-500" {}
|
||||||
|
}
|
||||||
|
h3 class="text-slate-400 text-sm font-medium uppercase tracking-wider mb-2" { "Total Albums" }
|
||||||
|
p class="text-4xl font-bold text-white mb-1" { "3,402" }
|
||||||
|
p class="text-xs text-emerald-400 flex items-center gap-1" {
|
||||||
|
i class="ph-bold ph-trend-up" {} "+12 this week"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="glass-card rounded-xl p-6 relative overflow-hidden group hover:-translate-y-1 transition-transform" {
|
||||||
|
div class="absolute top-0 right-0 p-4 opacity-20 group-hover:opacity-40 transition-opacity" {
|
||||||
|
i class="ph-fill ph-users text-6xl text-amber-500" {}
|
||||||
|
}
|
||||||
|
h3 class="text-slate-400 text-sm font-medium uppercase tracking-wider mb-2" { "Total Users" }
|
||||||
|
p class="text-4xl font-bold text-white mb-1" { "128" }
|
||||||
|
p class="text-xs text-emerald-400 flex items-center gap-1" {
|
||||||
|
i class="ph-bold ph-trend-up" {} "+3 this week"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="glass-card rounded-xl p-6 relative overflow-hidden group hover:-translate-y-1 transition-transform" {
|
||||||
|
div class="absolute top-0 right-0 p-4 opacity-20 group-hover:opacity-40 transition-opacity" {
|
||||||
|
i class="ph-fill ph-headphones text-6xl text-pink-500" {}
|
||||||
|
}
|
||||||
|
h3 class="text-slate-400 text-sm font-medium uppercase tracking-wider mb-2" { "Active Sessions" }
|
||||||
|
p class="text-4xl font-bold text-white flex items-center gap-3 mb-1" {
|
||||||
|
"14"
|
||||||
|
span class="relative flex h-3 w-3" {
|
||||||
|
span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-pink-400 opacity-75" {}
|
||||||
|
span class="relative inline-flex rounded-full h-3 w-3 bg-pink-500" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p class="text-xs text-slate-400 flex items-center gap-1" {
|
||||||
|
"Right now"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Health & Quick Actions
|
||||||
|
div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6" {
|
||||||
|
|
||||||
|
// Server Health
|
||||||
|
div class="lg:col-span-2 glass-card rounded-xl border border-white/5 overflow-hidden flex flex-col" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-heartbeat text-primary-400" {} "Server Health"
|
||||||
|
}
|
||||||
|
span class="px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 flex items-center gap-1.5" {
|
||||||
|
div class="w-1.5 h-1.5 rounded-full bg-emerald-400" {}
|
||||||
|
"Online"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 grid grid-cols-2 md:grid-cols-4 gap-6" {
|
||||||
|
div {
|
||||||
|
p class="text-slate-400 text-xs mb-1 uppercase tracking-wider font-semibold" { "Uptime" }
|
||||||
|
p class="text-xl font-medium text-white" { "14d 6h 22m" }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
p class="text-slate-400 text-xs mb-1 uppercase tracking-wider font-semibold" { "CPU Usage" }
|
||||||
|
p class="text-xl font-medium text-white flex items-center gap-2" {
|
||||||
|
"12%"
|
||||||
|
div class="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden" {
|
||||||
|
div class="h-full bg-primary-500 w-[12%]" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
p class="text-slate-400 text-xs mb-1 uppercase tracking-wider font-semibold" { "RAM Usage" }
|
||||||
|
p class="text-xl font-medium text-white flex items-center gap-2" {
|
||||||
|
"2.4 GB"
|
||||||
|
div class="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden" {
|
||||||
|
div class="h-full bg-amber-500 w-[45%]" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
p class="text-slate-400 text-xs mb-1 uppercase tracking-wider font-semibold" { "Last Scan" }
|
||||||
|
p class="text-xl font-medium text-white" { "2 mins ago" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent Errors
|
||||||
|
div class="px-6 py-3 bg-black/30 border-t border-white/5 border-b border-white/5" {
|
||||||
|
p class="text-xs font-medium text-slate-400 uppercase tracking-wider" { "Recent Log Activity" }
|
||||||
|
}
|
||||||
|
div class="flex-1 p-6 space-y-3 overflow-y-auto max-h-[300px] font-mono text-sm" {
|
||||||
|
div class="flex gap-3 text-slate-300" {
|
||||||
|
span class="text-slate-500 shrink-0" { "10:42:01 AM" }
|
||||||
|
span class="text-emerald-400 shrink-0 w-12" { "INFO " }
|
||||||
|
span class="truncate" { "Library scan completed in 4.2s." }
|
||||||
|
}
|
||||||
|
div class="flex gap-3 text-slate-300" {
|
||||||
|
span class="text-slate-500 shrink-0" { "10:39:15 AM" }
|
||||||
|
span class="text-amber-400 shrink-0 w-12" { "WARN " }
|
||||||
|
span class="truncate" { "Failed to resolve cover art for /music/Unknown/Track01.mp3" }
|
||||||
|
}
|
||||||
|
div class="flex gap-3 text-slate-300" {
|
||||||
|
span class="text-slate-500 shrink-0" { "09:12:33 AM" }
|
||||||
|
span class="text-rose-400 shrink-0 w-12" { "ERROR" }
|
||||||
|
span class="truncate" { "Database connection timeout occurred during sync." }
|
||||||
|
}
|
||||||
|
div class="flex gap-3 text-slate-300" {
|
||||||
|
span class="text-slate-500 shrink-0" { "08:00:00 AM" }
|
||||||
|
span class="text-emerald-400 shrink-0 w-12" { "INFO " }
|
||||||
|
span class="truncate" { "Daily backup completed successfully." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden flex flex-col" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-lightning text-amber-400" {} "Quick Actions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-4" {
|
||||||
|
button class="w-full flex items-center justify-between p-4 rounded-lg bg-primary-600 hover:bg-primary-500 text-white transition-colors group shadow-lg shadow-primary-900/20" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
i class="ph ph-arrows-clockwise text-xl group-hover:rotate-180 transition-transform duration-500" {}
|
||||||
|
span class="font-medium" { "Start Library Scan" }
|
||||||
|
}
|
||||||
|
i class="ph ph-caret-right text-slate-300" {}
|
||||||
|
}
|
||||||
|
|
||||||
|
button class="w-full flex items-center justify-between p-4 rounded-lg bg-slate-800 hover:bg-slate-700 text-white transition-colors border border-white/5" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
i class="ph ph-stop-circle text-xl text-rose-400" {}
|
||||||
|
span class="font-medium inline-block" { "Stop Current Scan" }
|
||||||
|
}
|
||||||
|
span class="text-xs bg-black/30 px-2 py-1 rounded text-slate-400" { "Idle" }
|
||||||
|
}
|
||||||
|
|
||||||
|
button class="w-full flex items-center justify-between p-4 rounded-lg bg-slate-800 hover:bg-slate-700 text-white transition-colors border border-white/5" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
i class="ph ph-broom text-xl text-amber-400" {}
|
||||||
|
span class="font-medium inline-block" { "Clear Image Cache" }
|
||||||
|
}
|
||||||
|
span class="text-xs text-slate-400" { "442 MB" }
|
||||||
|
}
|
||||||
|
|
||||||
|
hr class="border-white/5 my-2" {}
|
||||||
|
|
||||||
|
button class="w-full flex items-center justify-between p-4 rounded-lg bg-rose-900/40 hover:bg-rose-900/80 text-rose-200 transition-colors border border-rose-500/20" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
i class="ph ph-power text-xl" {}
|
||||||
|
span class="font-medium inline-block" { "Restart Server" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
109
src/admin/views/jobs.rs
Normal file
109
src/admin/views/jobs.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn jobs() -> Markup {
|
||||||
|
layout("Background Jobs", html! {
|
||||||
|
div class="flex justify-between items-center mb-6" {
|
||||||
|
div {
|
||||||
|
h2 class="text-lg font-medium text-white" { "Job Queue" }
|
||||||
|
p class="text-sm text-slate-400 mt-1" { "Monitor long-running tasks and scanners." }
|
||||||
|
}
|
||||||
|
button class="text-slate-300 hover:text-white transition-colors bg-black/30 p-2 rounded-lg border border-white/5 flex items-center gap-2" title="Refresh" {
|
||||||
|
i class="ph ph-arrows-clockwise text-lg" {} "Refresh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="p-6 space-y-4" {
|
||||||
|
|
||||||
|
// Active Job
|
||||||
|
div class="p-4 rounded-xl border border-primary-500/30 bg-primary-900/10 flex flex-col md:flex-row gap-4 items-center" {
|
||||||
|
div class="w-12 h-12 rounded-full bg-primary-500/20 text-primary-400 flex items-center justify-center shrink-0 border border-primary-500/30" {
|
||||||
|
i class="ph ph-magnifying-glass text-2xl animate-pulse" {}
|
||||||
|
}
|
||||||
|
div class="flex-1 w-full" {
|
||||||
|
div class="flex justify-between items-center mb-2" {
|
||||||
|
h3 class="text-sm font-medium text-primary-200" { "Library Metadata Rescan" }
|
||||||
|
span class="px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-primary-500/20 text-primary-400 border border-primary-500/30" { "Running" }
|
||||||
|
}
|
||||||
|
div class="flex justify-between text-xs text-primary-400/80 mb-1" {
|
||||||
|
span { "Processing /music/Lossless/Pink Floyd..." }
|
||||||
|
span { "45%" }
|
||||||
|
}
|
||||||
|
div class="w-full h-1.5 bg-black/40 rounded-full overflow-hidden border border-white/5" {
|
||||||
|
div class="h-full bg-primary-500 w-[45%] relative overflow-hidden" {
|
||||||
|
div class="absolute inset-0 w-full h-full bg-[linear-gradient(90deg,transparent,rgba(255,255,255,0.4),transparent)] animate-[shimmer_2s_infinite]" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="shrink-0 flex gap-2 w-full md:w-auto mt-4 md:mt-0" {
|
||||||
|
button class="w-full md:w-auto px-4 py-2 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-semibold transition-colors border border-rose-500/20" { "Cancel" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queued Job
|
||||||
|
div class="p-4 rounded-xl border border-white/5 bg-black/20 flex flex-col md:flex-row gap-4 items-center opacity-75 grayscale-[30%]" {
|
||||||
|
div class="w-12 h-12 rounded-full bg-slate-800 text-slate-400 flex items-center justify-center shrink-0 border border-white/10" {
|
||||||
|
i class="ph ph-image-square text-2xl" {}
|
||||||
|
}
|
||||||
|
div class="flex-1 w-full" {
|
||||||
|
div class="flex justify-between items-center" {
|
||||||
|
div {
|
||||||
|
h3 class="text-sm font-medium text-slate-200" { "Download Missing Artwork" }
|
||||||
|
p class="text-xs text-slate-400 mt-0.5" { "Queued behind Library Scan" }
|
||||||
|
}
|
||||||
|
span class="px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-slate-800 text-slate-400 border border-white/10" { "Pending" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="shrink-0 flex gap-2 w-full md:w-auto mt-4 md:mt-0" {
|
||||||
|
button class="w-full md:w-auto px-4 py-2 rounded-lg bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 text-xs font-semibold transition-colors border border-rose-500/20" { "Remove" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Completed Job
|
||||||
|
div class="p-4 rounded-xl border border-white/5 bg-black/20 flex flex-col md:flex-row gap-4 items-center" {
|
||||||
|
div class="w-12 h-12 rounded-full bg-emerald-500/10 text-emerald-400 flex items-center justify-center shrink-0 border border-emerald-500/20" {
|
||||||
|
i class="ph-fill ph-check-circle text-2xl" {}
|
||||||
|
}
|
||||||
|
div class="flex-1 w-full" {
|
||||||
|
div class="flex justify-between items-center" {
|
||||||
|
div {
|
||||||
|
h3 class="text-sm font-medium text-slate-200" { "Daily Database Backup" }
|
||||||
|
p class="text-xs text-slate-400 mt-0.5" { "Completed in 1.4s at 03:00 AM" }
|
||||||
|
}
|
||||||
|
span class="px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-emerald-500/10 text-emerald-400 border border-emerald-500/20" { "Success" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed Job
|
||||||
|
div class="p-4 rounded-xl border border-rose-500/20 bg-rose-500/5 flex flex-col md:flex-row gap-4 items-center" {
|
||||||
|
div class="w-12 h-12 rounded-full bg-rose-500/10 text-rose-400 flex items-center justify-center shrink-0 border border-rose-500/30" {
|
||||||
|
i class="ph-fill ph-warning-circle text-2xl" {}
|
||||||
|
}
|
||||||
|
div class="flex-1 w-full" {
|
||||||
|
div class="flex justify-between items-center" {
|
||||||
|
div {
|
||||||
|
h3 class="text-sm font-medium text-rose-200" { "Generate Waveforms" }
|
||||||
|
p class="text-xs text-rose-400/80 mt-0.5" { "Failed: Error running FFmpeg command on Track14.flac" }
|
||||||
|
}
|
||||||
|
span class="px-2 py-0.5 rounded text-[10px] uppercase font-bold tracking-wider bg-rose-500/20 text-rose-400 border border-rose-500/30" { "Failed" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="shrink-0 flex gap-2 w-full md:w-auto mt-4 md:mt-0" {
|
||||||
|
button class="w-full md:w-auto px-4 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold transition-colors border border-white/5" { "Retry" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style {
|
||||||
|
r#"
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
156
src/admin/views/library.rs
Normal file
156
src/admin/views/library.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn library() -> Markup {
|
||||||
|
layout("Library Management", html! {
|
||||||
|
div class="flex flex-col gap-6" {
|
||||||
|
|
||||||
|
// Library Paths
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 flex items-center justify-between bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-hard-drives text-primary-400" {} "Media Folders"
|
||||||
|
}
|
||||||
|
button class="bg-slate-800 hover:bg-slate-700 text-slate-300 border border-white/5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors flex items-center gap-2" {
|
||||||
|
i class="ph ph-plus" {} "Add Directory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 overflow-x-auto" {
|
||||||
|
table class="w-full text-left" {
|
||||||
|
thead class="text-xs uppercase tracking-wider text-slate-500 border-b border-white/5" {
|
||||||
|
tr {
|
||||||
|
th class="pb-3 font-semibold" { "Path" }
|
||||||
|
th class="pb-3 font-semibold w-32" { "Scan Mode" }
|
||||||
|
th class="pb-3 font-semibold text-right" { "Actions" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody class="text-sm divide-y divide-white/5" {
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors" {
|
||||||
|
td class="py-4 font-mono text-slate-300 flex items-center gap-3" {
|
||||||
|
i class="ph-fill ph-folder text-amber-500 text-lg" {}
|
||||||
|
"/data/music/FLACs"
|
||||||
|
}
|
||||||
|
td class="py-4" {
|
||||||
|
span class="px-2 py-1 rounded text-xs tracking-wide bg-blue-500/10 text-blue-400 border border-blue-500/20" { "Inotify Watch" }
|
||||||
|
}
|
||||||
|
td class="py-4 text-right space-x-2" {
|
||||||
|
button class="text-slate-400 hover:text-primary-400 transition-colors bg-black/30 p-2 rounded-md border border-white/5" title="Rescan immediately" { i class="ph ph-arrows-clockwise text-lg" {} }
|
||||||
|
button class="text-slate-400 hover:text-rose-400 transition-colors bg-black/30 p-2 rounded-md border border-white/5" title="Remove" { i class="ph ph-trash text-lg" {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors" {
|
||||||
|
td class="py-4 font-mono text-slate-300 flex items-center gap-3" {
|
||||||
|
i class="ph-fill ph-folder text-amber-500 text-lg" {}
|
||||||
|
"/mnt/external_hdd/MP3s"
|
||||||
|
}
|
||||||
|
td class="py-4" {
|
||||||
|
span class="px-2 py-1 rounded text-xs tracking-wide bg-slate-800 text-slate-400 border border-white/10" { "Scheduled" }
|
||||||
|
}
|
||||||
|
td class="py-4 text-right space-x-2" {
|
||||||
|
button class="text-slate-400 hover:text-primary-400 transition-colors bg-black/30 p-2 rounded-md border border-white/5" title="Rescan immediately" { i class="ph ph-arrows-clockwise text-lg" {} }
|
||||||
|
button class="text-slate-400 hover:text-rose-400 transition-colors bg-black/30 p-2 rounded-md border border-white/5" title="Remove" { i class="ph ph-trash text-lg" {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="grid grid-cols-1 lg:grid-cols-2 gap-6" {
|
||||||
|
// Scan Controls
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden flex flex-col" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-scan text-purple-400" {} "Scan Engine"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 flex-1 space-y-6" {
|
||||||
|
|
||||||
|
div class="space-y-3" {
|
||||||
|
button class="w-full flex items-center justify-between p-4 rounded-lg bg-primary-600/20 hover:bg-primary-600/40 text-primary-300 transition-colors border border-primary-500/30 group" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
i class="ph ph-magnifying-glass text-xl group-hover:scale-110 transition-transform" {}
|
||||||
|
div class="text-left" {
|
||||||
|
p class="font-medium text-primary-100" { "Full Metadata Scan" }
|
||||||
|
p class="text-xs text-primary-400/70" { "Scans all files and applies new tag modifications." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i class="ph ph-caret-right text-lg opacity-50" {}
|
||||||
|
}
|
||||||
|
|
||||||
|
button class="w-full flex items-center justify-between p-4 rounded-lg bg-emerald-600/20 hover:bg-emerald-600/40 text-emerald-300 transition-colors border border-emerald-500/30 group" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
i class="ph ph-music-notes-plus text-xl group-hover:scale-110 transition-transform" {}
|
||||||
|
div class="text-left" {
|
||||||
|
p class="font-medium text-emerald-100" { "Quick Scan" }
|
||||||
|
p class="text-xs text-emerald-400/70" { "Only scans for newly added or deleted files based on timestamps." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i class="ph ph-caret-right text-lg opacity-50" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr class="border-white/5" {}
|
||||||
|
|
||||||
|
div class="space-y-4" {
|
||||||
|
p class="text-sm font-medium text-slate-300 mb-2" { "Scan Scheduling" }
|
||||||
|
label class="flex items-center gap-3 text-sm text-slate-400" {
|
||||||
|
input type="checkbox" checked class="w-4 h-4 rounded bg-slate-800 border-white/20 text-primary-500 focus:ring-primary-500/50" {}
|
||||||
|
"Enable file system watchers (inotify) for instant updates"
|
||||||
|
}
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
label class="text-sm text-slate-400" { "Full scan interval:" }
|
||||||
|
select class="bg-black/30 border border-white/10 rounded border-white/10 px-3 py-1 text-sm text-white outline-none focus:border-primary-500" {
|
||||||
|
option value="0" { "Never" }
|
||||||
|
option value="24" selected { "Daily (at 3 AM)" }
|
||||||
|
option value="168" { "Weekly" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata Tools
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden flex flex-col" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-wrench text-amber-400" {} "Maintenance Tools"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 flex-1 space-y-4" {
|
||||||
|
|
||||||
|
div class="p-4 rounded-lg border border-rose-500/20 bg-rose-500/5 flex gap-4" {
|
||||||
|
div class="mt-0.5" { i class="ph ph-warning-circle text-xl text-rose-400" {} }
|
||||||
|
div {
|
||||||
|
h3 class="text-sm font-medium text-rose-200" { "Clean Database Orphan Links" }
|
||||||
|
p class="text-xs text-rose-400/70 mt-1 mb-3" { "Removes database entries for files that no longer exist on the filesystem. Use with caution." }
|
||||||
|
button class="px-4 py-1.5 rounded bg-rose-600/80 hover:bg-rose-500 text-white text-xs font-semibold uppercase tracking-wider transition-colors" { "Clean Orphans" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="p-4 rounded-lg border border-white/5 bg-black/20 flex gap-4" {
|
||||||
|
div class="mt-0.5" { i class="ph ph-image-square text-xl text-slate-400" {} }
|
||||||
|
div {
|
||||||
|
h3 class="text-sm font-medium text-slate-200" { "Rebuild Album Art Cache" }
|
||||||
|
p class="text-xs text-slate-500 mt-1 mb-3" { "Forces regeneration of thumbnail images and clears missing artwork cache." }
|
||||||
|
button class="px-4 py-1.5 rounded bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold uppercase tracking-wider transition-colors border border-white/5" { "Rebuild Art" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="p-4 rounded-lg border border-white/5 bg-black/20 flex gap-4" {
|
||||||
|
div class="mt-0.5" { i class="ph ph-waveform text-xl text-slate-400" {} }
|
||||||
|
div {
|
||||||
|
h3 class="text-sm font-medium text-slate-200" { "Generate Waveforms" }
|
||||||
|
p class="text-xs text-slate-500 mt-1 mb-3 flex items-center gap-2" {
|
||||||
|
"Pre-computes audio waveform files for faster sought-scrubbing. This is very CPU intensive."
|
||||||
|
span class="px-1.5 py-0.5 bg-indigo-500/20 border border-indigo-500/30 text-[10px] uppercase font-bold text-indigo-400 rounded" {"Coming Soon"}
|
||||||
|
}
|
||||||
|
button disabled class="px-4 py-1.5 rounded bg-slate-800/50 text-slate-600 text-xs font-semibold uppercase tracking-wider pointer-events-none border border-white/5" { "Generate" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
114
src/admin/views/login.rs
Normal file
114
src/admin/views/login.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use maud::{html, Markup, DOCTYPE};
|
||||||
|
use axum::{Form, response::Redirect};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LoginPayload {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_form() -> Markup {
|
||||||
|
render_login(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_post(Form(payload): Form<LoginPayload>) -> Result<Redirect, Markup> {
|
||||||
|
if payload.username == "admin" && payload.password == "admin" {
|
||||||
|
Ok(Redirect::to("/admin"))
|
||||||
|
} else {
|
||||||
|
Err(render_login(Some("Invalid username or password.")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_login(error: Option<&str>) -> Markup {
|
||||||
|
html! {
|
||||||
|
(DOCTYPE)
|
||||||
|
html lang="en" {
|
||||||
|
head {
|
||||||
|
meta charset="utf-8";
|
||||||
|
meta name="viewport" content="width=device-width, initial-scale=1";
|
||||||
|
title { "SoundSonic Admin - Login" }
|
||||||
|
script src="https://cdn.tailwindcss.com" {}
|
||||||
|
script {
|
||||||
|
(maud::PreEscaped(r##"
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a","950":"#172554"}
|
||||||
|
},
|
||||||
|
fontFamily: { sans: ['Inter', 'sans-serif'] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"##))
|
||||||
|
}
|
||||||
|
link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet";
|
||||||
|
script src="https://unpkg.com/@phosphor-icons/web" {}
|
||||||
|
style {
|
||||||
|
(maud::PreEscaped(r##"
|
||||||
|
body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #f8fafc; }
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(30, 41, 59, 0.6);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
"##))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body class="min-h-screen flex items-center justify-center relative overflow-hidden antialiased selection:bg-primary-500 selection:text-white" {
|
||||||
|
// Background Glows
|
||||||
|
div class="absolute top-[20%] left-[20%] w-[30%] h-[30%] rounded-full bg-primary-900/30 blur-[120px] pointer-events-none" {}
|
||||||
|
div class="absolute bottom-[20%] right-[20%] w-[30%] h-[30%] rounded-full bg-blue-900/20 blur-[120px] pointer-events-none" {}
|
||||||
|
|
||||||
|
div class="w-full max-w-sm glass-card rounded-2xl p-8 z-10 relative" {
|
||||||
|
|
||||||
|
div class="flex flex-col items-center mb-8" {
|
||||||
|
div class="w-16 h-16 rounded-full bg-gradient-to-tr from-primary-500 to-indigo-600 flex items-center justify-center border-4 border-black/40 shadow-xl shadow-primary-500/20 mb-4" {
|
||||||
|
i class="ph-fill ph-waves text-3xl text-white" {}
|
||||||
|
}
|
||||||
|
h1 class="text-2xl font-bold text-white tracking-tight" { "SoundSonic Admin" }
|
||||||
|
p class="text-slate-400 text-sm mt-1" { "Sign in to access control center" }
|
||||||
|
}
|
||||||
|
|
||||||
|
@if let Some(msg) = error {
|
||||||
|
div class="bg-rose-500/10 border border-rose-500/20 rounded-lg p-3 mb-6 flex items-start gap-3" {
|
||||||
|
i class="ph-fill ph-warning-circle text-rose-400 text-lg mt-0.5" {}
|
||||||
|
p class="text-sm text-rose-200" { (msg) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form action="/admin/login" method="POST" class="space-y-5" {
|
||||||
|
div {
|
||||||
|
label for="username" class="block text-sm font-medium text-slate-300 mb-1.5" { "Username" }
|
||||||
|
div class="relative" {
|
||||||
|
div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" {
|
||||||
|
i class="ph ph-user text-slate-500 text-lg" {}
|
||||||
|
}
|
||||||
|
input type="text" name="username" id="username" required class="block w-full pl-10 pr-4 py-2.5 bg-black/40 border border-white/10 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all sm:text-sm" placeholder="admin" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
div class="flex justify-between items-center mb-1.5" {
|
||||||
|
label for="password" class="block text-sm font-medium text-slate-300" { "Password" }
|
||||||
|
}
|
||||||
|
div class="relative" {
|
||||||
|
div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" {
|
||||||
|
i class="ph ph-lock-key text-slate-500 text-lg" {}
|
||||||
|
}
|
||||||
|
input type="password" name="password" id="password" required class="block w-full pl-10 pr-4 py-2.5 bg-black/40 border border-white/10 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all sm:text-sm" placeholder="••••••••" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button type="submit" class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm shadow-primary-900/20 text-sm font-medium text-white bg-primary-600 hover:bg-primary-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors mt-2" {
|
||||||
|
"Sign In"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/admin/views/logs.rs
Normal file
135
src/admin/views/logs.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn logs() -> Markup {
|
||||||
|
layout("Logs & Diagnostics", html! {
|
||||||
|
div class="flex flex-col gap-6 h-[calc(100vh-12rem)]" {
|
||||||
|
|
||||||
|
// Top Controls Row
|
||||||
|
div class="flex flex-col md:flex-row gap-4" {
|
||||||
|
// Filters
|
||||||
|
div class="glass-card rounded-xl border border-white/5 p-4 flex-1 flex flex-wrap gap-4 items-center" {
|
||||||
|
div class="flex items-center gap-2" {
|
||||||
|
i class="ph ph-funnel text-slate-400" {}
|
||||||
|
span class="text-sm font-medium text-slate-300" { "Filters:" }
|
||||||
|
}
|
||||||
|
|
||||||
|
select class="bg-black/30 border border-white/10 rounded-lg px-3 py-1.5 text-slate-300 focus:outline-none focus:border-primary-500 transition-all text-sm appearance-none min-w-[120px]" {
|
||||||
|
option value="all" { "All Levels" }
|
||||||
|
option value="info" { "INFO only" }
|
||||||
|
option value="warn" { "WARN & Above" }
|
||||||
|
option value="error" { "ERROR only" }
|
||||||
|
}
|
||||||
|
|
||||||
|
input type="text" placeholder="Search logs..." class="bg-black/30 border border-white/10 rounded-lg px-3 py-1.5 text-white placeholder-slate-500 focus:outline-none focus:border-primary-500 transition-all text-sm min-w-[200px]" {}
|
||||||
|
|
||||||
|
div class="flex-1" {} // spacer
|
||||||
|
|
||||||
|
button class="bg-slate-800 hover:bg-slate-700 text-slate-300 border border-white/5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2" {
|
||||||
|
i class="ph ph-download-simple" {} "Download Server Log"
|
||||||
|
}
|
||||||
|
button class="bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 border border-rose-500/20 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2" {
|
||||||
|
i class="ph ph-trash" {} "Clear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout Split: Log Viewer | Active Sessions
|
||||||
|
div class="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 min-h-0" {
|
||||||
|
|
||||||
|
// Log Viewer Panel (takes 2 cols)
|
||||||
|
div class="lg:col-span-2 glass-card rounded-xl border border-white/5 flex flex-col overflow-hidden" {
|
||||||
|
div class="px-4 py-3 border-b border-white/5 bg-black/40 flex justify-between items-center" {
|
||||||
|
h3 class="text-sm font-medium text-slate-300 flex items-center gap-2" {
|
||||||
|
i class="ph ph-terminal-window text-emerald-400" {} "Live Tail"
|
||||||
|
}
|
||||||
|
div class="flex items-center gap-2" {
|
||||||
|
span class="relative flex h-2 w-2" {
|
||||||
|
span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" {}
|
||||||
|
span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" {}
|
||||||
|
}
|
||||||
|
span class="text-xs text-slate-500 font-medium tracking-wide uppercase" { "Auto-scrolling" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="flex-1 p-4 bg-[#0a0f18] text-sm font-mono overflow-y-auto" {
|
||||||
|
div class="space-y-1" {
|
||||||
|
(log_line("14:02:11", "INFO", "text-blue-400", "Starting Subsonic API listener on 0.0.0.0:3311"))
|
||||||
|
(log_line("14:02:12", "INFO", "text-blue-400", "Connecting to SQLite database pool..."))
|
||||||
|
(log_line("14:02:12", "INFO", "text-blue-400", "Running pending migrations..."))
|
||||||
|
(log_line("14:05:43", "WARN", "text-amber-400", "Client 'DSub/5.4' requested unsupported downsample bitrates. Falling back to 128kbps."))
|
||||||
|
(log_line("14:09:01", "ERROR", "text-rose-400", "Failed to lookup LastFM metadata for artist 'Unknown': API timeout"))
|
||||||
|
(log_line("14:15:22", "INFO", "text-blue-400", "User 'admin' logged in from 192.168.1.5"))
|
||||||
|
(log_line("14:15:24", "INFO", "text-blue-400", "GET /rest/ping.view [200 OK] 4ms"))
|
||||||
|
(log_line("14:16:00", "INFO", "text-blue-400", "GET /rest/getIndexes.view [200 OK] 24ms"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active Sessions Panel
|
||||||
|
div class="glass-card rounded-xl border border-white/5 flex flex-col overflow-hidden" {
|
||||||
|
div class="px-4 py-3 border-b border-white/5 bg-black/40 flex justify-between items-center" {
|
||||||
|
h3 class="text-sm font-medium text-slate-300 flex items-center gap-2" {
|
||||||
|
i class="ph ph-users-three text-primary-400" {} "Active Sessions"
|
||||||
|
}
|
||||||
|
span class="bg-primary-500/20 text-primary-400 text-xs font-bold px-2 py-0.5 rounded-full" { "2" }
|
||||||
|
}
|
||||||
|
div class="flex-1 overflow-y-auto divide-y divide-white/5" {
|
||||||
|
|
||||||
|
// Session item
|
||||||
|
div class="p-4 hover:bg-white/[0.02] transition-colors" {
|
||||||
|
div class="flex justify-between items-start mb-2" {
|
||||||
|
div class="flex items-center gap-2" {
|
||||||
|
div class="w-2 h-2 rounded-full bg-emerald-500" {}
|
||||||
|
span class="font-medium text-sm text-slate-200" { "admin" }
|
||||||
|
}
|
||||||
|
span class="text-xs text-slate-500" { "10m ago" }
|
||||||
|
}
|
||||||
|
div class="space-y-1 mb-3" {
|
||||||
|
p class="text-xs text-slate-400 flex items-center gap-2" {
|
||||||
|
i class="ph ph-laptop" {} "Web Browser (Chrome)"
|
||||||
|
}
|
||||||
|
p class="text-xs text-slate-400 font-mono" { "192.168.1.5" }
|
||||||
|
}
|
||||||
|
div class="flex gap-2" {
|
||||||
|
button class="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 px-2 py-1.5 rounded text-xs font-medium transition-colors border border-white/5" { "Kill Session" }
|
||||||
|
button class="bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 px-2 py-1.5 rounded text-xs font-medium transition-colors border border-rose-500/20" title="Ban IP" { i class="ph ph-prohibit" {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session item 2
|
||||||
|
div class="p-4 hover:bg-white/[0.02] transition-colors" {
|
||||||
|
div class="flex justify-between items-start mb-2" {
|
||||||
|
div class="flex items-center gap-2" {
|
||||||
|
div class="w-2 h-2 rounded-full bg-emerald-500" {}
|
||||||
|
span class="font-medium text-sm text-slate-200" { "guest" }
|
||||||
|
}
|
||||||
|
span class="text-xs text-slate-500" { "Just now" }
|
||||||
|
}
|
||||||
|
div class="space-y-1 mb-3" {
|
||||||
|
p class="text-xs text-slate-400 flex items-center gap-2" {
|
||||||
|
i class="ph ph-device-mobile" {} "DSub / 5.4.3"
|
||||||
|
}
|
||||||
|
p class="text-xs text-slate-400 font-mono" { "10.0.0.42" }
|
||||||
|
}
|
||||||
|
div class="flex gap-2" {
|
||||||
|
button class="flex-1 bg-slate-800 hover:bg-slate-700 text-slate-300 px-2 py-1.5 rounded text-xs font-medium transition-colors border border-white/5" { "Kill Session" }
|
||||||
|
button class="bg-rose-500/10 hover:bg-rose-500/20 text-rose-400 px-2 py-1.5 rounded text-xs font-medium transition-colors border border-rose-500/20" title="Ban IP" { i class="ph ph-prohibit" {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_line(time: &str, level: &str, level_color: &str, msg: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
div class="flex gap-3 py-0.5 hover:bg-white/5 rounded px-1 transition-colors" {
|
||||||
|
span class="text-slate-500 shrink-0 w-20 text-xs" { (time) }
|
||||||
|
span class=(format!("shrink-0 w-12 text-xs font-bold {}", level_color)) { (level) }
|
||||||
|
span class="text-slate-300 break-all" { (msg) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/admin/views/mod.rs
Normal file
10
src/admin/views/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
pub mod about;
|
||||||
|
pub mod login;
|
||||||
|
pub mod clients;
|
||||||
|
pub mod dashboard;
|
||||||
|
pub mod jobs;
|
||||||
|
pub mod library;
|
||||||
|
pub mod logs;
|
||||||
|
pub mod playback;
|
||||||
|
pub mod settings;
|
||||||
|
pub mod users;
|
||||||
125
src/admin/views/playback.rs
Normal file
125
src/admin/views/playback.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn playback() -> Markup {
|
||||||
|
layout("Playback & Streaming", html! {
|
||||||
|
div class="max-w-4xl mx-auto space-y-6" {
|
||||||
|
|
||||||
|
// Transcoding Settings
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-waveform text-purple-400" {} "Transcoding Rules"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-6 form-group" {
|
||||||
|
|
||||||
|
label class="flex items-center gap-4 cursor-pointer" {
|
||||||
|
div class="relative flex items-center" {
|
||||||
|
input type="checkbox" checked class="peer sr-only" {}
|
||||||
|
div class="w-11 h-6 bg-slate-800 rounded-full border border-white/10 peer-checked:bg-purple-500 peer-checked:border-purple-400 transition-all" {}
|
||||||
|
div class="absolute left-[3px] top-[3px] w-4.5 h-4.5 bg-white rounded-full transition-all peer-checked:translate-x-[20px] shadow-sm" {}
|
||||||
|
}
|
||||||
|
span class="text-sm font-medium text-slate-300" { "Enable Server-side Transcoding" }
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Default Max Bitrate" }
|
||||||
|
select class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all text-sm appearance-none" {
|
||||||
|
option value="0" { "No limit (Original)" }
|
||||||
|
option value="320" selected { "320 kbps" }
|
||||||
|
option value="256" { "256 kbps" }
|
||||||
|
option value="192" { "192 kbps" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Transcode Format Preference" }
|
||||||
|
select class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all text-sm appearance-none" {
|
||||||
|
option value="mp3" { "MP3 (Maximum Compatibility)" }
|
||||||
|
option value="opus" selected { "Opus (Best Quality/Size ratio)" }
|
||||||
|
option value="aac" { "AAC (Apple Devices)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streaming Settings
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-broadcast text-sky-400" {} "Streaming Buffers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-6" {
|
||||||
|
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-3 gap-6" {
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Chunk Size"
|
||||||
|
span class="text-xs text-slate-500" { "MB" }
|
||||||
|
}
|
||||||
|
input type="number" value="1" min="1" max="10" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Buffer Threshold"
|
||||||
|
span class="text-xs text-slate-500" { "Seconds" }
|
||||||
|
}
|
||||||
|
input type="number" value="10" min="5" max="30" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Preload Next Track"
|
||||||
|
span class="text-xs text-slate-500" { "Seconds" }
|
||||||
|
}
|
||||||
|
input type="number" value="15" min="0" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Compatibility
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-plugs-connected text-amber-400" {} "Subsonic API Compatibility"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-4" {
|
||||||
|
div class="flex items-center gap-3 bg-amber-500/10 border border-amber-500/20 p-4 rounded-lg" {
|
||||||
|
i class="ph ph-info text-amber-500 text-xl shrink-0" {}
|
||||||
|
p class="text-sm text-amber-200/80" { "Changes here affect how third-party apps like DSub, Play:Sub, or Ultrasonic parse server responses." }
|
||||||
|
}
|
||||||
|
|
||||||
|
label class="flex items-center gap-4 cursor-pointer mt-4" {
|
||||||
|
div class="relative flex items-center" {
|
||||||
|
input type="checkbox" class="peer sr-only" {}
|
||||||
|
div class="w-11 h-6 bg-slate-800 rounded-full border border-white/10 peer-checked:bg-primary-500 peer-checked:border-primary-400 transition-all" {}
|
||||||
|
div class="absolute left-[3px] top-[3px] w-4.5 h-4.5 bg-white rounded-full transition-all peer-checked:translate-x-[20px] shadow-sm" {}
|
||||||
|
}
|
||||||
|
span class="text-sm font-medium text-slate-300" { "Strict API Mode" }
|
||||||
|
}
|
||||||
|
p class="text-xs text-slate-500 ml-15 pl-1" { "When enabled, drops unsupported legacy fields from JSON responses. May break older clients." }
|
||||||
|
|
||||||
|
div class="mt-4" {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Reported API Version" }
|
||||||
|
select class="w-full md:w-1/2 bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm appearance-none" {
|
||||||
|
option value="1.16.1" selected { "OpenSubsonic 1.16.1 (Modern)" }
|
||||||
|
option value="1.15.0" { "Subsonic 1.15.0 (Legacy Default)" }
|
||||||
|
option value="1.13.0" { "Subsonic 1.13.0 (Old Clients)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="flex justify-end" {
|
||||||
|
button type="button" class="bg-primary-600 hover:bg-primary-500 text-white px-6 py-2.5 rounded-lg text-sm font-medium transition-colors shadow-lg shadow-primary-900/20 flex items-center gap-2" {
|
||||||
|
i class="ph ph-floppy-disk text-lg" {} "Save Playback Options"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
120
src/admin/views/settings.rs
Normal file
120
src/admin/views/settings.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn settings() -> Markup {
|
||||||
|
layout("System Settings", html! {
|
||||||
|
div class="max-w-4xl mx-auto space-y-6" {
|
||||||
|
|
||||||
|
// Server Network Base Settings
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-globe text-primary-400" {} "Network & Domain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-6" {
|
||||||
|
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
|
||||||
|
div class="md:col-span-2" {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Public Server URL" }
|
||||||
|
input type="url" value="https://music.home.arpa" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm font-mono" {}
|
||||||
|
p class="text-xs text-slate-500 mt-1.5" { "Used for generating share links and podcast enclosures." }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Bind Port" }
|
||||||
|
input type="number" value="3311" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm font-mono" {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="flex items-end pb-2" {
|
||||||
|
label class="flex items-center gap-4 cursor-pointer" {
|
||||||
|
div class="relative flex items-center" {
|
||||||
|
input type="checkbox" checked class="peer sr-only" {}
|
||||||
|
div class="w-11 h-6 bg-slate-800 rounded-full border border-white/10 peer-checked:bg-primary-500 peer-checked:border-primary-400 transition-all" {}
|
||||||
|
div class="absolute left-[3px] top-[3px] w-4.5 h-4.5 bg-white rounded-full transition-all peer-checked:translate-x-[20px] shadow-sm" {}
|
||||||
|
}
|
||||||
|
span class="text-sm font-medium text-slate-300" { "Trust Proxy Headers (X-Forwarded-For)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security Settings
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-shield-check text-rose-400" {} "Security Limits"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-6" {
|
||||||
|
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
|
||||||
|
div class="flex flex-col justify-center" {
|
||||||
|
label class="flex items-center gap-4 cursor-pointer" {
|
||||||
|
div class="relative flex items-center" {
|
||||||
|
input type="checkbox" class="peer sr-only" {}
|
||||||
|
div class="w-11 h-6 bg-slate-800 rounded-full border border-white/10 peer-checked:bg-rose-500 peer-checked:border-rose-400 transition-all" {}
|
||||||
|
div class="absolute left-[3px] top-[3px] w-4.5 h-4.5 bg-white rounded-full transition-all peer-checked:translate-x-[20px] shadow-sm" {}
|
||||||
|
}
|
||||||
|
span class="text-sm font-medium text-slate-300" { "Allow Public Registration" }
|
||||||
|
}
|
||||||
|
p class="text-xs text-slate-500 ml-15 pl-1 mt-1" { "Users can self-provision accounts without an invite." }
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Session Timeout"
|
||||||
|
span class="text-xs text-slate-500" { "Days" }
|
||||||
|
}
|
||||||
|
input type="number" value="30" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Failed Login Threshold" }
|
||||||
|
input type="number" value="5" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Lockout Duration"
|
||||||
|
span class="text-xs text-slate-500" { "Minutes" }
|
||||||
|
}
|
||||||
|
input type="number" value="15" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage Locations
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="px-6 py-4 border-b border-white/5 bg-black/20" {
|
||||||
|
h2 class="text-lg font-medium text-white flex items-center gap-2" {
|
||||||
|
i class="ph ph-database text-emerald-400" {} "Internal Storage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div class="p-6 space-y-6" {
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Temp Directory" }
|
||||||
|
input type="text" value="/tmp/soundsonic" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all font-mono text-sm" {}
|
||||||
|
p class="text-xs text-slate-500 mt-1.5" { "Used for uploading chunks, transcoding frames, and zipping downloads." }
|
||||||
|
}
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Artwork Cache Size Limit"
|
||||||
|
span class="text-xs text-slate-500" { "GB" }
|
||||||
|
}
|
||||||
|
input type="number" value="1" step="0.5" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500 transition-all text-sm" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="flex justify-end gap-4" {
|
||||||
|
button type="button" class="bg-primary-600 hover:bg-primary-500 text-white px-6 py-2.5 rounded-lg text-sm font-medium transition-colors shadow-lg shadow-primary-900/20 flex items-center gap-2" {
|
||||||
|
i class="ph ph-floppy-disk text-lg" {} "Save Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
243
src/admin/views/users.rs
Normal file
243
src/admin/views/users.rs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
|
use crate::admin::layout::layout;
|
||||||
|
|
||||||
|
pub async fn users() -> Markup {
|
||||||
|
layout("Users Management", html! {
|
||||||
|
div class="flex justify-between items-center mb-6" {
|
||||||
|
div {
|
||||||
|
h2 class="text-lg font-medium text-white" { "All Users" }
|
||||||
|
p class="text-sm text-slate-400 mt-1" { "Manage server access, roles, and capabilities." }
|
||||||
|
}
|
||||||
|
a href="/admin/users/new" class="bg-primary-600 hover:bg-primary-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors shadow-lg shadow-primary-900/20 flex items-center gap-2" {
|
||||||
|
i class="ph ph-plus-circle text-lg" {}
|
||||||
|
"Create User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="glass-card rounded-xl border border-white/5 overflow-hidden" {
|
||||||
|
div class="overflow-x-auto" {
|
||||||
|
table class="w-full text-left border-collapse" {
|
||||||
|
thead class="bg-black/30 text-xs uppercase tracking-wider text-slate-400" {
|
||||||
|
tr {
|
||||||
|
th class="px-6 py-4 font-semibold" { "User" }
|
||||||
|
th class="px-6 py-4 font-semibold" { "Roles & Access" }
|
||||||
|
th class="px-6 py-4 font-semibold" { "Last Seen" }
|
||||||
|
th class="px-6 py-4 font-semibold w-24" { "Status" }
|
||||||
|
th class="px-6 py-4 font-semibold text-right" { "Actions" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody class="divide-y divide-white/5 text-sm" {
|
||||||
|
// Admin User Row
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors" {
|
||||||
|
td class="px-6 py-4" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
div class="w-8 h-8 rounded-full bg-gradient-to-tr from-primary-500 to-primary-700 flex items-center justify-center text-xs font-bold shadow-lg shadow-primary-500/20" { "A" }
|
||||||
|
div {
|
||||||
|
div class="font-medium text-white flex items-center gap-2" {
|
||||||
|
"admin"
|
||||||
|
i class="ph-fill ph-check-circle text-emerald-400 text-xs" title="Verified" {}
|
||||||
|
}
|
||||||
|
div class="text-xs text-slate-500 mt-0.5" { "admin@local" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td class="px-6 py-4" {
|
||||||
|
div class="flex flex-wrap gap-2" {
|
||||||
|
span class="px-2 py-0.5 rounded bg-amber-500/20 text-amber-400 border border-amber-500/30 text-[10px] font-bold tracking-wider" { "ADMIN" }
|
||||||
|
span class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 border border-white/10 text-[10px] uppercase font-medium tracking-wider" { "Stream" }
|
||||||
|
span class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 border border-white/10 text-[10px] uppercase font-medium tracking-wider" { "Download" }
|
||||||
|
span class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 border border-white/10 text-[10px] uppercase font-medium tracking-wider" { "Upload" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td class="px-6 py-4 text-slate-400 text-xs" {
|
||||||
|
"Active now"
|
||||||
|
}
|
||||||
|
td class="px-6 py-4" {
|
||||||
|
span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20" {
|
||||||
|
div class="w-1.5 h-1.5 rounded-full bg-emerald-400" {} "Active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td class="px-6 py-4 text-right space-x-3 text-lg font-medium" {
|
||||||
|
a href="/admin/users/1/edit" class="text-slate-400 hover:text-primary-400 transition-colors" title="Edit User" { i class="ph ph-note-pencil" {} }
|
||||||
|
button class="text-slate-400 hover:text-amber-400 transition-colors" title="Impersonate" { i class="ph ph-mask-happy" {} }
|
||||||
|
button class="text-slate-400 hover:text-rose-400 transition-colors" title="Disable" { i class="ph ph-prohibit" {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guest User Row
|
||||||
|
tr class="hover:bg-white/[0.02] transition-colors" {
|
||||||
|
td class="px-6 py-4" {
|
||||||
|
div class="flex items-center gap-3" {
|
||||||
|
div class="w-8 h-8 rounded-full bg-slate-800 border border-white/10 flex items-center justify-center text-xs font-bold text-slate-400" { "G" }
|
||||||
|
div {
|
||||||
|
div class="font-medium text-white" { "guest" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td class="px-6 py-4" {
|
||||||
|
div class="flex flex-wrap gap-2" {
|
||||||
|
span class="px-2 py-0.5 rounded bg-slate-800 text-slate-300 border border-white/10 text-[10px] uppercase font-medium tracking-wider" { "Stream" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td class="px-6 py-4 text-slate-400 text-xs" {
|
||||||
|
"2 days ago"
|
||||||
|
}
|
||||||
|
td class="px-6 py-4" {
|
||||||
|
span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-slate-800 text-slate-400 border border-slate-700" {
|
||||||
|
div class="w-1.5 h-1.5 rounded-full bg-slate-500" {} "Offline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td class="px-6 py-4 text-right space-x-3 text-lg font-medium" {
|
||||||
|
a href="/admin/users/2/edit" class="text-slate-400 hover:text-primary-400 transition-colors" title="Edit User" { i class="ph ph-note-pencil" {} }
|
||||||
|
button class="text-slate-400 hover:text-amber-400 transition-colors" title="Impersonate" { i class="ph ph-mask-happy" {} }
|
||||||
|
button class="text-slate-400 hover:text-rose-400 transition-colors" title="Delete" { i class="ph ph-trash" {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn user_create() -> Markup {
|
||||||
|
user_form_layout("Create New User", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn user_edit() -> Markup {
|
||||||
|
user_form_layout("Edit User: admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_form_layout(title_text: &str, is_new: bool) -> Markup {
|
||||||
|
layout(title_text, html! {
|
||||||
|
div class="max-w-4xl mx-auto" {
|
||||||
|
// Header actions
|
||||||
|
div class="flex items-center gap-4 mb-6" {
|
||||||
|
a href="/admin/users" class="p-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 transition-colors border border-white/5" {
|
||||||
|
i class="ph ph-arrow-left text-lg" {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
h2 class="text-xl font-medium text-white" { (title_text) }
|
||||||
|
p class="text-sm text-slate-400 mt-1" { "Configure account credentials, limits, and server permissions." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form class="space-y-6" {
|
||||||
|
div class="grid grid-cols-1 md:grid-cols-2 gap-6" {
|
||||||
|
|
||||||
|
// Left Column: Credentials & Limits
|
||||||
|
div class="space-y-6 flex flex-col" {
|
||||||
|
div class="glass-card rounded-xl p-6 border border-white/5" {
|
||||||
|
h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 mb-4 pb-3 border-b border-white/5" { "Credentials" }
|
||||||
|
div class="space-y-4" {
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Username" }
|
||||||
|
input type="text" value=(if!is_new{"admin"}else{""}) class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all font-mono text-sm" placeholder="e.g. music_lover" {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" {
|
||||||
|
"Password "
|
||||||
|
@if !is_new { span class="text-xs text-slate-500 font-normal ml-1" { "(Leave blank to keep unchanged)" } }
|
||||||
|
}
|
||||||
|
input type="password" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all text-sm" placeholder="••••••••" {}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5" { "Email (Optional)" }
|
||||||
|
input type="email" value=(if!is_new{"admin@local"}else{""}) class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all text-sm" placeholder="user@domain.com" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="glass-card rounded-xl p-6 border border-white/5 flex-1" {
|
||||||
|
h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 mb-4 pb-3 border-b border-white/5" { "Limits & Quotas" }
|
||||||
|
div class="space-y-4" {
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Max Audio Bitrate"
|
||||||
|
span class="text-slate-500 font-normal text-xs" { "Kbps" }
|
||||||
|
}
|
||||||
|
select class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all text-sm appearance-none" {
|
||||||
|
option value="0" { "Unlimited (Direct Stream)" }
|
||||||
|
option value="320" { "320 kbps (High)" }
|
||||||
|
option value="192" { "192 kbps (Standard)" }
|
||||||
|
option value="128" { "128 kbps (Low)" }
|
||||||
|
option value="64" { "64 kbps (Cellular)" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
label class="block text-sm font-medium text-slate-300 mb-1.5 flex justify-between" {
|
||||||
|
"Storage Quota"
|
||||||
|
span class="text-slate-500 font-normal text-xs" { "GB" }
|
||||||
|
}
|
||||||
|
input type="number" min="0" value="0" class="w-full bg-black/30 border border-white/10 rounded-lg px-4 py-2 text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500/50 focus:border-primary-500 transition-all text-sm" {}
|
||||||
|
p class="text-xs text-slate-500 mt-2" { "Set to 0 for unlimited storage (if Upload is allowed)." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Column: Permissions
|
||||||
|
div class="glass-card rounded-xl p-6 border border-white/5 h-full" {
|
||||||
|
h3 class="text-sm font-semibold uppercase tracking-wider text-slate-400 mb-4 pb-3 border-b border-white/5 flex items-center justify-between" {
|
||||||
|
"Permissions"
|
||||||
|
i class="ph ph-shield-check text-lg text-primary-400" {}
|
||||||
|
}
|
||||||
|
|
||||||
|
div class="space-y-4" {
|
||||||
|
// Admin Toggle
|
||||||
|
label class="flex items-start gap-4 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20 cursor-pointer hover:bg-amber-500/20 transition-colors group" {
|
||||||
|
div class="relative flex items-center" {
|
||||||
|
input type="checkbox" checked[(!is_new)] class="peer sr-only" {}
|
||||||
|
div class="w-11 h-6 bg-slate-800 rounded-full border border-white/10 peer-checked:bg-amber-500 peer-checked:border-amber-400 transition-all" {}
|
||||||
|
div class="absolute left-[3px] top-[3px] w-4.5 h-4.5 bg-white rounded-full transition-all peer-checked:translate-x-[20px] shadow-sm shadow-black/50" {}
|
||||||
|
}
|
||||||
|
div class="flex-1" {
|
||||||
|
p class="text-sm font-medium text-amber-500 group-hover:text-amber-400 transition-colors" { "Administrator Privileges" }
|
||||||
|
p class="text-xs text-amber-500/70 mt-1" { "Grants full access to modify server settings, manage users, and view all system logs." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr class="border-white/5 my-4" {}
|
||||||
|
|
||||||
|
// Capability Toggles
|
||||||
|
(capability_toggle("Stream Media", "Stream music and video", true, "ph-play-circle"))
|
||||||
|
(capability_toggle("Download Files", "Download original media files", true, "ph-download-simple"))
|
||||||
|
(capability_toggle("Upload Media", "Upload files and modify library files", !is_new, "ph-upload-simple"))
|
||||||
|
(capability_toggle("Create Playlists", "Create, edit, and delete playlists", true, "ph-list-dashes"))
|
||||||
|
(capability_toggle("Share Links", "Create public sharing links for media", !is_new, "ph-share-network"))
|
||||||
|
(capability_toggle("Transcoding", "Allow server-side format conversion", true, "ph-waveform"))
|
||||||
|
(capability_toggle("Folder Access", "See raw folder structure not just tags", true, "ph-folder"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer Buttons
|
||||||
|
div class="flex items-center justify-end gap-4 mt-8 pt-6 border-t border-white/5" {
|
||||||
|
a href="/admin/users" class="px-5 py-2.5 rounded-lg text-sm font-medium text-slate-300 hover:text-white hover:bg-white/5 transition-colors" { "Cancel" }
|
||||||
|
button type="button" class="bg-primary-600 hover:bg-primary-500 text-white px-6 py-2.5 rounded-lg text-sm font-medium transition-colors shadow-lg shadow-primary-900/20 flex items-center gap-2" {
|
||||||
|
i class="ph ph-floppy-disk text-lg" {}
|
||||||
|
"Save User"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capability_toggle(title: &str, desc: &str, checked_by_default: bool, icon: &str) -> Markup {
|
||||||
|
html! {
|
||||||
|
label class="flex items-start gap-4 p-3 rounded-lg border border-transparent hover:bg-white/5 hover:border-white/10 cursor-pointer transition-all" {
|
||||||
|
div class="relative flex items-center shrink-0 mt-0.5" {
|
||||||
|
input type="checkbox" checked class="peer w-4 h-4 rounded bg-slate-800 border-white/20 text-primary-500 focus:ring-primary-500/50 appearance-none checked:bg-primary-500 checked:border-primary-500 transition-all relative overflow-hidden flex items-center justify-center
|
||||||
|
after:content-[''] after:absolute after:w-2 after:h-2.5 after:border-b-2 after:border-r-2 after:border-white after:rotate-45 after:-translate-y-0.5 after:opacity-0 peer-checked:after:opacity-100" {}
|
||||||
|
}
|
||||||
|
div class="flex-1 flex gap-3" {
|
||||||
|
i class=(format!("ph {} text-xl text-slate-400 mt-0.5", icon)) {}
|
||||||
|
div {
|
||||||
|
p class="text-sm font-medium text-slate-200" { (title) }
|
||||||
|
p class="text-xs text-slate-500 mt-0.5" { (desc) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod admin;
|
||||||
mod db;
|
mod db;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod schema;
|
mod schema;
|
||||||
@@ -12,6 +13,7 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx
|
|||||||
use crate::routes::system_router::system_routers;
|
use crate::routes::system_router::system_routers;
|
||||||
use crate::routes::user_management_router::user_management_routs;
|
use crate::routes::user_management_router::user_management_routs;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
use crate::admin::routes::admin_router;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -28,6 +30,7 @@ async fn main() {
|
|||||||
.init();
|
.init();
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/rest", system_routers().merge(user_management_routs()))
|
.nest("/rest", system_routers().merge(user_management_routs()))
|
||||||
|
.nest("/admin", admin_router())
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user