fix
This commit is contained in:
1
go.mod
1
go.mod
@@ -9,6 +9,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/a-h/templ v0.3.977 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
github.com/go-ini/ini v1.67.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
|
||||||
|
github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_marek/zfs/internal/server/templates/pages"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Admin authentication and management handlers
|
// 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
|
// handleAdminUI serves the admin panel UI
|
||||||
func (s *Server) handleAdminUI(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdminUI(w http.ResponseWriter, r *http.Request) {
|
||||||
// Serve the embedded admin UI HTML
|
// 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")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Write([]byte(adminPanelHTML))
|
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
|
// handleAdminResetClientPassword resets a client's API key to a new random value
|
||||||
|
|||||||
@@ -527,6 +527,9 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/admin/admin/delete", s.handleAdminDeleteAdmin)
|
mux.HandleFunc("/admin/admin/delete", s.handleAdminDeleteAdmin)
|
||||||
mux.HandleFunc("/admin/admin/password", s.handleAdminChangePassword)
|
mux.HandleFunc("/admin/admin/password", s.handleAdminChangePassword)
|
||||||
|
|
||||||
|
// Admin static files
|
||||||
|
mux.HandleFunc("/admin/static/", s.handleAdminStatic)
|
||||||
|
|
||||||
// Admin UI (static files served from /admin/)
|
// Admin UI (static files served from /admin/)
|
||||||
mux.HandleFunc("/admin/", s.handleAdminUI)
|
mux.HandleFunc("/admin/", s.handleAdminUI)
|
||||||
}
|
}
|
||||||
|
|||||||
14
internal/server/static.go
Normal file
14
internal/server/static.go
Normal 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")
|
||||||
|
}
|
||||||
265
internal/server/templates/components/layout.templ
Normal file
265
internal/server/templates/components/layout.templ
Normal 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>
|
||||||
|
}
|
||||||
881
internal/server/templates/components/layout_templ.go
Normal file
881
internal/server/templates/components/layout_templ.go
Normal 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, "\">×</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
|
||||||
257
internal/server/templates/pages/admin.templ
Normal file
257
internal/server/templates/pages/admin.templ
Normal 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>
|
||||||
|
}
|
||||||
565
internal/server/templates/pages/admin_templ.go
Normal file
565
internal/server/templates/pages/admin_templ.go
Normal 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
|
||||||
49
internal/server/templates/pages/login.templ
Normal file
49
internal/server/templates/pages/login.templ
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
73
internal/server/templates/pages/login_templ.go
Normal file
73
internal/server/templates/pages/login_templ.go
Normal 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
|
||||||
562
internal/server/templates/static/admin.js
Normal file
562
internal/server/templates/static/admin.js
Normal 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();
|
||||||
23
internal/server/templates/static/login.js
Normal file
23
internal/server/templates/static/login.js
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user