···33import (
44 "database/sql"
55 "embed"
66+ "fmt"
77+ "html"
68 "html/template"
79 "log/slog"
810 "net/http"
911 "strconv"
1012 "time"
11131414+ "github.com/bluesky-social/indigo/atproto/syntax"
1215 "github.com/go-chi/chi/v5"
1316 "tangled.org/core/appview/pagination"
1417 "tangled.org/core/knotmirror/db"
···2124const repoPageSize = 20
22252326type AdminServer struct {
2424- db *sql.DB
2727+ db *sql.DB
2828+ resyncer *Resyncer
2929+ logger *slog.Logger
2530}
26312727-func NewAdminServer(database *sql.DB) *AdminServer {
2828- return &AdminServer{db: database}
3232+func NewAdminServer(l *slog.Logger, database *sql.DB, resyncer *Resyncer) *AdminServer {
3333+ return &AdminServer{
3434+ db: database,
3535+ resyncer: resyncer,
3636+ logger: l,
3737+ }
2938}
30393140func (s *AdminServer) Router() http.Handler {
3241 r := chi.NewRouter()
3342 r.Get("/repos", s.handleRepos())
3443 r.Get("/hosts", s.handleHosts())
4444+4545+ // not sure how to use these. should we vibe-code the admin page with React?
4646+ r.Post("/api/triggerRepoResync", s.handleRepoResyncTrigger())
4747+ r.Post("/api/cancelRepoResync", s.handleRepoResyncCancel())
4848+ r.Post("/api/testNotif", s.handleTestNotif)
3549 return r
3650}
3751···4054 "add": func(a, b int) int { return a + b },
4155 "sub": func(a, b int) int { return a - b },
4256 "readt": func(ts int64) string {
4343- if ts == 0 {
5757+ if ts <= 0 {
4458 return "n/a"
4559 }
4660 return time.Unix(ts, 0).Format("2006-01-02 15:04")
···112126 }
113127 }
114128}
129129+130130+func (s *AdminServer) handleRepoResyncTrigger() http.HandlerFunc {
131131+ return func(w http.ResponseWriter, r *http.Request) {
132132+ var repoQuery = r.FormValue("repo")
133133+134134+ repo, err := syntax.ParseATURI(repoQuery)
135135+ if err != nil || repo.RecordKey() == "" {
136136+ writeNotif(w, http.StatusBadRequest, fmt.Sprintf("repo parameter invalid: %s", repoQuery))
137137+ return
138138+ }
139139+140140+ if err := s.resyncer.TriggerResyncJob(r.Context(), repo); err != nil {
141141+ s.logger.Error("failed to trigger resync job", "err", err)
142142+ writeNotif(w, http.StatusInternalServerError, fmt.Sprintf("repo parameter invalid: %s", repoQuery))
143143+ return
144144+ }
145145+ writeNotif(w, http.StatusOK, "success")
146146+ }
147147+}
148148+149149+func (s *AdminServer) handleRepoResyncCancel() http.HandlerFunc {
150150+ return func(w http.ResponseWriter, r *http.Request) {
151151+ var repoQuery = r.FormValue("repo")
152152+153153+ repo, err := syntax.ParseATURI(repoQuery)
154154+ if err != nil || repo.RecordKey() == "" {
155155+ writeNotif(w, http.StatusBadRequest, fmt.Sprintf("repo parameter invalid: %s", repoQuery))
156156+ return
157157+ }
158158+159159+ s.resyncer.CancelResyncJob(repo)
160160+ writeNotif(w, http.StatusOK, "success")
161161+ }
162162+}
163163+164164+func (s *AdminServer) handleTestNotif(w http.ResponseWriter, r *http.Request) {
165165+ writeNotif(w, http.StatusOK, "new notifi")
166166+}
167167+168168+func writeNotif(w http.ResponseWriter, status int, msg string) {
169169+ w.Header().Set("Content-Type", "text/html")
170170+ w.WriteHeader(status)
171171+172172+ class := "info"
173173+ switch {
174174+ case status >= 500:
175175+ class = "error"
176176+ case status >= 400:
177177+ class = "warn"
178178+ }
179179+180180+ fmt.Fprintf(w,
181181+ `<div hx-swap-oob="beforeend:#notifications"><div class="notif %s">%s</div></div>`,
182182+ class,
183183+ html.EscapeString(msg),
184184+ )
185185+}
+1-1
knotmirror/knotmirror.go
···5959 knotstream := knotstream.NewKnotStream(logger, db, cfg)
6060 crawler := NewCrawler(logger, db)
6161 resyncer := NewResyncer(logger, db, gitm, cfg)
6262- adminpage := NewAdminServer(db)
6262+ adminpage := NewAdminServer(logger, db, resyncer)
63636464 // maintain repository list with tap
6565 // NOTE: this can be removed once we introduce did-for-repo because then we can just listen to KnotStream for #identity events.