A vibe coded tangled fork which supports pijul.
at e620e86a00c541e18e9e8097f97e51962631c5cd 132 lines 3.3 kB view raw
1package knotmirror 2 3import ( 4 "database/sql" 5 "embed" 6 "html/template" 7 "log/slog" 8 "net/http" 9 "strconv" 10 "time" 11 12 "github.com/go-chi/chi/v5" 13 "tangled.org/core/appview/pagination" 14 "tangled.org/core/knotmirror/db" 15 "tangled.org/core/knotmirror/models" 16 "tangled.org/core/orm" 17) 18 19//go:embed templates/*.html 20var templateFS embed.FS 21 22const repoPageSize = 20 23 24type AdminServer struct { 25 db *sql.DB 26} 27 28func NewAdminServer(database *sql.DB) *AdminServer { 29 return &AdminServer{db: database} 30} 31 32func (s *AdminServer) Router() http.Handler { 33 r := chi.NewRouter() 34 r.Get("/repos", s.handleRepos()) 35 r.Get("/hosts", s.handleHosts()) 36 return r 37} 38 39func funcmap() template.FuncMap { 40 return template.FuncMap{ 41 "add": func(a, b int) int { return a + b }, 42 "sub": func(a, b int) int { return a - b }, 43 "readt": func(ts int64) string { 44 if ts == 0 { 45 return "n/a" 46 } 47 return time.Unix(ts, 0).Format("2006-01-02 15:04") 48 }, 49 "const": func() map[string]any { 50 return map[string]any{ 51 "AllRepoStates": models.AllRepoStates, 52 "AllHostStatuses": models.AllHostStatuses, 53 } 54 }, 55 } 56} 57 58func (s *AdminServer) handleRepos() http.HandlerFunc { 59 // TODO: prepare template 60 tpl := template.Must(template.New("").Funcs(funcmap()).ParseFS(templateFS, "templates/base.html", "templates/repos.html")) 61 return func(w http.ResponseWriter, r *http.Request) { 62 pageNum, _ := strconv.Atoi(r.URL.Query().Get("page")) 63 if pageNum < 1 { 64 pageNum = 1 65 } 66 var ( 67 did = r.URL.Query().Get("did") 68 knot = r.URL.Query().Get("knot") 69 state = r.URL.Query().Get("state") 70 ) 71 72 page := pagination.Page{ 73 Offset: (pageNum - 1) * repoPageSize, 74 Limit: repoPageSize, 75 } 76 var filters []orm.Filter 77 78 if did != "" { 79 filters = append(filters, orm.FilterEq("did", did)) 80 } 81 if knot != "" { 82 filters = append(filters, orm.FilterEq("knot_domain", knot)) 83 } 84 if state != "" { 85 filters = append(filters, orm.FilterEq("state", state)) 86 } 87 88 repos, err := db.ListRepos(r.Context(), s.db, page, filters...) 89 if err != nil { 90 http.Error(w, err.Error(), http.StatusInternalServerError) 91 } 92 counts, err := db.GetRepoCountsByState(r.Context(), s.db) 93 if err != nil { 94 http.Error(w, err.Error(), http.StatusInternalServerError) 95 } 96 err = tpl.ExecuteTemplate(w, "base", map[string]any{ 97 "Repos": repos, 98 "RepoCounts": counts, 99 "Page": pageNum, 100 "FilterByDid": did, 101 "FilterByKnot": knot, 102 "FilterByState": models.RepoState(state), 103 }) 104 if err != nil { 105 slog.Error("failed to render", "err", err) 106 } 107 } 108} 109 110func (s *AdminServer) handleHosts() http.HandlerFunc { 111 tpl := template.Must(template.New("").Funcs(funcmap()).ParseFS(templateFS, "templates/base.html", "templates/hosts.html")) 112 return func(w http.ResponseWriter, r *http.Request) { 113 var status = r.URL.Query().Get("status") 114 115 var filters []orm.Filter 116 if status != "" { 117 filters = append(filters, orm.FilterEq("status", status)) 118 } 119 120 hosts, err := db.ListHosts(r.Context(), s.db, filters...) 121 if err != nil { 122 http.Error(w, err.Error(), http.StatusInternalServerError) 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}