package web import ( "context" "log" "time" "hearthforge/internal/db" ) // SyncRepos reconciles the repositories table with the repos on disk. // New bare repos get a row. Rows without a directory are kept, so an admin // can restore the directory or drop the data from the settings page. func (s *Server) SyncRepos(ctx context.Context) error { names, err := s.Git.SyncStartup(ctx) if err != nil { return err } onDisk := make(map[string]bool, len(names)) for _, n := range names { onDisk[n] = true } rows, err := s.DB.AllRepoRefs(ctx) if err != nil { return err } inDB := make(map[string]bool, len(rows)) for _, r := range rows { inDB[r.Name] = true if !onDisk[r.Name] { // Keep the row. Deleting it would cascade to issues and patches. // An admin drops it from the settings page. log.Printf("repo %q missing on disk, keeping its data", r.Name) } } for _, n := range names { if inDB[n] { continue } branch := s.Git.DefaultBranch(ctx, n) if _, err := s.DB.CreateRepo(ctx, n, nil, s.Cfg.ScannedRepoPrivate, branch, db.NowISO()); err != nil { return err } log.Printf("registered scanned repo %q", n) } return nil } // staleSignupAge is how long a passwordless signup may sit without a passkey // before the account is removed. const staleSignupAge = 15 * time.Minute // StartSessionCleanup deletes expired sessions and abandoned passwordless // signups once an hour. func (s *Server) StartSessionCleanup(ctx context.Context) { go func() { t := time.NewTicker(time.Hour) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: if err := s.DB.DeleteExpiredSessions(ctx); err != nil { log.Printf("session cleanup: %v", err) } cutoff := time.Now().UTC().Add(-staleSignupAge).Format(db.ISOLayout) if err := s.DB.DeleteStalePasskeySignups(ctx, cutoff); err != nil { log.Printf("stale signup cleanup: %v", err) } } } }() }