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 {
226226 }
227227 section("pinned:", []string{"PATH", "VISIBILITY", "DESCRIPTION"}, pinnedRows)
228228
229 activityRows := make([][]cell, len(d.Activity))
230 for i, e := range d.Activity {
231 activityRows[i] = []cell{cAge(e.CreatedAt), cText(e.Actor), cText(e.Kind), cRef(e.Repo), cFlex(string(e.Data))}
229 lines := FeedLines(events)
230 activityRows := make([][]cell, len(lines))
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)}
232237 }
233 section("recent activity:", []string{"WHEN", "ACTOR", "KIND", "REPO", "DATA"}, activityRows)
238 section("recent activity:", []string{"WHEN", "EVENT"}, activityRows)
234239
235240 buildRows := make([][]cell, len(d.Builds))
236241 for i, b := range d.Builds {
@@ -343,10 +348,15 @@ func runFeed(c *Ctx, args []string) int {
343348 return strconv.FormatInt(e.ID, 10)
344349 })
345350 ds := feedOutputs(events)
351 lines := FeedLines(events)
346352 return c.emitPage(p, ds, next, func(w io.Writer) {
347 tb := c.table(w, "WHEN", "ACTOR", "KIND", "REPO", "DATA")
348 for _, d := range ds {
349 tb.row(cAge(d.CreatedAt), cText(d.Actor), cText(d.Kind), cRef(d.Repo), cFlex(string(d.Data)))
353 tb := c.table(w, "WHEN", "EVENT")
354 for _, l := range lines {
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))
350360 }
351361 tb.flush()
352362 })
internal/control/dashboard_test.go +47
@@ -23,6 +23,53 @@ func TestDashboardEmptySectionsSayNone(t *testing.T) {
2323 }
2424}
2525
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
2673// The push queue is the one worker queue whose worst failure — a key_id
2774// or team_id Apple did not issue, which config validation cannot check —
2875// dead-letters every row on its first attempt with nothing but a log