Commit cf66fc30be

cf66fc30beb376347b314e343a0414c9539e6d14

parent: e0ae4017ac

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 06:34 UTC

dashboard, feed: render activity as the web's sentence, not the raw payload

Ref #265

Layout: unified · split

internal/control/dashboard.go +17 −7
@@ -226,11 +226,16 @@ func runDashboard(c *Ctx, args []string) int {
226 } 226 }
227 section("pinned:", []string{"PATH", "VISIBILITY", "DESCRIPTION"}, pinnedRows) 227 section("pinned:", []string{"PATH", "VISIBILITY", "DESCRIPTION"}, pinnedRows)
228 228
229 activityRows := make([][]cell, len(d.Activity)) 229 lines := FeedLines(events)
230 for i, e := range d.Activity { 230 activityRows := make([][]cell, len(lines))
231 activityRows[i] = []cell{cAge(e.CreatedAt), cText(e.Actor), cText(e.Kind), cRef(e.Repo), cFlex(string(e.Data))} 231 for i, l := range lines {
232 sentence := fmt.Sprintf("%s %s %s%s", l.Actor, l.Verb, l.Repo, l.Ref)
233 if l.Extra != "" {
234 sentence += " " + l.Extra
235 }
236 activityRows[i] = []cell{cAge(l.When), cFlex(sentence)}
232 } 237 }
233 section("recent activity:", []string{"WHEN", "ACTOR", "KIND", "REPO", "DATA"}, activityRows) 238 section("recent activity:", []string{"WHEN", "EVENT"}, activityRows)
234 239
235 buildRows := make([][]cell, len(d.Builds)) 240 buildRows := make([][]cell, len(d.Builds))
236 for i, b := range d.Builds { 241 for i, b := range d.Builds {
@@ -343,10 +348,15 @@ func runFeed(c *Ctx, args []string) int {
343 return strconv.FormatInt(e.ID, 10) 348 return strconv.FormatInt(e.ID, 10)
344 }) 349 })
345 ds := feedOutputs(events) 350 ds := feedOutputs(events)
351 lines := FeedLines(events)
346 return c.emitPage(p, ds, next, func(w io.Writer) { 352 return c.emitPage(p, ds, next, func(w io.Writer) {
347 tb := c.table(w, "WHEN", "ACTOR", "KIND", "REPO", "DATA") 353 tb := c.table(w, "WHEN", "EVENT")
348 for _, d := range ds { 354 for _, l := range lines {
349 tb.row(cAge(d.CreatedAt), cText(d.Actor), cText(d.Kind), cRef(d.Repo), cFlex(string(d.Data))) 355 sentence := fmt.Sprintf("%s %s %s%s", l.Actor, l.Verb, l.Repo, l.Ref)
356 if l.Extra != "" {
357 sentence += " " + l.Extra
358 }
359 tb.row(cAge(l.When), cFlex(sentence))
350 } 360 }
351 tb.flush() 361 tb.flush()
352 }) 362 })
internal/control/dashboard_test.go +47
@@ -23,6 +23,53 @@ func TestDashboardEmptySectionsSayNone(t *testing.T) {
23 } 23 }
24} 24}
25 25
26func TestDashboardActivityIsASentence(t *testing.T) {
27 c := notifTestCtx(t, "cmc")
28 repoID, err := c.Store.CreateRepo("user", c.User.ID, "gitbay", "public")
29 if err != nil {
30 t.Fatal(err)
31 }
32 repo, err := c.Store.RepoByID(repoID)
33 if err != nil {
34 t.Fatal(err)
35 }
36 c.Store.RecordEvent(repo.ID, c.User.ID, "issue.labeled", `{"number":262,"labels":["ops","security"]}`)
37
38 var out bytes.Buffer
39 c.Stdout, c.Stderr = &out, &out
40 if code := runDashboard(c, nil); code != 0 {
41 t.Fatalf("exit %d: %s", code, out.String())
42 }
43 if strings.Contains(out.String(), `{"number"`) {
44 t.Errorf("raw payload leaked into plain output:\n%s", out.String())
45 }
46 if !strings.Contains(out.String(), "cmc labelled cmc/gitbay#262 ops, security") {
47 t.Errorf("no sentence in output:\n%s", out.String())
48 }
49}
50
51func TestFeedIsASentence(t *testing.T) {
52 c := notifTestCtx(t, "cmc")
53 repoID, err := c.Store.CreateRepo("user", c.User.ID, "gitbay", "public")
54 if err != nil {
55 t.Fatal(err)
56 }
57 repo, err := c.Store.RepoByID(repoID)
58 if err != nil {
59 t.Fatal(err)
60 }
61 c.Store.RecordEvent(repo.ID, c.User.ID, "issue.created", `{"number":1}`)
62
63 var out bytes.Buffer
64 c.Stdout, c.Stderr = &out, &out
65 if code := runFeed(c, nil); code != 0 {
66 t.Fatalf("exit %d: %s", code, out.String())
67 }
68 if !strings.Contains(out.String(), "cmc opened issue cmc/gitbay#1") {
69 t.Errorf("no sentence in output:\n%s", out.String())
70 }
71}
72
26// The push queue is the one worker queue whose worst failure — a key_id 73// The push queue is the one worker queue whose worst failure — a key_id
27// or team_id Apple did not issue, which config validation cannot check — 74// or team_id Apple did not issue, which config validation cannot check —
28// dead-letters every row on its first attempt with nothing but a log 75// dead-letters every row on its first attempt with nothing but a log