Commit a453498589
a4534985894f672e00cda40aab9e231d9e507385
parent: 5063e69c97
Verified · cmc ci/build: success ci/test: success
cmc <hello@cleberg.net> · 2026-09-20 11:27 UTC
web: render the push queue on /admin
The page says it is the same read as dashboard --json for an admin,
and the Admin wiki page says the push row renders there, but the
template still listed five queues. Add the sixth, in dashboard's
order and the mail section's shape — it is the same queue shape, with
the device id where the recipient is.
The row names the queue row id and the device id, the two the
dead-lettered warning logs, so a log line can be found here. The
token is not among them, on an admin's page no more than on its
owner's.
The handler already passes the whole store.Queues, so it is unchanged.
Ref #89
internal/httpd/admin_test.go
added
+74
| @@ -0,0 +1,74 @@ |
| 1 | package httpd |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "strconv" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "gitbay.org/gitbay/internal/config" |
| 11 | "gitbay.org/gitbay/internal/store" |
| 12 | ) |
| 13 | |
| 14 | // The admin page renders every queue dashboard reports, push included, |
| 15 | // and names a push by its device id: a token is device-identifying and |
| 16 | // reaches an admin's page no more than it reaches its owner's. |
| 17 | func TestAdminPageShowsThePushQueue(t *testing.T) { |
| 18 | st, err := store.Open(":memory:") |
| 19 | if err != nil { |
| 20 | t.Fatal(err) |
| 21 | } |
| 22 | defer st.Close() |
| 23 | if err := st.MigrateUp(); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | uid, err := st.CreateUser("root", true) |
| 27 | if err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | if err := st.SetPushEnabled(uid, true); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | token := strings.Repeat("c", 64) |
| 34 | device, err := st.AddPushDevice(uid, token, "iphone") |
| 35 | if err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | if err := st.EnqueuePush(uid, "krz/gitbay", "alice opened #1", "/krz/gitbay/issues/1"); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | // Only rows that are retrying or dead-lettered are listed, so fail |
| 42 | // the queued one first. |
| 43 | due, err := st.DuePush(10) |
| 44 | if err != nil || len(due) != 1 { |
| 45 | t.Fatalf("DuePush: %v %v", due, err) |
| 46 | } |
| 47 | if err := st.MarkPushFailed(due[0].ID, "403 InvalidProviderToken", nil); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | |
| 51 | s := New(config.Default(), st) |
| 52 | rr := httptest.NewRecorder() |
| 53 | s.adminPage(rr, httptest.NewRequest("GET", "/admin", nil), store.User{ID: uid, Username: "root", IsAdmin: true}) |
| 54 | if rr.Code != http.StatusOK { |
| 55 | t.Fatalf("status %d, body %s", rr.Code, rr.Body.String()) |
| 56 | } |
| 57 | |
| 58 | body := rr.Body.String() |
| 59 | if !strings.Contains(body, `id="push"`) { |
| 60 | t.Fatalf("no push section:\n%s", body) |
| 61 | } |
| 62 | if !strings.Contains(body, `href="#push"`) { |
| 63 | t.Fatalf("push is missing from the jump list:\n%s", body) |
| 64 | } |
| 65 | if !strings.Contains(body, "device "+strconv.FormatInt(device, 10)) { |
| 66 | t.Fatalf("the dead-lettered push is not listed by device id:\n%s", body) |
| 67 | } |
| 68 | if !strings.Contains(body, "403 InvalidProviderToken") { |
| 69 | t.Fatalf("the dead-lettered push's error is not shown:\n%s", body) |
| 70 | } |
| 71 | if strings.Contains(body, token) { |
| 72 | t.Fatalf("the page printed a device token:\n%s", body) |
| 73 | } |
| 74 | } |
internal/web/templates/admin.html
+12
| @@ -13,6 +13,7 @@ |
| 13 | 13 | <ul> |
| 14 | 14 | <li><a href="#webhooks">Webhook deliveries</a></li> |
| 15 | 15 | <li><a href="#mail">Mail</a></li> |
| 16 | <li><a href="#push">Push</a></li> |
| 16 | 17 | <li><a href="#mirrors">Mirrors</a></li> |
| 17 | 18 | <li><a href="#builds">Builds</a></li> |
| 18 | 19 | <li><a href="#deps">Dependency checks</a></li> |
| @@ -41,6 +42,17 @@ |
| 41 | 42 | </section> |
| 42 | 43 | {{end}} |
| 43 | 44 | |
| 45 | {{/* A push row names the device id, never the token, as dashboard does. */}} |
| 46 | {{with .Queues.Push}} |
| 47 | <section id="push"> |
| 48 | <h2>Push <span class="count">{{.Pending}}</span></h2> |
| 49 | <p class="meta">{{.Pending}} pending · {{.Retrying}} retrying · {{.Failed}} failed{{if .OldestPending}} · oldest pending {{when .OldestPending}}{{end}}</p> |
| 50 | {{if .Items}}<div class="tablewrap"><table class="keys"><thead><tr class="cols"><th>Push</th><th>Device</th><th>Title</th><th>Attempts</th><th>State</th><th>Last error</th></tr></thead><tbody> |
| 51 | {{range .Items}}<tr><td class="mono">{{.ID}}</td><td class="mono">device {{.DeviceID}}</td><td>{{.Title}}</td><td>{{.Attempts}}</td><td>{{if .FailedAt}}failed {{when .FailedAt}}{{else}}retrying{{end}}</td><td>{{.LastError}}</td></tr> |
| 52 | {{end}}</tbody></table></div>{{else}}<p class="none">Nothing retrying or failed</p>{{end}} |
| 53 | </section> |
| 54 | {{end}} |
| 55 | |
| 44 | 56 | {{with .Queues.Mirrors}} |
| 45 | 57 | <section id="mirrors"> |
| 46 | 58 | <h2>Mirrors <span class="count">{{.Errors}}</span></h2> |