This commit is contained in:
2026-02-13 23:27:54 +01:00
parent 1825b50dec
commit 1e126734b0
13 changed files with 2743 additions and 3 deletions

View File

@@ -6,8 +6,11 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"time"
"git.ma-al.com/goc_marek/zfs/internal/server/templates/pages"
)
// Admin authentication and management handlers
@@ -679,9 +682,51 @@ func (s *Server) handleAdminChangePassword(w http.ResponseWriter, r *http.Reques
// handleAdminUI serves the admin panel UI
func (s *Server) handleAdminUI(w http.ResponseWriter, r *http.Request) {
// Serve the embedded admin UI HTML
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(adminPanelHTML))
// Check if admin is authenticated
admin, _ := s.authenticateAdmin(r)
if admin != nil {
// Serve the admin panel using templ
w.Header().Set("Content-Type", "text/html; charset=utf-8")
pages.AdminPage(admin.Username).Render(r.Context(), w)
} else {
// Serve the login page using templ
w.Header().Set("Content-Type", "text/html; charset=utf-8")
pages.LoginPage().Render(r.Context(), w)
}
}
// handleAdminStatic serves static files for the admin panel
func (s *Server) handleAdminStatic(w http.ResponseWriter, r *http.Request) {
// Extract the file path from the URL
// URL format: /admin/static/<filename>
path := r.URL.Path[len("/admin/static/"):]
// Get the embedded filesystem
staticFS, err := GetStaticFS()
if err != nil {
http.Error(w, "Failed to load static files", http.StatusInternalServerError)
return
}
// Serve the file
content, err := fs.ReadFile(staticFS, path)
if err != nil {
http.NotFound(w, r)
return
}
// Set content type based on file extension
switch {
case len(path) >= 3 && path[len(path)-3:] == ".js":
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
case len(path) >= 4 && path[len(path)-4:] == ".css":
w.Header().Set("Content-Type", "text/css; charset=utf-8")
default:
w.Header().Set("Content-Type", "application/octet-stream")
}
w.Write(content)
}
// handleAdminResetClientPassword resets a client's API key to a new random value

View File

@@ -527,6 +527,9 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/admin/admin/delete", s.handleAdminDeleteAdmin)
mux.HandleFunc("/admin/admin/password", s.handleAdminChangePassword)
// Admin static files
mux.HandleFunc("/admin/static/", s.handleAdminStatic)
// Admin UI (static files served from /admin/)
mux.HandleFunc("/admin/", s.handleAdminUI)
}

14
internal/server/static.go Normal file
View File

@@ -0,0 +1,14 @@
package server
import (
"embed"
"io/fs"
)
//go:embed templates/static/*
var staticFS embed.FS
// GetStaticFS returns the embedded static filesystem
func GetStaticFS() (fs.FS, error) {
return fs.Sub(staticFS, "templates/static")
}

View File

@@ -0,0 +1,265 @@
package components
import "fmt"
// Layout is the base HTML layout for the admin panel
templ Layout(title string, username string) {
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"/>
</head>
<body class="bg-gray-900 min-h-screen text-gray-100">
{ children... }
<script>
// Tailwind config for custom styles
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#3b82f6',
dark: '#2563eb',
},
accent: {
DEFAULT: '#8b5cf6',
dark: '#7c3aed',
},
surface: {
DEFAULT: '#1f2937',
dark: '#111827',
lighter: '#374151',
}
}
}
}
}
</script>
</body>
</html>
}
// Header renders the admin header with navigation
templ Header(username string) {
<header class="bg-surface-dark border-b border-gray-700 px-6 py-4 mb-6">
<div class="max-w-7xl mx-auto flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-br from-primary to-accent rounded-lg flex items-center justify-center">
<i class="fas fa-database text-white text-lg"></i>
</div>
<div>
<h1 class="text-xl font-bold text-white">ZFS Backup</h1>
<p class="text-xs text-gray-400">Admin Panel</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-gray-300">
<i class="fas fa-user-circle text-lg"></i>
<span class="text-sm">{ username }</span>
</div>
<button
class="px-4 py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 rounded-lg transition-colors flex items-center gap-2"
onclick="logout()"
>
<i class="fas fa-sign-out-alt"></i>
<span>Logout</span>
</button>
</div>
</div>
</header>
}
// StatsCard renders a single statistics card
templ StatsCard(title string, value string) {
<div class="bg-surface rounded-xl p-6 border border-gray-700 hover:border-primary/50 transition-colors">
<div class="flex items-center justify-between mb-2">
<span class="text-gray-400 text-sm">{ title }</span>
<i class={ "fas " + statsIcon(title) + " text-primary" }></i>
</div>
<div class="text-3xl font-bold text-white">{ value }</div>
</div>
}
// 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
templ TabButton(id string, label string, active bool) {
<button
class={ "px-5 py-2.5 rounded-lg font-medium transition-all flex items-center gap-2", tabButtonClass(active) }
data-tab={ id }
>
<i class={ "fas " + tabIcon(id) }></i>
{ label }
</button>
}
// 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
func tabButtonClass(active bool) string {
if active {
return "bg-primary text-white shadow-lg shadow-primary/25"
}
return "bg-surface text-gray-400 hover:bg-surface-lighter hover:text-white"
}
// Modal renders a modal dialog
templ Modal(id string, title string) {
<div id={ id } class="fixed inset-0 bg-black/70 backdrop-blur-sm hidden items-center justify-center z-50" data-modal-id={ id }>
<div class="bg-surface rounded-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto border border-gray-700 shadow-2xl modal-content">
<div class="flex justify-between items-center p-6 border-b border-gray-700">
<h3 class="text-lg font-semibold text-white">{ title }</h3>
<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"
data-modal-id={ id }
>
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6">
{ children... }
</div>
</div>
</div>
}
// FormInput renders a form input field
templ FormInput(id string, label string, inputType string, placeholder string, required bool) {
<div class="mb-4">
<label for={ id } class="block text-sm text-gray-400 mb-2">{ label }</label>
<input
type={ inputType }
id={ id }
name={ id }
placeholder={ placeholder }
if required { required }
class="w-full px-4 py-3 bg-surface-dark border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
/>
</div>
}
// FormSelect renders a form select field
templ FormSelect(id string, label string, options []SelectOption) {
<div class="mb-4">
<label for={ id } class="block text-sm text-gray-400 mb-2">{ label }</label>
<select
id={ id }
name={ id }
class="w-full px-4 py-3 bg-surface-dark border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all"
>
for _, opt := range options {
<option value={ opt.Value } if opt.Selected { selected }>{ opt.Label }</option>
}
</select>
</div>
}
// SelectOption represents an option in a select field
type SelectOption struct {
Value string
Label string
Selected bool
}
// FormCheckbox renders a form checkbox
templ FormCheckbox(id string, label string, checked bool) {
<div class="mb-4 flex items-center gap-3">
<input
type="checkbox"
id={ id }
name={ id }
if checked { checked }
class="w-5 h-5 bg-surface-dark border-gray-600 rounded text-primary focus:ring-primary focus:ring-2"
/>
<label for={ id } class="text-sm text-gray-300">{ label }</label>
</div>
}
// Button renders a styled button
templ Button(label string, variant string) {
<button
class={ "px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 " + buttonVariantClass(variant) }
>
{ label }
</button>
}
// buttonVariantClass returns the CSS class for a button variant
func buttonVariantClass(variant string) string {
switch variant {
case "primary":
return "bg-primary hover:bg-primary-dark text-white shadow-lg shadow-primary/25"
case "danger":
return "bg-red-500 hover:bg-red-600 text-white shadow-lg shadow-red-500/25"
case "success":
return "bg-emerald-500 hover:bg-emerald-600 text-white shadow-lg shadow-emerald-500/25"
case "warning":
return "bg-amber-500 hover:bg-amber-600 text-white shadow-lg shadow-amber-500/25"
case "purple":
return "bg-purple-500 hover:bg-purple-600 text-white shadow-lg shadow-purple-500/25"
case "orange":
return "bg-orange-500 hover:bg-orange-600 text-white shadow-lg shadow-orange-500/25"
default:
return "bg-gray-500 hover:bg-gray-600 text-white"
}
}
// Badge renders a status badge
templ Badge(text string, variant string) {
<span class={ "px-3 py-1 rounded-full text-xs font-medium " + badgeVariantClass(variant) }>
{ text }
</span>
}
// badgeVariantClass returns the CSS class for a badge variant
func badgeVariantClass(variant string) string {
switch variant {
case "success":
return "bg-emerald-500/20 text-emerald-400"
case "danger":
return "bg-red-500/20 text-red-400"
case "info":
return "bg-blue-500/20 text-blue-400"
case "warning":
return "bg-amber-500/20 text-amber-400"
default:
return "bg-gray-500/20 text-gray-400"
}
}
// ProgressBar renders a progress bar
templ ProgressBar(percent float64) {
<div class="w-full h-2 bg-gray-700 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-accent transition-all" style={ fmt.Sprintf("width: %.1f%%", percent) }></div>
</div>
}

