package main import ( "log/slog" "os" "sync" "time" "provenance/internal/appview" "provenance/internal/atproto" "provenance/internal/config" "provenance/internal/domain" "provenance/internal/httpx" "provenance/internal/logging" "provenance/internal/secure" "provenance/internal/storage" "provenance/internal/ui" "provenance/internal/upstream" ) const PruneInterval = time.Hour func main() { if err := logging.Setup("ui"); err != nil { slog.Error("log configuration failed", "error", err) os.Exit(1) } if err := run(); err != nil { slog.Error("UI stopped", "error", err) os.Exit(1) } } func run() error { cfg, err := config.LoadUI() if err != nil { return err } slog.Info("UI configuration loaded", "address", cfg.Address, "database", cfg.Database, "external_workers", cfg.ExternalWorkers, "origin", cfg.Origin, "appview_url", cfg.AppviewURL) store, err := storage.Open(cfg.Database) if err != nil { return err } defer store.Close() slog.Info("database opened", "database", cfg.Database) vault, err := secure.New(store, cfg.EncryptionKey) if err != nil { return err } ctx, cancel := httpx.Context() defer cancel() queue, err := upstream.New(ctx, cfg.ExternalWorkers) if err != nil { return err } defer queue.Close() auth, err := atproto.NewOAuthWithQueue(vault, cfg.Origin, queue) if err != nil { return err } client, err := appview.NewClient(cfg.AppviewURL, cfg.ServiceKey) if err != nil { return err } service := ui.New(auth, atproto.NewRepositoryWithQueue(domain.SoftDisabled, queue), client, vault) var workers sync.WaitGroup workers.Go(func() { ticker := time.NewTicker(PruneInterval) defer ticker.Stop() for { if err := store.Prune(ctx); err != nil && ctx.Err() == nil { slog.Warn("session cleanup failed", "error", err) } select { case <-ctx.Done(): return case <-ticker.C: } } }) err = httpx.Serve(ctx, cfg.Address, ui.Handler(service, cfg.Origin)) cancel() workers.Wait() return err }