Commit 258f283ca3

258f283ca3919c3591ff0571ac66d0d7631c2830

parent: 2eb4578570

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-19 22:00 UTC

web: section columns on settings, account and admin

Ref #226
internal/web/static/style.css −1
@@ -720,7 +720,6 @@ form.setform input[type="text"], form.setform input[type="number"], form.setform
720720form.setform > button, form.setform > .btngroup { justify-self: start; }
721721form.setform.stack { grid-template-columns: 1fr; }
722722form.setform.stack textarea, form.setform.stack select { width: 100%; max-width: 48rem; }
723nav.sections { display: flex; gap: var(--sp-4); flex-wrap: wrap; font-size: var(--fs-2); margin-bottom: var(--sp-5); }
724723ul.protlist { list-style: none; margin: var(--sp-2) 0; padding: 0; }
725724ul.protlist li {
726725 display: flex;
internal/web/templates/account.html +34 −9
@@ -1,11 +1,26 @@
1{{define "width"}}bounded{{end}}
1{{define "width"}}wide{{end}}
22{{define "title"}}account settings{{end}}
33{{define "content"}}
44<h1>Account settings</h1>
55{{if .Notice}}<p class="error" role="alert">{{.Notice}}</p>{{end}}
66{{if .Message}}<p class="notice" role="status">{{.Message}}</p>{{end}}
77
8<h2>Profile</h2>
8<div class="withcol narrow">
9<nav class="sidecol" aria-label="Sections">
10 <div class="grp"><h2 class="colhead">Sections</h2>
11 <ul>
12 <li><a href="#profile">Profile</a></li>
13 <li><a href="#keys">SSH keys</a></li>
14 <li><a href="#emails">Email addresses</a></li>
15 <li><a href="#pgp">OpenPGP keys</a></li>
16 <li><a href="#notifications">Notifications</a></li>
17 <li><a href="#appearance">Appearance</a></li>
18 <li><a href="#export">Export</a></li>
19 <li><a href="#cli">On the command line</a></li>
20 </ul></div>
21</nav>
22<div class="colmain">
23<section id="profile"><h2>Profile</h2>
924<p class="meta">Shown on your profile page, <a href="/{{.Viewer}}">/{{.Viewer}}</a>.</p>
1025{{if .Draft.Is "about"}}{{template "previewblock" .Draft.HTML}}{{end}}
1126<form method="post" action="/settings" class="setform stack">
@@ -21,8 +36,9 @@
2136 {{template "formatpicker" (.Draft.Or "about" "format" .Profile.AboutFormat)}}
2237 <span class="btngroup"><button type="submit" class="btn">Save profile</button>{{template "previewbtn"}}</span>
2338</form>
39</section>
2440
25<h2>SSH keys</h2>
41<section id="keys"><h2>SSH keys</h2>
2642<p class="meta">Your keys are your identity here. A <code>full</code> key can run
2743commands and push; a <code>git</code> key can only move git data, which is what
2844a CI checkout wants.</p>
@@ -53,8 +69,9 @@ a CI checkout wants.</p>
5369 <button type="submit" class="btn">Add key</button>
5470 </form>
5571</details>
72</section>
5673
57<h2>Email addresses</h2>
74<section id="emails"><h2>Email addresses</h2>
5875<p class="meta">A verified address is what ties your signed commits to this
5976account, and where notifications go.</p>
6077{{if .Emails}}<ul class="plain">
@@ -80,8 +97,9 @@ account, and where notifications go.</p>
8097 <button type="submit" class="btn">Verify</button>
8198 </form>
8299</details>
100</section>
83101
84<h2>OpenPGP keys</h2>
102<section id="pgp"><h2>OpenPGP keys</h2>
85103<p class="meta">Only needed if you sign commits with GPG. SSH signing
86104(<code>gpg.format = ssh</code>) uses the keys above.</p>
87105{{if .PGP}}<div class="tablewrap"><table class="keys">
@@ -102,8 +120,9 @@ account, and where notifications go.</p>
102120 <button type="submit" class="btn">Add key</button>
103121 </form>
104122</details>
123</section>
105124
106<h2>Notifications</h2>
125<section id="notifications"><h2>Notifications</h2>
107126<form method="post" action="/settings" class="setform">
108127 <input type="hidden" name="field" value="notify-mail">
109128 <label for="notify-mail">Activity by mail</label>
@@ -118,8 +137,9 @@ account, and where notifications go.</p>
118137 <button type="submit" class="btn">Save</button>
119138</form>
120139<p class="meta">Every issue and merge request on those repositories, as if you had watched each. A watch or mute on a repository still wins.</p>
140</section>
121141
122<h2>Appearance</h2>
142<section id="appearance"><h2>Appearance</h2>
123143<form method="post" action="/settings" class="setform">
124144 <input type="hidden" name="field" value="theme">
125145 <label for="theme">Colour scheme</label>
@@ -131,14 +151,16 @@ account, and where notifications go.</p>
131151 <button type="submit" class="btn">Save</button>
132152</form>
133153<p class="meta">The same setting as <code>gitbay web theme set</code>. Following the browser takes its light or dark preference.</p>
154</section>
134155
135<h2>Export</h2>
156<section id="export"><h2>Export</h2>
136157<p class="meta">Your profile, repositories, issues and merge requests as one
137158JSON bundle, the same one <code>gitbay account export</code> writes. Keys are
138159never included; a replayed bundle's emails arrive unverified.</p>
139160<p><a href="/settings/export">Download bundle</a></p>
161</section>
140162
141<h2>On the command line</h2>
163<section id="cli"><h2>On the command line</h2>
142164<p class="meta">No page here yet, and nothing refusing one: a credential is
143165easier to pipe than to paste, and a minted token is shown once.</p>
144166<pre class="message">gitbay auth token create --name laptop # API tokens
@@ -146,4 +168,7 @@ gitbay web sessions list # browser sessions
146168gitbay admin ... # instance administration</pre>
147169<p class="meta">All of it works from stock OpenSSH too:
148170<code>ssh git@{{.Host}} auth whoami</code>.</p>
171</section>
172</div>
173</div>
149174{{end}}
internal/web/templates/admin.html +26 −1
@@ -1,46 +1,71 @@
1{{define "width"}}bounded{{end}}
1{{define "width"}}wide{{end}}
22{{define "title"}}admin · {{.Site}}{{end}}
33{{define "content"}}
44<h1>Admin</h1>
55<p class="meta">Server build {{.Commit}}. Every background worker's backlog, the same read as <code>gitbay dashboard --json</code> for an admin.</p>
66<p class="meta"><a href="/admin/users">Accounts</a> — every account, with promote, demote, disable and enable.</p>
77
8<div class="withcol narrow">
9<nav class="sidecol" aria-label="Sections">
10 <div class="grp"><h2 class="colhead">Sections</h2>
11 <ul>
12 <li><a href="#webhooks">Webhook deliveries</a></li>
13 <li><a href="#mail">Mail</a></li>
14 <li><a href="#mirrors">Mirrors</a></li>
15 <li><a href="#builds">Builds</a></li>
16 <li><a href="#deps">Dependency checks</a></li>
17 </ul></div>
18</nav>
19<div class="colmain">
20
821{{with .Queues.Webhooks}}
22<section id="webhooks">
923<h2>Webhook deliveries <span class="count">{{.Pending}}</span></h2>
1024<p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} dead-lettered{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p>
1125{{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>URL</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody>
1226{{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{.URL}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}dead-lettered {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{if .Status}}{{.Status}} {{end}}{{.LastError}}</td></tr>
1327{{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or dead-lettered</p>{{end}}
28</section>
1429{{end}}
1530
1631{{with .Queues.Mail}}
32<section id="mail">
1733<h2>Mail <span class="count">{{.Pending}}</span></h2>
1834<p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} failed{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p>
1935{{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Mail</th><th>Recipient</th><th>Subject</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody>
2036{{range .Items}}<tr><td class="mono">{{.ID}}</td><td>{{.Recipient}}</td><td>{{.Subject}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}failed {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{.LastError}}</td></tr>
2137{{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or failed</p>{{end}}
38</section>
2239{{end}}
2340
2441{{with .Queues.Mirrors}}
42<section id="mirrors">
2543<h2>Mirrors <span class="count">{{.Errors}}</span></h2>
2644<p class="meta">{{.Dirty}} waiting for a sync · {{.Errors}} with a failed last sync</p>
2745{{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Direction</th><th>URL</th><th>Last sync</th><th>Error</th></tr></thead><tbody>
2846{{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{.Direction}}</td><td>{{.URL}}</td><td>{{if .LastSync}}{{when .LastSync}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr>
2947{{end}}</tbody></table></div>{{else}}<p class="none">Every mirror's last sync succeeded</p>{{end}}
48</section>
3049{{end}}
3150
3251{{with .Queues.Builds}}
52<section id="builds">
3353<h2>Builds <span class="count">{{.Pending}}</span></h2>
3454<p class="meta">{{.Pending}} pending · {{.Running}} running{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p>
3555{{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Build</th><th>Job</th><th>Status</th><th>Since</th></tr></thead><tbody>
3656{{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td><a href="/{{.Repo}}/builds/{{.Number}}">#{{.Number}}</a></td><td>{{.Job}}</td><td>{{.Status}}</td><td>{{if .StartedAt}}{{when .StartedAt}}{{else}}{{when .CreatedAt}}{{end}}</td></tr>
3757{{end}}</tbody></table></div>{{else}}<p class="none">No build running or pending</p>{{end}}
58</section>
3859{{end}}
3960
4061{{with .Queues.Deps}}
62<section id="deps">
4163<h2>Dependency checks <span class="count">{{.Errors}}</span></h2>
4264{{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Repository</th><th>Last check</th><th>Error</th></tr></thead><tbody>
4365{{range .Items}}<tr><td><a href="/{{.Repo}}">{{.Repo}}</a></td><td>{{if .LastCheck}}{{when .LastCheck}}{{else}}never{{end}}</td><td>{{.LastError}}</td></tr>
4466{{end}}</tbody></table></div>{{else}}<p class="none">No check has failed</p>{{end}}
67</section>
4568{{end}}
69</div>
70</div>
4671{{end}}
internal/web/templates/settings.html +18 −2
@@ -1,10 +1,24 @@
1{{define "width"}}bounded{{end}}
1{{define "width"}}wide{{end}}
22{{define "title"}}settings · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
33{{define "content"}}
44{{$base := printf "/%s/%s/settings" .Repo.OwnerName .Repo.Name}}
55<h1>Settings</h1>
66{{if .Notice}}{{if .Saved}}<p class="notice" role="status">{{.Notice}}</p>{{else}}<p class="error" role="alert">{{.Notice}}</p>{{end}}{{end}}
7<nav class="sections" aria-label="Sections"><a href="#identity">Identity</a><a href="#access">Access</a><a href="#gates">Merge gates</a><a href="#branches">Protected branches</a><a href="#tags">Protected tags</a><a href="#deps">Dependencies</a><a href="#runners">Runners</a><a href="#lifecycle">Lifecycle</a></nav>
7<div class="withcol narrow">
8<nav class="sidecol" aria-label="Sections">
9 <div class="grp"><h2 class="colhead">Sections</h2>
10 <ul>
11 <li><a href="#identity">Identity</a></li>
12 <li><a href="#access">Access</a></li>
13 <li><a href="#gates">Merge gates</a></li>
14 <li><a href="#branches">Protected branches</a></li>
15 <li><a href="#tags">Protected tags</a></li>
16 <li><a href="#deps">Dependencies</a></li>
17 <li><a href="#runners">Runners</a></li>
18 <li><a href="#lifecycle">Lifecycle</a></li>
19 </ul></div>
20</nav>
21<div class="colmain">
822
923<section id="identity"><h2>Identity</h2>
1024<form method="post" action="{{$base}}" class="setform">
@@ -184,4 +198,6 @@
184198<p class="meta">Deleting or transferring a repository is a CLI operation:
185199<code>gitbay repo delete {{.Repo.OwnerName}}/{{.Repo.Name}} --yes</code></p>
186200</section>
201</div>
202</div>
187203{{end}}
internal/web/web_test.go +2 −2
@@ -89,8 +89,8 @@ func TestWhenNamesTheZone(t *testing.T) {
8989// none. The merge request page picks wide for its diff view, so it gets
9090// a per-view define instead of a fixed one.
9191func TestMainWidthClass(t *testing.T) {
92 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "explore.html": true, "notifications.html": true}
93 bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "settings.html": true, "account.html": true, "admin.html": true, "adminusers.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true}
92 wide := map[string]bool{"tree.html": true, "blob.html": true, "blame.html": true, "log.html": true, "commit.html": true, "compare.html": true, "builds.html": true, "build.html": true, "search.html": true, "globalsearch.html": true, "edit.html": true, "dashboard.html": true, "issues.html": true, "mrs.html": true, "explore.html": true, "notifications.html": true, "settings.html": true, "account.html": true, "admin.html": true}
93 bounded := map[string]bool{"landing.html": true, "login.html": true, "register.html": true, "registered.html": true, "new.html": true, "issuenew.html": true, "mrnew.html": true, "adminusers.html": true, "snippetnew.html": true, "privacy.html": true, "404.html": true}
9494 perView := map[string]string{"mr.html": `{{define "width"}}{{if eq .View "diff"}}wide{{else}}reading{{end}}{{end}}`}
9595 for _, name := range Pages() {
9696 src, err := TemplateSource(name)
internal/web/widths_test.go +15
@@ -1,6 +1,7 @@
11package web
22
33import (
4 "regexp"
45 "strings"
56 "testing"
67)
@@ -33,4 +34,18 @@ func TestListPagesAreWide(t *testing.T) {
3334 if src, _ := templateFS.ReadFile("templates/explore.html"); !strings.Contains(string(src), `<ul class="repolist rows">`) {
3435 t.Error("explore.html does not use one-line rows")
3536 }
37 // Settings pages carry a section column: every section id has a link
38 // in the column, and the page is wide with the narrow grid.
39 for _, name := range []string{"settings.html", "account.html", "admin.html"} {
40 src, _ := templateFS.ReadFile("templates/" + name)
41 s := string(src)
42 if !strings.HasPrefix(s, `{{define "width"}}wide{{end}}`) || !strings.Contains(s, `<div class="withcol narrow">`) {
43 t.Errorf("%s lacks the narrow column layout", name)
44 }
45 for _, m := range regexp.MustCompile(`<section id="([a-z]+)"`).FindAllStringSubmatch(s, -1) {
46 if !strings.Contains(s, `href="#`+m[1]+`"`) {
47 t.Errorf("%s: section %q has no link in the column", name, m[1])
48 }
49 }
50 }
3651}