feat(matrix): add HTML formatted messages with severity colors

Matrix notifications now send both a plain-text fallback and an
HTML formatted_body (org.matrix.custom.html) so Element and other
Matrix clients render rich, colored alerts:

- Down/failed/error → red (#E74C3C)
- Warning/expiring  → yellow (#F39C12)
- Maintenance/paused → orange (#E67E22)
- Up/resolved       → green (#2ECC71)
- Default/info      → blue (#3498DB)

Detail lines from the plain-text message are rendered as a <ul>
bullet list beneath the colored heading. Plain Body is preserved
for clients without HTML support.

Also includes resource type (CPU/DISK/etc.) in fallback server
alert messages to improve context.
This commit is contained in:
MrSchneemann
2026-03-01 10:56:56 +01:00
parent 91ceddc489
commit 463299fa73
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"html"
"net/http"
"net/url"
"strings"
@@ -26,7 +27,10 @@ type matrixMessagePayload struct {
FormattedBody string `json:"formatted_body,omitempty"`
}
// SendNotification sends a plain message to a Matrix room
// SendNotification sends a formatted message to a Matrix room.
// It sends both a plain-text fallback body and an HTML formatted_body with
// severity-based colors so clients that support Matrix's custom HTML format
// display a rich, colored notification.
func (ms *MatrixService) SendNotification(config *AlertConfiguration, message string) error {
if config.MatrixHomeserver == "" || config.MatrixRoomID == "" || config.MatrixAccessToken == "" {
return fmt.Errorf("matrix homeserver, room ID and access token are required")
@@ -45,8 +49,10 @@ func (ms *MatrixService) SendNotification(config *AlertConfiguration, message st
)
payload := matrixMessagePayload{
MsgType: "m.text",
Body: message,
MsgType: "m.text",
Body: message,
Format: "org.matrix.custom.html",
FormattedBody: ms.buildHTMLBody(message),
}
jsonData, err := json.Marshal(payload)
@@ -75,6 +81,74 @@ func (ms *MatrixService) SendNotification(config *AlertConfiguration, message st
return nil
}
// buildHTMLBody converts a plain-text notification message into Matrix-compatible HTML.
// The first line becomes a colored bold heading based on alert severity; subsequent
// detail lines are rendered as a bullet list. Clients that don't support HTML fall
// back to the plain Body field.
func (ms *MatrixService) buildHTMLBody(plainText string) string {
lines := strings.Split(strings.TrimSpace(plainText), "\n")
if len(lines) == 0 {
return "<p>" + html.EscapeString(plainText) + "</p>"
}
title := strings.TrimSpace(lines[0])
color := ms.statusColor(title)
var sb strings.Builder
sb.WriteString(fmt.Sprintf("<p><strong><font color=%q>%s</font></strong></p>", color, html.EscapeString(title)))
var items []string
for _, line := range lines[1:] {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// Strip leading bullet markers added by the plain-text generators
line = strings.TrimPrefix(line, "•")
line = strings.TrimPrefix(line, "-")
line = strings.TrimSpace(line)
if line != "" {
items = append(items, "<li>"+html.EscapeString(line)+"</li>")
}
}
if len(items) > 0 {
sb.WriteString("<ul>")
for _, item := range items {
sb.WriteString(item)
}
sb.WriteString("</ul>")
}
sb.WriteString("<p><em>CheckCle System Alert</em></p>")
return sb.String()
}
// statusColor returns an HTML hex color string that reflects the alert severity
// detected from keywords and emoji in the message heading.
func (ms *MatrixService) statusColor(message string) string {
lower := strings.ToLower(message)
if strings.Contains(lower, "🔴") || strings.Contains(lower, "down") ||
strings.Contains(lower, "expired") || strings.Contains(lower, "failed") ||
strings.Contains(lower, "critical") || strings.Contains(lower, "error") {
return "#E74C3C" // Red
}
if strings.Contains(lower, "🟡") || strings.Contains(lower, "warning") ||
strings.Contains(lower, "expiring") {
return "#F39C12" // Yellow
}
if strings.Contains(lower, "🟠") || strings.Contains(lower, "maintenance") ||
strings.Contains(lower, "paused") {
return "#E67E22" // Orange
}
if strings.Contains(lower, "🟢") || strings.Contains(lower, "up") ||
strings.Contains(lower, "resolved") || strings.Contains(lower, "restored") ||
strings.Contains(lower, "success") {
return "#2ECC71" // Green
}
return "#3498DB" // Blue — info / default
}
// SendServerNotification sends a server-specific notification via Matrix
func (ms *MatrixService) SendServerNotification(config *AlertConfiguration, payload *NotificationPayload, template *ServerNotificationTemplate, resourceType string) error {
message := ms.generateServerMessage(payload, template, resourceType)
@@ -297,5 +371,9 @@ func (ms *MatrixService) generateDefaultServerMessage(payload *NotificationPaylo
case "warning":
statusEmoji = "🟡"
}
return fmt.Sprintf("%s 🖥️ Server %s (%s) status: %s", statusEmoji, payload.ServiceName, payload.Hostname, strings.ToUpper(payload.Status))
resource := strings.ToUpper(resourceType)
if resource == "" {
resource = "GENERAL"
}
return fmt.Sprintf("%s 🖥️ Server %s (%s) [%s] status: %s", statusEmoji, payload.ServiceName, payload.Hostname, resource, strings.ToUpper(payload.Status))
}