fix gradients

This commit is contained in:
2026-02-14 00:11:59 +01:00
parent 1e126734b0
commit 05c916e9a9
9 changed files with 729 additions and 442 deletions

69
.kilocodemodes Normal file
View 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

View File

@@ -125,7 +125,21 @@ func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset strin
fmt.Printf("Target: %s\n", targetDataset) fmt.Printf("Target: %s\n", targetDataset)
fmt.Printf("Size: %.2f GB\n", float64(snapshot.SizeBytes)/(1024*1024*1024)) fmt.Printf("Size: %.2f GB\n", float64(snapshot.SizeBytes)/(1024*1024*1024))
fmt.Printf("Storage: %s\n", snapshot.StorageType) fmt.Printf("Storage: %s\n", snapshot.StorageType)
fmt.Printf("Compressed: %v\n\n", snapshot.Compressed) fmt.Printf("Compressed: %v\n", snapshot.Compressed)
fmt.Printf("Incremental: %v\n\n", snapshot.Incremental)
// For incremental snapshots, we need special handling
if snapshot.Incremental && force {
// Check if target dataset exists
if _, err := zfs.GetDataset(targetDataset); err == nil {
fmt.Printf("→ Destroying existing dataset for incremental restore...\n")
// Destroy the existing dataset to allow clean restore
cmd := exec.Command("zfs", "destroy", "-r", targetDataset)
if err := cmd.Run(); err != nil {
fmt.Printf(" Warning: could not destroy dataset (may not exist): %v\n", err)
}
}
}
// Check if target dataset exists // Check if target dataset exists
if !force { if !force {

View File

@@ -12,62 +12,136 @@ templ Layout(title string, username string) {
<title>{ title }</title> <title>{ title }</title>
<script src="https://cdn.tailwindcss.com"></script> <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"/> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"/>
</head> <link rel="preconnect" href="https://fonts.googleapis.com"/>
<body class="bg-gray-900 min-h-screen text-gray-100"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
{ children... } <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<script> <script>
// Tailwind config for custom styles // Tailwind config - must be set before DOM loads
tailwind.config = { tailwind.config = {
darkMode: 'class', darkMode: 'class',
theme: { theme: {
extend: { extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
colors: { colors: {
primary: { primary: {
DEFAULT: '#3b82f6',
dark: '#2563eb',
},
accent: {
DEFAULT: '#8b5cf6', DEFAULT: '#8b5cf6',
dark: '#7c3aed', dark: '#7c3aed',
light: '#a78bfa',
}, },
surface: { accent: {
DEFAULT: '#1f2937', DEFAULT: '#c084fc',
dark: '#111827', dark: '#a855f7',
lighter: '#374151', 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> </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> </body>
</html> </html>
} }
// Header renders the admin header with navigation // Header renders the admin header with navigation
templ Header(username string) { 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="max-w-7xl mx-auto flex justify-between items-center">
<div class="flex items-center gap-3"> <div class="flex items-center gap-4">
<div class="w-10 h-10 bg-gradient-to-br from-primary to-accent rounded-lg flex items-center justify-center"> <div class="relative">
<i class="fas fa-database text-white text-lg"></i> <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>
<div> <div>
<h1 class="text-xl font-bold text-white">ZFS Backup</h1> <h1 class="text-xl font-bold text-slate-900 dark:text-white">ZFS Backup</h1>
<p class="text-xs text-gray-400">Admin Panel</p> <p class="text-xs text-slate-500 dark:text-slate-400 font-medium tracking-wide">Admin Panel</p>
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-gray-300"> <!-- Theme Toggle -->
<i class="fas fa-user-circle text-lg"></i> <button
<span class="text-sm">{ username }</span> 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> </div>
<button <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()" onclick="logout()"
> >
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt group-hover:scale-110 transition-transform"></i>
<span>Logout</span> <span class="font-medium">Logout</span>
</button> </button>
</div> </div>
</div> </div>
@@ -76,12 +150,17 @@ templ Header(username string) {
// StatsCard renders a single statistics card // StatsCard renders a single statistics card
templ StatsCard(title string, value string) { 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="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="flex items-center justify-between mb-2"> <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>
<span class="text-gray-400 text-sm">{ title }</span> <div class="relative">
<i class={ "fas " + statsIcon(title) + " text-primary" }></i> <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>
<div class="text-3xl font-bold text-white">{ value }</div>
</div> </div>
} }
@@ -102,7 +181,7 @@ func statsIcon(title string) string {
// TabButton renders a tab navigation button // TabButton renders a tab navigation button
templ TabButton(id string, label string, active bool) { templ TabButton(id string, label string, active bool) {
<button <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 } data-tab={ id }
> >
<i class={ "fas " + tabIcon(id) }></i> <i class={ "fas " + tabIcon(id) }></i>
@@ -127,20 +206,20 @@ func tabIcon(id string) string {
// tabButtonClass returns the CSS class for a tab button // tabButtonClass returns the CSS class for a tab button
func tabButtonClass(active bool) string { func tabButtonClass(active bool) string {
if active { 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 // Modal renders a modal dialog
templ Modal(id string, title string) { 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 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-surface rounded-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto border border-gray-700 shadow-2xl modal-content"> <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-gray-700"> <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-white">{ title }</h3> <h3 class="text-lg font-semibold text-slate-900 dark:text-white">{ title }</h3>
<button <button
type="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 } data-modal-id={ id }
> >
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -155,27 +234,27 @@ templ Modal(id string, title string) {
// FormInput renders a form input field // FormInput renders a form input field
templ FormInput(id string, label string, inputType string, placeholder string, required bool) { templ FormInput(id string, label string, inputType string, placeholder string, required bool) {
<div class="mb-4"> <div class="mb-5">
<label for={ id } class="block text-sm text-gray-400 mb-2">{ label }</label> <label for={ id } class="block text-sm font-medium text-slate-600 dark:text-slate-300 mb-2">{ label }</label>
<input <input
type={ inputType } type={ inputType }
id={ id } id={ id }
name={ id } name={ id }
placeholder={ placeholder } placeholder={ placeholder }
if required { required } 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> </div>
} }
// FormSelect renders a form select field // FormSelect renders a form select field
templ FormSelect(id string, label string, options []SelectOption) { templ FormSelect(id string, label string, options []SelectOption) {
<div class="mb-4"> <div class="mb-5">
<label for={ id } class="block text-sm text-gray-400 mb-2">{ label }</label> <label for={ id } class="block text-sm font-medium text-slate-600 dark:text-slate-300 mb-2">{ label }</label>
<select <select
id={ id } id={ id }
name={ 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 { for _, opt := range options {
<option value={ opt.Value } if opt.Selected { selected }>{ opt.Label }</option> <option value={ opt.Value } if opt.Selected { selected }>{ opt.Label }</option>
@@ -193,22 +272,22 @@ type SelectOption struct {
// FormCheckbox renders a form checkbox // FormCheckbox renders a form checkbox
templ FormCheckbox(id string, label string, checked bool) { 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 <input
type="checkbox" type="checkbox"
id={ id } id={ id }
name={ id } name={ id }
if checked { checked } 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> </div>
} }
// Button renders a styled button // Button renders a styled button
templ Button(label string, variant string) { templ Button(label string, variant string) {
<button <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 } { label }
</button> </button>
@@ -218,25 +297,25 @@ templ Button(label string, variant string) {
func buttonVariantClass(variant string) string { func buttonVariantClass(variant string) string {
switch variant { switch variant {
case "primary": 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": 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": 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": 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": 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": 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: 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 // Badge renders a status badge
templ Badge(text string, variant string) { 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 } { text }
</span> </span>
} }
@@ -245,21 +324,23 @@ templ Badge(text string, variant string) {
func badgeVariantClass(variant string) string { func badgeVariantClass(variant string) string {
switch variant { switch variant {
case "success": 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": 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": 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": 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: 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 // ProgressBar renders a progress bar
templ ProgressBar(percent float64) { templ ProgressBar(percent float64) {
<div class="w-full h-2 bg-gray-700 rounded-full overflow-hidden"> <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-primary to-accent transition-all" style={ fmt.Sprintf("width: %.1f%%", percent) }></div> <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> </div>
} }

View File

@@ -32,7 +32,7 @@ func Layout(title string, username string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"dark\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -45,7 +45,7 @@ func Layout(title string, username string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><script src=\"https://cdn.tailwindcss.com\"></script></head><body class=\"bg-gray-100 min-h-screen\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</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\"><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>\n\t\t\t\t// Tailwind config - must be set before DOM loads\n\t\t\t\ttailwind.config = {\n\t\t\t\t\tdarkMode: 'class',\n\t\t\t\t\ttheme: {\n\t\t\t\t\t\textend: {\n\t\t\t\t\t\t\tfontFamily: {\n\t\t\t\t\t\t\t\tsans: ['Inter', 'system-ui', 'sans-serif'],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tcolors: {\n\t\t\t\t\t\t\t\tprimary: {\n\t\t\t\t\t\t\t\t\tDEFAULT: '#8b5cf6',\n\t\t\t\t\t\t\t\t\tdark: '#7c3aed',\n\t\t\t\t\t\t\t\t\tlight: '#a78bfa',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\taccent: {\n\t\t\t\t\t\t\t\t\tDEFAULT: '#c084fc',\n\t\t\t\t\t\t\t\t\tdark: '#a855f7',\n\t\t\t\t\t\t\t\t\tlight: '#d8b4fe',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tanimation: {\n\t\t\t\t\t\t\t\t'float': 'float 6s ease-in-out infinite',\n\t\t\t\t\t\t\t\t'glow': 'glow 2s ease-in-out infinite alternate',\n\t\t\t\t\t\t\t\t'slide-up': 'slideUp 0.3s ease-out',\n\t\t\t\t\t\t\t\t'fade-in': 'fadeIn 0.3s ease-out',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tkeyframes: {\n\t\t\t\t\t\t\t\tfloat: {\n\t\t\t\t\t\t\t\t\t'0%, 100%': { transform: 'translateY(0)' },\n\t\t\t\t\t\t\t\t\t'50%': { transform: 'translateY(-10px)' },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tglow: {\n\t\t\t\t\t\t\t\t\t'0%': { boxShadow: '0 0 20px rgba(139, 92, 246, 0.4)' },\n\t\t\t\t\t\t\t\t\t'100%': { boxShadow: '0 0 40px rgba(139, 92, 246, 0.8)' },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tslideUp: {\n\t\t\t\t\t\t\t\t\t'0%': { transform: 'translateY(10px)', opacity: '0' },\n\t\t\t\t\t\t\t\t\t'100%': { transform: 'translateY(0)', opacity: '1' },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tfadeIn: {\n\t\t\t\t\t\t\t\t\t'0%': { opacity: '0' },\n\t\t\t\t\t\t\t\t\t'100%': { opacity: '1' },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t// Theme management - apply immediately\n\t\t\t\t(function() {\n\t\t\t\t\tconst theme = localStorage.getItem('theme') || 'dark';\n\t\t\t\t\tif (theme === 'dark') {\n\t\t\t\t\t\tdocument.documentElement.classList.add('dark');\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdocument.documentElement.classList.remove('dark');\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t\t\n\t\t\t\tfunction toggleTheme() {\n\t\t\t\t\tconst isDark = document.documentElement.classList.contains('dark');\n\t\t\t\t\tif (isDark) {\n\t\t\t\t\t\tdocument.documentElement.classList.remove('dark');\n\t\t\t\t\t\tlocalStorage.setItem('theme', 'light');\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdocument.documentElement.classList.add('dark');\n\t\t\t\t\t\tlocalStorage.setItem('theme', 'dark');\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t</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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -53,7 +53,7 @@ func Layout(title string, username string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<script>\n\t\t\t\t// Tailwind config for custom styles\n\t\t\t\ttailwind.config = {\n\t\t\t\t\ttheme: {\n\t\t\t\t\t\textend: {\n\t\t\t\t\t\t\tcolors: {\n\t\t\t\t\t\t\t\tprimary: '#3498db',\n\t\t\t\t\t\t\t\tdanger: '#e74c3c',\n\t\t\t\t\t\t\t\tsuccess: '#27ae60',\n\t\t\t\t\t\t\t\twarning: '#f39c12',\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t</script></body></html>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></body></html>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -83,20 +83,20 @@ func Header(username string) templ.Component {
templ_7745c5c3_Var3 = templ.NopComponent templ_7745c5c3_Var3 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<header class=\"bg-slate-800 text-white p-4 mb-4 rounded-lg flex justify-between items-center\"><h1 class=\"text-xl font-bold\">ZFS Backup Admin Panel</h1><div class=\"flex items-center gap-4\"><span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<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-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-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\"><!-- 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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var4 string var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(username) templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 41, Col: 19} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 137, Col: 84}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span> <button onclick=\"logout()\" class=\"bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded transition-colors\">Logout</button></div></header>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></div><button 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 group-hover:scale-110 transition-transform\"></i> <span class=\"font-medium\">Logout</span></button></div></div></header>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -126,33 +126,55 @@ func StatsCard(title string, value string) templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent templ_7745c5c3_Var5 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"bg-white p-4 rounded-lg shadow text-center\"><h4 class=\"text-gray-500 text-sm mb-2\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 55, Col: 48} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 157, Col: 80}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h4><div class=\"text-2xl font-bold text-slate-800\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><div class=\"w-10 h-10 rounded-xl bg-violet-100 dark:bg-violet-500/20 flex items-center justify-center\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 = []any{"fas " + statsIcon(title) + " text-violet-600 dark:text-violet-400"}
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(value) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 56, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<i class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"></i></div></div><div class=\"text-3xl font-bold text-slate-900 dark:text-white\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 162, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -160,6 +182,20 @@ func StatsCard(title string, value string) templ.Component {
}) })
} }
// statsIcon returns the Font Awesome icon for a stats card
func statsIcon(title string) string {
switch title {
case "Clients":
return "fa-users"
case "Total Snapshots":
return "fa-camera"
case "Total Storage":
return "fa-hard-drive"
default:
return "fa-chart-bar"
}
}
// TabButton renders a tab navigation button // TabButton renders a tab navigation button
func TabButton(id string, label string, active bool) templ.Component { func TabButton(id string, label string, active bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@@ -177,56 +213,78 @@ func TabButton(id string, label string, active bool) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx) templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil { if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var8 = templ.NopComponent templ_7745c5c3_Var10 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var9 = []any{"px-4 py-2 rounded transition-colors " + tabButtonClass(active)} var templ_7745c5c3_Var11 = []any{"px-6 py-3 rounded-xl font-medium transition-all duration-300 flex items-center gap-2.5", tabButtonClass(active)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<button class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<button class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" data-tab=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 64, Col: 15}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(label) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 66, Col: 9} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" data-tab=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 185, Col: 15}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 = []any{"fas " + tabIcon(id)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<i class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 188, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -234,12 +292,26 @@ func TabButton(id string, label string, active bool) templ.Component {
}) })
} }
// tabIcon returns the Font Awesome icon for a tab
func tabIcon(id string) string {
switch id {
case "clients":
return "fa-users"
case "snapshots":
return "fa-images"
case "admins":
return "fa-user-shield"
default:
return "fa-folder"
}
}
// tabButtonClass returns the CSS class for a tab button // tabButtonClass returns the CSS class for a tab button
func tabButtonClass(active bool) string { func tabButtonClass(active bool) string {
if active { if active {
return "bg-primary text-white" return "bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white shadow-lg shadow-violet-500/25"
} }
return "bg-white text-gray-600 hover:bg-gray-50" 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 // Modal renders a modal dialog
@@ -259,72 +331,72 @@ func Modal(id string, title string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx) templ_7745c5c3_Var17 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil { if templ_7745c5c3_Var17 == nil {
templ_7745c5c3_Var13 = templ.NopComponent templ_7745c5c3_Var17 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var18 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 80, Col: 13} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 216, Col: 13}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" class=\"fixed inset-0 bg-black/50 hidden items-center justify-center z-50\" data-modal-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" class=\"fixed inset-0 bg-black/50 dark:bg-black/70 backdrop-blur-sm hidden items-center justify-center z-50\" data-modal-id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 80, Col: 108} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 216, Col: 142}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"><div class=\"bg-white p-6 rounded-lg max-w-lg w-full max-h-[90vh] overflow-y-auto modal-content\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold text-slate-800\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"><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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 83, Col: 60} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 219, Col: 76}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</h3><button type=\"button\" class=\"text-gray-400 hover:text-gray-600 text-2xl leading-none modal-close\" data-modal-id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h3><button type=\"button\" 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=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var21 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 87, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 223, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">&times;</button></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\"><i class=\"fas fa-times\"></i></button></div><div class=\"p-6\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templ_7745c5c3_Var13.Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = templ_7745c5c3_Var17.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -349,100 +421,100 @@ func FormInput(id string, label string, inputType string, placeholder string, re
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var18 := templ.GetChildren(ctx) templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var18 == nil { if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var18 = templ.NopComponent templ_7745c5c3_Var22 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mb-4\"><label for=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mb-5\"><label for=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 100, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"block text-sm text-gray-600 mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 100, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</label> <input type=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(inputType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 102, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 103, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" name=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 104, Col: 12} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 238, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" placeholder=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" class=\"block text-sm font-medium text-slate-600 dark:text-slate-300 mb-2\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 105, Col: 28} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 238, Col: 101}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</label> <input type=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(inputType)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 240, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 241, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 242, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" placeholder=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 243, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if required { if required {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " required") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " required")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " class=\"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-primary\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " 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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -467,110 +539,110 @@ func FormSelect(id string, label string, options []SelectOption) templ.Component
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var25 := templ.GetChildren(ctx) templ_7745c5c3_Var29 := templ.GetChildren(ctx)
if templ_7745c5c3_Var25 == nil { if templ_7745c5c3_Var29 == nil {
templ_7745c5c3_Var25 = templ.NopComponent templ_7745c5c3_Var29 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"mb-4\"><label for=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"mb-5\"><label for=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var30 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 115, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 253, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"block text-sm text-gray-600 mb-1\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"block text-sm font-medium text-slate-600 dark:text-slate-300 mb-2\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(label) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 115, Col: 68} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 253, Col: 101}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</label> <select id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</label> <select id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var28 string var templ_7745c5c3_Var32 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 117, Col: 10} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 255, Col: 10}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" name=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" name=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var33 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 118, Col: 12} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 256, Col: 12}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"w-full px-3 py-2 border border-gray-300 rounded text-sm focus:outline-none focus:ring-2 focus:ring-primary\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" 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\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, opt := range options { for _, opt := range options {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<option value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<option value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var30 string var templ_7745c5c3_Var34 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Value) templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Value)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 122, Col: 29} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 260, Col: 29}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if opt.Selected { if opt.Selected {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " selected") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " selected")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, ">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var35 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label) templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 122, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 260, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</option>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</option>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</select></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</select></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -602,74 +674,74 @@ func FormCheckbox(id string, label string, checked bool) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var32 := templ.GetChildren(ctx) templ_7745c5c3_Var36 := templ.GetChildren(ctx)
if templ_7745c5c3_Var32 == nil { if templ_7745c5c3_Var36 == nil {
templ_7745c5c3_Var32 = templ.NopComponent templ_7745c5c3_Var36 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"mb-4 flex items-center gap-2\"><input type=\"checkbox\" id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<div class=\"mb-5 flex items-center gap-3\"><input type=\"checkbox\" id=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var33 string var templ_7745c5c3_Var37 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 140, Col: 10} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 278, Col: 10}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" name=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" name=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var34 string var templ_7745c5c3_Var38 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 141, Col: 12} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 279, Col: 12}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if checked { if checked {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " checked") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " checked")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, " class=\"w-4 h-4 text-primary rounded focus:ring-primary\"> <label for=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " 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=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var35 string var templ_7745c5c3_Var39 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(id) templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 145, Col: 17} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 283, Col: 17}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" class=\"text-sm text-gray-600\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" class=\"text-sm text-slate-600 dark:text-slate-300 font-medium cursor-pointer\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var36 string var templ_7745c5c3_Var40 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(label) templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 145, Col: 57} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 283, Col: 105}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</label></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</label></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -694,43 +766,43 @@ func Button(label string, variant string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var37 := templ.GetChildren(ctx) templ_7745c5c3_Var41 := templ.GetChildren(ctx)
if templ_7745c5c3_Var37 == nil { if templ_7745c5c3_Var41 == nil {
templ_7745c5c3_Var37 = templ.NopComponent templ_7745c5c3_Var41 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var38 = []any{"px-3 py-1.5 text-sm rounded transition-colors " + buttonVariantClass(variant)} var templ_7745c5c3_Var42 = []any{"px-5 py-2.5 rounded-xl font-medium transition-all duration-300 flex items-center gap-2 " + buttonVariantClass(variant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var42...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<button class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<button class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var39 string var templ_7745c5c3_Var43 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var38).String()) templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var42).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var40 string var templ_7745c5c3_Var44 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(label) templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 154, Col: 9} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 292, Col: 9}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</button>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</button>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -742,19 +814,19 @@ func Button(label string, variant string) templ.Component {
func buttonVariantClass(variant string) string { func buttonVariantClass(variant string) string {
switch variant { switch variant {
case "primary": case "primary":
return "bg-primary hover:bg-blue-600 text-white" 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": case "danger":
return "bg-danger hover:bg-red-600 text-white" 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": case "success":
return "bg-success hover:bg-green-600 text-white" 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": case "warning":
return "bg-warning hover:bg-yellow-600 text-white" 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": case "purple":
return "bg-purple-500 hover:bg-purple-600 text-white" 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": case "orange":
return "bg-orange-500 hover:bg-orange-600 text-white" 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: default:
return "bg-gray-500 hover:bg-gray-600 text-white" return "bg-slate-500 hover:bg-slate-600 text-white"
} }
} }
@@ -775,43 +847,43 @@ func Badge(text string, variant string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var41 := templ.GetChildren(ctx) templ_7745c5c3_Var45 := templ.GetChildren(ctx)
if templ_7745c5c3_Var41 == nil { if templ_7745c5c3_Var45 == nil {
templ_7745c5c3_Var41 = templ.NopComponent templ_7745c5c3_Var45 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var42 = []any{"px-2 py-0.5 rounded text-xs font-semibold " + badgeVariantClass(variant)} var templ_7745c5c3_Var46 = []any{"px-3 py-1.5 rounded-lg text-xs font-semibold " + badgeVariantClass(variant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var42...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var46...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<span class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var43 string var templ_7745c5c3_Var47 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var42).String()) templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var46).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var44 string var templ_7745c5c3_Var48 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(text) templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 181, Col: 8} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 319, Col: 8}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -823,15 +895,17 @@ func Badge(text string, variant string) templ.Component {
func badgeVariantClass(variant string) string { func badgeVariantClass(variant string) string {
switch variant { switch variant {
case "success": case "success":
return "bg-green-100 text-green-800" 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": case "danger":
return "bg-red-100 text-red-800" 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": case "info":
return "bg-blue-100 text-blue-800" 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": case "warning":
return "bg-yellow-100 text-yellow-800" 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: default:
return "bg-gray-100 text-gray-800" 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"
} }
} }
@@ -852,25 +926,25 @@ func ProgressBar(percent float64) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var45 := templ.GetChildren(ctx) templ_7745c5c3_Var49 := templ.GetChildren(ctx)
if templ_7745c5c3_Var45 == nil { if templ_7745c5c3_Var49 == nil {
templ_7745c5c3_Var45 = templ.NopComponent templ_7745c5c3_Var49 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<div class=\"w-24 h-2 bg-gray-200 rounded overflow-hidden\"><div class=\"h-full bg-primary transition-all\" style=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<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=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var46 string var templ_7745c5c3_Var50 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%", percent)) templ_7745c5c3_Var50, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%", percent))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 204, Col: 93} return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 344, Col: 156}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\"></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\"></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -8,38 +8,42 @@ templ LoginPage() {
<div class="min-h-screen flex items-center justify-center px-4"> <div class="min-h-screen flex items-center justify-center px-4">
<div class="w-full max-w-md"> <div class="w-full max-w-md">
<!-- Logo and Title --> <!-- Logo and Title -->
<div class="text-center mb-8"> <div class="text-center mb-10">
<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"> <div class="relative inline-block mb-6">
<i class="fas fa-database text-white text-2xl"></i> <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> </div>
<h1 class="text-2xl font-bold text-white mb-2">ZFS Backup Admin</h1> <h1 class="text-3xl font-bold text-slate-900 dark:text-white mb-3">ZFS Backup Admin</h1>
<p class="text-gray-400">Sign in to manage your backups</p> <p class="text-slate-500 dark:text-slate-400 font-medium">Sign in to manage your backups</p>
</div> </div>
<!-- Login Form --> <!-- 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"> <form id="login-form">
@components.FormInput("username", "Username", "text", "", true) @components.FormInput("username", "Username", "text", "", true)
@components.FormInput("password", "Password", "password", "", true) @components.FormInput("password", "Password", "password", "", true)
<button <button
type="submit" 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> <i class="fas fa-sign-in-alt"></i>
Sign In Sign In
</button> </button>
</form> </form>
<div class="mt-6 pt-6 border-t border-gray-700 text-center"> <div class="mt-8 pt-6 border-t border-slate-200 dark:border-slate-700/50 text-center">
<p class="text-gray-500 text-sm"> <p class="text-slate-500 dark:text-slate-500 text-sm flex items-center justify-center gap-2">
<i class="fas fa-info-circle mr-1"></i> <i class="fas fa-info-circle text-violet-500 dark:text-violet-400"></i>
Default: <span class="text-gray-400">admin / admin123</span> 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> </p>
</div> </div>
</div> </div>
<!-- Footer --> <!-- 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 Powered by ZFS Backup System
</p> </p>
</div> </div>

View File

@@ -44,7 +44,7 @@ func LoginPage() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -56,7 +56,7 @@ func LoginPage() templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -25,9 +25,42 @@ async function loadStats() {
const res = await fetch('/admin/stats'); const res = await fetch('/admin/stats');
const data = await res.json(); const data = await res.json();
document.getElementById('stats-grid').innerHTML = 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="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="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="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="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="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) { } catch (e) {
console.error('Failed to load stats:', 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 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 usedGB = (c.current_usage / (1024*1024*1024)).toFixed(2);
const maxGB = (c.max_size_bytes / (1024*1024*1024)).toFixed(0); const maxGB = (c.max_size_bytes / (1024*1024*1024)).toFixed(0);
return '<tr class="border-b hover:bg-gray-50">' + 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-2 px-2 font-semibold">' + c.client_id + '</td>' + '<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + 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-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-2 px-2">' + maxGB + ' GB</td>' + '<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + maxGB + ' GB</td>' +
'<td class="py-2 px-2">' + '<td class="py-4 px-4">' +
'<div>' + usedGB + ' GB (' + usedPercent + '%)</div>' + '<div class="text-slate-600 dark:text-slate-300">' + 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>' + '<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>' +
'<td class="py-2 px-2">' + c.snapshot_count + '</td>' + '<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + 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-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-2 px-2 whitespace-nowrap">' + '<td class="py-4 px-4 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-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-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-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-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-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-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>' + '<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>' + '</td>' +
'</tr>'; '</tr>';
}).join(''); }).join('');
@@ -84,16 +117,16 @@ async function loadSnapshots() {
const tbody = document.getElementById('snapshots-table'); const tbody = document.getElementById('snapshots-table');
tbody.innerHTML = snapshots.map(s => { tbody.innerHTML = snapshots.map(s => {
const sizeGB = (s.size_bytes / (1024*1024*1024)).toFixed(2); const sizeGB = (s.size_bytes / (1024*1024*1024)).toFixed(2);
return '<tr class="border-b hover:bg-gray-50">' + 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-2 px-2">' + s.client_id + '</td>' + '<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + s.client_id + '</td>' +
'<td class="py-2 px-2">' + s.snapshot_id + '</td>' + '<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + s.snapshot_id + '</td>' +
'<td class="py-2 px-2">' + new Date(s.timestamp).toLocaleString() + '</td>' + '<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + new Date(s.timestamp).toLocaleString() + '</td>' +
'<td class="py-2 px-2">' + sizeGB + ' GB</td>' + '<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + sizeGB + ' GB</td>' +
'<td class="py-2 px-2">' + '<td class="py-4 px-4">' +
(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.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-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">LZ4</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>' +
'<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>'; '</tr>';
}).join(''); }).join('');
} catch (e) { } catch (e) {
@@ -109,14 +142,14 @@ async function loadAdmins() {
const tbody = document.getElementById('admins-table'); const tbody = document.getElementById('admins-table');
tbody.innerHTML = admins.map(a => tbody.innerHTML = admins.map(a =>
'<tr class="border-b hover:bg-gray-50">' + '<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-2 px-2">' + a.id + '</td>' + '<td class="py-4 px-4 text-slate-400 dark:text-slate-500">' + a.id + '</td>' +
'<td class="py-2 px-2 font-semibold">' + a.username + '</td>' + '<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">' + 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-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-2 px-2">' + new Date(a.created_at).toLocaleDateString() + '</td>' + '<td class="py-4 px-4 text-slate-600 dark:text-slate-300">' + new Date(a.created_at).toLocaleDateString() + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' + '<td class="py-4 px-4 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-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-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>' + '<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>' + '</td>' +
'</tr>' '</tr>'
).join(''); ).join('');
@@ -129,11 +162,12 @@ async function loadAdmins() {
function showTab(tab) { function showTab(tab) {
currentTab = tab; currentTab = tab;
document.querySelectorAll('[data-tab]').forEach(t => { document.querySelectorAll('[data-tab]').forEach(t => {
t.classList.remove('bg-primary', 'text-white'); 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-white', 'text-gray-600'); 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'); const activeTab = document.querySelector('[data-tab="' + tab + '"]');
document.querySelector('[data-tab="' + tab + '"]').classList.add('bg-primary', 'text-white'); 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('clients-tab').classList.add('hidden');
document.getElementById('snapshots-tab').classList.add('hidden'); document.getElementById('snapshots-tab').classList.add('hidden');
@@ -507,16 +541,18 @@ document.getElementById('change-password-form').addEventListener('submit', async
} }
try { try {
const res = await fetch('/admin/admin/password', { const res = await fetch('/admin/admin/change-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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(); const data = await res.json();
if (data.success) { if (data.success) {
closeModal('change-password-modal'); closeModal('change-password-modal');
alert('Password changed successfully');
} else { } else {
alert(data.message || 'Failed to change password'); alert(data.message || 'Failed to change password');
} }
@@ -539,24 +575,27 @@ document.getElementById('client-password-form').addEventListener('submit', async
} }
try { try {
const res = await fetch('/admin/client/password', { const res = await fetch('/admin/client/set-password', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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(); const data = await res.json();
if (data.success) { if (data.success) {
closeModal('client-password-modal'); closeModal('client-password-modal');
alert('API key changed successfully for ' + clientId);
} else { } else {
alert(data.message || 'Failed to change API key'); alert(data.message || 'Failed to set API key');
} }
} catch (e) { } catch (e) {
alert('Failed to change API key'); alert('Failed to set API key');
} }
}); });
// Initialize // Initialize
checkAuth();
loadStats(); loadStats();
loadClients(); loadClients();