Initial commit: full Phases 1-6 implementation
CI / Lint + build + test (push) Has been cancelled

Post-repair hardware validation pipeline for Proxmox cluster hosts.
Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq
PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
This commit is contained in:
2026-04-17 21:32:10 -04:00
commit 9bb4b09a04
98 changed files with 11960 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
package notify
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// NtfyNotifier posts to ntfy.sh (or a self-hosted ntfy server). Message
// body is the plain text body; title and URL are passed via X-Title and
// X-Click headers so ntfy renders them as the push title + deep link.
type NtfyNotifier struct {
NameStr string
Server string // e.g. "https://ntfy.sh" or self-hosted
Topic string
HTTP *http.Client
}
func NewNtfy(name, server, topic string) *NtfyNotifier {
if server == "" {
server = "https://ntfy.sh"
}
return &NtfyNotifier{
NameStr: name,
Server: strings.TrimRight(server, "/"),
Topic: topic,
HTTP: &http.Client{Timeout: 10 * time.Second},
}
}
func (n *NtfyNotifier) Name() string { return n.NameStr }
func (n *NtfyNotifier) Send(ctx context.Context, ev Event) error {
if n.Topic == "" {
return fmt.Errorf("ntfy: no topic configured")
}
url := n.Server + "/" + n.Topic
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(ev.Body))
if err != nil {
return err
}
if ev.Title != "" {
req.Header.Set("X-Title", ev.Title)
}
if ev.URL != "" {
req.Header.Set("X-Click", ev.URL)
}
req.Header.Set("X-Priority", priorityForSeverity(ev.Severity))
req.Header.Set("X-Tags", ntfyTag(ev.Kind, ev.Severity))
resp, err := n.HTTP.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("ntfy: %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
return nil
}
// priorityForSeverity maps our severities to ntfy's 15 scale. "info"
// → 3 (default), warning → 4, critical → 5.
func priorityForSeverity(s Severity) string {
switch s {
case SeverityCritical:
return "5"
case SeverityWarning:
return "4"
default:
return "3"
}
}
func ntfyTag(k Kind, s Severity) string {
switch {
case s == SeverityCritical:
return "rotating_light," + string(k)
case k == KindRunCompleted:
return "white_check_mark," + string(k)
case k == KindHoldingOpened:
return "construction," + string(k)
default:
return string(k)
}
}