Add config validation and dashboard
Add YAML config loading with validation, env overrides for secrets, and a web dashboard exposing health, stats, and recent events.
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
module main
|
||||
|
||||
import os
|
||||
import flag
|
||||
|
||||
const version = '0.1.0'
|
||||
const author = 'Bruno Charest'
|
||||
|
||||
fn main() {
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
fp.application('ntfy-bridge')
|
||||
fp.version(version)
|
||||
fp.description('Homelab notification hub — aggregates Gitea, Docker, Uptime Kuma → Ntfy')
|
||||
fp.skip_executable()
|
||||
|
||||
config_path := fp.string('config', `c`, 'ntfy-bridge.yaml', 'Path to config file (YAML)')
|
||||
validate_only := fp.bool('validate', `v`, false, 'Validate config and exit')
|
||||
quiet := fp.bool('quiet', `q`, false, 'Quiet mode — suppress startup banner (for scripts/systemd)')
|
||||
version_only := fp.bool('version', `V`, false, 'Print version and exit')
|
||||
|
||||
// Show help if no args or --help/-h requested
|
||||
if os.args.len <= 1 || '--help' in os.args || '-h' in os.args {
|
||||
print_help()
|
||||
return
|
||||
}
|
||||
|
||||
fp.finalize() or {
|
||||
eprintln(err)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if version_only {
|
||||
println('ntfy-bridge ${version}')
|
||||
return
|
||||
}
|
||||
|
||||
cfg := load_config(config_path) or {
|
||||
eprintln('Failed to load config: ${err}')
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if validate_only {
|
||||
println('Config is valid')
|
||||
return
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
print_header()
|
||||
println('')
|
||||
print_startup_info(cfg)
|
||||
}
|
||||
|
||||
mut app := new_app(cfg, quiet)
|
||||
app.run() or {
|
||||
eprintln('Server error: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ── ASCII Art Header ─────────────────────────────────────────────────────
|
||||
|
||||
const box_w = 60 // inner width between ║ borders
|
||||
|
||||
fn build_header() string {
|
||||
// Tagline — centered within the box
|
||||
tag_text := 'Homelab Notification Hub · v${version}'
|
||||
tag_left := (box_w - tag_text.len) / 2
|
||||
tag_right := box_w - tag_left - tag_text.len
|
||||
tag_line := ' '.repeat(tag_left) + tag_text + ' '.repeat(tag_right)
|
||||
|
||||
// Version line — left-aligned with 5-space indent
|
||||
vinfo := ' Version ${version} │ ${author}'
|
||||
vinfo_pad := box_w - vinfo.len
|
||||
|
||||
return '╔══════════════════════════════════════════════════════════════╗\n' +
|
||||
'║ __ ____ __ _ __ ║\n' +
|
||||
'║ ____ / /_/ __/_ __ / /_ _____(_)___/ /___ ____ ║\n' +
|
||||
'║ / __ \\/ __/ /_/ / / /_____/ __ \\/ ___/ / __ / __ `/ _ \\ ║\n' +
|
||||
'║ / / / / /_/ __/ /_/ /_____/ /_/ / / / / /_/ / /_/ / __/ ║\n' +
|
||||
'║ /_/ /_/\\__/_/ \\__, / /_.___/_/ /_/\\__,_/\\__, /\\___/ ║\n' +
|
||||
'║ /____/ /____/ ║\n' +
|
||||
'║' + tag_line + '║\n' +
|
||||
'╠══════════════════════════════════════════════════════════════╣\n' +
|
||||
'║' + vinfo + ' '.repeat(vinfo_pad) + '║\n' +
|
||||
'╚══════════════════════════════════════════════════════════════╝'
|
||||
}
|
||||
|
||||
fn print_header() {
|
||||
println(build_header())
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println('')
|
||||
println(build_header())
|
||||
println('')
|
||||
println(' A simple daemon to aggregate notifications from multiple sources')
|
||||
println(' (Gitea, Uptime Kuma, Docker, HTTP health checks, Cron scripts)')
|
||||
println(' and forward them as formatted Ntfy push notifications.')
|
||||
println('')
|
||||
println('Usage:')
|
||||
println(' ntfy-bridge [flags]')
|
||||
println(' ntfy-bridge --config ntfy-bridge.yaml')
|
||||
println('')
|
||||
println('Flags:')
|
||||
println(' -c, --config <path> Path to config file (default: ntfy-bridge.yaml)')
|
||||
println(' -v, --validate Validate config and exit')
|
||||
println(' -q, --quiet Quiet mode — suppress startup banner')
|
||||
println(' -V, --version Print version and exit')
|
||||
println(' -h, --help Show this help message')
|
||||
println('')
|
||||
println('Config:')
|
||||
println(' Copy ntfy-bridge.example.yaml → ntfy-bridge.yaml and edit it.')
|
||||
println(' Use NTFY_TOKEN and NTFY_HMAC_SECRET env vars for secrets.')
|
||||
println('')
|
||||
println('Docs: https://git.dracodev.net/bruno/ntfy-bridge')
|
||||
println(line_double_r)
|
||||
println(' Version: ${version} | Author: ${author}')
|
||||
println(line_double_r)
|
||||
println('')
|
||||
}
|
||||
|
||||
fn print_startup_info(cfg Config) {
|
||||
println(line_double_r)
|
||||
println(' Ntfy server: ${cfg.server.url}')
|
||||
println(' Listening on: http://${cfg.server.listen}')
|
||||
println(' Dashboard: http://${normalize_listen(cfg.server.listen)}/')
|
||||
println(' Health check: http://${normalize_listen(cfg.server.listen)}/health')
|
||||
println('')
|
||||
println(' Sources configured:')
|
||||
|
||||
gitea_count := cfg.sources.gitea.len
|
||||
if gitea_count > 0 {
|
||||
println(' Gitea: ${gitea_count} webhook${if gitea_count > 1 { 's' } else { '' }}')
|
||||
}
|
||||
uk_count := cfg.sources.uptime_kuma.len
|
||||
if uk_count > 0 {
|
||||
println(' Uptime Kuma: ${uk_count} webhook${if uk_count > 1 { 's' } else { '' }}')
|
||||
}
|
||||
docker_count := cfg.sources.docker.len
|
||||
if docker_count > 0 {
|
||||
println(' Docker: ${docker_count} watcher${if docker_count > 1 { 's' } else { '' }}')
|
||||
}
|
||||
poll_count := cfg.sources.http_poll.len
|
||||
if poll_count > 0 {
|
||||
mut total_checks := 0
|
||||
for p in cfg.sources.http_poll {
|
||||
total_checks += p.checks.len
|
||||
}
|
||||
println(' HTTP Poll: ${poll_count} poller${if poll_count > 1 { 's' } else { '' }} (${total_checks} check${if total_checks > 1 { 's' } else { '' }})')
|
||||
}
|
||||
cron_count := cfg.sources.cron.len
|
||||
if cron_count > 0 {
|
||||
println(' Cron: ${cron_count} webhook${if cron_count > 1 { 's' } else { '' }}')
|
||||
}
|
||||
generic_count := cfg.sources.generic.len
|
||||
if generic_count > 0 {
|
||||
println(' Generic: ${generic_count} webhook${if generic_count > 1 { 's' } else { '' }}')
|
||||
}
|
||||
|
||||
println('')
|
||||
println(' Webhook URLs:')
|
||||
for src in cfg.sources.gitea {
|
||||
println(' http://${normalize_listen(cfg.server.listen)}${src.webhook_path} → ${src.topic} (Gitea: ${src.name})')
|
||||
}
|
||||
for src in cfg.sources.uptime_kuma {
|
||||
println(' http://${normalize_listen(cfg.server.listen)}${src.webhook_path} → ${src.topic} (Uptime Kuma: ${src.name})')
|
||||
}
|
||||
for src in cfg.sources.cron {
|
||||
println(' http://${normalize_listen(cfg.server.listen)}${src.webhook_path} → ${src.topic} (Cron: ${src.name})')
|
||||
}
|
||||
for src in cfg.sources.generic {
|
||||
println(' http://${normalize_listen(cfg.server.listen)}${src.webhook_path} → ${src.topic} (Generic: ${src.name})')
|
||||
}
|
||||
|
||||
println('')
|
||||
if cfg.dedup.enabled {
|
||||
println(' Dedup: enabled (TTL: ${cfg.dedup.ttl_seconds}s, rate: ${cfg.dedup.rate_limit.max_per_minute}/min)')
|
||||
} else {
|
||||
println(' Dedup: disabled')
|
||||
}
|
||||
|
||||
println(line_double_r)
|
||||
println('')
|
||||
}
|
||||
|
||||
// normalize_listen converts "0.0.0.0:9090" to "localhost:9090" for display URLs,
|
||||
// and leaves specific IPs intact (e.g. "192.168.30.101:9090" stays as-is).
|
||||
fn normalize_listen(listen string) string {
|
||||
if listen.starts_with('0.0.0.0') {
|
||||
return 'localhost' + listen[7..]
|
||||
}
|
||||
if listen.starts_with('127.0.0.1') {
|
||||
return listen // already localhost
|
||||
}
|
||||
return listen
|
||||
}
|
||||
|
||||
// ── Display Constants ─────────────────────────────────────────────────────
|
||||
|
||||
const line_double_r = '══════════════════════════════════════════════════════════════'
|
||||
Reference in New Issue
Block a user