fix gradients

This commit is contained in:
2026-02-14 00:11:59 +01:00
parent 1e126734b0
commit 0402923593
5 changed files with 370 additions and 171 deletions

View File

@@ -12,62 +12,136 @@ templ Layout(title string, username string) {
<title>{ title }</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"/>
</head>
<body class="bg-gray-900 min-h-screen text-gray-100">
{ children... }
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<script>
// Tailwind config for custom styles
// Tailwind config - must be set before DOM loads
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
colors: {
primary: {
DEFAULT: '#3b82f6',
dark: '#2563eb',
},
accent: {
DEFAULT: '#8b5cf6',
dark: '#7c3aed',
light: '#a78bfa',
},
surface: {
DEFAULT: '#1f2937',
dark: '#111827',
lighter: '#374151',
}
}
accent: {
DEFAULT: '#c084fc',
dark: '#a855f7',
light: '#d8b4fe',
},
},
animation: {
'float': 'float 6s ease-in-out infinite',
'glow': 'glow 2s ease-in-out infinite alternate',
'slide-up': 'slideUp 0.3s ease-out',
'fade-in': 'fadeIn 0.3s ease-out',
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
glow: {
'0%': { boxShadow: '0 0 20px rgba(139, 92, 246, 0.4)' },
'100%': { boxShadow: '0 0 40px rgba(139, 92, 246, 0.8)' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
},
}
}
}
// Theme management - apply immediately
(function() {
const theme = localStorage.getItem('theme') || 'dark';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();
function toggleTheme() {
const isDark = document.documentElement.classList.contains('dark');
if (isDark) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
}
</script>
</head>
<body class="bg-slate-100 dark:bg-slate-950 min-h-screen text-slate-900 dark:text-slate-100 font-sans transition-colors duration-300">
<!-- Animated background elements (dark mode only) -->
<div class="fixed inset-0 overflow-hidden pointer-events-none hidden dark:block">
<div class="absolute -top-40 -right-40 w-96 h-96 bg-violet-600/30 rounded-full blur-3xl"></div>
<div class="absolute top-1/2 -left-40 w-96 h-96 bg-indigo-600/30 rounded-full blur-3xl"></div>
<div class="absolute -bottom-40 right-1/3 w-96 h-96 bg-fuchsia-600/20 rounded-full blur-3xl"></div>
</div>
<div class="relative z-10">
{ children... }
</div>
</body>
</html>
}
// Header renders the admin header with navigation
templ Header(username string) {
<header class="bg-surface-dark border-b border-gray-700 px-6 py-4 mb-6">
<header class="backdrop-blur-xl bg-white/80 dark:bg-slate-900/80 border-b border-slate-200 dark:border-slate-700/50 px-6 py-4 mb-8 sticky top-0 z-40 transition-colors duration-300">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-primary to-accent rounded-lg flex items-center justify-center">
<i class="fas fa-database text-white text-lg"></i>
<div class="flex items-center gap-4">
<div class="relative">
<div class="w-12 h-12 bg-gradient-to-br from-violet-500 via-purple-500 to-fuchsia-500 rounded-2xl flex items-center justify-center shadow-lg shadow-violet-500/30">
<i class="fas fa-database text-white text-xl"></i>
</div>
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-400 rounded-full border-2 border-white dark:border-slate-900"></div>
</div>
<div>
<h1 class="text-xl font-bold text-white">ZFS Backup</h1>
<p class="text-xs text-gray-400">Admin Panel</p>
<h1 class="text-xl font-bold text-slate-900 dark:text-white">ZFS Backup</h1>
<p class="text-xs text-slate-500 dark:text-slate-400 font-medium tracking-wide">Admin Panel</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-gray-300">
<i class="fas fa-user-circle text-lg"></i>
<span class="text-sm">{ username }</span>
<!-- Theme Toggle -->
<button
onclick="toggleTheme()"
class="px-4 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 flex items-center gap-2 hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-200"
title="Toggle theme"
>
<i class="fas fa-moon text-violet-500 dark:hidden"></i>
<i class="fas fa-sun text-amber-400 hidden dark:block"></i>
<span class="text-sm font-medium text-slate-600 dark:text-slate-300 dark:hidden">Dark</span>
<span class="text-sm font-medium text-slate-300 hidden dark:block">Light</span>
</button>
<div class="flex items-center gap-3 px-4 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500 flex items-center justify-center">
<i class="fas fa-user text-white text-sm"></i>
</div>
<span class="text-sm font-medium text-slate-700 dark:text-slate-200">{ username }</span>
</div>
<button
class="px-4 py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 rounded-lg transition-colors flex items-center gap-2"
class="group px-5 py-2.5 bg-red-50 dark:bg-red-500/10 hover:bg-red-100 dark:hover:bg-red-500/20 text-red-600 dark:text-red-400 rounded-xl transition-all duration-300 flex items-center gap-2 border border-red-200 dark:border-red-500/30"
onclick="logout()"
>
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
<i class="fas fa-sign-out-alt group-hover:scale-110 transition-transform"></i>
<span class="font-medium">Logout</span>
</button>
</div>
</div>
@@ -76,12 +150,17 @@ templ Header(username string) {
// StatsCard renders a single statistics card
templ StatsCard(title string, value string) {
<div class="bg-surface rounded-xl p-6 border border-gray-700 hover:border-primary/50 transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="text-gray-400 text-sm">{ title }</span>
<i class={ "fas " + statsIcon(title) + " text-primary" }></i>
<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">
<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>
<div class="relative">
<div class="flex items-center justify-between mb-4">
<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">{ title }</span>
<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">
<i class={ "fas " + statsIcon(title) + " text-violet-600 dark:text-violet-400" }></i>
</div>
</div>
<div class="text-3xl font-bold text-slate-900 dark:text-white">{ value }</div>
</div>
<div class="text-3xl font-bold text-white">{ value }</div>
</div>
}
@@ -102,7 +181,7 @@ func statsIcon(title string) string {
// TabButton renders a tab navigation button
templ TabButton(id string, label string, active bool) {
<button
class={ "px-5 py-2.5 rounded-lg font-medium transition-all flex items-center gap-2", tabButtonClass(active) }
class={ "px-6 py-3 rounded-xl font-medium transition-all duration-300 flex items-center gap-2.5", tabButtonClass(active) }
data-tab={ id }
>
<i class={ "fas " + tabIcon(id) }></i>
@@ -127,20 +206,20 @@ func tabIcon(id string) string {
// tabButtonClass returns the CSS class for a tab button
func tabButtonClass(active bool) string {
if active {
return "bg-primary text-white shadow-lg shadow-primary/25"
return "bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white shadow-lg shadow-violet-500/25"
}
return "bg-surface text-gray-400 hover:bg-surface-lighter hover:text-white"
return "bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-white border border-slate-200 dark:border-slate-700"
}
// Modal renders a modal dialog
templ Modal(id string, title string) {
<div id={ id } class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden items-center justify-center z-50" data-modal-id={ id }>
<div class="bg-surface rounded-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto border border-gray-700 shadow-2xl modal-content">
<div class="flex justify-between items-center p-6 border-b border-gray-700">
<h3 class="text-lg font-semibold text-white">{ title }</h3>
<div id={ id } class="fixed inset-0 bg-black/50 dark:bg-black/70 backdrop-blur-sm hidden items-center justify-center z-50" data-modal-id={ id }>
<div class="bg-white dark:bg-slate-900 rounded-3xl max-w-lg w-full max-h-[90vh] overflow-y-auto border border-slate-200 dark:border-slate-700 shadow-2xl modal-content">
<div class="flex justify-between items-center p-6 border-b border-slate-200 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">{ title }</h3>
<button
type="button"
class="w-8 h-8 flex items-center justify-center rounded-lg text-gray-400 hover:text-white hover:bg-gray-700 transition-colors modal-close"
class="w-10 h-10 flex items-center justify-center rounded-xl text-slate-400 hover:text-slate-600 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800 transition-all duration-200 modal-close"
data-modal-id={ id }
>
<i class="fas fa-times"></i>
@@ -155,27 +234,27 @@ templ Modal(id string, title string) {
// FormInput renders a form input field
templ FormInput(id string, label string, inputType string, placeholder string, required bool) {
<div class="mb-4">
<label for={ id } class="block text-sm text-gray-400 mb-2">{ label }</label>
<div class="mb-5">
<label for={ id } class="block text-sm font-medium text-slate-600 dark:text-slate-300 mb-2">{ label }</label>
<input
type={ inputType }
id={ id }
name={ id }
placeholder={ placeholder }
if required { required }
class="w-full px-4 py-3 bg-surface-dark border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
class="w-full px-4 py-3.5 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-xl text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-all duration-200"
/>
</div>
}
// FormSelect renders a form select field
templ FormSelect(id string, label string, options []SelectOption) {
<div class="mb-4">
<label for={ id } class="block text-sm text-gray-400 mb-2">{ label }</label>
<div class="mb-5">
<label for={ id } class="block text-sm font-medium text-slate-600 dark:text-slate-300 mb-2">{ label }</label>
<select
id={ id }
name={ id }
class="w-full px-4 py-3 bg-surface-dark border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
class="w-full px-4 py-3.5 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-all duration-200 appearance-none cursor-pointer"
>
for _, opt := range options {
<option value={ opt.Value } if opt.Selected { selected }>{ opt.Label }</option>
@@ -193,22 +272,22 @@ type SelectOption struct {
// FormCheckbox renders a form checkbox
templ FormCheckbox(id string, label string, checked bool) {
<div class="mb-4 flex items-center gap-3">
<div class="mb-5 flex items-center gap-3">
<input
type="checkbox"
id={ id }
name={ id }
if checked { checked }
class="w-5 h-5 bg-surface-dark border-gray-600 rounded text-primary focus:ring-primary focus:ring-2"
class="w-5 h-5 bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 rounded text-violet-600 focus:ring-violet-500 focus:ring-2 cursor-pointer"
/>
<label for={ id } class="text-sm text-gray-300">{ label }</label>
<label for={ id } class="text-sm text-slate-600 dark:text-slate-300 font-medium cursor-pointer">{ label }</label>
</div>
}
// Button renders a styled button
templ Button(label string, variant string) {
<button
class={ "px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 " + buttonVariantClass(variant) }
class={ "px-5 py-2.5 rounded-xl font-medium transition-all duration-300 flex items-center gap-2 " + buttonVariantClass(variant) }
>
{ label }
</button>
@@ -218,25 +297,25 @@ templ Button(label string, variant string) {
func buttonVariantClass(variant string) string {
switch variant {
case "primary":
return "bg-primary hover:bg-primary-dark text-white shadow-lg shadow-primary/25"
return "bg-gradient-to-r from-violet-600 to-fuchsia-600 hover:from-violet-500 hover:to-fuchsia-500 text-white shadow-lg shadow-violet-500/25"
case "danger":
return "bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/25"
return "bg-gradient-to-r from-red-500 to-rose-600 hover:from-red-400 hover:to-rose-500 text-white shadow-lg shadow-red-500/25"
case "success":
return "bg-emerald-500 hover:bg-emerald-600 text-white shadow-lg shadow-emerald-500/25"
return "bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-400 hover:to-green-500 text-white shadow-lg shadow-emerald-500/25"
case "warning":
return "bg-amber-500 hover:bg-amber-600 text-white shadow-lg shadow-amber-500/25"
return "bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-400 hover:to-orange-500 text-white shadow-lg shadow-amber-500/25"
case "purple":
return "bg-purple-500 hover:bg-purple-600 text-white shadow-lg shadow-purple-500/25"
return "bg-gradient-to-r from-purple-500 to-violet-600 hover:from-purple-400 hover:to-violet-500 text-white shadow-lg shadow-purple-500/25"
case "orange":
return "bg-orange-500 hover:bg-orange-600 text-white shadow-lg shadow-orange-500/25"
return "bg-gradient-to-r from-orange-500 to-amber-600 hover:from-orange-400 hover:to-amber-500 text-white shadow-lg shadow-orange-500/25"
default:
return "bg-gray-500 hover:bg-gray-600 text-white"
return "bg-slate-500 hover:bg-slate-600 text-white"
}
}
// Badge renders a status badge
templ Badge(text string, variant string) {
<span class={ "px-3 py-1 rounded-full text-xs font-medium " + badgeVariantClass(variant) }>
<span class={ "px-3 py-1.5 rounded-lg text-xs font-semibold " + badgeVariantClass(variant) }>
{ text }
</span>
}
@@ -245,21 +324,23 @@ templ Badge(text string, variant string) {
func badgeVariantClass(variant string) string {
switch variant {
case "success":
return "bg-emerald-500/20 text-emerald-400"
return "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30"
case "danger":
return "bg-red-500/20 text-red-400"
return "bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400 border border-red-200 dark:border-red-500/30"
case "info":
return "bg-blue-500/20 text-blue-400"
return "bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30"
case "warning":
return "bg-amber-500/20 text-amber-400"
return "bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400 border border-amber-200 dark:border-amber-500/30"
case "purple":
return "bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-400 border border-purple-200 dark:border-purple-500/30"
default:
return "bg-gray-500/20 text-gray-400"
return "bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-400 border border-slate-200 dark:border-slate-500/30"
}
}
// ProgressBar renders a progress bar
templ ProgressBar(percent float64) {
<div class="w-full h-2 bg-gray-700 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-accent transition-all" style={ fmt.Sprintf("width: %.1f%%", percent) }></div>
<div class="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-violet-500 to-fuchsia-500 transition-all duration-500 rounded-full" style={ fmt.Sprintf("width: %.1f%%", percent) }></div>
</div>
}

View File

@@ -18,22 +18,24 @@ templ AdminPage(username string) {
</div>
<!-- Tabs -->
<div class="flex gap-3 mb-6">
<div class="flex gap-3 mb-8 flex-wrap">
@components.TabButton("clients", "Clients", true)
@components.TabButton("snapshots", "Snapshots", false)
@components.TabButton("admins", "Admins", false)
</div>
<!-- Clients Tab -->
<div id="clients-tab">
<div class="bg-surface rounded-xl border border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
<i class="fas fa-users text-primary"></i>
<div id="clients-tab" class="animate-fade-in">
<div class="bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl border border-slate-200 dark:border-slate-700/50 overflow-hidden shadow-sm dark:shadow-none transition-colors duration-300">
<div class="p-6 border-b border-slate-200 dark:border-slate-700/50 flex justify-between items-center">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">
<i class="fas fa-users text-violet-600 dark:text-violet-400"></i>
</div>
Clients
</h3>
<button
class="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center gap-2 shadow-lg shadow-emerald-500/25"
class="px-5 py-2.5 bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-400 hover:to-green-500 text-white rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-emerald-500/25"
data-action="show-modal"
data-modal="add-client-modal"
>
@@ -44,14 +46,14 @@ templ AdminPage(username string) {
<div class="p-6 overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Client ID</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Storage Type</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Quota</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Used</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Snapshots</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Status</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Actions</th>
<tr class="border-b border-slate-200 dark:border-slate-700/50">
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Client ID</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Storage Type</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Quota</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Used</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Snapshots</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Status</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Actions</th>
</tr>
</thead>
<tbody id="clients-table"></tbody>
@@ -61,27 +63,29 @@ templ AdminPage(username string) {
</div>
<!-- Snapshots Tab -->
<div id="snapshots-tab" class="hidden">
<div class="bg-surface rounded-xl border border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
<i class="fas fa-images text-primary"></i>
<div id="snapshots-tab" class="hidden animate-fade-in">
<div class="bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl border border-slate-200 dark:border-slate-700/50 overflow-hidden shadow-sm dark:shadow-none transition-colors duration-300">
<div class="p-6 border-b border-slate-200 dark:border-slate-700/50 flex justify-between items-center">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">
<i class="fas fa-images text-violet-600 dark:text-violet-400"></i>
</div>
Snapshots
</h3>
<select id="snapshot-client-filter" class="px-4 py-2 bg-surface-dark border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary">
<select id="snapshot-client-filter" class="px-4 py-2.5 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-600 rounded-xl text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-violet-500 focus:border-violet-500 transition-all duration-200 cursor-pointer">
<option value="">All Clients</option>
</select>
</div>
<div class="p-6 overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Client</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Snapshot ID</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Timestamp</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Size</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Type</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Actions</th>
<tr class="border-b border-slate-200 dark:border-slate-700/50">
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Client</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Snapshot ID</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Timestamp</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Size</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Type</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Actions</th>
</tr>
</thead>
<tbody id="snapshots-table"></tbody>
@@ -91,15 +95,17 @@ templ AdminPage(username string) {
</div>
<!-- Admins Tab -->
<div id="admins-tab" class="hidden">
<div class="bg-surface rounded-xl border border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
<i class="fas fa-user-shield text-primary"></i>
<div id="admins-tab" class="hidden animate-fade-in">
<div class="bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl border border-slate-200 dark:border-slate-700/50 overflow-hidden shadow-sm dark:shadow-none transition-colors duration-300">
<div class="p-6 border-b border-slate-200 dark:border-slate-700/50 flex justify-between items-center">
<h3 class="text-lg font-semibold text-slate-900 dark:text-white flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">
<i class="fas fa-user-shield text-violet-600 dark:text-violet-400"></i>
</div>
Admin Users
</h3>
<button
class="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center gap-2 shadow-lg shadow-emerald-500/25"
class="px-5 py-2.5 bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-400 hover:to-green-500 text-white rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-emerald-500/25"
data-action="show-modal"
data-modal="add-admin-modal"
>
@@ -110,12 +116,12 @@ templ AdminPage(username string) {
<div class="p-6 overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">ID</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Username</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Role</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Created</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Actions</th>
<tr class="border-b border-slate-200 dark:border-slate-700/50">
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">ID</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Username</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Role</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Created</th>
<th class="text-left py-4 px-4 text-slate-500 dark:text-slate-400 font-medium text-sm">Actions</th>
</tr>
</thead>
<tbody id="admins-table"></tbody>
@@ -150,8 +156,8 @@ templ AddClientModal() {
@components.FormInput("new-client-quota", "Quota (GB)", "number", "100", true)
@components.FormCheckbox("new-client-enabled", "Enabled", true)
<h4 class="text-gray-400 text-sm mt-6 mb-3 flex items-center gap-2">
<i class="fas fa-clock-rotate-left"></i>
<h4 class="text-slate-500 dark:text-slate-400 text-sm mt-6 mb-4 flex items-center gap-2 font-medium">
<i class="fas fa-clock-rotate-left text-violet-500 dark:text-violet-400"></i>
Rotation Policy
</h4>
<div class="grid grid-cols-2 gap-4">
@@ -161,7 +167,7 @@ templ AddClientModal() {
@components.FormInput("new-client-monthly", "Keep Monthly", "number", "12", false)
</div>
<button type="submit" class="w-full mt-6 px-4 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/25">
<button type="submit" class="w-full mt-6 px-4 py-3.5 bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-400 hover:to-green-500 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/25 font-medium">
<i class="fas fa-plus"></i>
Create Client
</button>
@@ -183,8 +189,8 @@ templ EditClientModal() {
@components.FormInput("edit-client-quota", "Quota (GB)", "number", "", true)
@components.FormCheckbox("edit-client-enabled", "Enabled", false)
<h4 class="text-gray-400 text-sm mt-6 mb-3 flex items-center gap-2">
<i class="fas fa-clock-rotate-left"></i>
<h4 class="text-slate-500 dark:text-slate-400 text-sm mt-6 mb-4 flex items-center gap-2 font-medium">
<i class="fas fa-clock-rotate-left text-violet-500 dark:text-violet-400"></i>
Rotation Policy
</h4>
<div class="grid grid-cols-2 gap-4">
@@ -194,7 +200,7 @@ templ EditClientModal() {
@components.FormInput("edit-client-monthly", "Keep Monthly", "number", "", false)
</div>
<button type="submit" class="w-full mt-6 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25">
<button type="submit" class="w-full mt-6 px-4 py-3.5 bg-gradient-to-r from-violet-600 to-fuchsia-600 hover:from-violet-500 hover:to-fuchsia-500 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 shadow-lg shadow-violet-500/25 font-medium">
<i class="fas fa-save"></i>
Update Client
</button>
@@ -211,7 +217,7 @@ templ AddAdminModal() {
@components.FormSelect("new-admin-role", "Role", []components.SelectOption{
{Value: "admin", Label: "Admin", Selected: true},
})
<button type="submit" class="w-full mt-6 px-4 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/25">
<button type="submit" class="w-full mt-6 px-4 py-3.5 bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-400 hover:to-green-500 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/25 font-medium">
<i class="fas fa-plus"></i>
Create Admin
</button>
@@ -227,7 +233,7 @@ templ ChangePasswordModal() {
@components.FormInput("change-password-username", "Admin Username", "text", "", true)
@components.FormInput("change-password-new", "New Password", "password", "", true)
@components.FormInput("change-password-confirm", "Confirm New Password", "password", "", true)
<button type="submit" class="w-full mt-6 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25">
<button type="submit" class="w-full mt-6 px-4 py-3.5 bg-gradient-to-r from-violet-600 to-fuchsia-600 hover:from-violet-500 hover:to-fuchsia-500 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 shadow-lg shadow-violet-500/25 font-medium">
<i class="fas fa-key"></i>
Change Password
</button>
@@ -243,7 +249,7 @@ templ ClientPasswordModal() {
@components.FormInput("client-password-client-name", "Client ID", "text", "", true)
@components.FormInput("client-password-new", "New API Key", "text", "", true)
@components.FormInput("client-password-confirm", "Confirm API Key", "text", "", true)
<button type="submit" class="w-full mt-6 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25">
<button type="submit" class="w-full mt-6 px-4 py-3.5 bg-gradient-to-r from-violet-600 to-fuchsia-600 hover:from-violet-500 hover:to-fuchsia-500 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 shadow-lg shadow-violet-500/25 font-medium">
<i class="fas fa-key"></i>
Set API Key
</button>

View File

@@ -8,38 +8,42 @@ templ LoginPage() {
<div class="min-h-screen flex items-center justify-center px-4">
<div class="w-full max-w-md">
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-primary/25">
<i class="fas fa-database text-white text-2xl"></i>
<div class="text-center mb-10">
<div class="relative inline-block mb-6">
<div class="w-20 h-20 bg-gradient-to-br from-violet-500 via-purple-500 to-fuchsia-500 rounded-3xl flex items-center justify-center shadow-2xl shadow-violet-500/30 animate-float">
<i class="fas fa-database text-white text-3xl"></i>
</div>
<div class="absolute -bottom-1 -right-1 w-6 h-6 bg-emerald-400 rounded-full border-4 border-white dark:border-slate-950 animate-pulse"></div>
</div>
<h1 class="text-2xl font-bold text-white mb-2">ZFS Backup Admin</h1>
<p class="text-gray-400">Sign in to manage your backups</p>
<h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-3">ZFS Backup Admin</h1>
<p class="text-slate-500 dark:text-slate-400 font-medium">Sign in to manage your backups</p>
</div>
<!-- Login Form -->
<div class="bg-surface rounded-2xl border border-gray-700 p-8 shadow-xl">
<div class="bg-white/90 dark:bg-slate-900/80 backdrop-blur-xl rounded-3xl border border-slate-200 dark:border-slate-700/50 p-8 shadow-xl dark:shadow-none transition-colors duration-300">
<form id="login-form">
@components.FormInput("username", "Username", "text", "", true)
@components.FormInput("password", "Password", "password", "", true)
<button
type="submit"
class="w-full mt-2 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25"
class="w-full mt-4 px-4 py-4 bg-gradient-to-r from-violet-600 to-fuchsia-600 hover:from-violet-500 hover:to-fuchsia-500 text-white rounded-xl transition-all duration-300 flex items-center justify-center gap-2 shadow-lg shadow-violet-500/25 hover:shadow-violet-500/40 font-semibold text-lg"
>
<i class="fas fa-sign-in-alt"></i>
Sign In
</button>
</form>
<div class="mt-6 pt-6 border-t border-gray-700 text-center">
<p class="text-gray-500 text-sm">
<i class="fas fa-info-circle mr-1"></i>
Default: <span class="text-gray-400">admin / admin123</span>
<div class="mt-8 pt-6 border-t border-slate-200 dark:border-slate-700/50 text-center">
<p class="text-slate-500 dark:text-slate-500 text-sm flex items-center justify-center gap-2">
<i class="fas fa-info-circle text-violet-500 dark:text-violet-400"></i>
Default: <span class="text-slate-700 dark:text-slate-300 font-mono bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded">admin / admin123</span>
</p>
</div>
</div>
<!-- Footer -->
<p class="text-center text-gray-600 text-xs mt-8">
<p class="text-center text-slate-400 dark:text-slate-600 text-xs mt-10 flex items-center justify-center gap-2">
<i class="fas fa-shield-halved text-violet-400 dark:text-violet-500/50"></i>
Powered by ZFS Backup System
</p>
</div>

View File

@@ -25,9 +25,42 @@ async function loadStats() {
const res = await fetch('/admin/stats');
const data = await res.json();
document.getElementById('stats-grid').innerHTML =
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Clients</h4><div class="text-2xl font-bold text-slate-800">' + data.client_count + '</div></div>' +
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Total Snapshots</h4><div class="text-2xl font-bold text-slate-800">' + data.total_snapshots + '</div></div>' +
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Total Storage</h4><div class="text-2xl font-bold text-slate-800">' + data.total_storage_gb.toFixed(2) + ' GB</div></div>';
'<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">' +
'<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>' +
'<div class="relative">' +
'<div class="flex items-center justify-between mb-4">' +
'<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">Clients</span>' +
'<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">' +
'<i class="fas fa-users text-violet-600 dark:text-violet-400"></i>' +
'</div>' +
'</div>' +
'<div class="text-3xl font-bold text-slate-900 dark:text-white">' + data.client_count + '</div>' +
'</div>' +
'</div>' +
'<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">' +
'<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>' +
'<div class="relative">' +
'<div class="flex items-center justify-between mb-4">' +
'<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">Total Snapshots</span>' +
'<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">' +
'<i class="fas fa-camera text-violet-600 dark:text-violet-400"></i>' +
'</div>' +
'</div>' +
'<div class="text-3xl font-bold text-slate-900 dark:text-white">' + data.total_snapshots + '</div>' +
'</div>' +
'</div>' +
'<div class="group relative bg-white dark:bg-slate-900/80 backdrop-blur-xl rounded-2xl p-6 border border-slate-200 dark:border-slate-700/50 hover:border-violet-400 dark:hover:border-violet-500/50 transition-all duration-300 overflow-hidden shadow-sm dark:shadow-none">' +
'<div class="absolute inset-0 bg-gradient-to-br from-violet-500/5 via-purple-500/5 to-transparent dark:from-violet-600/10 dark:via-purple-600/5"></div>' +
'<div class="relative">' +
'<div class="flex items-center justify-between mb-4">' +
'<span class="text-slate-500 dark:text-slate-400 text-sm font-medium">Total Storage</span>' +
'<div class="w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center">' +
'<i class="fas fa-hard-drive text-violet-600 dark:text-violet-400"></i>' +
'</div>' +
'</div>' +
'<div class="text-3xl font-bold text-slate-900 dark:text-white">' + data.total_storage_gb.toFixed(2) + ' GB</div>' +
'</div>' +
'</div>';
} catch (e) {
console.error('Failed to load stats:', e);
}
@@ -44,21 +77,21 @@ async function loadClients() {
const usedPercent = c.max_size_bytes > 0 ? (c.current_usage / c.max_size_bytes * 100).toFixed(1) : 0;
const usedGB = (c.current_usage / (1024*1024*1024)).toFixed(2);
const maxGB = (c.max_size_bytes / (1024*1024*1024)).toFixed(0);
return '<tr class="border-b hover:bg-gray-50">' +
'<td class="py-2 px-2 font-semibold">' + c.client_id + '</td>' +
'<td class="py-2 px-2"><span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">' + c.storage_type + '</span></td>' +
'<td class="py-2 px-2">' + maxGB + ' GB</td>' +
'<td class="py-2 px-2">' +
'<div>' + usedGB + ' GB (' + usedPercent + '%)</div>' +
'<div class="w-24 h-2 bg-gray-200 rounded overflow-hidden mt-1"><div class="h-full bg-primary transition-all" style="width: ' + Math.min(usedPercent, 100) + '%"></div></div>' +
return '<tr class="border-b border-slate-100 dark:border-slate-700/30 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">' +
'<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + c.client_id + '</td>' +
'<td class="py-4 px-4"><span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30">' + c.storage_type + '</span></td>' +
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + maxGB + ' GB</td>' +
'<td class="py-4 px-4">' +
'<div class="text-slate-600 dark:text-slate-300">' + usedGB + ' GB (' + usedPercent + '%)</div>' +
'<div class="w-24 h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden mt-2"><div class="h-full bg-gradient-to-r from-violet-500 to-fuchsia-500 transition-all rounded-full" style="width: ' + Math.min(usedPercent, 100) + '%"></div></div>' +
'</td>' +
'<td class="py-2 px-2">' + c.snapshot_count + '</td>' +
'<td class="py-2 px-2">' + (c.enabled ? '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-800">Enabled</span>' : '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-red-100 text-red-800">Disabled</span>') + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' +
'<button class="px-2 py-1 text-xs rounded bg-warning hover:bg-yellow-600 text-white mr-1" data-action="edit-client" data-client-id="' + c.client_id + '">Edit</button>' +
'<button class="px-2 py-1 text-xs rounded bg-purple-500 hover:bg-purple-600 text-white mr-1" data-action="set-client-key" data-client-id="' + c.client_id + '">Set Key</button>' +
'<button class="px-2 py-1 text-xs rounded bg-orange-500 hover:bg-orange-600 text-white mr-1" data-action="reset-client-key" data-client-id="' + c.client_id + '">Reset Key</button>' +
'<button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-client" data-client-id="' + c.client_id + '">Delete</button>' +
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + c.snapshot_count + '</td>' +
'<td class="py-4 px-4">' + (c.enabled ? '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30">Enabled</span>' : '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400 border border-red-200 dark:border-red-500/30">Disabled</span>') + '</td>' +
'<td class="py-4 px-4 whitespace-nowrap">' +
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white mr-1.5 shadow-lg shadow-amber-500/20 transition-all" data-action="edit-client" data-client-id="' + c.client_id + '"><i class="fas fa-edit mr-1"></i>Edit</button>' +
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-purple-500 to-violet-500 hover:from-purple-400 hover:to-violet-400 text-white mr-1.5 shadow-lg shadow-purple-500/20 transition-all" data-action="set-client-key" data-client-id="' + c.client_id + '"><i class="fas fa-key mr-1"></i>Set Key</button>' +
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 hover:from-orange-400 hover:to-amber-400 text-white mr-1.5 shadow-lg shadow-orange-500/20 transition-all" data-action="reset-client-key" data-client-id="' + c.client_id + '"><i class="fas fa-rotate mr-1"></i>Reset</button>' +
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400 text-white shadow-lg shadow-red-500/20 transition-all" data-action="delete-client" data-client-id="' + c.client_id + '"><i class="fas fa-trash mr-1"></i>Delete</button>' +
'</td>' +
'</tr>';
}).join('');
@@ -84,16 +117,16 @@ async function loadSnapshots() {
const tbody = document.getElementById('snapshots-table');
tbody.innerHTML = snapshots.map(s => {
const sizeGB = (s.size_bytes / (1024*1024*1024)).toFixed(2);
return '<tr class="border-b hover:bg-gray-50">' +
'<td class="py-2 px-2">' + s.client_id + '</td>' +
'<td class="py-2 px-2">' + s.snapshot_id + '</td>' +
'<td class="py-2 px-2">' + new Date(s.timestamp).toLocaleString() + '</td>' +
'<td class="py-2 px-2">' + sizeGB + ' GB</td>' +
'<td class="py-2 px-2">' +
(s.incremental ? '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">Incremental</span>' : '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-800">Full</span>') +
(s.compressed ? ' <span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">LZ4</span>' : '') +
return '<tr class="border-b border-slate-100 dark:border-slate-700/30 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">' +
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + s.client_id + '</td>' +
'<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + s.snapshot_id + '</td>' +
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + new Date(s.timestamp).toLocaleString() + '</td>' +
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + sizeGB + ' GB</td>' +
'<td class="py-4 px-4">' +
(s.incremental ? '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30">Incremental</span>' : '<span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-500/30">Full</span>') +
(s.compressed ? ' <span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-400 border border-purple-200 dark:border-purple-500/30">LZ4</span>' : '') +
'</td>' +
'<td class="py-2 px-2"><button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-snapshot" data-client-id="' + s.client_id + '" data-snapshot-id="' + s.snapshot_id + '">Delete</button></td>' +
'<td class="py-4 px-4"><button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400 text-white shadow-lg shadow-red-500/20 transition-all" data-action="delete-snapshot" data-client-id="' + s.client_id + '" data-snapshot-id="' + s.snapshot_id + '"><i class="fas fa-trash mr-1"></i>Delete</button></td>' +
'</tr>';
}).join('');
} catch (e) {
@@ -109,14 +142,14 @@ async function loadAdmins() {
const tbody = document.getElementById('admins-table');
tbody.innerHTML = admins.map(a =>
'<tr class="border-b hover:bg-gray-50">' +
'<td class="py-2 px-2">' + a.id + '</td>' +
'<td class="py-2 px-2 font-semibold">' + a.username + '</td>' +
'<td class="py-2 px-2"><span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">' + a.role + '</span></td>' +
'<td class="py-2 px-2">' + new Date(a.created_at).toLocaleDateString() + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' +
'<button class="px-2 py-1 text-xs rounded bg-warning hover:bg-yellow-600 text-white mr-1" data-action="change-admin-password" data-admin-id="' + a.id + '" data-admin-username="' + a.username + '">Change Password</button>' +
'<button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-admin" data-admin-id="' + a.id + '">Delete</button>' +
'<tr class="border-b border-slate-100 dark:border-slate-700/30 hover:bg-slate-50 dark:hover:bg-slate-800/30 transition-colors">' +
'<td class="py-4 px-4 text-slate-400 dark:text-slate-500">' + a.id + '</td>' +
'<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + a.username + '</td>' +
'<td class="py-4 px-4"><span class="px-3 py-1.5 rounded-lg text-xs font-semibold bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400 border border-blue-200 dark:border-blue-500/30">' + a.role + '</span></td>' +
'<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + new Date(a.created_at).toLocaleDateString() + '</td>' +
'<td class="py-4 px-4 whitespace-nowrap">' +
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white mr-1.5 shadow-lg shadow-amber-500/20 transition-all" data-action="change-admin-password" data-admin-id="' + a.id + '" data-admin-username="' + a.username + '"><i class="fas fa-key mr-1"></i>Change Password</button>' +
'<button class="px-3 py-1.5 text-xs rounded-lg bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400 text-white shadow-lg shadow-red-500/20 transition-all" data-action="delete-admin" data-admin-id="' + a.id + '"><i class="fas fa-trash mr-1"></i>Delete</button>' +
'</td>' +
'</tr>'
).join('');
@@ -129,11 +162,12 @@ async function loadAdmins() {
function showTab(tab) {
currentTab = tab;
document.querySelectorAll('[data-tab]').forEach(t => {
t.classList.remove('bg-primary', 'text-white');
t.classList.add('bg-white', 'text-gray-600');
t.classList.remove('bg-gradient-to-r', 'from-violet-600', 'to-fuchsia-600', 'text-white', 'shadow-lg', 'shadow-violet-500/25');
t.classList.add('bg-slate-100', 'dark:bg-slate-800', 'text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-200', 'dark:hover:bg-slate-700', 'hover:text-slate-900', 'dark:hover:text-white', 'border', 'border-slate-200', 'dark:border-slate-700');
});
document.querySelector('[data-tab="' + tab + '"]').classList.remove('bg-white', 'text-gray-600');
document.querySelector('[data-tab="' + tab + '"]').classList.add('bg-primary', 'text-white');
const activeTab = document.querySelector('[data-tab="' + tab + '"]');
activeTab.classList.remove('bg-slate-100', 'dark:bg-slate-800', 'text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-200', 'dark:hover:bg-slate-700', 'hover:text-slate-900', 'dark:hover:text-white', 'border', 'border-slate-200', 'dark:border-slate-700');
activeTab.classList.add('bg-gradient-to-r', 'from-violet-600', 'to-fuchsia-600', 'text-white', 'shadow-lg', 'shadow-violet-500/25');
document.getElementById('clients-tab').classList.add('hidden');
document.getElementById('snapshots-tab').classList.add('hidden');
@@ -507,16 +541,18 @@ document.getElementById('change-password-form').addEventListener('submit', async
}
try {
const res = await fetch('/admin/admin/password', {
const res = await fetch('/admin/admin/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ admin_id: parseInt(adminId), new_password: newPassword })
body: JSON.stringify({
admin_id: parseInt(adminId),
new_password: newPassword
})
});
const data = await res.json();
if (data.success) {
closeModal('change-password-modal');
alert('Password changed successfully');
} else {
alert(data.message || 'Failed to change password');
}
@@ -539,24 +575,27 @@ document.getElementById('client-password-form').addEventListener('submit', async
}
try {
const res = await fetch('/admin/client/password', {
const res = await fetch('/admin/client/set-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, api_key: newKey })
body: JSON.stringify({
client_id: clientId,
new_api_key: newKey
})
});
const data = await res.json();
if (data.success) {
closeModal('client-password-modal');
alert('API key changed successfully for ' + clientId);
} else {
alert(data.message || 'Failed to change API key');
alert(data.message || 'Failed to set API key');
}
} catch (e) {
alert('Failed to change API key');
alert('Failed to set API key');
}
});
// Initialize
checkAuth();
loadStats();
loadClients();