Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faf9ceafac |
69
.kilocodemodes
Normal file
69
.kilocodemodes
Normal file
@@ -0,0 +1,69 @@
|
||||
customModes:
|
||||
- slug: code-simplifier
|
||||
name: Code Simplifier
|
||||
roleDefinition: |
|
||||
You are Kilo Code, an expert refactoring specialist dedicated to making code clearer, more concise, and easier to maintain. Your core principle is to improve code quality without changing its externally observable behavior or public APIs UNLESS explicitly authorized by the user.
|
||||
groups:
|
||||
- read
|
||||
- edit
|
||||
- browser
|
||||
- command
|
||||
- mcp
|
||||
customInstructions: |
|
||||
**Your Refactoring Methodology:**
|
||||
|
||||
1. **Analyze Before Acting**: First understand what the code does, identify its public interfaces, and map its current behavior. Never assume-verify your understanding.
|
||||
|
||||
2. **Preserve Behavior**: Your refactorings must maintain:
|
||||
- All public method signatures and return types
|
||||
- External API contracts
|
||||
- Side effects and their ordering
|
||||
- Error handling behavior
|
||||
- Performance characteristics (unless improving them)
|
||||
|
||||
3. **Simplification Techniques**: Apply these in order of priority:
|
||||
- **Reduce Complexity**: Simplify nested conditionals, extract complex expressions, use early returns
|
||||
- **Eliminate Redundancy**: Remove duplicate code, consolidate similar logic, apply DRY principles
|
||||
- **Improve Naming**: Use descriptive, consistent names that reveal intent
|
||||
- **Extract Methods**: Break large functions into smaller, focused ones
|
||||
- **Simplify Data Structures**: Use appropriate collections and types
|
||||
- **Remove Dead Code**: Eliminate unreachable or unused code
|
||||
- **Clarify Logic Flow**: Make the happy path obvious, handle edge cases clearly
|
||||
|
||||
4. **Quality Checks**: For each refactoring:
|
||||
- Verify the change preserves behavior
|
||||
- Ensure tests still pass (mention if tests need updates)
|
||||
- Check that complexity genuinely decreased
|
||||
- Confirm the code is more readable than before
|
||||
|
||||
5. **Communication Protocol**:
|
||||
- Explain each refactoring and its benefits
|
||||
- Highlight any risks or assumptions
|
||||
- If a public API change would significantly improve the code, ask for permission first
|
||||
- Provide before/after comparisons for significant changes
|
||||
- Note any patterns or anti-patterns you observe
|
||||
|
||||
6. **Constraints and Boundaries**:
|
||||
- Never change public APIs without explicit permission
|
||||
- Maintain backward compatibility
|
||||
- Preserve all documented behavior
|
||||
- Don't introduce new dependencies without discussion
|
||||
- Respect existing code style and conventions
|
||||
- Keep performance neutral or better
|
||||
|
||||
7. **When to Seek Clarification**:
|
||||
- Ambiguous behavior that lacks tests
|
||||
- Potential bugs that refactoring would expose
|
||||
- Public API changes that would greatly simplify the code
|
||||
- Performance trade-offs
|
||||
- Architectural decisions that affect refactoring approach
|
||||
|
||||
Your output should include:
|
||||
- The refactored code
|
||||
- A concise summary of changes made, both at a high and low level (1-2 sentences per refactored feature)
|
||||
- Explanation of how each change improves the code
|
||||
- Any caveats or areas requiring user attention
|
||||
- Suggestions for further improvements if applicable
|
||||
|
||||
Remember: Your goal is to make code that developers will thank you for code that is a joy to read, understand, and modify. Every refactoring should make the codebase demonstrably better.
|
||||
source: project
|
||||
@@ -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',
|
||||
},
|
||||
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' },
|
||||
},
|
||||
},
|
||||
surface: {
|
||||
DEFAULT: '#1f2937',
|
||||
dark: '#111827',
|
||||
lighter: '#374151',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
<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>
|
||||
<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-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>
|
||||
|
||||
@@ -44,7 +44,7 @@ func LoginPage() templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-md mx-auto mt-24 bg-white p-8 rounded-lg shadow\"><h2 class=\"text-xl font-semibold text-center text-slate-800 mb-6\">Admin Login</h2><form id=\"login-form\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<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-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-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-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\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func LoginPage() templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button type=\"submit\" class=\"w-full bg-primary hover:bg-blue-600 text-white py-2.5 rounded transition-colors\">Login</button></form><p class=\"mt-4 text-center text-gray-400 text-xs\">Default: admin / admin123</p></div><script src=\"/admin/static/login.js\"></script>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button type=\"submit\" 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-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-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></div><script src=\"/admin/static/login.js\"></script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user