Compare commits
11 Commits
v0.0.1-rc1
...
v0.0.1-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a1ea39f78 | |||
| e8656b09a7 | |||
| 5c54a1cd91 | |||
| 7b45c8fcf0 | |||
| cbb311d877 | |||
| d30dd5906a | |||
| 40c0d9e5a0 | |||
| d1913c4278 | |||
| eedd86e571 | |||
| c07079497b | |||
| 4cfd7159bf |
@@ -22,9 +22,9 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.23'
|
go-version: '1.24'
|
||||||
check-latest: true
|
check-latest: false
|
||||||
cache: true
|
cache: false
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -race ./...
|
run: go test -race ./...
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ A single static binary contains the daemon, the CLI, and everything in
|
|||||||
between. Inter-node traffic is mutual TLS with SSH-style fingerprint
|
between. Inter-node traffic is mutual TLS with SSH-style fingerprint
|
||||||
trust — no central CA, no shared secret.
|
trust — no central CA, no shared secret.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From pre-built binary
|
||||||
|
|
||||||
|
This can be done in one step, either by downloading the latest release from
|
||||||
|
the [Gitea releases page](https://git.cer.sh/axodouble/quptime/releases) or by running the following script:
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.cer.sh/Axodouble/QUptime/raw/branch/master/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
Most uptime monitors are either a SaaS or a single box that, by
|
Most uptime monitors are either a SaaS or a single box that, by
|
||||||
@@ -210,17 +220,17 @@ every two seconds.
|
|||||||
|
|
||||||
Keybindings:
|
Keybindings:
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|---|---|
|
| ------------------- | --------------------------------------------------------- |
|
||||||
| `↑` / `↓` | move cursor within a tab |
|
| `↑` / `↓` | move cursor within a tab |
|
||||||
| `Tab` / `Shift+Tab` | next / previous tab |
|
| `Tab` / `Shift+Tab` | next / previous tab |
|
||||||
| `1` / `2` / `3` | jump to Peers / Checks / Alerts |
|
| `1` / `2` / `3` | jump to Peers / Checks / Alerts |
|
||||||
| `r` | force-refresh |
|
| `r` | force-refresh |
|
||||||
| `a` | add (opens a picker on Checks/Alerts; node form on Peers) |
|
| `a` | add (opens a picker on Checks/Alerts; node form on Peers) |
|
||||||
| `d` | remove the selected row (confirmation prompt) |
|
| `d` | remove the selected row (confirmation prompt) |
|
||||||
| `t` | send a test message to the selected alert |
|
| `t` | send a test message to the selected alert |
|
||||||
| `D` | toggle the selected alert's `default` flag |
|
| `D` | toggle the selected alert's `default` flag |
|
||||||
| `q` / `Ctrl+C` | quit |
|
| `q` / `Ctrl+C` | quit |
|
||||||
|
|
||||||
Forms run the same control-plane methods the CLI does, so any side
|
Forms run the same control-plane methods the CLI does, so any side
|
||||||
effect (a mutation, a node add, an alert test) ends up routed through
|
effect (a mutation, a node add, an alert test) ends up routed through
|
||||||
@@ -247,21 +257,27 @@ qu alert add smtp ops --host ... --from ... --to ... \
|
|||||||
|
|
||||||
Available template variables:
|
Available template variables:
|
||||||
|
|
||||||
| Variable | Meaning |
|
| Variable | Meaning |
|
||||||
|---|---|
|
| ----------------------- | ------------------------------------------ |
|
||||||
| `{{.Check.Name}}` | check name |
|
| `{{.Check.Name}}` | check name |
|
||||||
| `{{.Check.Type}}` | `http` / `tcp` / `icmp` |
|
| `{{.Check.Type}}` | `http` / `tcp` / `icmp` |
|
||||||
| `{{.Check.Target}}` | URL or host:port being probed |
|
| `{{.Check.Target}}` | URL or host:port being probed |
|
||||||
| `{{.Check.ID}}` | UUID |
|
| `{{.Check.ID}}` | UUID |
|
||||||
| `{{.From}}` | previous state (`up` / `down` / `unknown`) |
|
| `{{.From}}` | previous state (`up` / `down` / `unknown`) |
|
||||||
| `{{.To}}` | new state |
|
| `{{.To}}` | new state |
|
||||||
| `{{.Verb}}` | `UP` / `DOWN` / `RECOVERED` |
|
| `{{.Verb}}` | `UP` / `DOWN` / `RECOVERED` |
|
||||||
| `{{.Snapshot.Reports}}` | total per-node reports counted |
|
| `{{.Snapshot.Reports}}` | total per-node reports counted |
|
||||||
| `{{.Snapshot.OKCount}}` | how many reported OK |
|
| `{{.Snapshot.OKCount}}` | how many reported OK |
|
||||||
| `{{.Snapshot.NotOK}}` | how many reported failure |
|
| `{{.Snapshot.NotOK}}` | how many reported failure |
|
||||||
| `{{.Snapshot.Detail}}` | first failure detail string |
|
| `{{.Snapshot.Detail}}` | first failure detail string |
|
||||||
| `{{.NodeID}}` | master that dispatched |
|
| `{{.NodeID}}` | master that dispatched |
|
||||||
| `{{.When}}` | RFC3339 timestamp |
|
| `{{.When}}` | RFC3339 timestamp |
|
||||||
|
|
||||||
|
The same variable list is surfaced in-app: `qu alert add smtp --help`,
|
||||||
|
`qu alert add discord --help`, and `qu alert edit --help` each print
|
||||||
|
it under their flag table, and `qu tui` shows a compact reminder of
|
||||||
|
the supported variables as a hint when the cursor lands on a Subject
|
||||||
|
or Body template field in the add/edit alert forms.
|
||||||
|
|
||||||
`qu alert test <name>` exercises the template against a synthetic
|
`qu alert test <name>` exercises the template against a synthetic
|
||||||
"homepage going DOWN" transition, so you can verify rendering before
|
"homepage going DOWN" transition, so you can verify rendering before
|
||||||
|
|||||||
+74
-20
@@ -1,24 +1,78 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Check if ~/.local/bin exists, if not, create it
|
# Helper function which echo's all commands before executing them in grayscale prefixed with >
|
||||||
if [ ! -d "$HOME/.local/bin" ]; then
|
echo_cmd() {
|
||||||
mkdir -p "$HOME/.local/bin"
|
echo -e "\033[90m> $1\033[0m"
|
||||||
fi
|
eval "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# Check if ~/.local/bin is in the PATH, if not, give the user a command to add it
|
# Check if jq and curl are installed, if not, error out and ask the user to install them
|
||||||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
if ! command -v jq > /dev/null; then
|
||||||
echo "Please add the following line to your shell configuration file (e.g., ~/.bashrc, ~/.zshrc) to include ~/.local/bin in your PATH:"
|
echo "Error: jq is not installed. Please install jq and try again."
|
||||||
echo 'export PATH="$HOME/.local/bin:$PATH"'
|
|
||||||
echo "After adding the line, please restart your terminal or run 'source ~/.bashrc' (or the appropriate command for your shell) to apply the changes."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Download the binary from git.cer.sh/axodouble/quptime
|
|
||||||
# Check whether curl or wget is available
|
|
||||||
if command -v curl > /dev/null; then
|
|
||||||
curl -L -o "$HOME/.local/bin/quptime" "https://git.cer.sh/axodouble/quptime/-/raw/main/quptime"
|
|
||||||
elif command -v wget > /dev/null; then
|
|
||||||
wget -O "$HOME/.local/bin/quptime" "https://git.cer.sh/axodouble/quptime/-/raw/main/quptime"
|
|
||||||
else
|
|
||||||
echo "Error: Neither curl nor wget is installed. Please install one of these tools to download the quptime binary."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if ! command -v curl > /dev/null; then
|
||||||
|
echo "Error: curl is not installed. Please install curl and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the user is allowed to write to /usr/local/bin, if so, install qu there, else error out and ask the user to install qu manually
|
||||||
|
if [ -w "/usr/local/bin" ]; then
|
||||||
|
# Get release tag by $(curl -s https://git.cer.sh/api/v1/repos/axodouble/quptime/releases/latest | jq -r '.tag_name')
|
||||||
|
RELEASE=$(curl -s https://git.cer.sh/api/v1/repos/axodouble/quptime/releases/latest | jq -r '.tag_name')
|
||||||
|
# Download the latest release binary from the Git repository and save it to /usr/local/bin/qu
|
||||||
|
if command -v curl > /dev/null; then
|
||||||
|
echo_cmd "curl -L -o \"/usr/local/bin/qu\" \"https://git.cer.sh/axodouble/quptime/releases/download/${RELEASE}/qu-${RELEASE}-linux-amd64\""
|
||||||
|
echo_cmd "chmod +x \"/usr/local/bin/qu\""
|
||||||
|
echo "> qu has been installed to /usr/local/bin/qu"
|
||||||
|
|
||||||
|
# Drop completions into the directories each shell already scans.
|
||||||
|
# No rc-file edits, and uninstall is just `rm`. Silently skips
|
||||||
|
# shells whose completion dir is absent.
|
||||||
|
write_completion() {
|
||||||
|
local shell=$1 path=$2
|
||||||
|
[ -d "$(dirname "$path")" ] || return 1
|
||||||
|
if /usr/local/bin/qu completion "$shell" > "$path" 2>/dev/null; then
|
||||||
|
echo "> installed $shell completion -> $path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
rm -f "$path"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
write_completion bash /usr/share/bash-completion/completions/qu \
|
||||||
|
|| write_completion bash /etc/bash_completion.d/qu
|
||||||
|
write_completion zsh /usr/share/zsh/site-functions/_qu
|
||||||
|
write_completion fish /usr/share/fish/vendor_completions.d/qu.fish
|
||||||
|
else
|
||||||
|
echo "Error: curl is not installed. Please install curl and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Error: You are not allowed to write to /usr/local/bin. Please install qu manually, or run this script with sudo."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the user has systemd, if so create a systemd service file for qu serve
|
||||||
|
if command -v systemctl > /dev/null; then
|
||||||
|
echo "> Creating systemd service file for qu serve..."
|
||||||
|
cat <<EOL > /etc/systemd/system/qu-serve.service
|
||||||
|
[Unit]
|
||||||
|
Description=QUptime Serve
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/qu serve
|
||||||
|
Restart=always
|
||||||
|
User=$(whoami)
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
echo_cmd "systemctl daemon-reload"
|
||||||
|
echo_cmd "systemctl enable qu-serve.service"
|
||||||
|
echo "> qu serve service has been created and enabled. You can start it with 'systemctl start qu-serve.service'"
|
||||||
|
else
|
||||||
|
echo "> Warning: systemd is not available on this system. qu serve will not be automatically started on boot. You can start it manually with '/usr/local/bin/qu serve'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installation complete, before starting `qu serve`, make sure to run `qu init` and read the documentation."
|
||||||
|
|||||||
@@ -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}}`
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"git.cer.sh/axodouble/quptime/internal/alerts"
|
||||||
"git.cer.sh/axodouble/quptime/internal/config"
|
"git.cer.sh/axodouble/quptime/internal/config"
|
||||||
"git.cer.sh/axodouble/quptime/internal/daemon"
|
"git.cer.sh/axodouble/quptime/internal/daemon"
|
||||||
"git.cer.sh/axodouble/quptime/internal/transport"
|
"git.cer.sh/axodouble/quptime/internal/transport"
|
||||||
@@ -21,9 +22,9 @@ import (
|
|||||||
// variants (if non-empty) and returns the effective subject + body
|
// variants (if non-empty) and returns the effective subject + body
|
||||||
// template strings. Inline flags take precedence over file flags.
|
// template strings. Inline flags take precedence over file flags.
|
||||||
func bindTemplateFlags(cmd *cobra.Command) {
|
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("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")
|
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.
|
take effect; everything else is preserved.
|
||||||
|
|
||||||
The type (smtp/discord) cannot be changed in place — delete and re-add
|
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),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
||||||
@@ -308,6 +311,7 @@ func buildSMTPAddCmd() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "smtp <name>",
|
Use: "smtp <name>",
|
||||||
Short: "Add an SMTP relay alert",
|
Short: "Add an SMTP relay alert",
|
||||||
|
Long: "Add an SMTP relay alert.\n\n" + alerts.TemplateVarsHelp(),
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
||||||
@@ -365,6 +369,7 @@ func buildDiscordAddCmd() *cobra.Command {
|
|||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "discord <name>",
|
Use: "discord <name>",
|
||||||
Short: "Add a Discord webhook alert",
|
Short: "Add a Discord webhook alert",
|
||||||
|
Long: "Add a Discord webhook alert.\n\n" + alerts.TemplateVarsHelp(),
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"git.cer.sh/axodouble/quptime/internal/alerts"
|
||||||
"git.cer.sh/axodouble/quptime/internal/config"
|
"git.cer.sh/axodouble/quptime/internal/config"
|
||||||
"git.cer.sh/axodouble/quptime/internal/daemon"
|
"git.cer.sh/axodouble/quptime/internal/daemon"
|
||||||
"git.cer.sh/axodouble/quptime/internal/transport"
|
"git.cer.sh/axodouble/quptime/internal/transport"
|
||||||
@@ -272,7 +273,7 @@ func newAddDiscordForm() *form {
|
|||||||
textField("Name", "human-friendly identifier", true),
|
textField("Name", "human-friendly identifier", true),
|
||||||
textField("Webhook URL", "https://discord.com/api/webhooks/...", true),
|
textField("Webhook URL", "https://discord.com/api/webhooks/...", true),
|
||||||
textField("Default", "yes/no — attach to every check automatically", false),
|
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 newForm("Add Discord alert", fields, func(vals []string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
@@ -303,8 +304,8 @@ func newAddSMTPForm() *form {
|
|||||||
textField("To", "comma-separated recipient addresses", true),
|
textField("To", "comma-separated recipient addresses", true),
|
||||||
textField("StartTLS", "yes/no — default yes", false),
|
textField("StartTLS", "yes/no — default yes", false),
|
||||||
textField("Default", "yes/no — attach to every check", false),
|
textField("Default", "yes/no — attach to every check", false),
|
||||||
textField("Subject template", "optional", false),
|
textField("Subject template", alerts.TemplateVarsHint(), false),
|
||||||
textField("Body template", "optional", false),
|
textField("Body template", alerts.TemplateVarsHint(), false),
|
||||||
}
|
}
|
||||||
return newForm("Add SMTP alert", fields, func(vals []string) tea.Cmd {
|
return newForm("Add SMTP alert", fields, func(vals []string) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
@@ -445,7 +446,7 @@ func newEditDiscordForm(existing config.Alert) *form {
|
|||||||
textFieldWithValue("Name", "human-friendly identifier", existing.Name, true),
|
textFieldWithValue("Name", "human-friendly identifier", existing.Name, true),
|
||||||
textFieldWithValue("Webhook URL", "https://discord.com/api/webhooks/...", existing.DiscordWebhook, 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("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
|
id := existing.ID
|
||||||
subject := existing.SubjectTemplate
|
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("To", "comma-separated recipient addresses", strings.Join(existing.SMTPTo, ","), true),
|
||||||
textFieldWithValue("StartTLS", "yes/no — default yes", boolStr(existing.SMTPStartTLS), false),
|
textFieldWithValue("StartTLS", "yes/no — default yes", boolStr(existing.SMTPStartTLS), false),
|
||||||
textFieldWithValue("Default", "yes/no — attach to every check", boolStr(existing.Default), false),
|
textFieldWithValue("Default", "yes/no — attach to every check", boolStr(existing.Default), false),
|
||||||
textFieldWithValue("Subject template", "optional", existing.SubjectTemplate, false),
|
textFieldWithValue("Subject template", alerts.TemplateVarsHint(), existing.SubjectTemplate, false),
|
||||||
textFieldWithValue("Body template", "optional", existing.BodyTemplate, false),
|
textFieldWithValue("Body template", alerts.TemplateVarsHint(), existing.BodyTemplate, false),
|
||||||
}
|
}
|
||||||
id := existing.ID
|
id := existing.ID
|
||||||
return newForm("Edit SMTP alert", fields, func(vals []string) tea.Cmd {
|
return newForm("Edit SMTP alert", fields, func(vals []string) tea.Cmd {
|
||||||
|
|||||||
@@ -58,14 +58,18 @@ var (
|
|||||||
stateUnknownStyle = lipgloss.NewStyle().Foreground(colorMuted)
|
stateUnknownStyle = lipgloss.NewStyle().Foreground(colorMuted)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// renderState returns a plain-text state label for use inside the
|
||||||
|
// bubbles table. The table truncates cells with runewidth.Truncate
|
||||||
|
// which counts the printable bytes of ANSI escape sequences toward
|
||||||
|
// column width, so a styled value gets chopped down to just "…".
|
||||||
func renderState(s string) string {
|
func renderState(s string) string {
|
||||||
switch s {
|
switch s {
|
||||||
case "up":
|
case "up":
|
||||||
return stateUpStyle.Render("● up")
|
return "● up"
|
||||||
case "down":
|
case "down":
|
||||||
return stateDownStyle.Render("● down")
|
return "● down"
|
||||||
default:
|
default:
|
||||||
return stateUnknownStyle.Render("○ unknown")
|
return "○ unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user