Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions config/alertmanager/alertmanager.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,30 @@ route:
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
receiver: default
receiver: ntfy
routes:
- match:
severity: critical
receiver: default
receiver: ntfy
continue: true

receivers:
- name: ntfy
webhook_configs:
# IMPORTANT: Replace 'example.com' with your actual domain in the URL below
# This URL will not auto-expand ${DOMAIN} unless Alertmanager is started with --config.expand-env
# and has DOMAIN in its environment. Update to your actual domain: https://ntfy.your-domain.com/homelab-alerts
- url: 'https://ntfy.example.com/homelab-alerts'
send_resolved: true
max_alerts: 10
http_config:
tls_config:
insecure_skip_verify: false

- name: default
# Uncomment and configure one of the following:
# Fallback receiver (uncomment and configure if needed)
# webhook_configs:
# - url: http://gotify:80/message?token=YOUR_TOKEN
# - url: http://gotify:8080/message?token=${GOTIFY_TOKEN}
# slack_configs:
# - api_url: YOUR_SLACK_WEBHOOK
# channel: #alerts
Expand Down
51 changes: 51 additions & 0 deletions config/ntfy/server.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# ntfy server configuration
# Documentation: https://docs.ntfy.sh/config/

# Base URL for external access - configure via NTFY_BASE_URL environment variable
# Leave unset here and configure via NTFY_BASE_URL environment variable.
# base-url: "https://ntfy.example.com"

# Authentication settings
auth-default-access: "deny-all"
auth-file: "/var/lib/ntfy/user.db"

# Cache settings
cache-file: "/var/cache/ntfy/cache.db"
cache-duration: "12h"

# Behind reverse proxy
behind-proxy: true

# Rate limiting
global-rate-limit: "200 burst=50"
visitor-rate-limit: "30 burst=10"

# Message limits
message-size-limit: 4096
message-total-size-limit: "5120MB"
message-delay-max: "3h"

# Topics
topic-list-allowed: false
topic-publish-unauthorized: ["homelab-alerts", "homelab-updates", "homelab-test"]

# Attachments
attachment-cache-dir: "/var/lib/ntfy/attachments"
attachment-size-limit: "15MB"
attachment-total-size-limit: "5120MB"
attachment-expiry-duration: "3h"

# Logging
log-level: "info"

# Web UI
enable-login: true
enable-signup: false

# SMTP for email notifications (optional)
# smtp-sender-addr: "smtp.gmail.com:587"
# smtp-sender-user: "your-email@gmail.com"
# smtp-sender-pass: "your-password"

# Firebase Cloud Messaging (optional, for iOS/Android push)
# firebase-key-file: "/etc/ntfy/firebase.json"
236 changes: 236 additions & 0 deletions scripts/notify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#!/usr/bin/env bash
# Unified notification script for homelab-stack
# Supports ntfy and Gotify notification services
# Usage: notify.sh <topic> <title> <message> [priority] [tags]

set -euo pipefail

# Default configuration
NTFY_BASE_URL="${NTFY_BASE_URL:-https://ntfy.${DOMAIN:-example.com}}"
GOTIFY_URL="${GOTIFY_URL:-http://gotify:8080}"
GOTIFY_TOKEN="${GOTIFY_TOKEN:-}"

# Priority mapping (1=min, 5=max, default=3)
declare -A PRIORITY_MAP=(
["min"]=1 ["1"]=1 ["low"]=2 ["2"]=2
["default"]=3 ["3"]=3 ["normal"]=3
["high"]=4 ["4"]=4 ["urgent"]=5 ["5"]=5 ["max"]=5
)

# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Log function
log() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}

error() {
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1" >&2
}

