feat: split admin surface into bookmarkable pages (#138)

This commit is contained in:
2026-08-21 16:54:54 +07:00
parent 0a245a0dde
commit 030ffdc26e
5 changed files with 795 additions and 108 deletions
+47 -15
View File
@@ -640,10 +640,9 @@ type fakeLanes struct{ status latest.Status }
func (f fakeLanes) LaneStatus() latest.Status { return f.status }
// The roster moved off the reading page onto its own address: the owner gets a
// link, everyone else gets nothing, and the page itself lists every Reader with
// the counters and the two controls.
func TestAdminPageCarriesRosterAndOwnerLink(t *testing.T) {
// Every admin address carries the same navigation, while the roster only lives
// on its own page and the other pages keep their shells independent.
func TestAdminPagesCarrySharedNavigation(t *testing.T) {
router, st, _ := oauthWebTestServer(t)
theirCookie := signInCookie(t, router)
ownerCookie := sessionCookie(t, st)
@@ -654,12 +653,11 @@ func TestAdminPageCarriesRosterAndOwnerLink(t *testing.T) {
router.ServeHTTP(rr, req)
body := rr.Body.String()
if strings.Contains(body, `id="readers"`) {
t.Error("the reading page still carries the roster; it belongs on /admin")
t.Error("the reading page still carries the roster; it belongs on /admin/readers")
}
if !strings.Contains(body, `href="/admin"`) {
t.Error("the owner's reading page offers no link to the admin page")
}
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(theirCookie)
rr = httptest.NewRecorder()
@@ -668,21 +666,55 @@ func TestAdminPageCarriesRosterAndOwnerLink(t *testing.T) {
t.Error("a non-owner was offered the admin link")
}
req = httptest.NewRequest(http.MethodGet, "/admin", nil)
for _, page := range []struct {
path string
name string
}{
{"/admin", "Overview"},
{"/admin/lanes", "Lanes"},
{"/admin/readers", "Readers"},
{"/admin/series", "Series"},
} {
t.Run(page.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, page.path, nil)
req.AddCookie(ownerCookie)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("GET %s status = %d, want 200", page.path, rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, `class="topbar-actions"`) {
t.Errorf("%s has no topbar action cluster:\n%s", page.path, body)
}
if !strings.Contains(body, `aria-label="Admin pages"`) {
t.Errorf("%s has no admin navigation:\n%s", page.path, body)
}
if strings.Count(body, `aria-current="page"`) != 1 {
t.Errorf("%s has %d active admin tabs, want 1:\n%s", page.path, strings.Count(body, `aria-current="page"`), body)
}
if !strings.Contains(body, page.name) {
t.Errorf("%s does not name its active page %q:\n%s", page.path, page.name, body)
}
if !strings.Contains(body, `href="/static/admin.css"`) {
t.Errorf("%s does not load the admin foundation stylesheet", page.path)
}
if page.name == "Lanes" && strings.Count(body, `hx-trigger="every 30s"`) != 1 {
t.Errorf("%s has %d Lane timers, want exactly 1", page.path, strings.Count(body, `hx-trigger="every 30s"`))
}
})
}
req = httptest.NewRequest(http.MethodGet, "/admin/readers", nil)
req.AddCookie(ownerCookie)
rr = httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("GET /admin status = %d, want 200", rr.Code)
}
body = rr.Body.String()
for _, want := range []string{`id="readers"`, testOwnerID, "Revoke sessions", "Clear marks", "confirmed"} {
if !strings.Contains(body, want) {
t.Errorf("admin page lacks %q:\n%s", want, body)
t.Errorf("readers page lacks %q:\n%s", want, body)
}
}
// Exactly one revocable row: the other Reader's. The owner's own row carries
// the same session count and no button.
if n := strings.Count(body, "/revoke"); n != 1 {
t.Fatalf("roster has %d revoke controls, want 1 (the owner's own row must have none):\n%s", n, body)
}
@@ -845,7 +877,7 @@ func TestAdminPageWithoutAPollerSaysSo(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
router, st, _ := oauthWebTestServer(t, tc.lanes...)
req := httptest.NewRequest(http.MethodGet, "/admin", nil)
req := httptest.NewRequest(http.MethodGet, "/admin/lanes", nil)
req.AddCookie(sessionCookie(t, st))
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
@@ -881,7 +913,7 @@ func TestOwnerClearsReaderMarks(t *testing.T) {
t.Fatalf("mark reader: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/admin", nil)
req := httptest.NewRequest(http.MethodGet, "/admin/readers", nil)
req.AddCookie(cookie)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)