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