Files
Maintainarr/internal/views/views.go

301 lines
13 KiB
Go

package views
import (
"embed"
"encoding/json"
"html/template"
"net/http"
"path"
"strconv"
"strings"
"time"
"maintainarr/internal/models"
)
//go:embed layouts/*.gohtml pages/*.gohtml
var templatesFS embed.FS
type Renderer struct {
functions template.FuncMap
}
type ViewData struct {
Title string
Shell string
PageTemplate string
ThemeClass string
ThemeMode string
CurrentPath string
Flash string
User any
Organization any
AvailableGroups any
AvailableTags any
Content any
}
func NewRenderer() (*Renderer, error) {
functions := template.FuncMap{
"icon": icon,
"contains": strings.Contains,
"distroIconClass": distroIconClass,
"nodeIconClass": nodeIconClass,
"nodeIconPending": nodeIconPending,
"packageManagerIconClass": packageManagerIconClass,
"packageManagerLabel": packageManagerLabel,
"groupIcons": groupIcons,
"hasUpdateDay": hasUpdateDay,
"updateWindowSummary": updateWindowSummary,
"updatePackages": updatePackages,
"uptime": formatUptime,
"safeHTML": func(value string) template.HTML { return template.HTML(value) },
"nowYear": func() int { return time.Now().Year() },
"lower": strings.ToLower,
"splitLines": func(value string) []string {
var lines []string
for _, line := range strings.Split(value, "\n") {
line = strings.TrimSpace(line)
if line != "" {
lines = append(lines, line)
}
}
return lines
},
"seq": func(start, end int) []int {
values := make([]int, 0, end-start+1)
for i := start; i <= end; i++ {
values = append(values, i)
}
return values
},
}
return &Renderer{functions: functions}, nil
}
func (r *Renderer) Render(w http.ResponseWriter, name string, data ViewData) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
layout := "layouts/app.gohtml"
if data.Shell == "auth" {
layout = "layouts/auth.gohtml"
} else if data.Shell == "console" {
layout = "layouts/console.gohtml"
}
parsed, err := template.New("base").Funcs(r.functions).ParseFS(
templatesFS,
"layouts/base.gohtml",
layout,
path.Join("pages", name+".gohtml"),
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = parsed.ExecuteTemplate(w, "base", data)
}
func (r *Renderer) RenderPageTemplate(w http.ResponseWriter, page, templateName string, data any) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
parsed, err := template.New("page").Funcs(r.functions).ParseFS(
templatesFS,
path.Join("pages", page+".gohtml"),
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = parsed.ExecuteTemplate(w, templateName, data)
}
func formatUptime(seconds int64) string {
days := seconds / 86400
hours := (seconds % 86400) / 3600
minutes := (seconds % 3600) / 60
if days > 0 {
return template.HTMLEscapeString(strings.TrimSpace(strings.Join([]string{
formatPart(days, "d"),
formatPart(hours, "h"),
formatPart(minutes, "m"),
}, " ")))
}
return template.HTMLEscapeString(strings.TrimSpace(strings.Join([]string{
formatPart(hours, "h"),
formatPart(minutes, "m"),
}, " ")))
}
func formatPart(value int64, suffix string) string {
if value == 0 {
return ""
}
return strconv.FormatInt(value, 10) + suffix
}
func distroIconClass(distro string) string {
value := strings.ToLower(strings.TrimSpace(distro))
switch {
case strings.Contains(value, "ubuntu"):
return "fl-ubuntu"
case strings.Contains(value, "debian"):
return "fl-debian"
case strings.Contains(value, "arch"), strings.Contains(value, "manjaro"), strings.Contains(value, "endeavouros"):
return "fl-archlinux"
default:
return "ti ti-server-2"
}
}
func nodeIconClass(distro, packageManager string) string {
if className := distroIconClass(distro); className != "ti ti-server-2" {
return className
}
switch strings.ToLower(strings.TrimSpace(packageManager)) {
case "apt":
return "fl-debian"
case "pacman":
return "fl-archlinux"
default:
return "ti ti-server-2"
}
}
func nodeIconPending(distro, packageManager string) bool {
distroValue := strings.ToLower(strings.TrimSpace(distro))
packageValue := strings.ToLower(strings.TrimSpace(packageManager))
return packageValue == "" && (distroValue == "" || distroValue == "linux")
}
func packageManagerIconClass(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "apt":
return "ti ti-brand-debian"
case "pacman":
return "ti ti-brand-archlinux"
case "dnf", "yum", "zypper", "apk", "nix", "emerge":
return "ti ti-package"
default:
return "ti ti-package"
}
}
func packageManagerLabel(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "Unknown"
}
switch strings.ToLower(trimmed) {
case "apt":
return "APT"
case "dnf":
return "DNF"
case "yum":
return "YUM"
default:
return strings.ToUpper(trimmed[:1]) + strings.ToLower(trimmed[1:])
}
}
func updatePackages(value string) []models.NodeUpdatePackage {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
var packages []models.NodeUpdatePackage
if err := json.Unmarshal([]byte(trimmed), &packages); err != nil {
return nil
}
return packages
}
func groupIcons() []string {
return []string{
"ti ti-stack-2", "ti ti-server", "ti ti-server-2", "ti ti-server-bolt", "ti ti-server-cog", "ti ti-server-off",
"ti ti-server-spark", "ti ti-cloud", "ti ti-cloud-bolt", "ti ti-cloud-cog", "ti ti-cloud-code", "ti ti-cloud-data-connection",
"ti ti-cloud-computing", "ti ti-cloud-check", "ti ti-cloud-up", "ti ti-cloud-down", "ti ti-cloud-lock", "ti ti-cloud-off",
"ti ti-cloud-x", "ti ti-database", "ti ti-database-export", "ti ti-database-import", "ti ti-database-cog", "ti ti-database-off",
"ti ti-database-search", "ti ti-database-smile", "ti ti-table", "ti ti-table-options", "ti ti-table-export", "ti ti-table-import",
"ti ti-network", "ti ti-router", "ti ti-route", "ti ti-world", "ti ti-world-www", "ti ti-access-point",
"ti ti-wifi", "ti ti-wifi-off", "ti ti-antenna-bars-5", "ti ti-broadcast", "ti ti-radar-2", "ti ti-satellite",
"ti ti-satellite-off", "ti ti-direction-sign", "ti ti-git-branch", "ti ti-git-commit", "ti ti-git-merge", "ti ti-git-pull-request",
"ti ti-brand-docker", "ti ti-brand-kubernetes", "ti ti-brand-github", "ti ti-brand-gitlab", "ti ti-brand-openai", "ti ti-brand-aws",
"ti ti-brand-google", "ti ti-brand-windows", "ti ti-brand-debian", "ti ti-brand-ubuntu", "ti ti-brand-archlinux", "ti ti-brand-python",
"ti ti-brand-javascript", "ti ti-brand-typescript", "ti ti-brand-go", "ti ti-brand-c-sharp", "ti ti-brand-vscode", "ti ti-brand-powershell",
"ti ti-device-desktop", "ti ti-device-imac", "ti ti-device-laptop", "ti ti-device-tablet", "ti ti-device-mobile", "ti ti-devices",
"ti ti-devices-2", "ti ti-screen-share", "ti ti-screen-share-off", "ti ti-app-window", "ti ti-browser", "ti ti-browser-check",
"ti ti-browser-cog", "ti ti-browser-off", "ti ti-layout-dashboard", "ti ti-layout-grid", "ti ti-layout-list", "ti ti-layout-kanban",
"ti ti-layout-navbar", "ti ti-layout-sidebar", "ti ti-menu-2", "ti ti-panel-left", "ti ti-panel-right", "ti ti-panorama-horizontal",
"ti ti-package", "ti ti-package-import", "ti ti-package-export", "ti ti-package-off", "ti ti-box", "ti ti-box-multiple",
"ti ti-box-model", "ti ti-archive", "ti ti-archive-off", "ti ti-folders", "ti ti-folder", "ti ti-folder-open",
"ti ti-folder-cog", "ti ti-folder-bolt", "ti ti-file-stack", "ti ti-file-database", "ti ti-file-settings", "ti ti-file-code",
"ti ti-file-zip", "ti ti-files", "ti ti-scan", "ti ti-search", "ti ti-search-off", "ti ti-filter",
"ti ti-filter-cog", "ti ti-filter-search", "ti ti-zoom-scan", "ti ti-map-search", "ti ti-binary", "ti ti-braces",
"ti ti-brackets", "ti ti-code", "ti ti-code-circle", "ti ti-code-dots", "ti ti-code-plus", "ti ti-code-minus",
"ti ti-api", "ti ti-terminal", "ti ti-terminal-2", "ti ti-command", "ti ti-prompt", "ti ti-script",
"ti ti-bug", "ti ti-bug-off", "ti ti-test-pipe", "ti ti-progress-check", "ti ti-checks", "ti ti-checkup-list",
"ti ti-chart-bar", "ti ti-chart-donut", "ti ti-chart-line", "ti ti-chart-area", "ti ti-chart-histogram", "ti ti-chart-infographic",
"ti ti-activity", "ti ti-activity-heartbeat", "ti ti-pulse", "ti ti-timeline", "ti ti-gauge", "ti ti-meter-cube",
"ti ti-wave-sine", "ti ti-wave-square", "ti ti-wave-triangle", "ti ti-heart-rate-monitor", "ti ti-clock", "ti ti-clock-cog",
"ti ti-calendar-time", "ti ti-history", "ti ti-refresh", "ti ti-reload", "ti ti-repeat", "ti ti-rotate-clockwise-2",
"ti ti-cpu", "ti ti-cpu-2", "ti ti-microchip", "ti ti-memory", "ti ti-ram", "ti ti-gpu-card",
"ti ti-disc", "ti ti-disc-off", "ti ti-hard-drive", "ti ti-device-sd-card", "ti ti-sd-card", "ti ti-device-usb",
"ti ti-usb", "ti ti-plug", "ti ti-plug-connected", "ti ti-power", "ti ti-bolt", "ti ti-bolt-off",
"ti ti-battery", "ti ti-battery-vertical", "ti ti-sun-electricity", "ti ti-circle-dashed", "ti ti-circle-check", "ti ti-circle-x",
"ti ti-circle-plus", "ti ti-circle-minus", "ti ti-circle-key", "ti ti-key", "ti ti-key-off", "ti ti-keyframe",
"ti ti-lock", "ti ti-lock-access", "ti ti-lock-check", "ti ti-shield", "ti ti-shield-check", "ti ti-shield-lock",
"ti ti-shield-half-filled", "ti ti-shield-x", "ti ti-eye-shield", "ti ti-fingerprint", "ti ti-automation", "ti ti-robot",
"ti ti-settings", "ti ti-settings-2", "ti ti-settings-automation", "ti ti-adjustments", "ti ti-adjustments-alt", "ti ti-adjustments-bolt",
"ti ti-tool", "ti ti-tools", "ti ti-wrench", "ti ti-hammer", "ti ti-screwdriver", "ti ti-stethoscope",
"ti ti-tags", "ti ti-tag", "ti ti-tag-starred", "ti ti-pin", "ti ti-pinned", "ti ti-bookmark",
"ti ti-flag", "ti ti-flag-2", "ti ti-user-cog", "ti ti-user-shield", "ti ti-users-group", "ti ti-building-community",
}
}
func hasUpdateDay(days, code string) bool {
for _, item := range strings.Split(strings.ToLower(strings.TrimSpace(days)), ",") {
if strings.TrimSpace(item) == strings.ToLower(strings.TrimSpace(code)) {
return true
}
}
return false
}
func updateWindowSummary(start, end, days string) string {
if strings.TrimSpace(start) == "" || strings.TrimSpace(end) == "" {
if strings.TrimSpace(days) == "" {
return "Any time"
}
return strings.ToUpper(strings.ReplaceAll(days, ",", " "))
}
if strings.TrimSpace(days) == "" {
return start + " - " + end
}
return strings.ToUpper(strings.ReplaceAll(days, ",", " ")) + " · " + start + " - " + end
}
func icon(name string) template.HTML {
icons := map[string]string{
"dashboard": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 13h7V4H4zm9 7h7v-9h-7zM4 20h7v-5H4zm9-9h7V4h-7z"/></svg>`,
"nodes": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="6" rx="2"/><rect x="3" y="14" width="18" height="6" rx="2"/><path d="M7 7h.01M7 17h.01"/></svg>`,
"groups": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M7 7h10M7 12h10M7 17h6"/><rect x="3" y="4" width="18" height="16" rx="2"/></svg>`,
"jobs": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="8"/><path d="M12 8v5l3 2"/></svg>`,
"settings": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 3l2.1 2.4 3.1-.4.9 3 2.8 1.4-1.4 2.8 1.4 2.8-2.8 1.4-.9 3-3.1-.4L12 21l-2.1-2.4-3.1.4-.9-3-2.8-1.4 1.4-2.8-1.4-2.8L5.9 8l.9-3 3.1.4z"/><circle cx="12" cy="12" r="3"/></svg>`,
"logout": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="M10 17l5-5-5-5"/><path d="M15 12H3"/></svg>`,
"server": `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="4" y="5" width="16" height="5" rx="2"/><rect x="4" y="14" width="16" height="5" rx="2"/><path d="M8 7.5h.01M8 16.5h.01"/></svg>`,
"ubuntu": `<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="3"/><circle cx="6" cy="12" r="2"/><circle cx="15" cy="6.5" r="2"/><circle cx="15" cy="17.5" r="2"/><path d="M8 12h2M13 8l-1.5 2M13 16l-1.5-2"/></svg>`,
"debian": `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M14 5c2.9 0 5 2 5 5 0 2.1-1.3 4-3.3 4.8.7-.8 1.1-1.9 1.1-3.1 0-2.5-1.7-4.4-4.3-4.4-2.3 0-4 1.7-4 4 0 2.2 1.5 4.1 3.8 5.1 1.2.5 2.6.7 3.7 1.4.9.6 1.4 1.4 1.4 2.3 0 1.8-1.7 3-4.2 3-3.3 0-5.9-2-5.9-4.8 0-1.5.8-2.6 2.2-3.2-.3.5-.4 1-.4 1.6 0 2.2 2 3.8 4.7 3.8 1.9 0 3.1-.7 3.1-1.9 0-.6-.4-1.1-1.2-1.5-1-.5-2.4-.7-3.8-1.3C8.8 16 7 13.9 7 11.2 7 7.7 9.8 5 14 5Z"/></svg>`,
"arch": `<svg viewBox="0 0 24 24" fill="currentColor"><path d="m12 3 6.2 14.8c-.9-.5-1.9-.8-2.8-.6-.6.1-1.1.5-1.6 1-.3-.3-.6-.8-.8-1.5-.3-1-.5-2.4-.4-4.2-.4.9-.7 1.9-.8 2.9-.1.9-.1 1.8 0 2.6-.8-.4-1.7-.5-2.6-.2-.6.2-1.2.5-1.7 1.1L12 3Z"/></svg>`,
}
if svg, ok := icons[name]; ok {
return template.HTML(svg)
}
return template.HTML(`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/></svg>`)
}