diff --git a/internal/alerts/help.go b/internal/alerts/help.go new file mode 100644 index 0000000..81901bf --- /dev/null +++ b/internal/alerts/help.go @@ -0,0 +1,44 @@ +package alerts + +// TemplateVarsHint returns a compact, multi-line listing of the +// variables a subject/body template can reference. Designed for +// embedding in TUI form hints where vertical space is tight. +// +// Continuation lines are pre-indented so they line up under the +// first line when the caller prepends a fixed indent (e.g. " "). +func TemplateVarsHint() string { + return "Go text/template — leave empty to use the built-in format.\n" + + " Vars: {{.Check.Name}}, {{.Check.Target}}, {{.Check.Type}}, {{.Check.ID}},\n" + + " {{.Verb}} (UP|DOWN|RECOVERED), {{.From}}, {{.To}}, {{.NodeID}}, {{.When}},\n" + + " {{.Snapshot.Detail}}, {{.Snapshot.Reports}}, {{.Snapshot.OKCount}}, {{.Snapshot.NotOK}}" +} + +// TemplateVarsHelp returns the long-form documentation for available +// template variables, suitable for embedding in a CLI command's Long +// help text. Each variable is described on its own line and an +// example template is included at the end. +func TemplateVarsHelp() string { + return `Subject and body templates use Go text/template syntax. They are +optional — leaving them empty falls back to the built-in format. +Discord ignores the subject template (it has no subject line); SMTP +uses both. + +Available variables: + {{.Check.Name}} check name (e.g. "homepage") + {{.Check.Target}} URL / host:port / host being probed + {{.Check.Type}} http | tcp | icmp + {{.Check.ID}} stable check UUID + {{.Verb}} UP | DOWN | RECOVERED + {{.From}} previous state name + {{.To}} new state name + {{.NodeID}} master node that rendered the message + {{.When}} RFC3339 timestamp of the transition + {{.Snapshot.Detail}} probe detail string (e.g. "connection refused") + {{.Snapshot.Reports}} total reports in the flip window + {{.Snapshot.OKCount}} ok report count + {{.Snapshot.NotOK}} failing report count + +Example body template: + {{.Check.Name}} is {{.Verb}} (target {{.Check.Target}}). + Detail: {{.Snapshot.Detail}}` +} diff --git a/internal/cli/alert.go b/internal/cli/alert.go index 28acb03..b6d3f2c 100644 --- a/internal/cli/alert.go +++ b/internal/cli/alert.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/spf13/cobra" + "git.cer.sh/axodouble/quptime/internal/alerts" "git.cer.sh/axodouble/quptime/internal/config" "git.cer.sh/axodouble/quptime/internal/daemon" "git.cer.sh/axodouble/quptime/internal/transport" @@ -21,9 +22,9 @@ import ( // variants (if non-empty) and returns the effective subject + body // template strings. Inline flags take precedence over file flags. func bindTemplateFlags(cmd *cobra.Command) { - cmd.Flags().String("subject", "", "subject template (text/template syntax — SMTP only)") + cmd.Flags().String("subject", "", "subject template, Go text/template (SMTP only; see --help for variables)") cmd.Flags().String("subject-file", "", "path to a file containing the subject template") - cmd.Flags().String("body", "", "body template (text/template syntax)") + cmd.Flags().String("body", "", "body template, Go text/template (see --help for variables)") cmd.Flags().String("body-file", "", "path to a file containing the body template") } @@ -172,7 +173,9 @@ func buildAlertEditCmd() *cobra.Command { take effect; everything else is preserved. The type (smtp/discord) cannot be changed in place — delete and re-add -the alert if you need to switch channels.`, +the alert if you need to switch channels. + +` + alerts.TemplateVarsHelp(), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second) @@ -308,6 +311,7 @@ func buildSMTPAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "smtp ", Short: "Add an SMTP relay alert", + Long: "Add an SMTP relay alert.\n\n" + alerts.TemplateVarsHelp(), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second) @@ -365,6 +369,7 @@ func buildDiscordAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "discord ", Short: "Add a Discord webhook alert", + Long: "Add a Discord webhook alert.\n\n" + alerts.TemplateVarsHelp(), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second) diff --git a/internal/tui/forms.go b/internal/tui/forms.go index 007c6df..ef88911 100644 --- a/internal/tui/forms.go +++ b/internal/tui/forms.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/google/uuid" + "git.cer.sh/axodouble/quptime/internal/alerts" "git.cer.sh/axodouble/quptime/internal/config" "git.cer.sh/axodouble/quptime/internal/daemon" "git.cer.sh/axodouble/quptime/internal/transport" @@ -272,7 +273,7 @@ func newAddDiscordForm() *form { textField("Name", "human-friendly identifier", true), textField("Webhook URL", "https://discord.com/api/webhooks/...", true), textField("Default", "yes/no — attach to every check automatically", false), - textField("Body template", "leave empty for default formatting", false), + textField("Body template", alerts.TemplateVarsHint(), false), } return newForm("Add Discord alert", fields, func(vals []string) tea.Cmd { return func() tea.Msg { @@ -303,8 +304,8 @@ func newAddSMTPForm() *form { textField("To", "comma-separated recipient addresses", true), textField("StartTLS", "yes/no — default yes", false), textField("Default", "yes/no — attach to every check", false), - textField("Subject template", "optional", false), - textField("Body template", "optional", false), + textField("Subject template", alerts.TemplateVarsHint(), false), + textField("Body template", alerts.TemplateVarsHint(), false), } return newForm("Add SMTP alert", fields, func(vals []string) tea.Cmd { return func() tea.Msg { @@ -445,7 +446,7 @@ func newEditDiscordForm(existing config.Alert) *form { textFieldWithValue("Name", "human-friendly identifier", existing.Name, true), textFieldWithValue("Webhook URL", "https://discord.com/api/webhooks/...", existing.DiscordWebhook, true), textFieldWithValue("Default", "yes/no — attach to every check automatically", boolStr(existing.Default), false), - textFieldWithValue("Body template", "leave empty for default formatting", existing.BodyTemplate, false), + textFieldWithValue("Body template", alerts.TemplateVarsHint(), existing.BodyTemplate, false), } id := existing.ID subject := existing.SubjectTemplate @@ -483,8 +484,8 @@ func newEditSMTPForm(existing config.Alert) *form { textFieldWithValue("To", "comma-separated recipient addresses", strings.Join(existing.SMTPTo, ","), true), textFieldWithValue("StartTLS", "yes/no — default yes", boolStr(existing.SMTPStartTLS), false), textFieldWithValue("Default", "yes/no — attach to every check", boolStr(existing.Default), false), - textFieldWithValue("Subject template", "optional", existing.SubjectTemplate, false), - textFieldWithValue("Body template", "optional", existing.BodyTemplate, false), + textFieldWithValue("Subject template", alerts.TemplateVarsHint(), existing.SubjectTemplate, false), + textFieldWithValue("Body template", alerts.TemplateVarsHint(), existing.BodyTemplate, false), } id := existing.ID return newForm("Edit SMTP alert", fields, func(vals []string) tea.Cmd {