# Help function
show_help() {
cat << EOF
Unified Notification Script for Homelab Stack

Usage: notify.sh <topic> <title> <message> [priority] [tags]

Arguments:
topic Notification topic/channel (e.g., homelab-alerts, homelab-updates)
title Notification title
message Notification message body
priority Priority level (default: 3)
Options: min(1), low(2), default(3), high(4), max(5)
tags Comma-separated tags (e.g., warning,error,success)

Environment Variables:
NTFY_BASE_URL Ntfy server URL (default: https://ntfy.\${DOMAIN})
GOTIFY_URL Gotify server URL (default: http://gotify:8080)
GOTIFY_TOKEN Gotify application token (optional)
DOMAIN Your domain for ntfy (default: example.com)

Comment on lines +50 to +55
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script always sends an Authorization: Bearer ... header even when NTFY_TOKEN is unset/empty, and NTFY_TOKEN isn’t documented in the help text. Only include the header when a token is set, and document the variable (or support basic auth) to avoid confusing auth failures.

Copilot uses AI. Check for mistakes.
Examples:
# Basic notification
notify.sh homelab-test "Test" "Hello World"

# High priority alert
notify.sh homelab-alerts "Alert" "Service down" high error

# With tags
notify.sh homelab-updates "Update" "Container updated" normal success,docker

# Using environment variables
DOMAIN=mydomain.com notify.sh test "Test" "Message"
EOF
}

# Send notification via ntfy
send_ntfy() {
local topic="$1"
local title="$2"
local message="$3"
local priority="$4"
local tags="$5"

local json_payload="{
\"topic\": \"${topic}\",
\"title\": \"${title}\",
\"message\": \"${message}\",
\"priority\": ${priority}"

if [[ -n "$tags" ]]; then
# Convert comma-separated tags to array
IFS=',' read -ra TAG_ARRAY <<< "$tags"
local tags_json="["
for tag in "${TAG_ARRAY[@]}"; do
tags_json+="\"${tag}\","
done
tags_json="${tags_json%,}]"
json_payload+=", \"tags\": ${tags_json}"
fi

json_payload+="}"

Comment on lines +79 to +97
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ntfy JSON payload is built via string interpolation without JSON escaping. If title or message contains quotes, backslashes, or newlines, the payload becomes invalid JSON (or can be used to inject fields). Build JSON with a proper encoder (e.g., jq -n --arg ...) instead of manual concatenation.

Suggested change
local json_payload="{
\"topic\": \"${topic}\",
\"title\": \"${title}\",
\"message\": \"${message}\",
\"priority\": ${priority}"
if [[ -n "$tags" ]]; then
# Convert comma-separated tags to array
IFS=',' read -ra TAG_ARRAY <<< "$tags"
local tags_json="["
for tag in "${TAG_ARRAY[@]}"; do
tags_json+="\"${tag}\","
done
tags_json="${tags_json%,}]"
json_payload+=", \"tags\": ${tags_json}"
fi
json_payload+="}"
local json_payload
if [[ -n "$tags" ]]; then
# Build JSON with tags, safely escaped via jq
json_payload="$(jq -n \
--arg topic "$topic" \
--arg title "$title" \
--arg message "$message" \
--argjson priority "$priority" \
--arg tags "$tags" \
'{topic: $topic,
title: $title,
message: $message,
priority: $priority,
tags: ($tags | split(",") | map(select(length > 0)))}')"
else
# Build JSON without tags, safely escaped via jq
json_payload="$(jq -n \
--arg topic "$topic" \
--arg title "$title" \
--arg message "$message" \
--argjson priority "$priority" \
'{topic: $topic,
title: $title,
message: $message,
priority: $priority}')"
fi

Copilot uses AI. Check for mistakes.
log "Sending ntfy notification to topic: ${topic}"

if curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${NTFY_TOKEN:-}" \
-d "${json_payload}" \
"${NTFY_BASE_URL}"; then
log "ntfy notification sent successfully"
else
error "Failed to send ntfy notification"
return 1
fi
Comment on lines +98 to +109
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curl -s does not fail on HTTP 4xx/5xx, so the script may log β€œsent successfully” even when the server rejected the request. Use curl -f (and ideally capture/print response body on failure) so HTTP errors are treated as failures.

Copilot uses AI. Check for mistakes.
}

# Send notification via Gotify
send_gotify() {
local topic="$1"
local title="$2"
local message="$3"
local priority="$4"
local tags="$5"

# Gotify doesn't have topics, so we include it in the title
local gotify_title="[${topic}] ${title}"

local json_payload="{
\"title\": \"${gotify_title}\",
\"message\": \"${message}\",
\"priority\": ${priority}"

if [[ -n "$tags" ]]; then
json_payload+=", \"extras\": {
\"client::display\": {
\"contentType\": \"text/markdown\"
}
}"
fi

json_payload+="}"

if [[ -z "$GOTIFY_TOKEN" ]]; then
log "Gotify token not set, skipping Gotify notification"
return 0
fi

log "Sending Gotify notification"

if curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-Gotify-Key: ${GOTIFY_TOKEN}" \
-d "${json_payload}" \
"${GOTIFY_URL}/message"; then
log "Gotify notification sent successfully"
else
error "Failed to send Gotify notification"
return 1
fi
Comment on lines +143 to +154
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as ntfy: Gotify requests use curl -s which won’t fail on HTTP 4xx/5xx. This can report success when the token is invalid or the server returns an error. Use -f and handle non-2xx responses as failures.

Copilot uses AI. Check for mistakes.
}

# Main function
main() {
# Check for help
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
show_help
exit 0
fi

# Validate arguments
if [[ $# -lt 3 ]]; then
error "Missing required arguments"
show_help
exit 1
fi

local topic="$1"
local title="$2"
local message="$3"
local priority="${4:-default}"
local tags="$5"

# Validate and normalize priority
if [[ -n "${PRIORITY_MAP[$priority]}" ]]; then
priority="${PRIORITY_MAP[$priority]}"
else
# Try to parse as number
if [[ "$priority" =~ ^[1-5]$ ]]; then
priority="$priority"
else
error "Invalid priority: $priority. Using default (3)"
priority=3
fi
fi

# Sanitize topic (only allow alphanumeric, dash, underscore)
local sanitized_topic=$(echo "$topic" | tr -cd '[:alnum:]-_')

if [[ "$sanitized_topic" != "$topic" ]]; then
log "Sanitized topic from '$topic' to '$sanitized_topic'"
topic="$sanitized_topic"
fi
Comment on lines +191 to +197
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After sanitizing the topic, the script doesn’t validate that it’s still non-empty. Inputs like --- or only invalid characters will produce an empty topic and result in a broken publish request. Add a post-sanitization check and fail with a clear error.

Copilot uses AI. Check for mistakes.

log "Sending notification:"
log " Topic: ${topic}"
log " Title: ${title}"
log " Message: ${message}"
log " Priority: ${priority}"
[[ -n "$tags" ]] && log " Tags: ${tags}"

# Send notifications
local errors=0

# Send via ntfy (primary)
if ! send_ntfy "$topic" "$title" "$message" "$priority" "$tags"; then
((errors++))
fi

# Send via Gotify (fallback/alternative)
if ! send_gotify "$topic" "$title" "$message" "$priority" "$tags"; then
((errors++))
fi

if [[ $errors -eq 2 ]]; then
error "All notification methods failed"
exit 1
elif [[ $errors -eq 1 ]]; then
log "One notification method failed, but others succeeded"
else
log "All notifications sent successfully"
fi
}

# Make script executable and run
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
# Ensure script is executable
if [[ ! -x "$0" ]]; then
chmod +x "$0"
fi
main "$@"
fi
Loading