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/web/auth_handlers.go +36

@@ -346,7 +346,19 @@ func (w *Web) Logout(rw http.ResponseWriter, r *http.Request) {
346 346 if c, err := r.Cookie(sessionCookieName); err == nil && c.Value != "" && w.Sessions != nil {
347 347 _ = w.Sessions.Revoke(c.Value)
348 348 }
349 if c, err := r.Cookie(devSessionCookieName); err == nil && c.Value != "" && w.Sessions != nil {
350 _ = w.Sessions.Revoke(c.Value)
351 }
349 352 w.clearCookie(rw, sessionCookieName)
353 if w.devAccount != nil {
354 // Log out must stick even though loopback would otherwise sign us back
355 // in. /dev/login clears this opt-out.
356 w.clearCookieFor(rw, devSessionCookieName, false)
357 http.SetCookie(rw, &http.Cookie{
358 Name: devOffCookieName, Value: "1", Path: "/", HttpOnly: true,
359 SameSite: http.SameSiteLaxMode, MaxAge: 3600,
360 })
361 }
350 362 http.Redirect(rw, r, "/", http.StatusSeeOther)
351 363 }
352 364
@@ -462,3 +474,27 @@ func (w *Web) AddPasskeyFinish(rw http.ResponseWriter, r *http.Request) {
462 474 }
463 475 writeJSON(rw, http.StatusOK, map[string]any{"ok": true})
464 476 }
477
478 // DevLogin signs a loopback browser in as the account named by dev_login. It is
479 // a development shortcut, not a second authentication path: the listener must be
480 // loopback, the account is resolved at startup, and a normal instance has no
481 // devAccount at all.
482 func (w *Web) DevLogin(rw http.ResponseWriter, r *http.Request) {
483 if w.devAccount == nil || !isLoopbackRequest(r) {
484 http.NotFound(rw, r)
485 return
486 }
487 if w.Sessions == nil {
488 http.Error(rw, "sessions are unavailable", http.StatusInternalServerError)
489 return
490 }
491 // Visiting /dev/login is the explicit way back after a logout opted out.
492 w.clearCookieFor(rw, devOffCookieName, false)
493 sess := w.ensureDevSession()
494 if sess == nil {
495 http.Error(rw, "cannot create a development session", http.StatusInternalServerError)
496 return
497 }
498 w.setDevSessionCookie(rw, sess)
499 http.Redirect(rw, r, "/", http.StatusSeeOther)
500 }