// Package templates provides embedded HTML templates for the RouteWatch application package templates import ( _ "embed" "html/template" "net/url" "strings" "sync" "time" "git.eeqj.de/sneak/routewatch/internal/version" ) //go:embed index.html var indexHTML string //go:embed status.html var statusHTML string //go:embed as_detail.html var asDetailHTML string //go:embed prefix_detail.html var prefixDetailHTML string //go:embed prefix_length.html var prefixLengthHTML string // Templates contains all parsed templates type Templates struct { // Index is the template for the home page Index *template.Template // Status is the template for the main status page Status *template.Template // ASDetail is the template for displaying AS (Autonomous System) details ASDetail *template.Template // PrefixDetail is the template for displaying prefix details PrefixDetail *template.Template // PrefixLength is the template for displaying prefixes by length PrefixLength *template.Template } var ( //nolint:gochecknoglobals // Singleton pattern for templates defaultTemplates *Templates //nolint:gochecknoglobals // Singleton pattern for templates once sync.Once ) const ( hoursPerDay = 24 daysPerMonth = 30 cidrPartCount = 2 // A CIDR has two parts: prefix and length ) // timeSince returns a human-readable duration since the given time func timeSince(t time.Time) string { duration := time.Since(t) if duration < time.Minute { return "just now" } if duration < time.Hour { minutes := int(duration.Minutes()) if minutes == 1 { return "1 minute ago" } return duration.Truncate(time.Minute).String() + " ago" } if duration < hoursPerDay*time.Hour { hours := int(duration.Hours()) if hours == 1 { return "1 hour ago" } return duration.Truncate(time.Hour).String() + " ago" } days := int(duration.Hours() / hoursPerDay) if days == 1 { return "1 day ago" } if days < daysPerMonth { return duration.Truncate(hoursPerDay*time.Hour).String() + " ago" } return t.Format("2006-01-02") } // prefixURL generates a URL path for a prefix in CIDR notation. // Takes a prefix like "192.168.1.0/24" and returns "/prefix/192.168.1.0/24" // with the prefix part URL-encoded to handle IPv6 colons. func prefixURL(cidr string) string { // Split CIDR into prefix and length parts := strings.SplitN(cidr, "/", cidrPartCount) if len(parts) != cidrPartCount { // Fallback if no slash found return "/prefix/" + url.PathEscape(cidr) + "/0" } return "/prefix/" + url.PathEscape(parts[0]) + "/" + parts[1] } // initTemplates parses all embedded templates func initTemplates() { var err error defaultTemplates = &Templates{} // Create common template functions funcs := template.FuncMap{ "timeSince": timeSince, "urlEncode": url.QueryEscape, "prefixURL": prefixURL, "appName": func() string { return version.Name }, "appAuthor": func() string { return version.Author }, "appAuthorURL": func() string { return version.AuthorURL }, "appLicense": func() string { return version.License }, "appRepoURL": func() string { return version.RepoURL }, "appGitRevision": func() string { return version.GitRevisionShort }, "appGitCommitURL": func() string { return version.CommitURL() }, } // Parse index template defaultTemplates.Index, err = template.New("index").Funcs(funcs).Parse(indexHTML) if err != nil { panic("failed to parse index template: " + err.Error()) } // Parse status template defaultTemplates.Status, err = template.New("status").Funcs(funcs).Parse(statusHTML) if err != nil { panic("failed to parse status template: " + err.Error()) } // Parse AS detail template defaultTemplates.ASDetail, err = template.New("asDetail").Funcs(funcs).Parse(asDetailHTML) if err != nil { panic("failed to parse AS detail template: " + err.Error()) } // Parse prefix detail template defaultTemplates.PrefixDetail, err = template.New("prefixDetail").Funcs(funcs).Parse(prefixDetailHTML) if err != nil { panic("failed to parse prefix detail template: " + err.Error()) } // Parse prefix length template defaultTemplates.PrefixLength, err = template.New("prefixLength").Funcs(funcs).Parse(prefixLengthHTML) if err != nil { panic("failed to parse prefix length template: " + err.Error()) } } // Get returns the singleton Templates instance func Get() *Templates { once.Do(initTemplates) return defaultTemplates } // IndexTemplate returns the parsed index template func IndexTemplate() *template.Template { return Get().Index } // StatusTemplate returns the parsed status template func StatusTemplate() *template.Template { return Get().Status } // ASDetailTemplate returns the parsed AS detail template func ASDetailTemplate() *template.Template { return Get().ASDetail } // PrefixDetailTemplate returns the parsed prefix detail template func PrefixDetailTemplate() *template.Template { return Get().PrefixDetail } // PrefixLengthTemplate returns the parsed prefix length template func PrefixLengthTemplate() *template.Template { return Get().PrefixLength }