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/templates/layout.html +130
| @@ -0,0 +1,130 @@ | |||
| 1 | {{define "header"}}<!doctype html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"> | ||
| 5 | <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
| 6 | <meta name="csrf" content="{{.CSRF}}"> | ||
| 7 | <meta name="color-scheme" content="dark"> | ||
| 8 | <title>{{.Title}} · Vole</title> | ||
| 9 | <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"> | ||
| 10 | <link rel="stylesheet" href="/static/vole.css"> | ||
| 11 | <script src="/static/htmx.min.js" defer></script> | ||
| 12 | <script src="/auth.js" defer></script> | ||
| 13 | <script src="/static/app.js" defer></script> | ||
| 14 | </head> | ||
| 15 | <body> | ||
| 16 | <header class="topbar"> | ||
| 17 | <a class="brand" href="/" aria-label="Vole home"><span class="mark" aria-hidden="true">v</span>vole</a> | ||
| 18 | <nav class="topnav" aria-label="Global"> | ||
| 19 | <a href="/">{{icon "book-open"}}Repositories</a> | ||
| 20 | <a href="/runners">{{icon "server"}}Runners</a> | ||
| 21 | </nav> | ||
| 22 | <div class="topright"> | ||
| 23 | {{if .LoggedIn}} | ||
| 24 | <a class="account" href="/account">{{icon "user"}}{{.User}}</a> | ||
| 25 | {{if .DevLogin}}<span class="label" title="Development login">dev</span>{{end}} | ||
| 26 | <form class="inline" method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="link" type="submit">Log out</button></form> | ||
| 27 | {{else}}{{if .DevLogin}}<a href="/dev/login">Dev login</a> · {{end}}<a href="/login">Log in</a>{{end}} | ||
| 28 | </div> | ||
| 29 | </header> | ||
| 30 | <main class="page"> | ||
| 31 | {{if .Error}}<div class="flash bad">{{.Error}}</div>{{end}} | ||
| 32 | {{end}} | ||
| 33 | |||
| 34 | {{define "footer"}} | ||
| 35 | </main> | ||
| 36 | </body> | ||
| 37 | </html> | ||
| 38 | {{end}} | ||
| 39 | |||
| 40 | {{define "repohead"}} | ||
| 41 | <div class="repohead"> | ||
| 42 | <a class="reponame" href="/r/{{.Name}}">{{.Name}}</a> | ||
| 43 | <nav class="tabs" aria-label="Repository"> | ||
| 44 | <a href="/r/{{.Name}}"{{if eq .Tab "overview"}} class="active" aria-current="page"{{end}}>{{icon "layout-dashboard"}}Overview</a> | ||
| 45 | <a href="/r/{{.Name}}/activity"{{if eq .Tab "activity"}} class="active" aria-current="page"{{end}}>{{icon "activity"}}Activity</a> | ||
| 46 | <a href="/r/{{.Name}}/code"{{if eq .Tab "code"}} class="active" aria-current="page"{{end}}>{{icon "code"}}Code</a> | ||
| 47 | <a href="/r/{{.Name}}/issues"{{if eq .Tab "issues"}} class="active" aria-current="page"{{end}}>{{icon "circle-dot"}}Issues</a> | ||
| 48 | <a href="/r/{{.Name}}/requests"{{if eq .Tab "requests"}} class="active" aria-current="page"{{end}}>{{icon "git-pull-request"}}Changes</a> | ||
| 49 | <a href="/r/{{.Name}}/releases"{{if eq .Tab "releases"}} class="active" aria-current="page"{{end}}>{{icon "tag"}}Releases</a> | ||
| 50 | <a href="/r/{{.Name}}/runs"{{if eq .Tab "runs"}} class="active" aria-current="page"{{end}}>{{icon "play"}}Runs</a> | ||
| 51 | <a href="/r/{{.Name}}/locks"{{if eq .Tab "locks"}} class="active" aria-current="page"{{end}}>{{icon "lock"}}Locks</a> | ||
| 52 | </nav> | ||
| 53 | </div> | ||
| 54 | {{end}} | ||
| 55 | |||
| 56 | {{define "notfound"}} | ||
| 57 | {{template "header" .}} | ||
| 58 | <div class="empty-page"> | ||
| 59 | <p class="eyebrow">Error</p> | ||
| 60 | <h1>{{.Title}}</h1> | ||
| 61 | <p class="muted">{{.Message}}</p> | ||
| 62 | <p><a class="button" href="/">Back to repositories</a></p> | ||
| 63 | </div> | ||
| 64 | {{template "footer" .}} | ||
| 65 | {{end}} | ||
| 66 | |||
| 67 | {{define "token_input"}} | ||
| 68 | <div class="label-input" data-label-input data-label-item="{{.Item}}"> | ||
| 69 | <div class="label-tokens" data-label-tokens> | ||
| 70 | <input type="text" class="label-entry" name="{{.Field}}" value="{{join .Selected ", "}}" data-label-entry autocomplete="off" placeholder="{{.Placeholder}}" aria-label="{{.Placeholder}}"> | ||
| 71 | </div> | ||
| 72 | <div class="label-suggest" data-label-suggest hidden> | ||
| 73 | {{range .All}}<button type="button" class="label-option" data-label-option="{{.}}">{{.}}</button>{{end}} | ||
| 74 | </div> | ||
| 75 | </div> | ||
| 76 | {{end}} | ||
| 77 | |||
| 78 | {{define "commitlist"}} | ||
| 79 | {{if .Commits}} | ||
| 80 | <table class="commits"> | ||
| 81 | <tbody> | ||
| 82 | {{range .Commits}} | ||
| 83 | <tr> | ||
| 84 | <td class="sha"><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a></td> | ||
| 85 | <td><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a></td> | ||
| 86 | <td class="muted">{{.Author}}</td> | ||
| 87 | <td class="muted nowrap">{{since .When}}</td> | ||
| 88 | </tr> | ||
| 89 | {{end}} | ||
| 90 | </tbody> | ||
| 91 | </table> | ||
| 92 | {{else}} | ||
| 93 | <p class="muted">No commits yet.</p> | ||
| 94 | {{end}} | ||
| 95 | {{end}} | ||
| 96 | |||
| 97 | {{define "diff"}} | ||
| 98 | <div class="diffsum muted">{{len .Files}} file(s) changed{{if .Added}} · <span class="adds">+{{.Added}}</span>{{end}}{{if .Removed}} · <span class="dels">−{{.Removed}}</span>{{end}}{{if .Truncated}} · showing the first 512 KiB{{end}}</div> | ||
| 99 | {{range .Files}} | ||
| 100 | {{$file := .}} | ||
| 101 | <section class="diff"> | ||
| 102 | <h3><span class="path">{{if and .OldPath (ne .OldPath .NewPath)}}{{.OldPath}} → {{end}}{{.Label}}</span>{{if .Binary}} <span class="label">binary</span>{{end}}</h3> | ||
| 103 | {{if .Binary}} | ||
| 104 | <p class="binary">Binary file: no line diff to show. Use <code>git show</code> to inspect it.</p> | ||
| 105 | {{end}} | ||
| 106 | {{if .Hunks}} | ||
| 107 | <table> | ||
| 108 | <tbody> | ||
| 109 | {{range .Hunks}} | ||
| 110 | <tr class="hunk"><td colspan="4">{{.Header}}</td></tr> | ||
| 111 | {{range .Lines}} | ||
| 112 | {{$side := "new"}}{{$path := $file.NewPath}} | ||
| 113 | {{if eq .Kind "del"}}{{$side = "old"}}{{$path = $file.OldPath}}{{end}} | ||
| 114 | <tr class="{{.Kind}}"> | ||
| 115 | <td class="comment"> | ||
| 116 | {{if and $.LineCommentURL $path}}<a href="{{$.LineCommentURL}}?path={{urlquery $path}}&line={{if eq $side "old"}}{{.OldNo}}{{else}}{{.NewNo}}{{end}}&side={{$side}}#request-comment-form" title="Comment on this line" aria-label="Comment on this line">{{icon "plus"}}</a>{{end}} | ||
| 117 | </td> | ||
| 118 | <td class="ln">{{if .OldNo}}{{.OldNo}}{{end}}</td> | ||
| 119 | <td class="ln">{{if .NewNo}}{{.NewNo}}{{end}}</td> | ||
| 120 | <td class="code">{{.Text}}</td> | ||
| 121 | </tr> | ||
| 122 | {{if .Comments}}<tr class="note"><td colspan="4">{{range .Comments}}<a class="label" href="#c-{{.ID}}">{{who .Author}} commented</a>{{end}}</td></tr>{{end}} | ||
| 123 | {{end}} | ||
| 124 | {{end}} | ||
| 125 | </tbody> | ||
| 126 | </table> | ||
| 127 | {{end}} | ||
| 128 | </section> | ||
| 129 | {{end}} | ||
| 130 | {{end}} | ||