A vibe coded tangled fork which supports pijul.
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}