Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Build out the forge web UI

f09ef39efe44de693748053a7b596ef328fcf06b · Keenan Nemetz · 15d ago · parent 70b70264

Build out the forge web UI

Turn the web surface into a compact, server-rendered forge with HTMX as the
progressive dynamic layer.

- Split templates and assets into embedded files; add a tokenised dark theme,
  Lucide icons, and vendored HTMX.
- Add the repository Overview page and a separate chronological Activity feed
  with type filters and HTMX fragment updates.
- Finish the issue and change workflows: create pages, comments, close/reopen,
  labels and assignees, approval, discussion, line-anchored review comments,
  and HTMX list filters.
- Add syntax-highlighted blobs, contributor/statistics derivation, branch-aware
  code browsing, and a repository Runs list.
- Add a loopback-only development login for local write testing, with explicit
  configuration and listener checks.
- Extend tests for the new pages, fragments, labels, assignees, anchors, and
  development login.

server/server.go +20−1

@@ -97,6 +97,9 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error)
97 97 wp.Locks = inst.Locks
98 98 wp.Sessions = inst.Sessions
99 99 wp.Logger = func(format string, args ...any) { log.Warn(fmt.Sprintf(format, args...)) }
100 if cfg.DevLogin != "" {
101 log.Warn("development login enabled", "account", cfg.DevLogin, "http_addr", cfg.HTTPAddr)
102 }
100 103 if wp.Auth != nil && !wp.Auth.Enabled() {
101 104 log.Info("browser login disabled", "reason", wp.Auth.Disabled)
102 105 } else if cfg.OpenRegistration {
@@ -139,8 +142,10 @@ func (s *Server) routes() *http.ServeMux {
139 142 mux := http.NewServeMux()
140 143 w := s.Web
141 144
145 mux.Handle("GET /static/", web.Static())
142 146 mux.HandleFunc("GET /{$}", w.IndexPage)
143 147 mux.HandleFunc("GET /login", w.LoginPage)
148 mux.HandleFunc("GET /dev/login", w.DevLogin)
144 149 mux.HandleFunc("GET /register", w.RegisterPage)
145 150 mux.HandleFunc("POST /auth/register/begin", w.RegisterBegin)
146 151 mux.HandleFunc("POST /auth/register/finish", w.RegisterFinish)
@@ -158,20 +163,29 @@ func (s *Server) routes() *http.ServeMux {
158 163 mux.HandleFunc("POST /account/passkeys/finish", w.AddPasskeyFinish)
159 164 mux.HandleFunc("GET /auth.js", w.AuthJS)
160 165 mux.HandleFunc("GET /runners", w.RunnersPage)
161 mux.HandleFunc("GET /r/{name}", w.Repo)
166 mux.HandleFunc("GET /r/{name}", w.Overview)
167 mux.HandleFunc("GET /r/{name}/activity", w.Activity)
168 mux.HandleFunc("GET /r/{name}/code", w.Code)
162 169 mux.HandleFunc("GET /r/{name}/tree/{ref}/{path...}", w.Tree)
163 170 mux.HandleFunc("GET /r/{name}/blob/{ref}/{path...}", w.Blob)
164 171 mux.HandleFunc("GET /r/{name}/raw/{ref}/{path...}", w.Raw)
165 172 mux.HandleFunc("GET /r/{name}/commits/{ref}", w.Commits)
166 173 mux.HandleFunc("GET /r/{name}/commit/{sha}", w.CommitView)
167 174 mux.HandleFunc("GET /r/{name}/issues", w.Issues)
175 mux.HandleFunc("GET /r/{name}/issues/new", w.NewIssuePage)
168 176 mux.HandleFunc("POST /r/{name}/issues", w.CreateIssue)
169 177 mux.HandleFunc("GET /r/{name}/issues/{id}", w.IssueView)
170 178 mux.HandleFunc("POST /r/{name}/issues/{id}/comments", w.AddIssueComment)
179 mux.HandleFunc("POST /r/{name}/issues/{id}/state", w.ChangeIssueState)
180 mux.HandleFunc("POST /r/{name}/issues/{id}/labels", w.SetIssueLabels)
181 mux.HandleFunc("POST /r/{name}/issues/{id}/assignees", w.SetIssueAssignees)
171 182 mux.HandleFunc("GET /r/{name}/requests", w.Requests)
183 mux.HandleFunc("GET /r/{name}/requests/new", w.NewChangePage)
172 184 mux.HandleFunc("POST /r/{name}/requests", w.CreateChangeRequest)
173 185 mux.HandleFunc("GET /r/{name}/requests/{id}", w.RequestView)
174 186 mux.HandleFunc("POST /r/{name}/requests/{id}/approve", w.ApproveRequest)
187 mux.HandleFunc("POST /r/{name}/requests/{id}/comments", w.AddRequestComment)
188 mux.HandleFunc("POST /r/{name}/requests/{id}/state", w.ChangeRequestState)
175 189 mux.HandleFunc("POST /r/{name}/ci/allow", w.AllowContributorCI)
176 190 // A tag may contain "/", so the detail route takes the rest of the path; the
177 191 // two archive routes are more specific and win for their suffixes.
@@ -181,10 +195,15 @@ func (s *Server) routes() *http.ServeMux {
181 195 mux.HandleFunc("GET /r/{name}/releases/{tag}/archive.tar", w.ReleaseTar)
182 196 mux.HandleFunc("GET /r/{name}/releases/{tag}/archive.tar.gz", w.ReleaseTarGz)
183 197 mux.HandleFunc("GET /r/{name}/releases/{tag}/artifacts/{artifact}", w.ReleaseArtifact)
198 mux.HandleFunc("GET /r/{name}/runs", w.RunsPage)
184 199 mux.HandleFunc("GET /r/{name}/runs/{id}", w.RunPage)
185 200 mux.HandleFunc("GET /r/{name}/runs/{id}/log", w.RunLog)
186 201 mux.HandleFunc("GET /r/{name}/runs/{id}/artifacts/{artifact}", w.RunArtifact)
187 202
203 // Unknown paths get the same styled 404 as a missing or private repository.
204 // More specific patterns above still win; this only catches the rest.
205 mux.HandleFunc("/{path...}", w.NotFoundPage)
206
188 207 mux.HandleFunc("GET /healthz", func(rw http.ResponseWriter, r *http.Request) {
189 208 rw.Header().Set("Content-Type", "text/plain")
190 209 fmt.Fprintf(rw, "ok\nusers=%d\n", s.Identity.Snapshot().Count)