View File

@@ -0,0 +1,881 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
// Layout is the base HTML layout for the admin panel
func Layout(title string, username string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 12, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Header renders the admin header with navigation
func Header(username string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 41, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// StatsCard renders a single statistics card
func StatsCard(title string, value string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 55, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h4><div class=\"text-2xl font-bold text-slate-800\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, 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: 56, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// TabButton renders a tab navigation button
func TabButton(id string, label string, active bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var9 = []any{"px-4 py-2 rounded transition-colors " + tabButtonClass(active)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, 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: 66, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// tabButtonClass returns the CSS class for a tab button
func tabButtonClass(active bool) string {
if active {
return "bg-primary text-white"
}
return "bg-white text-gray-600 hover:bg-gray-50"
}
// Modal renders a modal dialog
func Modal(id string, title string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, 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: 80, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, 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: 80, Col: 108}
}
_, 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, "\"><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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 83, Col: 60}
}
_, 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, "</h3><button type=\"button\" class=\"text-gray-400 hover:text-gray-600 text-2xl leading-none modal-close\" data-modal-id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, 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: 87, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\">&times;</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var13.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// FormInput renders a form input field
func FormInput(id string, label string, inputType string, placeholder string, required bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
if templ_7745c5c3_Var18 == nil {
templ_7745c5c3_Var18 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"mb-4\"><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 {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, 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: 104, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" placeholder=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, 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: 105, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if required {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " required")
if templ_7745c5c3_Err != nil {
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>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// FormSelect renders a form select field
func FormSelect(id string, label string, options []SelectOption) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
if templ_7745c5c3_Var25 == nil {
templ_7745c5c3_Var25 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"mb-4\"><label for=\"")
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: 115, Col: 17}
}
_, 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, 29, "\" class=\"block text-sm text-gray-600 mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, 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: 115, Col: 68}
}
_, 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, 30, "</label> <select id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, 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: 117, Col: 10}
}
_, 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, 31, "\" name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, 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: 118, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, opt := range options {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 122, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if opt.Selected {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, ">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 122, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</select></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// SelectOption represents an option in a select field
type SelectOption struct {
Value string
Label string
Selected bool
}
// FormCheckbox renders a form checkbox
func FormCheckbox(id string, label string, checked bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var32 := templ.GetChildren(ctx)
if templ_7745c5c3_Var32 == nil {
templ_7745c5c3_Var32 = templ.NopComponent
}
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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, 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: 140, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, 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: 141, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if checked {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " checked")
if templ_7745c5c3_Err != nil {
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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, 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: 145, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" class=\"text-sm text-gray-600\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, 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: 145, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</label></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Button renders a styled button
func Button(label string, variant string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var37 := templ.GetChildren(ctx)
if templ_7745c5c3_Var37 == nil {
templ_7745c5c3_Var37 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var38 = []any{"px-3 py-1.5 text-sm rounded transition-colors " + buttonVariantClass(variant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<button class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var38).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_Var39))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, 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: 154, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// buttonVariantClass returns the CSS class for a button variant
func buttonVariantClass(variant string) string {
switch variant {
case "primary":
return "bg-primary hover:bg-blue-600 text-white"
case "danger":
return "bg-danger hover:bg-red-600 text-white"
case "success":
return "bg-success hover:bg-green-600 text-white"
case "warning":
return "bg-warning hover:bg-yellow-600 text-white"
case "purple":
return "bg-purple-500 hover:bg-purple-600 text-white"
case "orange":
return "bg-orange-500 hover:bg-orange-600 text-white"
default:
return "bg-gray-500 hover:bg-gray-600 text-white"
}
}
// Badge renders a status badge
func Badge(text string, variant string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var41 := templ.GetChildren(ctx)
if templ_7745c5c3_Var41 == nil {
templ_7745c5c3_Var41 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var42 = []any{"px-2 py-0.5 rounded text-xs font-semibold " + badgeVariantClass(variant)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var42...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var42).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_Var43))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 181, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// badgeVariantClass returns the CSS class for a badge variant
func badgeVariantClass(variant string) string {
switch variant {
case "success":
return "bg-green-100 text-green-800"
case "danger":
return "bg-red-100 text-red-800"
case "info":
return "bg-blue-100 text-blue-800"
case "warning":
return "bg-yellow-100 text-yellow-800"
default:
return "bg-gray-100 text-gray-800"
}
}
// ProgressBar renders a progress bar
func ProgressBar(percent float64) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var45 := templ.GetChildren(ctx)
if templ_7745c5c3_Var45 == nil {
templ_7745c5c3_Var45 = templ.NopComponent
}
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=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%", percent))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/server/templates/components/layout.templ`, Line: 204, Col: 93}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,257 @@
package pages
import (
"git.ma-al.com/goc_marek/zfs/internal/server/templates/components"
)
// AdminPage renders the main admin panel
templ AdminPage(username string) {
@components.Layout("ZFS Backup Admin Panel", username) {
<div class="max-w-7xl mx-auto px-6">
@components.Header(username)
<!-- Stats Grid -->
<div id="stats-grid" class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
@components.StatsCard("Clients", "Loading...")
@components.StatsCard("Total Snapshots", "Loading...")
@components.StatsCard("Total Storage", "Loading...")
</div>
<!-- Tabs -->
<div class="flex gap-3 mb-6">
@components.TabButton("clients", "Clients", true)
@components.TabButton("snapshots", "Snapshots", false)
@components.TabButton("admins", "Admins", false)
</div>
<!-- Clients Tab -->
<div id="clients-tab">
<div class="bg-surface rounded-xl border border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
<i class="fas fa-users text-primary"></i>
Clients
</h3>
<button
class="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center gap-2 shadow-lg shadow-emerald-500/25"
data-action="show-modal"
data-modal="add-client-modal"
>
<i class="fas fa-plus"></i>
Add Client
</button>
</div>
<div class="p-6 overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Client ID</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Storage Type</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Quota</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Used</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Snapshots</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Status</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Actions</th>
</tr>
</thead>
<tbody id="clients-table"></tbody>
</table>
</div>
</div>
</div>
<!-- Snapshots Tab -->
<div id="snapshots-tab" class="hidden">
<div class="bg-surface rounded-xl border border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
<i class="fas fa-images text-primary"></i>
Snapshots
</h3>
<select id="snapshot-client-filter" class="px-4 py-2 bg-surface-dark border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary">
<option value="">All Clients</option>
</select>
</div>
<div class="p-6 overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Client</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Snapshot ID</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Timestamp</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Size</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Type</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Actions</th>
</tr>
</thead>
<tbody id="snapshots-table"></tbody>
</table>
</div>
</div>
</div>
<!-- Admins Tab -->
<div id="admins-tab" class="hidden">
<div class="bg-surface rounded-xl border border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-lg font-semibold text-white flex items-center gap-2">
<i class="fas fa-user-shield text-primary"></i>
Admin Users
</h3>
<button
class="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center gap-2 shadow-lg shadow-emerald-500/25"
data-action="show-modal"
data-modal="add-admin-modal"
>
<i class="fas fa-plus"></i>
Add Admin
</button>
</div>
<div class="p-6 overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b border-gray-700">
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">ID</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Username</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Role</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Created</th>
<th class="text-left py-3 px-4 text-gray-400 font-medium text-sm">Actions</th>
</tr>
</thead>
<tbody id="admins-table"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modals -->
@AddClientModal()
@EditClientModal()
@AddAdminModal()
@ChangePasswordModal()
@ClientPasswordModal()
@AdminScripts()
}
}
// AddClientModal renders the add client modal
templ AddClientModal() {
@components.Modal("add-client-modal", "Add New Client") {
<form id="add-client-form">
@components.FormInput("new-client-id", "Client ID", "text", "", true)
@components.FormInput("new-client-apikey", "API Key", "text", "", true)
@components.FormSelect("new-client-storage", "Storage Type", []components.SelectOption{
{Value: "s3", Label: "S3", Selected: true},
{Value: "local", Label: "Local ZFS", Selected: false},
})
@components.FormInput("new-client-dataset", "Target Dataset (for local storage)", "text", "backup/client1", false)
@components.FormInput("new-client-quota", "Quota (GB)", "number", "100", true)
@components.FormCheckbox("new-client-enabled", "Enabled", true)
<h4 class="text-gray-400 text-sm mt-6 mb-3 flex items-center gap-2">
<i class="fas fa-clock-rotate-left"></i>
Rotation Policy
</h4>
<div class="grid grid-cols-2 gap-4">
@components.FormInput("new-client-hourly", "Keep Hourly", "number", "24", false)
@components.FormInput("new-client-daily", "Keep Daily", "number", "7", false)
@components.FormInput("new-client-weekly", "Keep Weekly", "number", "4", false)
@components.FormInput("new-client-monthly", "Keep Monthly", "number", "12", false)
</div>
<button type="submit" class="w-full mt-6 px-4 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/25">
<i class="fas fa-plus"></i>
Create Client
</button>
</form>
}
}
// EditClientModal renders the edit client modal
templ EditClientModal() {
@components.Modal("edit-client-modal", "Edit Client") {
<form id="edit-client-form">
<input type="hidden" id="edit-client-id"/>
@components.FormInput("edit-client-apikey", "New API Key (leave empty to keep current)", "text", "Leave empty to keep current", false)
@components.FormSelect("edit-client-storage", "Storage Type", []components.SelectOption{
{Value: "s3", Label: "S3", Selected: true},
{Value: "local", Label: "Local ZFS", Selected: false},
})
@components.FormInput("edit-client-dataset", "Target Dataset", "text", "", false)
@components.FormInput("edit-client-quota", "Quota (GB)", "number", "", true)
@components.FormCheckbox("edit-client-enabled", "Enabled", false)
<h4 class="text-gray-400 text-sm mt-6 mb-3 flex items-center gap-2">
<i class="fas fa-clock-rotate-left"></i>
Rotation Policy
</h4>
<div class="grid grid-cols-2 gap-4">
@components.FormInput("edit-client-hourly", "Keep Hourly", "number", "", false)
@components.FormInput("edit-client-daily", "Keep Daily", "number", "", false)
@components.FormInput("edit-client-weekly", "Keep Weekly", "number", "", false)
@components.FormInput("edit-client-monthly", "Keep Monthly", "number", "", false)
</div>
<button type="submit" class="w-full mt-6 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25">
<i class="fas fa-save"></i>
Update Client
</button>
</form>
}
}
// AddAdminModal renders the add admin modal
templ AddAdminModal() {
@components.Modal("add-admin-modal", "Add New Admin") {
<form id="add-admin-form">
@components.FormInput("new-admin-username", "Username", "text", "", true)
@components.FormInput("new-admin-password", "Password", "password", "", true)
@components.FormSelect("new-admin-role", "Role", []components.SelectOption{
{Value: "admin", Label: "Admin", Selected: true},
})
<button type="submit" class="w-full mt-6 px-4 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-500/25">
<i class="fas fa-plus"></i>
Create Admin
</button>
</form>
}
}
// ChangePasswordModal renders the change password modal for admins
templ ChangePasswordModal() {
@components.Modal("change-password-modal", "Change Password") {
<form id="change-password-form">
<input type="hidden" id="change-password-admin-id"/>
@components.FormInput("change-password-username", "Admin Username", "text", "", true)
@components.FormInput("change-password-new", "New Password", "password", "", true)
@components.FormInput("change-password-confirm", "Confirm New Password", "password", "", true)
<button type="submit" class="w-full mt-6 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25">
<i class="fas fa-key"></i>
Change Password
</button>
</form>
}
}
// ClientPasswordModal renders the set client API key modal
templ ClientPasswordModal() {
@components.Modal("client-password-modal", "Set Client API Key") {
<form id="client-password-form">
<input type="hidden" id="client-password-client-id"/>
@components.FormInput("client-password-client-name", "Client ID", "text", "", true)
@components.FormInput("client-password-new", "New API Key", "text", "", true)
@components.FormInput("client-password-confirm", "Confirm API Key", "text", "", true)
<button type="submit" class="w-full mt-6 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25">
<i class="fas fa-key"></i>
Set API Key
</button>
</form>
}
}
// AdminScripts renders the JavaScript for the admin panel
templ AdminScripts() {
<script src="/admin/static/admin.js"></script>
}

View File

@@ -0,0 +1,565 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package pages
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"git.ma-al.com/goc_marek/zfs/internal/server/templates/components"
)
// AdminPage renders the main admin panel
func AdminPage(username string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"max-w-6xl mx-auto p-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.Header(username).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Stats Grid --><div id=\"stats-grid\" class=\"grid grid-cols-1 md:grid-cols-3 gap-4 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.StatsCard("Clients", "Loading...").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.StatsCard("Total Snapshots", "Loading...").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.StatsCard("Total Storage", "Loading...").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><!-- Tabs --><div class=\"flex gap-2 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.TabButton("clients", "Clients", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.TabButton("snapshots", "Snapshots", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.TabButton("admins", "Admins", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><!-- Clients Tab --><div id=\"clients-tab\"><div class=\"bg-white rounded-lg shadow\"><div class=\"p-4 border-b flex justify-between items-center\"><h3 class=\"font-semibold text-slate-800\">Clients</h3><button class=\"bg-success hover:bg-green-600 text-white px-4 py-2 rounded transition-colors\" data-action=\"show-modal\" data-modal=\"add-client-modal\">+ Add Client</button></div><div class=\"p-4 overflow-x-auto\"><table class=\"w-full\"><thead><tr class=\"border-b\"><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Client ID</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Storage Type</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Quota</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Used</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Snapshots</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Status</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Actions</th></tr></thead> <tbody id=\"clients-table\"></tbody></table></div></div></div><!-- Snapshots Tab --><div id=\"snapshots-tab\" class=\"hidden\"><div class=\"bg-white rounded-lg shadow\"><div class=\"p-4 border-b flex justify-between items-center\"><h3 class=\"font-semibold text-slate-800\">Snapshots</h3><select id=\"snapshot-client-filter\" class=\"px-3 py-2 border rounded\"><option value=\"\">All Clients</option></select></div><div class=\"p-4 overflow-x-auto\"><table class=\"w-full\"><thead><tr class=\"border-b\"><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Client</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Snapshot ID</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Timestamp</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Size</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Type</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Actions</th></tr></thead> <tbody id=\"snapshots-table\"></tbody></table></div></div></div><!-- Admins Tab --><div id=\"admins-tab\" class=\"hidden\"><div class=\"bg-white rounded-lg shadow\"><div class=\"p-4 border-b flex justify-between items-center\"><h3 class=\"font-semibold text-slate-800\">Admin Users</h3><button class=\"bg-success hover:bg-green-600 text-white px-4 py-2 rounded transition-colors\" data-action=\"show-modal\" data-modal=\"add-admin-modal\">+ Add Admin</button></div><div class=\"p-4 overflow-x-auto\"><table class=\"w-full\"><thead><tr class=\"border-b\"><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">ID</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Username</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Role</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Created</th><th class=\"text-left py-2 px-2 text-gray-600 font-semibold\">Actions</th></tr></thead> <tbody id=\"admins-table\"></tbody></table></div></div></div></div><!-- Modals --> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AddClientModal().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = EditClientModal().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AddAdminModal().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ChangePasswordModal().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ClientPasswordModal().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AdminScripts().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Layout("ZFS Backup Admin Panel", username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// AddClientModal renders the add client modal
func AddClientModal() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<form id=\"add-client-form\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-id", "Client ID", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-apikey", "API Key", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormSelect("new-client-storage", "Storage Type", []components.SelectOption{
{Value: "s3", Label: "S3", Selected: true},
{Value: "local", Label: "Local ZFS", Selected: false},
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-dataset", "Target Dataset (for local storage)", "text", "backup/client1", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-quota", "Quota (GB)", "number", "100", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormCheckbox("new-client-enabled", "Enabled", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<h4 class=\"text-gray-500 text-sm mt-4 mb-2\">Rotation Policy</h4><div class=\"grid grid-cols-2 gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-hourly", "Keep Hourly", "number", "24", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-daily", "Keep Daily", "number", "7", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-weekly", "Keep Weekly", "number", "4", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-client-monthly", "Keep Monthly", "number", "12", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><button type=\"submit\" class=\"w-full bg-success hover:bg-green-600 text-white py-2.5 rounded mt-4 transition-colors\">Create Client</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Modal("add-client-modal", "Add New Client").Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// EditClientModal renders the edit client modal
func EditClientModal() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<form id=\"edit-client-form\"><input type=\"hidden\" id=\"edit-client-id\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-apikey", "New API Key (leave empty to keep current)", "text", "Leave empty to keep current", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormSelect("edit-client-storage", "Storage Type", []components.SelectOption{
{Value: "s3", Label: "S3", Selected: true},
{Value: "local", Label: "Local ZFS", Selected: false},
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-dataset", "Target Dataset", "text", "", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-quota", "Quota (GB)", "number", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormCheckbox("edit-client-enabled", "Enabled", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<h4 class=\"text-gray-500 text-sm mt-4 mb-2\">Rotation Policy</h4><div class=\"grid grid-cols-2 gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-hourly", "Keep Hourly", "number", "", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-daily", "Keep Daily", "number", "", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-weekly", "Keep Weekly", "number", "", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("edit-client-monthly", "Keep Monthly", "number", "", false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div><button type=\"submit\" class=\"w-full bg-success hover:bg-green-600 text-white py-2.5 rounded mt-4 transition-colors\">Update Client</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Modal("edit-client-modal", "Edit Client").Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// AddAdminModal renders the add admin modal
func AddAdminModal() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<form id=\"add-admin-form\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-admin-username", "Username", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("new-admin-password", "Password", "password", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormSelect("new-admin-role", "Role", []components.SelectOption{
{Value: "admin", Label: "Admin", Selected: true},
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<button type=\"submit\" class=\"w-full bg-success hover:bg-green-600 text-white py-2.5 rounded mt-4 transition-colors\">Create Admin</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Modal("add-admin-modal", "Add New Admin").Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ChangePasswordModal renders the change password modal for admins
func ChangePasswordModal() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var10 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<form id=\"change-password-form\"><input type=\"hidden\" id=\"change-password-admin-id\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("change-password-username", "Admin Username", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("change-password-new", "New Password", "password", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("change-password-confirm", "Confirm New Password", "password", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<button type=\"submit\" class=\"w-full bg-success hover:bg-green-600 text-white py-2.5 rounded mt-4 transition-colors\">Change Password</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Modal("change-password-modal", "Change Password").Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// ClientPasswordModal renders the set client API key modal
func ClientPasswordModal() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
if templ_7745c5c3_Var11 == nil {
templ_7745c5c3_Var11 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var12 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<form id=\"client-password-form\"><input type=\"hidden\" id=\"client-password-client-id\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("client-password-client-name", "Client ID", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("client-password-new", "New API Key", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("client-password-confirm", "Confirm API Key", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button type=\"submit\" class=\"w-full bg-success hover:bg-green-600 text-white py-2.5 rounded mt-4 transition-colors\">Set API Key</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Modal("client-password-modal", "Set Client API Key").Render(templ.WithChildren(ctx, templ_7745c5c3_Var12), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// AdminScripts renders the JavaScript for the admin panel
func AdminScripts() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var13 := templ.GetChildren(ctx)
if templ_7745c5c3_Var13 == nil {
templ_7745c5c3_Var13 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<script src=\"/admin/static/admin.js\"></script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,49 @@
package pages
import "git.ma-al.com/goc_marek/zfs/internal/server/templates/components"
// LoginPage renders the login page
templ LoginPage() {
@components.Layout("Admin Login", "") {
<div class="min-h-screen flex items-center justify-center px-4">
<div class="w-full max-w-md">
<!-- Logo and Title -->
<div class="text-center mb-8">
<div class="w-16 h-16 bg-gradient-to-br from-primary to-accent rounded-2xl flex items-center justify-center mx-auto mb-4 shadow-lg shadow-primary/25">
<i class="fas fa-database text-white text-2xl"></i>
</div>
<h1 class="text-2xl font-bold text-white mb-2">ZFS Backup Admin</h1>
<p class="text-gray-400">Sign in to manage your backups</p>
</div>
<!-- Login Form -->
<div class="bg-surface rounded-2xl border border-gray-700 p-8 shadow-xl">
<form id="login-form">
@components.FormInput("username", "Username", "text", "", true)
@components.FormInput("password", "Password", "password", "", true)
<button
type="submit"
class="w-full mt-2 px-4 py-3 bg-primary hover:bg-primary-dark text-white rounded-lg transition-all flex items-center justify-center gap-2 shadow-lg shadow-primary/25"
>
<i class="fas fa-sign-in-alt"></i>
Sign In
</button>
</form>
<div class="mt-6 pt-6 border-t border-gray-700 text-center">
<p class="text-gray-500 text-sm">
<i class="fas fa-info-circle mr-1"></i>
Default: <span class="text-gray-400">admin / admin123</span>
</p>
</div>
</div>
<!-- Footer -->
<p class="text-center text-gray-600 text-xs mt-8">
Powered by ZFS Backup System
</p>
</div>
</div>
<script src="/admin/static/login.js"></script>
}
}

View File

@@ -0,0 +1,73 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package pages
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.ma-al.com/goc_marek/zfs/internal/server/templates/components"
// LoginPage renders the login page
func LoginPage() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
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\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("username", "Username", "text", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.FormInput("password", "Password", "password", "", true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button type=\"submit\" class=\"w-full bg-primary hover:bg-blue-600 text-white py-2.5 rounded transition-colors\">Login</button></form><p class=\"mt-4 text-center text-gray-400 text-xs\">Default: admin / admin123</p></div><script src=\"/admin/static/login.js\"></script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = components.Layout("Admin Login", "").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -0,0 +1,562 @@
let currentTab = 'clients';
// Check authentication on load
async function checkAuth() {
try {
const res = await fetch('/admin/check');
const data = await res.json();
if (!data.authenticated) {
window.location.href = '/admin/';
}
} catch (e) {
console.error('Auth check failed:', e);
}
}
// Logout
async function logout() {
await fetch('/admin/logout', { method: 'POST' });
location.reload();
}
// Load stats
async function loadStats() {
try {
const res = await fetch('/admin/stats');
const data = await res.json();
document.getElementById('stats-grid').innerHTML =
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Clients</h4><div class="text-2xl font-bold text-slate-800">' + data.client_count + '</div></div>' +
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Total Snapshots</h4><div class="text-2xl font-bold text-slate-800">' + data.total_snapshots + '</div></div>' +
'<div class="bg-white p-4 rounded-lg shadow text-center"><h4 class="text-gray-500 text-sm mb-2">Total Storage</h4><div class="text-2xl font-bold text-slate-800">' + data.total_storage_gb.toFixed(2) + ' GB</div></div>';
} catch (e) {
console.error('Failed to load stats:', e);
}
}
// Load clients
async function loadClients() {
try {
const res = await fetch('/admin/clients');
const clients = await res.json();
const tbody = document.getElementById('clients-table');
tbody.innerHTML = clients.map(c => {
const usedPercent = c.max_size_bytes > 0 ? (c.current_usage / c.max_size_bytes * 100).toFixed(1) : 0;
const usedGB = (c.current_usage / (1024*1024*1024)).toFixed(2);
const maxGB = (c.max_size_bytes / (1024*1024*1024)).toFixed(0);
return '<tr class="border-b hover:bg-gray-50">' +
'<td class="py-2 px-2 font-semibold">' + c.client_id + '</td>' +
'<td class="py-2 px-2"><span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">' + c.storage_type + '</span></td>' +
'<td class="py-2 px-2">' + maxGB + ' GB</td>' +
'<td class="py-2 px-2">' +
'<div>' + usedGB + ' GB (' + usedPercent + '%)</div>' +
'<div class="w-24 h-2 bg-gray-200 rounded overflow-hidden mt-1"><div class="h-full bg-primary transition-all" style="width: ' + Math.min(usedPercent, 100) + '%"></div></div>' +
'</td>' +
'<td class="py-2 px-2">' + c.snapshot_count + '</td>' +
'<td class="py-2 px-2">' + (c.enabled ? '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-800">Enabled</span>' : '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-red-100 text-red-800">Disabled</span>') + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' +
'<button class="px-2 py-1 text-xs rounded bg-warning hover:bg-yellow-600 text-white mr-1" data-action="edit-client" data-client-id="' + c.client_id + '">Edit</button>' +
'<button class="px-2 py-1 text-xs rounded bg-purple-500 hover:bg-purple-600 text-white mr-1" data-action="set-client-key" data-client-id="' + c.client_id + '">Set Key</button>' +
'<button class="px-2 py-1 text-xs rounded bg-orange-500 hover:bg-orange-600 text-white mr-1" data-action="reset-client-key" data-client-id="' + c.client_id + '">Reset Key</button>' +
'<button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-client" data-client-id="' + c.client_id + '">Delete</button>' +
'</td>' +
'</tr>';
}).join('');
// Update snapshot filter
const filter = document.getElementById('snapshot-client-filter');
filter.innerHTML = '<option value="">All Clients</option>' +
clients.map(c => '<option value="' + c.client_id + '">' + c.client_id + '</option>').join('');
} catch (e) {
console.error('Failed to load clients:', e);
}
}
// Load snapshots
async function loadSnapshots() {
const clientId = document.getElementById('snapshot-client-filter').value;
const url = '/admin/snapshots' + (clientId ? '?client_id=' + clientId : '');
try {
const res = await fetch(url);
const snapshots = await res.json();
const tbody = document.getElementById('snapshots-table');
tbody.innerHTML = snapshots.map(s => {
const sizeGB = (s.size_bytes / (1024*1024*1024)).toFixed(2);
return '<tr class="border-b hover:bg-gray-50">' +
'<td class="py-2 px-2">' + s.client_id + '</td>' +
'<td class="py-2 px-2">' + s.snapshot_id + '</td>' +
'<td class="py-2 px-2">' + new Date(s.timestamp).toLocaleString() + '</td>' +
'<td class="py-2 px-2">' + sizeGB + ' GB</td>' +
'<td class="py-2 px-2">' +
(s.incremental ? '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">Incremental</span>' : '<span class="px-2 py-0.5 rounded text-xs font-semibold bg-green-100 text-green-800">Full</span>') +
(s.compressed ? ' <span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">LZ4</span>' : '') +
'</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>' +
'</tr>';
}).join('');
} catch (e) {
console.error('Failed to load snapshots:', e);
}
}
// Load admins
async function loadAdmins() {
try {
const res = await fetch('/admin/admins');
const admins = await res.json();
const tbody = document.getElementById('admins-table');
tbody.innerHTML = admins.map(a =>
'<tr class="border-b hover:bg-gray-50">' +
'<td class="py-2 px-2">' + a.id + '</td>' +
'<td class="py-2 px-2 font-semibold">' + a.username + '</td>' +
'<td class="py-2 px-2"><span class="px-2 py-0.5 rounded text-xs font-semibold bg-blue-100 text-blue-800">' + a.role + '</span></td>' +
'<td class="py-2 px-2">' + new Date(a.created_at).toLocaleDateString() + '</td>' +
'<td class="py-2 px-2 whitespace-nowrap">' +
'<button class="px-2 py-1 text-xs rounded bg-warning hover:bg-yellow-600 text-white mr-1" data-action="change-admin-password" data-admin-id="' + a.id + '" data-admin-username="' + a.username + '">Change Password</button>' +
'<button class="px-2 py-1 text-xs rounded bg-danger hover:bg-red-600 text-white" data-action="delete-admin" data-admin-id="' + a.id + '">Delete</button>' +
'</td>' +
'</tr>'
).join('');
} catch (e) {
console.error('Failed to load admins:', e);
}
}
// Tab switching
function showTab(tab) {
currentTab = tab;
document.querySelectorAll('[data-tab]').forEach(t => {
t.classList.remove('bg-primary', 'text-white');
t.classList.add('bg-white', 'text-gray-600');
});
document.querySelector('[data-tab="' + tab + '"]').classList.remove('bg-white', 'text-gray-600');
document.querySelector('[data-tab="' + tab + '"]').classList.add('bg-primary', 'text-white');
document.getElementById('clients-tab').classList.add('hidden');
document.getElementById('snapshots-tab').classList.add('hidden');
document.getElementById('admins-tab').classList.add('hidden');
document.getElementById(tab + '-tab').classList.remove('hidden');
if (tab === 'snapshots') loadSnapshots();
if (tab === 'admins') loadAdmins();
}
// Modal functions
function showModal(id) {
const modal = document.getElementById(id);
modal.classList.remove('hidden');
modal.classList.add('flex');
}
function closeModal(id) {
const modal = document.getElementById(id);
modal.classList.add('hidden');
modal.classList.remove('flex');
}
// Edit client
async function editClient(clientId) {
try {
const res = await fetch('/admin/client?client_id=' + clientId);
const data = await res.json();
const c = data.client;
document.getElementById('edit-client-id').value = c.client_id;
document.getElementById('edit-client-apikey').value = '';
document.getElementById('edit-client-storage').value = c.storage_type;
document.getElementById('edit-client-dataset').value = c.dataset;
document.getElementById('edit-client-quota').value = Math.round(c.max_size_bytes / (1024*1024*1024));
document.getElementById('edit-client-enabled').checked = c.enabled;
if (c.rotation_policy) {
document.getElementById('edit-client-hourly').value = c.rotation_policy.keep_hourly || 0;
document.getElementById('edit-client-daily').value = c.rotation_policy.keep_daily || 0;
document.getElementById('edit-client-weekly').value = c.rotation_policy.keep_weekly || 0;
document.getElementById('edit-client-monthly').value = c.rotation_policy.keep_monthly || 0;
}
showModal('edit-client-modal');
} catch (e) {
alert('Failed to load client data');
}
}
// Delete client
async function deleteClient(clientId) {
if (!confirm('Are you sure you want to delete client "' + clientId + '"? This will also delete all snapshots.')) {
return;
}
try {
const res = await fetch('/admin/client/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId })
});
const data = await res.json();
if (data.success) {
loadClients();
loadStats();
} else {
alert(data.message || 'Failed to delete client');
}
} catch (e) {
alert('Failed to delete client');
}
}
// Delete snapshot
async function deleteSnapshot(clientId, snapshotId) {
if (!confirm('Are you sure you want to delete snapshot "' + snapshotId + '"?')) {
return;
}
try {
const res = await fetch('/admin/snapshot/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, snapshot_id: snapshotId })
});
const data = await res.json();
if (data.success) {
loadSnapshots();
loadStats();
} else {
alert(data.message || 'Failed to delete snapshot');
}
} catch (e) {
alert('Failed to delete snapshot');
}
}
// Delete admin
async function deleteAdmin(adminId) {
if (!confirm('Are you sure you want to delete this admin?')) {
return;
}
try {
const res = await fetch('/admin/admin/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ admin_id: adminId })
});
const data = await res.json();
if (data.success) {
loadAdmins();
} else {
alert(data.message || 'Failed to delete admin');
}
} catch (e) {
alert('Failed to delete admin');
}
}
// Show change password modal
function showChangePassword(adminId, username) {
document.getElementById('change-password-admin-id').value = adminId;
document.getElementById('change-password-username').value = username;
document.getElementById('change-password-new').value = '';
document.getElementById('change-password-confirm').value = '';
showModal('change-password-modal');
}
// Show client change password modal
function showClientChangePassword(clientId) {
document.getElementById('client-password-client-id').value = clientId;
document.getElementById('client-password-client-name').value = clientId;
document.getElementById('client-password-new').value = '';
document.getElementById('client-password-confirm').value = '';
showModal('client-password-modal');
}
// Reset client password
async function resetClientPassword(clientId) {
if (!confirm('Are you sure you want to reset the API key for "' + clientId + '"? A new random key will be generated.')) {
return;
}
try {
const res = await fetch('/admin/client/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId })
});
const data = await res.json();
if (data.success) {
alert('New API key for ' + clientId + ': ' + data.api_key + '\n\nPlease save this key as it will not be shown again.');
} else {
alert(data.message || 'Failed to reset API key');
}
} catch (e) {
alert('Failed to reset API key');
}
}
// Event delegation for click handlers
document.addEventListener('click', function(e) {
const target = e.target;
// Handle modal content clicks - stop propagation
if (target.classList.contains('modal-content') || target.closest('.modal-content')) {
// Don't process if clicking on close button
if (!target.classList.contains('modal-close')) {
return;
}
}
// Handle modal close buttons
if (target.classList.contains('modal-close')) {
const modalId = target.getAttribute('data-modal-id');
if (modalId) {
closeModal(modalId);
}
return;
}
// Handle modal overlay clicks (close on backdrop click)
if (target.classList.contains('fixed') && target.hasAttribute('data-modal-id')) {
closeModal(target.id);
return;
}
// Handle data-action buttons
const action = target.getAttribute('data-action');
if (!action) return;
switch (action) {
case 'show-modal':
const modalId = target.getAttribute('data-modal');
if (modalId) showModal(modalId);
break;
case 'edit-client':
const clientId = target.getAttribute('data-client-id');
if (clientId) editClient(clientId);
break;
case 'delete-client':
const delClientId = target.getAttribute('data-client-id');
if (delClientId) deleteClient(delClientId);
break;
case 'set-client-key':
const setKeyClientId = target.getAttribute('data-client-id');
if (setKeyClientId) showClientChangePassword(setKeyClientId);
break;
case 'reset-client-key':
const resetKeyClientId = target.getAttribute('data-client-id');
if (resetKeyClientId) resetClientPassword(resetKeyClientId);
break;
case 'delete-snapshot':
const snapClientId = target.getAttribute('data-client-id');
const snapshotId = target.getAttribute('data-snapshot-id');
if (snapClientId && snapshotId) deleteSnapshot(snapClientId, snapshotId);
break;
case 'change-admin-password':
const adminId = target.getAttribute('data-admin-id');
const adminUsername = target.getAttribute('data-admin-username');
if (adminId && adminUsername) showChangePassword(adminId, adminUsername);
break;
case 'delete-admin':
const delAdminId = target.getAttribute('data-admin-id');
if (delAdminId) deleteAdmin(parseInt(delAdminId));
break;
}
});
// Tab click handler
document.addEventListener('click', function(e) {
const tab = e.target.getAttribute('data-tab');
if (tab) {
showTab(tab);
}
});
// Snapshot filter change handler
document.addEventListener('change', function(e) {
if (e.target.id === 'snapshot-client-filter') {
loadSnapshots();
}
});
// Add client form
document.getElementById('add-client-form').addEventListener('submit', async (e) => {
e.preventDefault();
const client = {
client_id: document.getElementById('new-client-id').value,
api_key: document.getElementById('new-client-apikey').value,
storage_type: document.getElementById('new-client-storage').value,
dataset: document.getElementById('new-client-dataset').value,
max_size_bytes: parseInt(document.getElementById('new-client-quota').value) * 1024 * 1024 * 1024,
enabled: document.getElementById('new-client-enabled').checked,
rotation_policy: {
keep_hourly: parseInt(document.getElementById('new-client-hourly').value) || 0,
keep_daily: parseInt(document.getElementById('new-client-daily').value) || 0,
keep_weekly: parseInt(document.getElementById('new-client-weekly').value) || 0,
keep_monthly: parseInt(document.getElementById('new-client-monthly').value) || 0
}
};
try {
const res = await fetch('/admin/client/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(client)
});
const data = await res.json();
if (data.success) {
closeModal('add-client-modal');
document.getElementById('add-client-form').reset();
loadClients();
loadStats();
} else {
alert(data.message || 'Failed to create client');
}
} catch (e) {
alert('Failed to create client');
}
});
// Edit client form
document.getElementById('edit-client-form').addEventListener('submit', async (e) => {
e.preventDefault();
const client = {
client_id: document.getElementById('edit-client-id').value,
api_key: document.getElementById('edit-client-apikey').value,
storage_type: document.getElementById('edit-client-storage').value,
dataset: document.getElementById('edit-client-dataset').value,
max_size_bytes: parseInt(document.getElementById('edit-client-quota').value) * 1024 * 1024 * 1024,
enabled: document.getElementById('edit-client-enabled').checked,
rotation_policy: {
keep_hourly: parseInt(document.getElementById('edit-client-hourly').value) || 0,
keep_daily: parseInt(document.getElementById('edit-client-daily').value) || 0,
keep_weekly: parseInt(document.getElementById('edit-client-weekly').value) || 0,
keep_monthly: parseInt(document.getElementById('edit-client-monthly').value) || 0
}
};
try {
const res = await fetch('/admin/client/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(client)
});
const data = await res.json();
if (data.success) {
closeModal('edit-client-modal');
loadClients();
loadStats();
} else {
alert(data.message || 'Failed to update client');
}
} catch (e) {
alert('Failed to update client');
}
});
// Add admin form
document.getElementById('add-admin-form').addEventListener('submit', async (e) => {
e.preventDefault();
const admin = {
username: document.getElementById('new-admin-username').value,
password: document.getElementById('new-admin-password').value,
role: document.getElementById('new-admin-role').value
};
try {
const res = await fetch('/admin/admin/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(admin)
});
const data = await res.json();
if (data.success) {
closeModal('add-admin-modal');
document.getElementById('add-admin-form').reset();
loadAdmins();
} else {
alert(data.message || 'Failed to create admin');
}
} catch (e) {
alert('Failed to create admin');
}
});
// Change password form
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
e.preventDefault();
const adminId = document.getElementById('change-password-admin-id').value;
const newPassword = document.getElementById('change-password-new').value;
const confirmPassword = document.getElementById('change-password-confirm').value;
if (newPassword !== confirmPassword) {
alert('Passwords do not match');
return;
}
try {
const res = await fetch('/admin/admin/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ admin_id: parseInt(adminId), new_password: newPassword })
});
const data = await res.json();
if (data.success) {
closeModal('change-password-modal');
alert('Password changed successfully');
} else {
alert(data.message || 'Failed to change password');
}
} catch (e) {
alert('Failed to change password');
}
});
// Client password form
document.getElementById('client-password-form').addEventListener('submit', async (e) => {
e.preventDefault();
const clientId = document.getElementById('client-password-client-id').value;
const newKey = document.getElementById('client-password-new').value;
const confirmKey = document.getElementById('client-password-confirm').value;
if (newKey !== confirmKey) {
alert('API keys do not match');
return;
}
try {
const res = await fetch('/admin/client/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, api_key: newKey })
});
const data = await res.json();
if (data.success) {
closeModal('client-password-modal');
alert('API key changed successfully for ' + clientId);
} else {
alert(data.message || 'Failed to change API key');
}
} catch (e) {
alert('Failed to change API key');
}
});
// Initialize
loadStats();
loadClients();

View File

@@ -0,0 +1,23 @@
// Login form handler
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const res = await fetch('/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.message || 'Login failed');
}
} catch (e) {
alert('Connection error');
}
});