Files
terdut-server/cmd/terdut/main.go
T
Niklas Ye b114251f45 Add notification channels and a provider abstraction
Users get a notification_channels table (replacing users.ntfy_topic); pages fan
out to every enabled channel through a notify.Provider registry, with ntfy as
the first provider. Fallbacks are typed {type, address}. Schema edited in place:
existing databases must be recreated.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-12 21:00:40 +02:00

103 lines
2.9 KiB
Go

package main
import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"
"git.ryuvia.com/niklas/terdut-server/internal/api"
"git.ryuvia.com/niklas/terdut-server/internal/config"
"git.ryuvia.com/niklas/terdut-server/internal/db"
"git.ryuvia.com/niklas/terdut-server/internal/notify"
)
var version = "dev"
func main() {
cfg := config.Load()
if err := cfg.Validate(); err != nil {
log.Fatalf("config: %v", err)
}
database, err := db.Open(cfg.DSN)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer database.Close()
if err := db.Migrate(database); err != nil {
log.Fatalf("migrate: %v", err)
}
notifyCfg := api.NotifyConfig{
Providers: notifyProviders(cfg),
Fallback: notifyFallback(cfg),
PublicURL: cfg.PublicURL,
RepeatEvery: cfg.NotifyRepeat,
}
// The behaviour knobs move into the database on first start, after which an
// administrator owns them and a redeploy leaves them alone.
if err := api.SeedSettings(context.Background(), database, cfg); err != nil {
log.Fatalf("seed settings: %v", err)
}
if err := api.SeedOperatorKey(context.Background(), database, cfg.OperatorKey); err != nil {
log.Fatalf("%v", err)
}
router := api.NewRouter(database, notifyCfg, cfg, version)
srv := &http.Server{
Addr: cfg.Addr,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go api.StartArchiver(ctx, database, cfg.ArchiveAfter, cfg.StaleAfter, notifyCfg)
go api.StartNotifier(ctx, database, notifyCfg)
go func() {
log.Printf("terdut-server %s listening on %s", version, cfg.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
}
}
// notifyProviders registers the notification providers this install has
// configuration for. A provider with no configuration is simply absent, which is
// what keeps it out of the web UI's list of channel types.
func notifyProviders(cfg config.Config) *notify.Registry {
var providers []notify.Provider
if cfg.NtfyURL != "" {
providers = append(providers, notify.NewNtfy(cfg.NtfyURL, cfg.NtfyToken))
}
return notify.NewRegistry(providers...)
}
// notifyFallback is where an incident goes when nobody on call can be reached.
// Only ntfy has a fallback setting today; a second provider adds its own.
func notifyFallback(cfg config.Config) notify.Target {
if cfg.NtfyURL != "" && cfg.NtfyFallbackTopic != "" {
return notify.Target{Type: notify.TypeNtfy, Address: cfg.NtfyFallbackTopic}
}
return notify.Target{}
}