diff --git a/Cargo.lock b/Cargo.lock index 89ddaec..37e8df2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,30 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "md5" version = "0.8.0" @@ -567,6 +591,18 @@ dependencies = [ "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]] name = "quote" version = "1.0.44" @@ -770,6 +806,7 @@ dependencies = [ "axum", "diesel", "dotenvy", + "maud", "md5", "serde", "serde_json", @@ -1013,6 +1050,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index c8b223d..43cdf14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ tokio = { version = "1.49.0", features = ["full"] } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } md5 = "0.8.0" tower-http = {version = "0.6.8", features = ["trace"]} +maud = { version = "0.27.0", features = ["axum"] } diff --git a/src/admin/layout.rs b/src/admin/layout.rs new file mode 100644 index 0000000..d49e4ce --- /dev/null +++ b/src/admin/layout.rs @@ -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) + } + } + } + } + } + } +} diff --git a/src/admin/mod.rs b/src/admin/mod.rs new file mode 100644 index 0000000..b292499 --- /dev/null +++ b/src/admin/mod.rs @@ -0,0 +1,3 @@ +pub mod layout; +pub mod routes; +pub mod views; diff --git a/src/admin/routes.rs b/src/admin/routes.rs new file mode 100644 index 0000000..166a242 --- /dev/null +++ b/src/admin/routes.rs @@ -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 { + 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)) +} diff --git a/src/admin/views/about.rs b/src/admin/views/about.rs new file mode 100644 index 0000000..06ecaee --- /dev/null +++ b/src/admin/views/about.rs @@ -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" + } + } + } + } + } + } + }) +} diff --git a/src/admin/views/clients.rs b/src/admin/views/clients.rs new file mode 100644 index 0000000..ff37588 --- /dev/null +++ b/src/admin/views/clients.rs @@ -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" + } + } + } + } + } + } + } + } + }) +} diff --git a/src/admin/views/dashboard.rs b/src/admin/views/dashboard.rs new file mode 100644 index 0000000..aef4a1e --- /dev/null +++ b/src/admin/views/dashboard.rs @@ -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" } + } + } + } + } + } + }) +} diff --git a/src/admin/views/jobs.rs b/src/admin/views/jobs.rs new file mode 100644 index 0000000..063a370 --- /dev/null +++ b/src/admin/views/jobs.rs @@ -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%); } + } + "# + } + }) +} diff --git a/src/admin/views/library.rs b/src/admin/views/library.rs new file mode 100644 index 0000000..f326c39 --- /dev/null +++ b/src/admin/views/library.rs @@ -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" } + } + } + } + } + } + } + }) +} diff --git a/src/admin/views/login.rs b/src/admin/views/login.rs new file mode 100644 index 0000000..6bf899f --- /dev/null +++ b/src/admin/views/login.rs @@ -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) -> Result { + 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" + } + } + } + } + } + } +} diff --git a/src/admin/views/logs.rs b/src/admin/views/logs.rs new file mode 100644 index 0000000..af4c881 --- /dev/null +++ b/src/admin/views/logs.rs @@ -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) } + } + } +} diff --git a/src/admin/views/mod.rs b/src/admin/views/mod.rs new file mode 100644 index 0000000..4970d8a --- /dev/null +++ b/src/admin/views/mod.rs @@ -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; diff --git a/src/admin/views/playback.rs b/src/admin/views/playback.rs new file mode 100644 index 0000000..a8c1185 --- /dev/null +++ b/src/admin/views/playback.rs @@ -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" + } + } + } + }) +} diff --git a/src/admin/views/settings.rs b/src/admin/views/settings.rs new file mode 100644 index 0000000..06fe595 --- /dev/null +++ b/src/admin/views/settings.rs @@ -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" + } + } + } + }) +} diff --git a/src/admin/views/users.rs b/src/admin/views/users.rs new file mode 100644 index 0000000..32ffb73 --- /dev/null +++ b/src/admin/views/users.rs @@ -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) } + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 696d6be..bbcc150 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod admin; mod db; mod routes; mod schema; @@ -12,6 +13,7 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx use crate::routes::system_router::system_routers; use crate::routes::user_management_router::user_management_routs; use crate::state::AppState; +use crate::admin::routes::admin_router; #[tokio::main] async fn main() { @@ -28,6 +30,7 @@ async fn main() { .init(); let app = Router::new() .nest("/rest", system_routers().merge(user_management_routs())) + .nest("/admin", admin_router()) .layer(TraceLayer::new_for_http()) .with_state(state);