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.go +24−898
| @@ -1,901 +1,27 @@ | |||
| 1 | 1 | package web | |
| 2 | 2 | ||
| 3 | const templates = ` | ||
| 4 | {{define "header"}}<!doctype html> | ||
| 5 | <html lang="en"> | ||
| 6 | <head> | ||
| 7 | <meta charset="utf-8"> | ||
| 8 | <meta name="viewport" content="width=device-width,initial-scale=1"> | ||
| 9 | <meta name="csrf" content="{{.CSRF}}"> | ||
| 10 | <title>{{.Title}} · Vole</title> | ||
| 11 | <style> | ||
| 12 | :root{ | ||
| 13 | --bg:#0f1115; --fg:#e6e6e6; --muted:#8b93a1; --line:#242a35; | ||
| 14 | --accent:#6ea8fe; --card:#161a21; --bad:#f85149; | ||
| 3 | // Templates and static assets are embedded so an instance is still one file. | ||
| 4 | // Page templates are split by surface under templates/; shared chrome and the | ||
| 5 | // diff component live in layout.html. | ||
| 6 | |||
| 7 | import ( | ||
| 8 | "embed" | ||
| 9 | "io/fs" | ||
| 10 | "net/http" | ||
| 11 | ) | ||
| 12 | |||
| 13 | //go:embed templates/*.html | ||
| 14 | var templateFS embed.FS | ||
| 15 | |||
| 16 | //go:embed static/* | ||
| 17 | var staticFS embed.FS | ||
| 18 | |||
| 19 | // Static serves the embedded stylesheet, favicon, and small scripts. Assets are | ||
| 20 | // compiled into the binary, so there is no directory to deploy beside it. | ||
| 21 | func Static() http.Handler { | ||
| 22 | sub, err := fs.Sub(staticFS, "static") | ||
| 23 | if err != nil { | ||
| 24 | panic(err) | ||
| 25 | } | ||
| 26 | return http.StripPrefix("/static/", http.FileServerFS(sub)) | ||
| 15 | 27 | } | |
| 16 | *{box-sizing:border-box} | ||
| 17 | body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif} | ||
| 18 | a{color:var(--accent);text-decoration:none} | ||
| 19 | a:hover{text-decoration:underline} | ||
| 20 | header{display:flex;align-items:center;gap:1rem;padding:.9rem 1.25rem;border-bottom:1px solid var(--line);background:var(--card)} | ||
| 21 | .brand{font-weight:700;letter-spacing:.02em;color:var(--fg)} | ||
| 22 | header nav{margin-left:auto;color:var(--muted);font-size:.85rem} | ||
| 23 | main{max-width:62rem;margin:0 auto;padding:1.5rem 1.25rem 4rem} | ||
| 24 | h1{font-size:1.45rem;margin:.2rem 0 1rem} | ||
| 25 | h2{font-size:1.05rem;margin:1.75rem 0 .6rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em} | ||
| 26 | .muted{color:var(--muted)} | ||
| 27 | table{width:100%;border-collapse:collapse;font-size:.93rem} | ||
| 28 | th,td{text-align:left;padding:.45rem .5rem;border-bottom:1px solid var(--line);vertical-align:top} | ||
| 29 | th{color:var(--muted);font-weight:500;font-size:.82rem;text-transform:uppercase;letter-spacing:.04em} | ||
| 30 | code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace} | ||
| 31 | code{background:var(--card);border:1px solid var(--line);border-radius:4px;padding:.1rem .35rem;font-size:.88em} | ||
| 32 | pre{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:1rem;overflow:auto;font-size:.86rem;line-height:1.5} | ||
| 33 | select,input[type=text],input[type=password],textarea{background:var(--card);color:var(--fg);border:1px solid var(--line);border-radius:6px;padding:.45rem .55rem;font:inherit;font-size:.9rem} | ||
| 34 | input[type=text],input[type=password],textarea{width:100%;max-width:32rem} | ||
| 35 | textarea{min-height:8rem;resize:vertical} | ||
| 36 | label{display:block;margin:.6rem 0;color:var(--muted);font-size:.9rem} | ||
| 37 | button{background:var(--accent);color:#08101d;border:0;border-radius:6px;padding:.45rem .8rem;font:inherit;font-weight:600;cursor:pointer} | ||
| 38 | button:disabled{opacity:.5;cursor:default} | ||
| 39 | button.link{border:0;background:none;color:var(--accent);padding:0;font:inherit;font-weight:400;cursor:pointer} | ||
| 40 | button.link:hover{text-decoration:underline} | ||
| 41 | form.inline{display:inline} | ||
| 42 | .auth{max-width:36rem} | ||
| 43 | .error{color:var(--bad)} | ||
| 44 | .bad{color:var(--bad)} | ||
| 45 | .card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:1rem;margin:1rem 0} | ||
| 46 | .row{display:flex;gap:1.5rem;flex-wrap:wrap} | ||
| 47 | .row>div{flex:1 1 18rem} | ||
| 48 | .crumbs{color:var(--muted);font-size:.9rem;margin-bottom:.75rem} | ||
| 49 | .dir{color:var(--accent)} | ||
| 50 | .sha{font-family:ui-monospace,monospace;font-size:.85rem;color:var(--muted)} | ||
| 51 | .label{display:inline-block;margin-left:.4rem;padding:.05rem .4rem;border:1px solid var(--line);border-radius:10px;font-size:.75rem;color:var(--muted)} | ||
| 52 | .md{line-height:1.6} | ||
| 53 | .md h1,.md h2,.md h3{text-transform:none;letter-spacing:0;color:var(--fg);margin:1.1rem 0 .5rem} | ||
| 54 | .md h1{font-size:1.25rem}.md h2{font-size:1.1rem}.md h3{font-size:1rem} | ||
| 55 | .md p{margin:.6rem 0} | ||
| 56 | .md code{background:var(--card)} | ||
| 57 | .md pre{background:var(--card);border:1px solid var(--line)} | ||
| 58 | .md blockquote{margin:.6rem 0;padding-left:.8rem;border-left:3px solid var(--line);color:var(--muted)} | ||
| 59 | .md ul,.md ol{padding-left:1.4rem} | ||
| 60 | .md table{width:auto} | ||
| 61 | .diff{border:1px solid var(--line);border-radius:8px;margin:1rem 0;overflow:hidden} | ||
| 62 | .diff h3{margin:0;padding:.55rem .8rem;background:var(--card);border-bottom:1px solid var(--line);font-size:.9rem;font-weight:600} | ||
| 63 | .diff table{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.82rem;border-collapse:collapse} | ||
| 64 | .diff td{border:0;padding:0 .5rem;white-space:pre-wrap;word-break:break-word;vertical-align:top} | ||
| 65 | .diff td.ln{width:3.5rem;text-align:right;color:var(--muted);user-select:none;border-right:1px solid var(--line)} | ||
| 66 | .diff td.code{padding-left:.75rem} | ||
| 67 | .diff tr.add{background:rgba(46,160,67,.12)} | ||
| 68 | .diff tr.add td.ln{color:#3fb950} | ||
| 69 | .diff tr.del{background:rgba(248,81,73,.10)} | ||
| 70 | .diff tr.del td.ln{color:var(--bad)} | ||
| 71 | .conflict{color:var(--bad)} | ||
| 72 | .diff tr.hunk td{color:var(--muted);background:var(--card);padding:.25rem .5rem;font-size:.78rem} | ||
| 73 | .diff tr.note td{background:rgba(110,168,254,.07);padding:.15rem .5rem .3rem;font-size:.78rem} | ||
| 74 | .diff tr.note a.label{color:var(--accent);border-color:var(--accent)} | ||
| 75 | .diff .binary{padding:.7rem .8rem;color:var(--muted);font-size:.85rem} | ||
| 76 | </style> | ||
| 77 | </head> | ||
| 78 | <body> | ||
| 79 | <header> | ||
| 80 | <a class="brand" href="/">vole</a> | ||
| 81 | <nav> | ||
| 82 | {{if .LoggedIn}}<span class="muted">{{.User}}</span> · <a href="/account">account</a> · | ||
| 83 | <form class="inline" method="post" action="/logout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="link" type="submit">log out</button></form> | ||
| 84 | {{else}}<a href="/login">log in</a>{{end}} | ||
| 85 | · <a href="/runners">runners</a> · push over ssh | ||
| 86 | </nav> | ||
| 87 | </header> | ||
| 88 | <main> | ||
| 89 | {{if .Error}}<p class="error">{{.Error}}</p>{{end}} | ||
| 90 | <script src="/auth.js" defer></script> | ||
| 91 | {{end}} | ||
| 92 | |||
| 93 | {{define "footer"}} | ||
| 94 | </main> | ||
| 95 | </body> | ||
| 96 | </html> | ||
| 97 | {{end}} | ||
| 98 | |||
| 99 | {{define "index"}} | ||
| 100 | {{template "header" .}} | ||
| 101 | <h1>Repositories</h1> | ||
| 102 | {{if .Repos}} | ||
| 103 | <table> | ||
| 104 | <tr><th>Name</th><th>Description</th><th>Created</th></tr> | ||
| 105 | {{range .Repos}} | ||
| 106 | <tr> | ||
| 107 | <td><a href="/r/{{.Name}}">{{.Name}}</a></td> | ||
| 108 | <td class="muted">{{.Description}}</td> | ||
| 109 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 110 | </tr> | ||
| 111 | {{end}} | ||
| 112 | </table> | ||
| 113 | {{else}} | ||
| 114 | <p class="muted">No public repositories.</p> | ||
| 115 | {{end}} | ||
| 116 | {{template "footer" .}} | ||
| 117 | {{end}} | ||
| 118 | |||
| 119 | {{define "repo-ci"}} | ||
| 120 | {{if .Runs}} | ||
| 121 | <h2>CI</h2> | ||
| 122 | <table> | ||
| 123 | <tr><th>Job</th><th>Commit</th><th>Status</th><th>Attempts</th><th>Age</th></tr> | ||
| 124 | {{range .Runs}} | ||
| 125 | <tr> | ||
| 126 | <td><a href="/r/{{$.Name}}/runs/{{.ID}}">{{.Job}}</a> <span class="muted">{{.Workflow}}</span></td> | ||
| 127 | <td class="sha">{{short .SHA}}</td> | ||
| 128 | <td class="muted">{{.Status}}{{if eq .Status "failed"}} (exit {{.ExitCode}}){{end}}</td> | ||
| 129 | <td class="muted">{{.Attempts}}</td> | ||
| 130 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 131 | {{if or .LogOID .Log}}<td><a href="/r/{{$.Name}}/runs/{{.ID}}/log">log</a></td>{{else}}<td></td>{{end}} | ||
| 132 | </tr> | ||
| 133 | {{end}} | ||
| 134 | </table> | ||
| 135 | {{end}} | ||
| 136 | {{end}} | ||
| 137 | |||
| 138 | {{define "repo"}} | ||
| 139 | {{template "header" .}} | ||
| 140 | <h1>{{.Name}}</h1> | ||
| 141 | {{if .Description}}<p class="muted">{{.Description}}</p>{{end}} | ||
| 142 | {{if .Site}}<p class="muted">Site: <a href="{{.Site}}">{{.Site}}</a></p>{{end}} | ||
| 143 | {{if .Readme}}<div class="md readme">{{.Readme}}</div>{{end}} | ||
| 144 | |||
| 145 | <div class="row"> | ||
| 146 | <div> | ||
| 147 | <h2>Clone</h2> | ||
| 148 | <p><code>{{.CloneURL}}</code></p> | ||
| 149 | <p class="muted">Writing needs SSH and a registered key:</p> | ||
| 150 | <p><code>git clone {{.SSHURL}}</code></p> | ||
| 151 | </div> | ||
| 152 | <div> | ||
| 153 | <h2>Branch</h2> | ||
| 154 | <form method="get" action="/r/{{.Name}}"> | ||
| 155 | <select name="ref" onchange="this.form.submit()"> | ||
| 156 | {{range .Branches}}<option value="{{.}}"{{if eq . $.Ref}} selected{{end}}>{{.}}</option>{{end}} | ||
| 157 | </select> | ||
| 158 | </form> | ||
| 159 | <p class="muted">{{.Count}} commit(s) · head <span class="sha">{{short .Commit}}</span></p> | ||
| 160 | </div> | ||
| 161 | </div> | ||
| 162 | |||
| 163 | <p><a href="/r/{{.Name}}/issues">Issues</a> <span class="muted">({{.IssueCount}})</span></p> | ||
| 164 | <p><a href="/r/{{.Name}}/requests">Change requests</a></p> | ||
| 165 | <p><a href="/r/{{.Name}}/releases">Releases</a> <span class="muted">({{.ReleaseCount}})</span></p> | ||
| 166 | {{if .LockCount}}<p><a href="/r/{{.Name}}/locks">Locks</a> <span class="muted">({{.LockCount}} held)</span></p>{{end}} | ||
| 167 | |||
| 168 | <h2>Files</h2> | ||
| 169 | {{if .Entries}} | ||
| 170 | <table> | ||
| 171 | {{range .Entries}} | ||
| 172 | <tr> | ||
| 173 | {{if eq .Type "tree"}} | ||
| 174 | <td><a class="dir" href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{.Name}}">{{.Name}}/</a></td> | ||
| 175 | {{else}} | ||
| 176 | <td><a href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{.Name}}">{{.Name}}</a></td> | ||
| 177 | {{end}} | ||
| 178 | <td class="muted">{{.Mode}}</td> | ||
| 179 | </tr> | ||
| 180 | {{end}} | ||
| 181 | </table> | ||
| 182 | {{else}} | ||
| 183 | <p class="muted">This repository is empty.</p> | ||
| 184 | {{end}} | ||
| 185 | |||
| 186 | <h2>Recent commits</h2> | ||
| 187 | {{template "commitlist" .}} | ||
| 188 | |||
| 189 | {{if .ContributorBranches}} | ||
| 190 | <h2>Contributor branches</h2> | ||
| 191 | <p class="muted">CI does not run for these branches until a maintainer allows the current commit.</p> | ||
| 192 | <table> | ||
| 193 | <tr><th>Branch</th><th>CI</th></tr> | ||
| 194 | {{range .ContributorBranches}} | ||
| 195 | <tr> | ||
| 196 | <td><code>{{.}}</code></td> | ||
| 197 | <td> | ||
| 198 | {{if $.CanAllowCI}} | ||
| 199 | <form method="post" action="/r/{{$.Name}}/ci/allow"> | ||
| 200 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> | ||
| 201 | <input type="hidden" name="ref" value="refs/heads/{{.}}"> | ||
| 202 | <button type="submit">Allow CI for this tip</button> | ||
| 203 | </form> | ||
| 204 | {{else}}<span class="muted">waiting for a maintainer to allow it</span>{{end}} | ||
| 205 | </td> | ||
| 206 | </tr> | ||
| 207 | {{end}} | ||
| 208 | </table> | ||
| 209 | {{end}} | ||
| 210 | |||
| 211 | {{template "repo-ci" .}} | ||
| 212 | {{template "footer" .}} | ||
| 213 | {{end}} | ||
| 214 | |||
| 215 | {{define "commitlist"}} | ||
| 216 | {{if .Commits}} | ||
| 217 | <table> | ||
| 218 | {{range .Commits}} | ||
| 219 | <tr> | ||
| 220 | <td><a class="sha" href="/r/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a></td> | ||
| 221 | <td><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a></td> | ||
| 222 | <td class="muted">{{.Author}}</td> | ||
| 223 | <td class="muted">{{since .When}}</td> | ||
| 224 | </tr> | ||
| 225 | {{end}} | ||
| 226 | </table> | ||
| 227 | {{else}} | ||
| 228 | <p class="muted">No commits yet.</p> | ||
| 229 | {{end}} | ||
| 230 | {{end}} | ||
| 231 | |||
| 232 | {{define "tree"}} | ||
| 233 | {{template "header" .}} | ||
| 234 | <h1><a href="/r/{{.Name}}">{{.Name}}</a></h1> | ||
| 235 | <p class="crumbs"> | ||
| 236 | <a href="/r/{{.Name}}/tree/{{esc .Ref}}">{{.Ref}}</a> | ||
| 237 | {{range .Crumbs}} / <span>{{.Name}}</span>{{end}} | ||
| 238 | </p> | ||
| 239 | <table> | ||
| 240 | {{range .Entries}} | ||
| 241 | <tr> | ||
| 242 | {{if eq .Type "tree"}} | ||
| 243 | <td><a class="dir" href="/r/{{$.Name}}/tree/{{esc $.Ref}}/{{.Name}}">{{.Name}}/</a></td> | ||
| 244 | {{else}} | ||
| 245 | <td><a href="/r/{{$.Name}}/blob/{{esc $.Ref}}/{{.Name}}">{{.Name}}</a></td> | ||
| 246 | {{end}} | ||
| 247 | <td class="muted">{{.Mode}}</td> | ||
| 248 | </tr> | ||
| 249 | {{end}} | ||
| 250 | </table> | ||
| 251 | {{template "footer" .}} | ||
| 252 | {{end}} | ||
| 253 | |||
| 254 | {{define "diff"}} | ||
| 255 | <p class="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}}</p> | ||
| 256 | {{range .Files}} | ||
| 257 | <div class="diff"> | ||
| 258 | <h3>{{if and .OldPath (ne .OldPath .NewPath)}}{{.OldPath}} → {{end}}{{.Label}}{{if .Binary}} <span class="muted">binary</span>{{end}}</h3> | ||
| 259 | {{if .Binary}} | ||
| 260 | <p class="binary">Binary file: no line diff to show. Use <code>git show</code> to inspect it.</p> | ||
| 261 | {{end}} | ||
| 262 | {{if .Hunks}} | ||
| 263 | <table> | ||
| 264 | {{range .Hunks}} | ||
| 265 | <tr class="hunk"><td colspan="3">{{.Header}}</td></tr> | ||
| 266 | {{range .Lines}} | ||
| 267 | <tr class="{{.Kind}}"> | ||
| 268 | <td class="ln">{{if .OldNo}}{{.OldNo}}{{end}}</td> | ||
| 269 | <td class="ln">{{if .NewNo}}{{.NewNo}}{{end}}</td> | ||
| 270 | <td class="code">{{.Text}}</td> | ||
| 271 | </tr> | ||
| 272 | {{if .Comments}}<tr class="note"><td colspan="3">{{range .Comments}}<a class="label" href="#c-{{.ID}}">{{who .Author}} commented</a>{{end}}</td></tr>{{end}} | ||
| 273 | {{end}} | ||
| 274 | {{end}} | ||
| 275 | </table> | ||
| 276 | {{end}} | ||
| 277 | </div> | ||
| 278 | {{end}} | ||
| 279 | {{end}} | ||
| 280 | |||
| 281 | {{define "commit"}} | ||
| 282 | {{template "header" .}} | ||
| 283 | <h1>{{.Commit.Subject}}</h1> | ||
| 284 | <p class="muted"><span class="sha">{{.Commit.SHA}}</span> · {{.Commit.Author}} · {{since .Commit.When}}</p> | ||
| 285 | {{if .Body}}<pre>{{.Body}}</pre>{{end}} | ||
| 286 | {{template "diff" .}} | ||
| 287 | {{template "footer" .}} | ||
| 288 | {{end}} | ||
| 289 | |||
| 290 | {{define "blob"}} | ||
| 291 | {{template "header" .}} | ||
| 292 | <h1><a href="/r/{{.Name}}">{{.Name}}</a></h1> | ||
| 293 | <p class="crumbs"> | ||
| 294 | <a href="/r/{{.Name}}/blob/{{esc .Ref}}">{{.Path}}</a> · | ||
| 295 | <a href="/r/{{.Name}}/raw/{{esc .Ref}}/{{.Path}}">raw</a> · | ||
| 296 | <span class="muted">{{.Size}} bytes</span> | ||
| 297 | </p> | ||
| 298 | {{if .Binary}} | ||
| 299 | <p class="muted">Binary file, {{.Size}} bytes. <a href="/r/{{.Name}}/raw/{{esc .Ref}}/{{.Path}}">Download it</a>.</p> | ||
| 300 | {{else}} | ||
| 301 | {{if .Truncated}}<p class="muted">Showing the first 1 MiB.</p>{{end}} | ||
| 302 | {{if .Markdown}}<div class="md">{{.Markdown}}</div> | ||
| 303 | {{else}}<pre>{{.Content}}</pre>{{end}} | ||
| 304 | {{end}} | ||
| 305 | {{template "footer" .}} | ||
| 306 | {{end}} | ||
| 307 | |||
| 308 | {{define "issues"}} | ||
| 309 | {{template "header" .}} | ||
| 310 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · issues</h1> | ||
| 311 | <form method="get" action="/r/{{.Name}}/issues" class="filters"> | ||
| 312 | <input name="q" type="text" placeholder="search title and body" value="{{.Filter.Query}}"> | ||
| 313 | <select name="state"> | ||
| 314 | <option value="">all states</option> | ||
| 315 | <option value="open"{{if eq .Filter.State "open"}} selected{{end}}>open</option> | ||
| 316 | <option value="closed"{{if eq .Filter.State "closed"}} selected{{end}}>closed</option> | ||
| 317 | </select> | ||
| 318 | <input type="hidden" name="label" value="{{.Filter.Label}}"> | ||
| 319 | <input type="hidden" name="assignee" value="{{.Filter.Assignee}}"> | ||
| 320 | <button type="submit">Filter</button> | ||
| 321 | {{if .Filtered}}<a href="/r/{{.Name}}/issues">clear</a>{{end}} | ||
| 322 | </form> | ||
| 323 | <p class="muted">{{.Open}} open · {{.Total}} total</p> | ||
| 324 | {{if .CanDiscuss}} | ||
| 325 | <form method="post" action="/r/{{.Name}}/issues" class="card"> | ||
| 326 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 327 | <label>Title <input type="text" name="title" required maxlength="200"></label> | ||
| 328 | <label>Description <textarea name="body" placeholder="Markdown is welcome."></textarea></label> | ||
| 329 | <button type="submit">Open issue</button> | ||
| 330 | </form> | ||
| 331 | {{end}} | ||
| 332 | {{if .Labels}} | ||
| 333 | <p class="muted">Labels: | ||
| 334 | {{range .Labels}}<a class="label" href="/r/{{$.Name}}/issues?label={{.}}">{{.}}</a>{{end}} | ||
| 335 | </p> | ||
| 336 | {{end}} | ||
| 337 | {{if .Assignees}} | ||
| 338 | <p class="muted">Assignees: | ||
| 339 | {{range .Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{.}}">@{{.}}</a>{{end}} | ||
| 340 | </p> | ||
| 341 | {{end}} | ||
| 342 | {{if .Issues}} | ||
| 343 | <table> | ||
| 344 | <tr><th>ID</th><th>Title</th><th>State</th><th>Author</th><th>Replies</th><th>Age</th></tr> | ||
| 345 | {{range .Issues}} | ||
| 346 | <tr> | ||
| 347 | <td class="sha">{{short .ID}}</td> | ||
| 348 | <td><a href="/r/{{$.Name}}/issues/{{.ID}}">{{.Title}}</a>{{range .Labels}}<span class="label">{{.}}</span>{{end}}{{range .Assignees}} <a class="label" href="/r/{{$.Name}}/issues?assignee={{.}}">@{{.}}</a>{{end}}</td> | ||
| 349 | <td class="muted">{{.State}}</td> | ||
| 350 | <td class="muted">{{who .Author}}</td> | ||
| 351 | <td class="muted">{{.Comments}}</td> | ||
| 352 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 353 | </tr> | ||
| 354 | {{end}} | ||
| 355 | </table> | ||
| 356 | {{else if .Filtered}} | ||
| 357 | <p class="muted">No issues match that filter.</p> | ||
| 358 | {{else}} | ||
| 359 | <p class="muted">No issues yet. From a clone of this repository: | ||
| 360 | <code>vole issue new --title "..." -m "..."</code></p> | ||
| 361 | <p class="muted">An issue is also just a file on the <code>vole</code> branch: | ||
| 362 | <code>issues/<ulid>/issue.md</code> — a frontmatter block with the title, state and | ||
| 363 | labels, then the body as markdown, which you can write with any editor.</p> | ||
| 364 | {{end}} | ||
| 365 | {{template "footer" .}} | ||
| 366 | {{end}} | ||
| 367 | |||
| 368 | {{define "issue"}} | ||
| 369 | {{template "header" .}} | ||
| 370 | <h1>{{.Issue.Title}}</h1> | ||
| 371 | <p class="crumbs"> | ||
| 372 | <span class="sha">{{.Issue.ID}}</span> · {{.Issue.State}} · opened by {{who .Issue.Author}} {{since .Issue.Created.Unix}} | ||
| 373 | {{if .Labels}}<span class="label">{{.Labels}}</span>{{end}} | ||
| 374 | {{range .Issue.Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{.}}">@{{.}}</a>{{end}} | ||
| 375 | </p> | ||
| 376 | <article class="md">{{.Description}}</article> | ||
| 377 | <h2>{{len .Comments}} comment(s)</h2> | ||
| 378 | {{range .Comments}} | ||
| 379 | <div class="card" id="c-{{.ID}}"> | ||
| 380 | <p class="muted"><span class="sha">{{short .ID}}</span> · {{who .Author}} · {{since .Created.Unix}} | ||
| 381 | {{if .Anchor}}· on <code>{{.Anchor.Path}}:{{.Anchor.Line}}</code> ({{.Anchor.Side}} side) | ||
| 382 | {{if not .Current}}· <span class="conflict">the file has changed since this comment</span>{{end}}{{end}} | ||
| 383 | </p> | ||
| 384 | <article class="md">{{.Body}}</article> | ||
| 385 | </div> | ||
| 386 | {{else}} | ||
| 387 | <p class="muted">No comments yet. From a clone of this repository: | ||
| 388 | <code>vole issue comment {{.Issue.ID}} -m "..."</code></p> | ||
| 389 | {{end}} | ||
| 390 | {{if .CanDiscuss}} | ||
| 391 | <form method="post" action="/r/{{.Name}}/issues/{{.Issue.ID}}/comments" class="card"> | ||
| 392 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 393 | <label>Comment <textarea name="body" required></textarea></label> | ||
| 394 | <button type="submit">Comment</button> | ||
| 395 | </form> | ||
| 396 | {{end}} | ||
| 397 | {{if .Backlinks}} | ||
| 398 | <h2>Referenced in commits</h2> | ||
| 399 | <table> | ||
| 400 | {{range .Backlinks}} | ||
| 401 | <tr> | ||
| 402 | <td><a class="sha" href="/r/{{$.Name}}/commit/{{.SHA}}">{{short .SHA}}</a></td> | ||
| 403 | <td><a href="/r/{{$.Name}}/commit/{{.SHA}}">{{.Subject}}</a></td> | ||
| 404 | <td class="muted">{{.Author}}</td> | ||
| 405 | <td class="muted">{{since .When}}</td> | ||
| 406 | </tr> | ||
| 407 | {{end}} | ||
| 408 | </table> | ||
| 409 | {{end}} | ||
| 410 | {{template "footer" .}} | ||
| 411 | {{end}} | ||
| 412 | |||
| 413 | {{define "commits"}} | ||
| 414 | {{template "header" .}} | ||
| 415 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · history</h1> | ||
| 416 | <p class="crumbs"><span class="muted">{{.Ref}}</span></p> | ||
| 417 | {{template "commitlist" .}} | ||
| 418 | {{template "footer" .}} | ||
| 419 | {{end}} | ||
| 420 | |||
| 421 | |||
| 422 | {{define "requests"}} | ||
| 423 | {{template "header" .}} | ||
| 424 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · change requests</h1> | ||
| 425 | <p class="muted">{{.Open}} open · {{.Total}} total</p> | ||
| 426 | {{if .CanOpen}} | ||
| 427 | <form method="post" action="/r/{{.Name}}/requests" class="card"> | ||
| 428 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 429 | <label>Title <input type="text" name="title" required maxlength="200"></label> | ||
| 430 | <div class="row"> | ||
| 431 | <div><label>Base <select name="base">{{range .BaseBranches}}<option value="{{.}}"{{if eq . $.Base}} selected{{end}}>{{.}}</option>{{end}}</select></label></div> | ||
| 432 | <div><label>Head <select name="head">{{range .HeadBranches}}<option value="{{.}}">{{.}}</option>{{end}}</select></label></div> | ||
| 433 | </div> | ||
| 434 | <label>Description <textarea name="body" placeholder="Why should this land?"></textarea></label> | ||
| 435 | <button type="submit">Open change request</button> | ||
| 436 | {{if not .HeadBranches}}<p class="muted">Push a branch first. Contributors can push only under <code>refs/heads/contrib/<account-name>/</code>.</p>{{end}} | ||
| 437 | </form> | ||
| 438 | {{end}} | ||
| 439 | {{if .Rows}} | ||
| 440 | <table> | ||
| 441 | <tr><th>ID</th><th>Title</th><th>Base</th><th>Head</th><th>Approvals</th><th>State</th><th>Age</th></tr> | ||
| 442 | {{range .Rows}} | ||
| 443 | <tr> | ||
| 444 | <td class="sha">{{short .ID}}</td> | ||
| 445 | <td><a href="/r/{{$.Name}}/requests/{{.ID}}">{{.Title}}</a></td> | ||
| 446 | <td class="muted">{{.Base}}</td> | ||
| 447 | <td class="sha">{{short .Head}}</td> | ||
| 448 | <td class="muted">{{len .Approvals}}</td> | ||
| 449 | <td class="muted">{{.State}}</td> | ||
| 450 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 451 | </tr> | ||
| 452 | {{end}} | ||
| 453 | </table> | ||
| 454 | {{else}} | ||
| 455 | <p class="muted">No change requests. From a clone of this repository: | ||
| 456 | <code>vole change new --title "..." --base main</code></p> | ||
| 457 | <p class="muted">A request is also just a directory on the <code>vole</code> branch: | ||
| 458 | <code>requests/<ulid>/request.md</code> naming the <code>base</code> branch and the <code>head</code> | ||
| 459 | commit to land, plus <code>approvals/<account-id>.md</code> from each approver.</p> | ||
| 460 | {{end}} | ||
| 461 | {{template "footer" .}} | ||
| 462 | {{end}} | ||
| 463 | |||
| 464 | {{define "request"}} | ||
| 465 | {{template "header" .}} | ||
| 466 | <h1>{{.Request.Title}}</h1> | ||
| 467 | <p class="muted"> | ||
| 468 | {{.Request.State}} · <span class="sha">{{short .Request.ID}}</span> · | ||
| 469 | by {{who .Request.Author}} · {{since .Request.Created.Unix}} | ||
| 470 | </p> | ||
| 471 | <p> | ||
| 472 | <strong>{{.Request.Base}}</strong> ← <span class="sha">{{short .Request.Head}}</span> | ||
| 473 | {{if .Request.Head}}<a href="/r/{{.Name}}/commits/{{.Request.Base}}">history</a>{{end}} | ||
| 474 | </p> | ||
| 475 | {{if .Description}}<div class="markdown">{{.Description}}</div>{{end}} | ||
| 476 | |||
| 477 | <h2>Discussion</h2> | ||
| 478 | {{range .Comments}} | ||
| 479 | <div class="card" id="c-{{.ID}}"> | ||
| 480 | <p class="muted"><span class="sha">{{short .ID}}</span> · {{who .Author}} · {{since .Created.Unix}} | ||
| 481 | {{if .Anchor}}· on <code>{{.Anchor.Path}}:{{.Anchor.Line}}</code> ({{.Anchor.Side}} side) | ||
| 482 | {{if not .Current}}· <span class="conflict">the file has changed since this comment</span>{{end}}{{end}} | ||
| 483 | </p> | ||
| 484 | <article class="md">{{.Body}}</article> | ||
| 485 | </div> | ||
| 486 | {{else}} | ||
| 487 | <p class="muted">No discussion yet. From a clone of this repository: | ||
| 488 | <code>vole change comment {{.Request.ID}} -m "..."</code></p> | ||
| 489 | {{end}} | ||
| 490 | |||
| 491 | <h2>Approvals</h2> | ||
| 492 | {{if .CanApprove}} | ||
| 493 | <form method="post" action="/r/{{.Name}}/requests/{{.Request.ID}}/approve" class="card"> | ||
| 494 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 495 | <p class="muted">Approving names the current head <span class="sha">{{short .Request.Head}}</span>; a new head starts over.</p> | ||
| 496 | <button type="submit">Approve this head</button> | ||
| 497 | </form> | ||
| 498 | {{end}} | ||
| 499 | {{if .Required}}<p class="muted">{{.Current}} of {{.Required}} required approval(s).</p> | ||
| 500 | {{else}}<p class="muted">This repository requires no approvals: only an admin may push.</p> | ||
| 501 | {{end}} | ||
| 502 | {{if .Approvals}} | ||
| 503 | <table> | ||
| 504 | <tr><th>Approver</th><th>Head approved</th><th>Base</th><th>Status</th><th>When</th></tr> | ||
| 505 | {{range .Approvals}} | ||
| 506 | <tr> | ||
| 507 | <td>{{.Name}}</td> | ||
| 508 | <td class="sha">{{short .Head}}</td> | ||
| 509 | <td class="muted">{{.Base}}</td> | ||
| 510 | <td class="muted">{{if .Current}}current{{else}}stale (head or base has changed){{end}}</td> | ||
| 511 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 512 | </tr> | ||
| 513 | {{end}} | ||
| 514 | </table> | ||
| 515 | {{else}} | ||
| 516 | <p class="muted">No approvals yet. An approval names one commit, so pushing a new head starts over.</p> | ||
| 517 | {{end}} | ||
| 518 | |||
| 519 | <h2>Checks</h2> | ||
| 520 | {{if .Checks}} | ||
| 521 | <table> | ||
| 522 | <tr><th>Job</th><th>Status</th></tr> | ||
| 523 | {{range .Checks}} | ||
| 524 | <tr> | ||
| 525 | <td><a href="/r/{{$.Name}}/runs/{{.ID}}">{{.Job}}</a> <span class="muted">{{.Workflow}}</span></td> | ||
| 526 | <td class="muted">{{.Status}}{{if eq .Status "failed"}} (exit {{.Exit}}){{end}}</td> | ||
| 527 | </tr> | ||
| 528 | {{end}} | ||
| 529 | </table> | ||
| 530 | {{else}} | ||
| 531 | <p class="muted">No CI run for this commit yet.</p> | ||
| 532 | {{end}} | ||
| 533 | |||
| 534 | <h2>What would land</h2> | ||
| 535 | {{if .MergeChecked}}{{if .Conflicts}} | ||
| 536 | <p class="conflict">This branch no longer merges into {{.Request.Base}} cleanly: | ||
| 537 | {{range .Conflicts}}<code>{{.}}</code> {{end}}— rebase or merge {{.Request.Base}} before landing it.</p> | ||
| 538 | {{else}} | ||
| 539 | <p class="muted">Merges into {{.Request.Base}} cleanly.</p> | ||
| 540 | {{end}}{{end}} | ||
| 541 | {{if .AlreadyMerged}} | ||
| 542 | <p class="muted">Already merged into {{.Request.Base}}.</p> | ||
| 543 | {{else if .Files}} | ||
| 544 | {{template "diff" .}} | ||
| 545 | {{else}} | ||
| 546 | <p class="muted">No changes, or the head is not reachable from this repository yet.</p> | ||
| 547 | {{end}} | ||
| 548 | {{template "footer" .}} | ||
| 549 | {{end}} | ||
| 550 | |||
| 551 | {{define "locks"}} | ||
| 552 | {{template "header" .}} | ||
| 553 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · locks</h1> | ||
| 554 | <p class="muted">A lock is a claim on one path, held until its holder releases it. It | ||
| 555 | exists for files git cannot merge: while it is held, a push that changes that path is | ||
| 556 | refused, so two people cannot overwrite each other's work on the same binary.</p> | ||
| 557 | <p class="muted">Take and release them with <code>git lfs lock <path></code> and | ||
| 558 | <code>git lfs unlock <path></code>, or by id with <code>git lfs unlock --id</code>.</p> | ||
| 559 | {{if .Problem}}<p class="muted">{{.Problem}}</p>{{end}} | ||
| 560 | {{if .Locks}} | ||
| 561 | <table> | ||
| 562 | <tr><th>Path</th><th>Held by</th><th>Since</th></tr> | ||
| 563 | {{range .Locks}} | ||
| 564 | <tr> | ||
| 565 | <td><code>{{.Path}}</code></td> | ||
| 566 | <td>{{.Owner}}{{if .Ref}} <span class="muted">on {{.Ref}}</span>{{end}}</td> | ||
| 567 | <td class="muted">{{since .Since.Unix}}</td> | ||
| 568 | </tr> | ||
| 569 | {{end}} | ||
| 570 | </table> | ||
| 571 | {{else}} | ||
| 572 | <p class="muted">No path is locked. Nothing is reserving a file, so anybody with write | ||
| 573 | access can push a change to anything.</p> | ||
| 574 | {{end}} | ||
| 575 | {{template "footer" .}} | ||
| 576 | {{end}} | ||
| 577 | |||
| 578 | {{define "releases"}} | ||
| 579 | {{template "header" .}} | ||
| 580 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · releases</h1> | ||
| 581 | {{if .Tags}} | ||
| 582 | <table> | ||
| 583 | <tr><th>Tag</th><th>Notes</th><th>Tagger</th><th>Age</th><th>Source</th></tr> | ||
| 584 | {{range .Tags}} | ||
| 585 | <tr> | ||
| 586 | <td><a href="/r/{{$.Name}}/releases/{{.Name}}">{{.Name}}</a>{{if not .Annotated}} <span class="label">lightweight</span>{{end}}</td> | ||
| 587 | <td class="muted">{{.Subject}}</td> | ||
| 588 | <td class="muted">{{if .Tagger}}{{.Tagger}}{{else}}—{{end}}</td> | ||
| 589 | <td class="muted">{{since .When}}</td> | ||
| 590 | <td><a href="/r/{{$.Name}}/releases/{{.Name}}/archive.tar">tar</a> · <a href="/r/{{$.Name}}/releases/{{.Name}}/archive.tar.gz">tar.gz</a></td> | ||
| 591 | </tr> | ||
| 592 | {{end}} | ||
| 593 | </table> | ||
| 594 | {{else}} | ||
| 595 | <p class="muted">No releases yet. A release is an annotated tag, so this is the whole flow:</p> | ||
| 596 | <pre>git tag -a v1.0.0 -F notes.md | ||
| 597 | git push --follow-tags</pre> | ||
| 598 | {{end}} | ||
| 599 | {{if .Lightweight}} | ||
| 600 | <p class="muted">{{.Lightweight}} lightweight tag(s) are listed but carry no notes: <code>git tag -a</code> | ||
| 601 | records a tagger and a message, which is what makes a tag a release.</p> | ||
| 602 | {{end}} | ||
| 603 | {{template "footer" .}} | ||
| 604 | {{end}} | ||
| 605 | |||
| 606 | {{define "release"}} | ||
| 607 | {{template "header" .}} | ||
| 608 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · {{.Tag.Name}}</h1> | ||
| 609 | <p class="muted"> | ||
| 610 | {{if .Tag.Annotated}}annotated tag{{else}}lightweight tag{{end}} | ||
| 611 | {{if .Tag.Tagger}}by {{.Tag.Tagger}}{{end}} | ||
| 612 | · {{since .Tag.When}} | ||
| 613 | · commit <a class="sha" href="/r/{{.Name}}/commit/{{.Tag.Commit}}">{{short .Tag.Commit}}</a> | ||
| 614 | </p> | ||
| 615 | |||
| 616 | {{if .Notes}} | ||
| 617 | <div class="md">{{.Notes}}</div> | ||
| 618 | {{else if not .Tag.Annotated}} | ||
| 619 | <p class="muted">A lightweight tag has no notes. Annotate it (<code>git tag -a</code>) to give this | ||
| 620 | release a description; the tarball below works either way.</p> | ||
| 621 | {{end}} | ||
| 622 | |||
| 623 | <div class="card"> | ||
| 624 | <h2>Artifacts</h2> | ||
| 625 | {{if .Artifacts}} | ||
| 626 | <table> | ||
| 627 | <tr><th>File</th><th>Size</th><th>SHA-256</th><th></th></tr> | ||
| 628 | {{range .Artifacts}} | ||
| 629 | <tr> | ||
| 630 | <td><a href="/r/{{$.Name}}/releases/{{$.Tag.Name}}/artifacts/{{.Name}}">{{.Name}}</a></td> | ||
| 631 | <td class="muted">{{.Size}}</td> | ||
| 632 | <td class="muted"><code>{{short .OID}}</code></td> | ||
| 633 | <td><a href="/r/{{$.Name}}/releases/{{$.Tag.Name}}/artifacts/{{.Name}}">download</a></td> | ||
| 634 | </tr> | ||
| 635 | {{end}} | ||
| 636 | </table> | ||
| 637 | <p class="muted">Bytes live once in the instance's content-addressed store, so the same | ||
| 638 | file on two releases costs one copy.</p> | ||
| 639 | {{else}} | ||
| 640 | <p class="muted">No artifacts attached. Push the tag first, then:</p> | ||
| 641 | <pre>vole release attach {{.Tag.Name}} dist/*</pre> | ||
| 642 | {{end}} | ||
| 643 | </div> | ||
| 644 | |||
| 645 | <div class="card"> | ||
| 646 | <h2>Source</h2> | ||
| 647 | <p><a href="/r/{{.Name}}/releases/{{.Tag.Name}}/archive.tar">Download .tar</a> | ||
| 648 | · <a href="/r/{{.Name}}/releases/{{.Tag.Name}}/archive.tar.gz">Download .tar.gz</a></p> | ||
| 649 | <p class="muted">Generated from the tag when you ask for it, not stored — the same bytes on | ||
| 650 | every request, so a cached copy cannot go stale.</p> | ||
| 651 | {{if .LFSPointers}}<p class="muted">This repository tracks files with Git LFS: those files appear | ||
| 652 | in the tarball as pointer files, not their contents. Clone with <code>git lfs</code> for the | ||
| 653 | real bytes.</p>{{end}} | ||
| 654 | </div> | ||
| 655 | |||
| 656 | <h2>{{if .Prev}}{{.Count}} commit(s) since {{.Prev}}{{else}}{{.Count}} commit(s){{end}}</h2> | ||
| 657 | {{template "commitlist" .}} | ||
| 658 | {{if .Capped}}<p class="muted">Showing the first {{.CappedAt}}; the rest are in the history.</p>{{end}} | ||
| 659 | {{template "footer" .}} | ||
| 660 | {{end}} | ||
| 661 | |||
| 662 | |||
| 663 | {{define "login"}} | ||
| 664 | {{template "header" .}} | ||
| 665 | <div class="auth card"> | ||
| 666 | <h1>Log in</h1> | ||
| 667 | {{if not .Enabled}} | ||
| 668 | <p class="muted">{{.Reason}}</p> | ||
| 669 | {{else}} | ||
| 670 | <form data-auth="login" data-next="{{.Next}}"> | ||
| 671 | <label>Account name | ||
| 672 | <input type="text" name="username" autocomplete="username webauthn" required autofocus> | ||
| 673 | </label> | ||
| 674 | <button type="submit">Use a passkey</button> | ||
| 675 | <p class="error" data-auth-error></p> | ||
| 676 | </form> | ||
| 677 | {{if .Registration}} | ||
| 678 | <p class="muted">No account yet? <a href="/register">Create one</a>.</p> | ||
| 679 | {{else}} | ||
| 680 | <p class="muted">No account yet? Ask an administrator for a one-time enrollment link.</p> | ||
| 681 | {{end}} | ||
| 682 | {{end}} | ||
| 683 | </div> | ||
| 684 | {{template "footer" .}} | ||
| 685 | {{end}} | ||
| 686 | |||
| 687 | {{define "register"}} | ||
| 688 | {{template "header" .}} | ||
| 689 | <div class="auth card"> | ||
| 690 | <h1>Create an account</h1> | ||
| 691 | <p class="muted">Pick a name and register a passkey. You can add an SSH key after signing in.</p> | ||
| 692 | <form data-auth="register"> | ||
| 693 | <label>Account name | ||
| 694 | <input type="text" name="username" pattern="[a-z0-9][a-z0-9_-]{1,31}" maxlength="32" autocomplete="username webauthn" required autofocus> | ||
| 695 | </label> | ||
| 696 | <label>Display name <span class="muted">(optional, shown to other people)</span> | ||
| 697 | <input type="text" name="display" maxlength="64" autocomplete="name"> | ||
| 698 | </label> | ||
| 699 | <button type="submit">Create account and passkey</button> | ||
| 700 | <p class="error" data-auth-error></p> | ||
| 701 | </form> | ||
| 702 | <p class="muted">Names are 2–32 characters: lowercase letters, digits, <code>-</code> and <code>_</code>. | ||
| 703 | The account name is permanent — it is the folder your branches live under and cannot be | ||
| 704 | changed later. The display name is a label and can be changed any time.</p> | ||
| 705 | </div> | ||
| 706 | {{template "footer" .}} | ||
| 707 | {{end}} | ||
| 708 | |||
| 709 | {{define "invite"}} | ||
| 710 | {{template "header" .}} | ||
| 711 | <div class="auth card"> | ||
| 712 | {{if .Account}} | ||
| 713 | <h1>Join {{.Account.Name}}</h1> | ||
| 714 | {{if not .Enabled}} | ||
| 715 | <p class="muted">{{.Reason}}</p> | ||
| 716 | {{else if .Token}} | ||
| 717 | <p class="muted">Register this device as a passkey for <strong>{{.Account.Name}}</strong>. The link works once.</p> | ||
| 718 | <form data-auth="invite" data-token="{{.Token}}"> | ||
| 719 | <button type="submit">Create a passkey</button> | ||
| 720 | <p class="error" data-auth-error></p> | ||
| 721 | </form> | ||
| 722 | {{else}} | ||
| 723 | <p class="error">This enrollment link is no longer valid.</p> | ||
| 724 | {{end}} | ||
| 725 | {{else}} | ||
| 726 | <h1>Enrollment link</h1> | ||
| 727 | <p class="error">This enrollment link is invalid or has expired. Ask an administrator for a new one.</p> | ||
| 728 | {{end}} | ||
| 729 | </div> | ||
| 730 | {{template "footer" .}} | ||
| 731 | {{end}} | ||
| 732 | |||
| 733 | {{define "account"}} | ||
| 734 | {{template "header" .}} | ||
| 735 | <h1>Account</h1> | ||
| 736 | <p><strong>{{.Account.Label}}</strong> <span class="muted">{{.Account.Name}} · {{.Account.ID}}</span></p> | ||
| 737 | |||
| 738 | <h2>Display name</h2> | ||
| 739 | <p class="muted">Shown next to your account on requests, issues and approvals. Your account | ||
| 740 | name <strong>{{.Account.Name}}</strong> is permanent: your branches live under | ||
| 741 | <code>refs/heads/contrib/{{.Account.Name}}/</code>.</p> | ||
| 742 | <form method="post" action="/account/display" class="card"> | ||
| 743 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 744 | <label>Display name | ||
| 745 | <input type="text" name="display" maxlength="64" value="{{.Account.Display}}" placeholder="{{.Account.Name}}"> | ||
| 746 | </label> | ||
| 747 | <button type="submit">Save display name</button> | ||
| 748 | </form> | ||
| 749 | |||
| 750 | <h2>SSH keys</h2> | ||
| 751 | {{if .Account.Keys}} | ||
| 752 | <table> | ||
| 753 | <tr><th>Fingerprint</th><th>Type</th><th>Comment</th><th></th></tr> | ||
| 754 | {{range .Account.Keys}} | ||
| 755 | <tr> | ||
| 756 | <td class="sha">{{.Fingerprint}}</td> | ||
| 757 | <td class="muted">{{.Type}}</td> | ||
| 758 | <td class="muted">{{.Comment}}</td> | ||
| 759 | <td> | ||
| 760 | <form method="post" action="/account/keys/remove"> | ||
| 761 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> | ||
| 762 | <input type="hidden" name="fingerprint" value="{{.Fingerprint}}"> | ||
| 763 | <button class="link" type="submit">remove</button> | ||
| 764 | </form> | ||
| 765 | </td> | ||
| 766 | </tr> | ||
| 767 | {{end}} | ||
| 768 | </table> | ||
| 769 | {{else}} | ||
| 770 | <p class="muted">No SSH keys yet.</p> | ||
| 771 | {{end}} | ||
| 772 | <form method="post" action="/account/keys" class="card"> | ||
| 773 | <input type="hidden" name="csrf" value="{{.CSRF}}"> | ||
| 774 | <label>Public key <textarea name="key" rows="3" placeholder="ssh-ed25519 AAAA... you@example.com" required></textarea></label> | ||
| 775 | <button type="submit">Add SSH key</button> | ||
| 776 | </form> | ||
| 777 | |||
| 778 | <h2>Passkeys</h2> | ||
| 779 | {{if .Credentials}} | ||
| 780 | <table> | ||
| 781 | <tr><th>Credential</th><th>Label</th><th>Created</th><th>Last used</th></tr> | ||
| 782 | {{range .Credentials}} | ||
| 783 | <tr> | ||
| 784 | <td class="sha">{{credid .ID}}</td> | ||
| 785 | <td>{{if .Label}}{{.Label}}{{else}}passkey{{end}}</td> | ||
| 786 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 787 | <td class="muted">{{if .LastUsed.IsZero}}never{{else}}{{since .LastUsed.Unix}}{{end}}</td> | ||
| 788 | </tr> | ||
| 789 | {{end}} | ||
| 790 | </table> | ||
| 791 | {{else}} | ||
| 792 | <p class="muted">No passkeys yet.</p> | ||
| 793 | {{end}} | ||
| 794 | |||
| 795 | {{if .Enabled}} | ||
| 796 | <h2>Add a passkey</h2> | ||
| 797 | <form data-auth="passkey"> | ||
| 798 | <label>Label <input type="text" name="label" placeholder="laptop, phone, security key"></label> | ||
| 799 | <button type="submit">Register this device</button> | ||
| 800 | <p class="error" data-auth-error></p> | ||
| 801 | </form> | ||
| 802 | {{else}} | ||
| 803 | <p class="muted">{{.Reason}}</p> | ||
| 804 | {{end}} | ||
| 805 | |||
| 806 | <h2>Sessions</h2> | ||
| 807 | {{if .Sessions}} | ||
| 808 | <table> | ||
| 809 | <tr><th>Created</th><th>Last seen</th><th>Expires</th><th>Client</th></tr> | ||
| 810 | {{range .Sessions}} | ||
| 811 | <tr> | ||
| 812 | <td class="muted">{{since .Created.Unix}}</td> | ||
| 813 | <td class="muted">{{since .LastSeen.Unix}}</td> | ||
| 814 | <td class="muted">{{.Expires.Format "2006-01-02 15:04"}}</td> | ||
| 815 | <td class="muted">{{.UserAgent}}</td> | ||
| 816 | </tr> | ||
| 817 | {{end}} | ||
| 818 | </table> | ||
| 819 | {{else}} | ||
| 820 | <p class="muted">No sessions.</p> | ||
| 821 | {{end}} | ||
| 822 | {{template "footer" .}} | ||
| 823 | {{end}} | ||
| 824 | |||
| 825 | {{define "runners"}} | ||
| 826 | {{template "header" .}} | ||
| 827 | <h1>Runners</h1> | ||
| 828 | <p class="muted">A runner is an SSH connection that leases jobs. Nothing is registered: a | ||
| 829 | channel that is open and being refreshed is a runner, and it disappears when it closes.</p> | ||
| 830 | {{if .Sessions}} | ||
| 831 | <table> | ||
| 832 | <tr><th>Runner</th><th>Takes</th><th>State</th><th>Last seen</th></tr> | ||
| 833 | {{range .Sessions}} | ||
| 834 | <tr> | ||
| 835 | <td>{{.Account}}</td> | ||
| 836 | <td class="muted">{{if .Labels}}{{.Labels}}{{else}}<span class="muted">any job</span>{{end}}</td> | ||
| 837 | <td>{{if .Idle}}<span class="muted">waiting for work</span>{{else if .RunID}}<a href="/r/{{.Repo}}/runs/{{.RunID}}">{{.Job}}</a> on <a href="/r/{{.Repo}}">{{.Repo}}</a>{{else}}<span class="muted">busy</span>{{end}}</td> | ||
| 838 | <td class="muted">{{since .Seen.Unix}}</td> | ||
| 839 | </tr> | ||
| 840 | {{end}} | ||
| 841 | </table> | ||
| 842 | {{else}} | ||
| 843 | <p class="muted">No runner is connected: none has been seen in the last {{.Window}}. Start one | ||
| 844 | with <code>vole runner --remote ssh://git@<host></code>. If a job asks for a machine with | ||
| 845 | <code>runs-on</code>, the runner has to carry that label with <code>--label</code>.</p> | ||
| 846 | {{end}} | ||
| 847 | |||
| 848 | <h2>Queue</h2> | ||
| 849 | {{if .Queue}} | ||
| 850 | <table> | ||
| 851 | <tr><th>Repository</th><th>Waiting</th><th>Running</th></tr> | ||
| 852 | {{range .Queue}} | ||
| 853 | <tr> | ||
| 854 | <td><a href="/r/{{.Name}}">{{.Name}}</a></td> | ||
| 855 | <td class="muted">{{.Queued}}</td> | ||
| 856 | <td class="muted">{{.Running}}{{if .Cap}}{{if ge .Running .Cap}} of {{.Cap}} <span class="muted">(at the cap)</span>{{else}} of {{.Cap}}{{end}}{{end}}</td> | ||
| 857 | </tr> | ||
| 858 | {{end}} | ||
| 859 | </table> | ||
| 860 | {{else}} | ||
| 861 | <p class="muted">Nothing is waiting and nothing is running.</p> | ||
| 862 | {{end}} | ||
| 863 | {{template "footer" .}} | ||
| 864 | {{end}} | ||
| 865 | |||
| 866 | {{define "run"}} | ||
| 867 | {{template "header" .}} | ||
| 868 | <h1><a href="/r/{{.Name}}">{{.Name}}</a> · run {{.Run.Job}}</h1> | ||
| 869 | <p class="muted"> | ||
| 870 | {{.Run.Workflow}} · {{if .Run.Tag}}tag {{.Run.Tag}}{{else}}{{.Run.Branch}}{{end}} at <a class="sha" href="/r/{{.Name}}/commit/{{.Run.SHA}}">{{short .Run.SHA}}</a> | ||
| 871 | · {{.Run.Status}}{{if eq .Run.Status "failed"}} (exit {{.Run.ExitCode}}){{end}} | ||
| 872 | · attempt {{.Run.Attempts}} | ||
| 873 | · {{since .Run.Created.Unix}} | ||
| 874 | </p> | ||
| 875 | {{if .Run.OS}}<p class="muted">ran on {{.Run.OS}}/{{.Run.Arch}}{{if .Run.Image}} · in {{if .Run.ImageFile}}image-file <code>{{.Run.ImageFile}}</code> ({{short .Run.ImageSHA}}){{else}}<code>{{.Run.Image}}</code>{{end}} via {{.Run.Runtime}}{{end}}</p>{{end}} | ||
| 876 | {{if .Attestation}}<p class="muted">reported by <strong>{{.Signer}}</strong>{{if .Attestation.CommitID}} · <a href="/r/{{.Name}}/commit/{{.Attestation.CommitID}}">the commit that records it</a>{{end}} · the instance accepted this from that account's key at the push boundary, and logged it</p>{{end}} | ||
| 877 | {{if .Counts}}<p class="muted">counts as a check for this repository</p>{{else if .CountsWhy}}<p class="muted">does not count as a check: {{.CountsWhy}}</p>{{end}} | ||
| 878 | {{if .HasLog}}<p><a href="/r/{{.Name}}/runs/{{.Run.ID}}/log">log</a>{{if gt (len .Logs) 1}} · by attempt: | ||
| 879 | {{range .Logs}}<a href="/r/{{$.Name}}/runs/{{$.Run.ID}}/log?attempt={{.Attempt}}">{{.Attempt}}</a> {{end}}{{end}}</p>{{end}} | ||
| 880 | |||
| 881 | <h2>Artifacts</h2> | ||
| 882 | {{if .Artifacts}} | ||
| 883 | <table> | ||
| 884 | <tr><th>File</th><th>Size</th><th>SHA-256</th><th></th></tr> | ||
| 885 | {{range .Artifacts}} | ||
| 886 | <tr> | ||
| 887 | <td><a href="/r/{{$.Name}}/runs/{{$.Run.ID}}/artifacts/{{.Name}}">{{.Name}}</a></td> | ||
| 888 | <td class="muted">{{bytes .Size}}</td> | ||
| 889 | <td class="muted"><code>{{short .OID}}</code></td> | ||
| 890 | <td><a href="/r/{{$.Name}}/runs/{{$.Run.ID}}/artifacts/{{.Name}}">download</a></td> | ||
| 891 | </tr> | ||
| 892 | {{end}} | ||
| 893 | </table> | ||
| 894 | <p class="muted">Built from {{short .Run.SHA}} by this run. The bytes live once in the | ||
| 895 | instance's content-addressed store, and are swept only when nothing names them.</p> | ||
| 896 | {{else}} | ||
| 897 | <p class="muted">No artifacts published by this run.</p> | ||
| 898 | {{end}} | ||
| 899 | {{template "footer" .}} | ||
| 900 | {{end}} | ||
| 901 | ` | ||