vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Expand the admin status page into an operational dashboard
4506a4d3d279556435fdd414265e7499c3dada16 · Keenan Nemetz · 14d ago · parent 2e024cc9
server/web/templates/status.html +161−16
| @@ -1,39 +1,184 @@ | |||
| 1 | 1 | {{define "status"}} | |
| 2 | 2 | {{template "header" .}} | |
| 3 | <div class="page-head"> | ||
| 3 | <div class="page-head status-head"> | ||
| 4 | 4 | <div class="page-head-main"> | |
| 5 | 5 | <h1>Status</h1> | |
| 6 | <p class="page-sub">Core services and queue state. Uptime counted from {{since .Started.Unix}}.</p> | ||
| 6 | <p class="page-sub">Storage, usage, runners, and configuration for this instance. Uptime counted from {{since .Started.Unix}}.</p> | ||
| 7 | 7 | </div> | |
| 8 | 8 | </div> | |
| 9 | 9 | ||
| 10 | {{if .Healthy}} | ||
| 11 | <div class="flash good">All core services are responding.</div> | ||
| 10 | <nav class="tabs status-tabs" aria-label="Status"> | ||
| 11 | <a class="{{if eq .Tab "overview"}}active{{end}}" href="/status"{{if eq .Tab "overview"}} aria-current="page"{{end}}>{{icon "layout-dashboard"}}Overview</a> | ||
| 12 | <a class="{{if eq .Tab "storage"}}active{{end}}" href="/status?tab=storage"{{if eq .Tab "storage"}} aria-current="page"{{end}}>{{icon "hard-drive"}}Storage</a> | ||
| 13 | <a class="{{if eq .Tab "runners"}}active{{end}}" href="/status?tab=runners"{{if eq .Tab "runners"}} aria-current="page"{{end}}>{{icon "server"}}Runners</a> | ||
| 14 | <a class="{{if eq .Tab "configuration"}}active{{end}}" href="/status?tab=configuration"{{if eq .Tab "configuration"}} aria-current="page"{{end}}>{{icon "sliders-horizontal"}}Configuration</a> | ||
| 15 | </nav> | ||
| 16 | |||
| 17 | {{if eq .Tab "overview"}} | ||
| 18 | <div class="status-grid"> | ||
| 19 | <div class="card"> | ||
| 20 | <span class="muted">Storage used</span> | ||
| 21 | <strong>{{bytes .Storage.Used}}</strong> | ||
| 22 | {{if .Storage.Disk.Total}}<span class="status-note">of {{bytes .Storage.Disk.Total}} total</span>{{end}} | ||
| 23 | </div> | ||
| 24 | <div class="card"> | ||
| 25 | <span class="muted">Free space</span> | ||
| 26 | <strong>{{if .Storage.DiskErr}}—{{else}}{{bytes .Storage.Disk.Available}}{{end}}</strong> | ||
| 27 | {{if .Storage.DiskErr}}<span class="status-note bad">unavailable</span>{{else}}<span class="status-note">available of {{bytes .Storage.Disk.Total}}</span>{{end}} | ||
| 28 | </div> | ||
| 29 | <div class="card"> | ||
| 30 | <span class="muted">LFS objects</span> | ||
| 31 | <strong>{{bytes .LFS.Total.Bytes}}</strong> | ||
| 32 | <span class="status-note">{{.LFS.Total.Objects}} object(s)</span> | ||
| 33 | </div> | ||
| 34 | <div class="card"> | ||
| 35 | <span class="muted">Page views</span> | ||
| 36 | <strong>{{if .Views.Disabled}}—{{else}}{{.Views.Month.Views}}{{end}}</strong> | ||
| 37 | <span class="status-note">{{if .Views.Disabled}}not recorded{{else}}last 30 days{{end}}</span> | ||
| 38 | </div> | ||
| 39 | <div class="card"> | ||
| 40 | <span class="muted">Unique users</span> | ||
| 41 | <strong>{{if .Views.Disabled}}—{{else}}{{.Views.Month.Users}}{{end}}</strong> | ||
| 42 | <span class="status-note">{{if .Views.Disabled}}not recorded{{else}}signed in, last 30 days{{end}}</span> | ||
| 43 | </div> | ||
| 44 | </div> | ||
| 45 | |||
| 46 | <p class="muted status-inventory">{{.Accounts}} account(s) · {{.Repos}} repository(s) · {{.Queued}} queued · {{.Running}} running · {{.Runners}} runner(s)</p> | ||
| 47 | |||
| 48 | <h2>Page views</h2> | ||
| 49 | {{if .Views.Disabled}} | ||
| 50 | <p class="muted">Page-view tracking is not wired on this instance.</p> | ||
| 12 | 51 | {{else}} | |
| 13 | <div class="flash bad">One or more core services are failing. Check the detail below.</div> | ||
| 52 | {{if .Views.Err}}<div class="flash bad">{{.Views.Err}}</div>{{end}} | ||
| 53 | <div class="panel"> | ||
| 54 | <table> | ||
| 55 | <thead><tr><th>Period</th><th>Views</th><th>Unique signed-in users</th></tr></thead> | ||
| 56 | <tbody> | ||
| 57 | <tr><td>Today (UTC)</td><td>{{.Views.Today.Views}}</td><td>{{.Views.Today.Users}}</td></tr> | ||
| 58 | <tr><td>Last 7 days</td><td>{{.Views.Week.Views}}</td><td>{{.Views.Week.Users}}</td></tr> | ||
| 59 | <tr><td>Last 30 days</td><td>{{.Views.Month.Views}}</td><td>{{.Views.Month.Users}}</td></tr> | ||
| 60 | </tbody> | ||
| 61 | </table> | ||
| 62 | </div> | ||
| 63 | <p class="muted">Counts successful HTML pages. Static files, Git traffic, redirects, HTMX fragments, and this status page are excluded. Anonymous views count toward Views; Unique users counts signed-in accounts.</p> | ||
| 64 | {{end}} | ||
| 14 | 65 | {{end}} | |
| 15 | 66 | ||
| 16 | <div class="status-grid"> | ||
| 17 | <div class="card"><span class="muted">Accounts</span><strong>{{.Users}}</strong></div> | ||
| 18 | <div class="card"><span class="muted">Repositories</span><strong>{{.Repos}}</strong></div> | ||
| 19 | <div class="card"><span class="muted">Queued runs</span><strong>{{.Queued}}</strong></div> | ||
| 20 | <div class="card"><span class="muted">Running runs</span><strong>{{.Running}}</strong></div> | ||
| 21 | <div class="card"><span class="muted">Runners</span><strong>{{.Runners}}</strong></div> | ||
| 67 | {{if eq .Tab "storage"}} | ||
| 68 | <h2>Storage</h2> | ||
| 69 | {{if .Storage.Err}}<div class="flash bad">The data directory could not be fully measured: {{.Storage.Err}}</div>{{end}} | ||
| 70 | <div class="panel"> | ||
| 71 | <table> | ||
| 72 | <thead><tr><th>Category</th><th>Size</th><th>Files</th></tr></thead> | ||
| 73 | <tbody> | ||
| 74 | {{range .Storage.Categories}} | ||
| 75 | <tr><td>{{.Name}}</td><td>{{bytes .Bytes}}</td><td class="muted">{{.Files}}</td></tr> | ||
| 76 | {{end}} | ||
| 77 | <tr class="status-total"><td>Total</td><td>{{bytes .Storage.Used}}</td><td class="muted">{{.Storage.Files}}</td></tr> | ||
| 78 | </tbody> | ||
| 79 | </table> | ||
| 22 | 80 | </div> | |
| 81 | {{if .Storage.Disk.Total}} | ||
| 82 | <p class="muted">{{bytes .Storage.Disk.Available}} available of {{bytes .Storage.Disk.Total}} on the filesystem; {{bytes .Storage.Disk.Used}} used. Snapshot taken {{since .Storage.Measured.Unix}}.</p> | ||
| 83 | {{else if .Storage.DiskErr}} | ||
| 84 | <p class="bad">Filesystem capacity is unavailable: {{.Storage.DiskErr}}</p> | ||
| 85 | {{end}} | ||
| 23 | 86 | ||
| 87 | <h2>LFS and build objects</h2> | ||
| 88 | {{if .LFS.Disabled}} | ||
| 89 | <p class="muted">The LFS object store is disabled.</p> | ||
| 90 | {{else}} | ||
| 91 | {{if .LFS.Err}}<div class="flash bad">{{.LFS.Err}}</div>{{end}} | ||
| 24 | 92 | <div class="panel"> | |
| 25 | 93 | <table> | |
| 26 | <thead><tr><th>Check</th><th>State</th><th>Detail</th></tr></thead> | ||
| 94 | <thead><tr><th>Kind</th><th>Objects</th><th>Size</th></tr></thead> | ||
| 27 | 95 | <tbody> | |
| 28 | {{range .Checks}} | ||
| 96 | <tr><td>CI logs</td><td>{{.LFS.Logs.Objects}}</td><td>{{bytes .LFS.Logs.Bytes}}</td></tr> | ||
| 97 | <tr><td>CI artifacts</td><td>{{.LFS.Artifacts.Objects}}</td><td>{{bytes .LFS.Artifacts.Bytes}}</td></tr> | ||
| 98 | <tr><td>Repository LFS and other</td><td>{{.LFS.Other.Objects}}</td><td>{{bytes .LFS.Other.Bytes}}</td></tr> | ||
| 99 | <tr class="status-total"><td>Object pool</td><td>{{.LFS.Total.Objects}}</td><td>{{bytes .LFS.Total.Bytes}}</td></tr> | ||
| 100 | </tbody> | ||
| 101 | </table> | ||
| 102 | </div> | ||
| 103 | <p class="muted">Sizes are the object pool's content. The storage table above reports allocated disk blocks, so the same bytes can appear slightly larger there. “Repository LFS and other” includes Git LFS files and any pool object the run queue does not reference.</p> | ||
| 104 | {{end}} | ||
| 105 | {{end}} | ||
| 106 | |||
| 107 | {{if eq .Tab "runners"}} | ||
| 108 | <h2 id="runners">Runners and queue</h2> | ||
| 109 | {{if .RunnerErr}}<div class="flash bad">{{.RunnerErr}}</div>{{end}} | ||
| 110 | {{if .RunnerSessions}} | ||
| 111 | <div class="panel"> | ||
| 112 | <table> | ||
| 113 | <thead><tr><th>Runner</th><th>Takes</th><th>State</th><th>Last seen</th></tr></thead> | ||
| 114 | <tbody> | ||
| 115 | {{range .RunnerSessions}} | ||
| 116 | <tr> | ||
| 117 | <td>{{.Account}}</td> | ||
| 118 | <td class="muted">{{if .Labels}}{{.Labels}}{{else}}any job{{end}}</td> | ||
| 119 | <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> | ||
| 120 | <td class="muted">{{since .Seen.Unix}}</td> | ||
| 121 | </tr> | ||
| 122 | {{end}} | ||
| 123 | </tbody> | ||
| 124 | </table> | ||
| 125 | </div> | ||
| 126 | {{else}} | ||
| 127 | <p class="muted">No runner is connected: none has been seen in the last {{.RunnerWindow}}. Start one | ||
| 128 | with <code>vole runner --remote ssh://git@<host></code>. If a job asks for a machine with | ||
| 129 | <code>runs-on</code>, the runner has to carry that label with <code>--label</code>.</p> | ||
| 130 | {{end}} | ||
| 131 | |||
| 132 | <h3>Queue</h3> | ||
| 133 | {{if .RunnerQueue}} | ||
| 134 | <div class="panel"> | ||
| 135 | <table> | ||
| 136 | <thead><tr><th>Repository</th><th>Waiting</th><th>Running</th></tr></thead> | ||
| 137 | <tbody> | ||
| 138 | {{range .RunnerQueue}} | ||
| 139 | <tr> | ||
| 140 | <td><a href="/r/{{.Name}}">{{.Name}}</a></td> | ||
| 141 | <td class="muted">{{.Queued}}</td> | ||
| 142 | <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> | ||
| 143 | </tr> | ||
| 144 | {{end}} | ||
| 145 | </tbody> | ||
| 146 | </table> | ||
| 147 | </div> | ||
| 148 | {{else}} | ||
| 149 | <p class="muted">Nothing is waiting and nothing is running.</p> | ||
| 150 | {{end}} | ||
| 151 | {{end}} | ||
| 152 | |||
| 153 | {{if eq .Tab "configuration"}} | ||
| 154 | {{if .Config}} | ||
| 155 | <h2 id="configuration">Configuration</h2> | ||
| 156 | <p class="muted">Effective values read at startup. This page is read-only; edit the file on the host and restart Vole to apply changes.</p> | ||
| 157 | {{if .Config.Path}} | ||
| 158 | <p class="muted">Config file: <code>{{.Config.Path}}</code></p> | ||
| 159 | {{if not .Config.Loaded}}<div class="flash warn">That file was not found or was empty at startup, so the built-in defaults below are in use.</div>{{end}} | ||
| 160 | {{else}} | ||
| 161 | <p class="muted">No config file was read; the built-in defaults below are in use.</p> | ||
| 162 | {{end}} | ||
| 163 | <div class="panel"> | ||
| 164 | <table class="config-table"> | ||
| 165 | <thead><tr><th>Setting</th><th>Effective value</th></tr></thead> | ||
| 166 | <tbody> | ||
| 167 | {{range .Config.Groups}} | ||
| 168 | <tr class="config-group"><td colspan="2">{{.Name}}</td></tr> | ||
| 169 | {{range .Settings}} | ||
| 29 | 170 | <tr> | |
| 30 | <td>{{.Name}}</td> | ||
| 31 | <td><span class="check-state {{.State}}">{{.State}}</span></td> | ||
| 32 | <td class="muted">{{.Detail}}</td> | ||
| 171 | <td><code>{{.Key}}</code></td> | ||
| 172 | <td>{{.Value}}{{if .Note}} <span class="muted">— {{.Note}}</span>{{end}}{{if .Warn}} <span class="check-state warn">warning</span>{{end}}</td> | ||
| 33 | 173 | </tr> | |
| 34 | 174 | {{end}} | |
| 175 | {{end}} | ||
| 35 | 176 | </tbody> | |
| 36 | 177 | </table> | |
| 37 | 178 | </div> | |
| 179 | {{else}} | ||
| 180 | <p class="muted">Configuration is unavailable.</p> | ||
| 181 | {{end}} | ||
| 182 | {{end}} | ||
| 38 | 183 | {{template "footer" .}} | |
| 39 | 184 | {{end}} | |