This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotmirror / adminpage.go
5.7 kB 212 lines
1package knotmirror 2 3import ( 4 "database/sql" 5 "embed" 6 "encoding/json" 7 "fmt" 8 "html" 9 "html/template" 10 "log/slog" 11 "net/http" 12 "strconv" 13 "time" 14 15 "github.com/bluesky-social/indigo/atproto/syntax" 16 "github.com/go-chi/chi/v5" 17 "tangled.org/core/appview/pagination" 18 "tangled.org/core/knotmirror/db" 19 "tangled.org/core/knotmirror/models" 20 "tangled.org/core/knotmirror/xrpc" 21) 22 23//go:embed templates/*.html 24var templateFS embed.FS 25 26const repoPageSize = 20 27 28type AdminServer struct { 29 db *sql.DB 30 resyncer *Resyncer 31 xrpc *xrpc.Xrpc 32 logger *slog.Logger 33} 34 35func NewAdminServer(l *slog.Logger, database *sql.DB, resyncer *Resyncer, x *xrpc.Xrpc) *AdminServer { 36 return &AdminServer{ 37 db: database, 38 resyncer: resyncer, 39 xrpc: x, 40 logger: l, 41 } 42} 43 44func (s *AdminServer) Router() http.Handler { 45 r := chi.NewRouter() 46 r.Get("/", s.handleIndex()) 47 r.Get("/repos", s.handleRepos()) 48 r.Get("/hosts", s.handleHosts()) 49 50 r.Post("/api/triggerRepoResync", s.handleRepoResyncTrigger()) 51 r.Post("/api/cancelRepoResync", s.handleRepoResyncCancel()) 52 r.Get("/api/inflight", s.handleInflight()) 53 return r 54} 55 56func (s *AdminServer) handleInflight() http.HandlerFunc { 57 return func(w http.ResponseWriter, r *http.Request) { 58 entries := s.xrpc.Inflight() 59 w.Header().Set("Content-Type", "application/json") 60 _ = json.NewEncoder(w).Encode(entries) 61 } 62} 63 64func funcmap() template.FuncMap { 65 return template.FuncMap{ 66 "add": func(a, b int) int { return a + b }, 67 "sub": func(a, b int) int { return a - b }, 68 "readt": func(ts int64) string { 69 if ts <= 0 { 70 return "n/a" 71 } 72 return time.Unix(ts, 0).Format("2006-01-02 15:04") 73 }, 74 "const": func() map[string]any { 75 return map[string]any{ 76 "AllRepoStates": models.AllRepoStates, 77 "AllHostStatuses": models.AllHostStatuses, 78 } 79 }, 80 } 81} 82 83func (s *AdminServer) handleIndex() http.HandlerFunc { 84 tpl := template.Must(template.New("").Funcs(funcmap()).ParseFS(templateFS, "templates/base.html", "templates/index.html")) 85 return func(w http.ResponseWriter, r *http.Request) { 86 err := tpl.ExecuteTemplate(w, "base", nil) 87 if err != nil { 88 slog.Error("failed to render", "err", err) 89 } 90 } 91} 92 93func (s *AdminServer) handleRepos() http.HandlerFunc { 94 tpl := template.Must(template.New("").Funcs(funcmap()).ParseFS(templateFS, "templates/base.html", "templates/repos.html")) 95 return func(w http.ResponseWriter, r *http.Request) { 96 pageNum, _ := strconv.Atoi(r.URL.Query().Get("page")) 97 if pageNum < 1 { 98 pageNum = 1 99 } 100 page := pagination.Page{ 101 Offset: (pageNum - 1) * repoPageSize, 102 Limit: repoPageSize, 103 } 104 105 var ( 106 did = r.URL.Query().Get("did") 107 knot = r.URL.Query().Get("knot") 108 state = r.URL.Query().Get("state") 109 name = r.URL.Query().Get("name") 110 ) 111 112 repos, err := db.ListRepos(r.Context(), s.db, page, did, knot, state) 113 repos, err := db.ListRepos(r.Context(), s.db, page, did, knot, state, name) 114 if err != nil { 115 http.Error(w, err.Error(), http.StatusInternalServerError) 116 return 117 } 118 counts, err := db.GetRepoCountsByState(r.Context(), s.db) 119 if err != nil { 120 http.Error(w, err.Error(), http.StatusInternalServerError) 121 return 122 } 123 err = tpl.ExecuteTemplate(w, "base", map[string]any{ 124 "Repos": repos, 125 "RepoCounts": counts, 126 "Page": pageNum, 127 "FilterByDid": did, 128 "FilterByKnot": knot, 129 "FilterByState": models.RepoState(state), 130 "FilterByName": name, 131 }) 132 if err != nil { 133 slog.Error("failed to render", "err", err) 134 } 135 } 136} 137 138func (s *AdminServer) handleHosts() http.HandlerFunc { 139 tpl := template.Must(template.New("").Funcs(funcmap()).ParseFS(templateFS, "templates/base.html", "templates/hosts.html")) 140 return func(w http.ResponseWriter, r *http.Request) { 141 var status = models.HostStatus(r.URL.Query().Get("status")) 142 if status == "" { 143 status = models.HostStatusActive 144 } 145 146 hosts, err := db.ListHosts(r.Context(), s.db, status) 147 if err != nil { 148 http.Error(w, err.Error(), http.StatusInternalServerError) 149 return 150 } 151 err = tpl.ExecuteTemplate(w, "base", map[string]any{ 152 "Hosts": hosts, 153 "FilterByStatus": models.HostStatus(status), 154 }) 155 if err != nil { 156 slog.Error("failed to render", "err", err) 157 } 158 } 159} 160 161func (s *AdminServer) handleRepoResyncTrigger() http.HandlerFunc { 162 return func(w http.ResponseWriter, r *http.Request) { 163 var repoQuery = r.FormValue("repo") 164 165 repo, err := syntax.ParseATURI(repoQuery) 166 if err != nil || repo.RecordKey() == "" { 167 writeNotif(w, http.StatusBadRequest, fmt.Sprintf("repo parameter invalid: %s", repoQuery)) 168 return 169 } 170 171 if err := s.resyncer.TriggerResyncJob(r.Context(), repo); err != nil { 172 s.logger.Error("failed to trigger resync job", "err", err) 173 writeNotif(w, http.StatusInternalServerError, fmt.Sprintf("repo parameter invalid: %s", repoQuery)) 174 return 175 } 176 writeNotif(w, http.StatusOK, "success") 177 } 178} 179 180func (s *AdminServer) handleRepoResyncCancel() http.HandlerFunc { 181 return func(w http.ResponseWriter, r *http.Request) { 182 var repoQuery = r.FormValue("repo") 183 184 repo, err := syntax.ParseATURI(repoQuery) 185 if err != nil || repo.RecordKey() == "" { 186 writeNotif(w, http.StatusBadRequest, fmt.Sprintf("repo parameter invalid: %s", repoQuery)) 187 return 188 } 189 190 s.resyncer.CancelResyncJob(repo) 191 writeNotif(w, http.StatusOK, "success") 192 } 193} 194 195func writeNotif(w http.ResponseWriter, status int, msg string) { 196 w.Header().Set("Content-Type", "text/html") 197 w.WriteHeader(status) 198 199 class := "info" 200 switch { 201 case status >= 500: 202 class = "error" 203 case status >= 400: 204 class = "warn" 205 } 206 207 fmt.Fprintf(w, 208 `<div hx-swap-oob="beforeend:#notifications"><div class="notif %s">%s</div></div>`, 209 class, 210 html.EscapeString(msg), 211 ) 212}