301 lines
13 KiB
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>`)
|
|
}
|