CI / test (push) Failing after 24s
Phase 3: - Config reload via SIGHUP (handle_signals loop: .hup reloads config, .int/.term shutdown) - Hot reload: cfg, webhook_routes, ntfy client, dedup updated without restart Phase 4: - Silence rules: POST /api/silence?duration=30m, GET /api/silence, DELETE /api/silence - Grouping: buffer 10s, events grouped with (×N in 10s) suffix - CI/CD: .gitea/workflows/ci.yml (build + test on push/PR) Docs: - ROADMAP.md: phases 3 & 4 marked DONE, version table updated - ARCHITECTURE.md: synced with actual code Version: 0.1.0 → 0.4.0 (main.v, v.mod)
203 lines
7.3 KiB
V
203 lines
7.3 KiB
V
module main
|
|
|
|
import os
|
|
import flag
|
|
|
|
const version = '0.4.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, config_path, 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 = '══════════════════════════════════════════════════════════════'
|