CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

fix(dashboard): design-sweep fixes for the /dashboard visual audit (#211) #5461

Merged⚡ AI-generatedXSccantynz wants to mergesweep/dashboardmainopened 24d ago
4 changed files+235−14
Addedsrc/__tests__/recent-activity.test.ts+130−0View fileUnifiedSplit
1/**
2 * Tests for the pure dedupe/collapse helper exported from
3 * src/lib/recent-activity.ts (`collapseRecentActivity`).
4 *
5 * Pins the dashboard-audit fixes for duplicated Recent Activity rows:
6 * - "Merged PR #N" + "Opened PR #N" for the same PR collapse into the
7 * merged row (the merge implies the open).
8 * - Consecutive "Pushed code in X" rows inside a short window collapse
9 * into one counted row.
10 * The DB never enters the picture — the helper is pure.
11 */
12
13import { describe, it, expect } from "bun:test";
14import {
15 collapseRecentActivity,
16 type RecentActivityItem,
17} from "../lib/recent-activity";
18
19const T0 = new Date("2026-08-08T12:00:00Z").getTime();
20const at = (minsAgo: number) => new Date(T0 - minsAgo * 60 * 1000);
21
22const item = (
23 action: string,
24 overrides: Partial<RecentActivityItem> = {}
25): RecentActivityItem => ({
26 action,
27 repoName: "repo-a",
28 ownerUsername: "owner",
29 createdAt: at(0),
30 ...overrides,
31});
32
33describe("collapseRecentActivity — merge implies open", () => {
34 it("drops the pr_open row when the same PR's pr_merge row is present", () => {
35 const merged: RecentActivityItem[] = [
36 item("pr_merge", { number: 7, title: "Fix", createdAt: at(1) }),
37 item("pr_open", { number: 7, title: "Fix", createdAt: at(2) }),
38 ];
39 const out = collapseRecentActivity(merged);
40 expect(out).toHaveLength(1);
41 expect(out[0].action).toBe("pr_merge");
42 expect(out[0].number).toBe(7);
43 });
44
45 it("keeps pr_open rows for PRs that were not merged in the window", () => {
46 const merged: RecentActivityItem[] = [
47 item("pr_merge", { number: 7, createdAt: at(1) }),
48 item("pr_open", { number: 8, createdAt: at(2) }),
49 item("pr_open", { number: 7, createdAt: at(3) }),
50 ];
51 const out = collapseRecentActivity(merged);
52 expect(out.map((o) => `${o.action}#${o.number}`)).toEqual([
53 "pr_merge#7",
54 "pr_open#8",
55 ]);
56 });
57
58 it("does not cross-match the same PR number in a different repo", () => {
59 const merged: RecentActivityItem[] = [
60 item("pr_merge", { number: 7, repoName: "repo-a", createdAt: at(1) }),
61 item("pr_open", { number: 7, repoName: "repo-b", createdAt: at(2) }),
62 ];
63 const out = collapseRecentActivity(merged);
64 expect(out).toHaveLength(2);
65 });
66});
67
68describe("collapseRecentActivity — push bursts", () => {
69 it("collapses consecutive pushes to the same repo within the window", () => {
70 const merged: RecentActivityItem[] = [
71 item("push", { createdAt: at(1) }),
72 item("push", { createdAt: at(4) }),
73 item("push", { createdAt: at(8) }),
74 ];
75 const out = collapseRecentActivity(merged);
76 expect(out).toHaveLength(1);
77 expect(out[0].action).toBe("push");
78 expect(out[0].count).toBe(3);
79 });
80
81 it("does not collapse pushes to different repos", () => {
82 const merged: RecentActivityItem[] = [
83 item("push", { repoName: "repo-a", createdAt: at(1) }),
84 item("push", { repoName: "repo-b", createdAt: at(2) }),
85 ];
86 const out = collapseRecentActivity(merged);
87 expect(out).toHaveLength(2);
88 expect(out.every((o) => !o.count)).toBe(true);
89 });
90
91 it("does not collapse pushes further apart than the window", () => {
92 const merged: RecentActivityItem[] = [
93 item("push", { createdAt: at(1) }),
94 item("push", { createdAt: at(30) }),
95 ];
96 const out = collapseRecentActivity(merged);
97 expect(out).toHaveLength(2);
98 });
99
100 it("does not collapse pushes separated by another action", () => {
101 const merged: RecentActivityItem[] = [
102 item("push", { createdAt: at(1) }),
103 item("issue_open", { number: 3, createdAt: at(2) }),
104 item("push", { createdAt: at(3) }),
105 ];
106 const out = collapseRecentActivity(merged);
107 expect(out.map((o) => o.action)).toEqual(["push", "issue_open", "push"]);
108 });
109});
110
111describe("collapseRecentActivity — existing behavior preserved", () => {
112 it("still collapses repo_create bursts with names + count", () => {
113 const merged: RecentActivityItem[] = [
114 item("repo_create", { repoName: "r1", createdAt: at(1) }),
115 item("repo_create", { repoName: "r2", createdAt: at(2) }),
116 item("repo_create", { repoName: "r3", createdAt: at(3) }),
117 ];
118 const out = collapseRecentActivity(merged);
119 expect(out).toHaveLength(1);
120 expect(out[0].count).toBe(3);
121 expect(out[0].title).toBe("r1, r2, r3");
122 });
123
124 it("respects the limit", () => {
125 const merged: RecentActivityItem[] = Array.from({ length: 10 }, (_, i) =>
126 item("issue_open", { number: i + 1, createdAt: at(i * 60) })
127 );
128 expect(collapseRecentActivity(merged, 4)).toHaveLength(4);
129 });
130});
Modifiedsrc/lib/recent-activity.ts+45−1View fileUnifiedSplit
4242/** Actions where a burst is one gesture (bulk import), not N separate ones. */
4343const COLLAPSIBLE = new Set(["repo_create"]);
4444
45/** Consecutive pushes to the same repo inside this window are one gesture. */
46const PUSH_BURST_WINDOW_MS = 10 * 60 * 1000;
47
4548const PER_SOURCE_LIMIT = 40;
4649
4750export async function buildRecentActivity(
177180
178181 merged.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
179182
183 return collapseRecentActivity(merged, limit);
184}
185
186/**
187 * Pure post-processing of the merged, newest-first item list. Exported so
188 * the dedupe/collapse semantics are testable without a DB:
189 *
190 * 1. A merge implies the open: when both "Merged PR #N" and "Opened PR #N"
191 * for the same PR are in the list, drop the open row — the pair renders
192 * as near-duplicate adjacent lines.
193 * 2. Bulk-import bursts of repo creations collapse into one counted row.
194 * 3. Consecutive pushes to the same repo within a short window collapse
195 * into one row ("Pushed code ×3") — one work session, not N events.
196 */
197export function collapseRecentActivity(
198 merged: RecentActivityItem[],
199 limit = 20
200): RecentActivityItem[] {
201 const mergedPrKeys = new Set(
202 merged
203 .filter((m) => m.action === "pr_merge" && m.number != null)
204 .map((m) => `${m.ownerUsername}/${m.repoName}#${m.number}`)
205 );
206 const deduped = merged.filter(
207 (m) =>
208 m.action !== "pr_open" ||
209 m.number == null ||
210 !mergedPrKeys.has(`${m.ownerUsername}/${m.repoName}#${m.number}`)
211 );
212
180213 // Collapse bursts: a bulk import creates dozens of repos in one gesture,
181214 // and 37 identical "Created repository" rows would BE the whole feed.
182215 const collapsed: RecentActivityItem[] = [];
183 for (const item of merged) {
216 for (const item of deduped) {
184217 const prev = collapsed[collapsed.length - 1];
185218 if (
186219 prev &&
197230 }
198231 continue;
199232 }
233 if (
234 prev &&
235 item.action === "push" &&
236 prev.action === "push" &&
237 prev.repoName === item.repoName &&
238 prev.ownerUsername === item.ownerUsername &&
239 prev.createdAt.getTime() - item.createdAt.getTime() < PUSH_BURST_WINDOW_MS
240 ) {
241 prev.count = (prev.count ?? 1) + 1;
242 continue;
243 }
200244 collapsed.push({ ...item });
201245 if (collapsed.length >= limit) break;
202246 }
Modifiedsrc/routes/dashboard.tsx+43−11View fileUnifiedSplit
309309 );
310310 }
311311
312 // Get all user's repos
312 // Get all user's repos. Probe artifacts never reach the command center:
313 // scripts/agent-journey.ts creates a `journey-<14 digits>-<4 chars>` repo
314 // per run and deletes it in its last step, but a run that dies mid-flight
315 // leaves the repo behind — same defensive filter as /explore.
313316 const repos = await db
314317 .select()
315318 .from(repositories)
316 .where(eq(repositories.ownerId, user.id))
319 .where(
320 and(
321 eq(repositories.ownerId, user.id),
322 sql`${repositories.name} !~ '^journey-[0-9]{14}-[a-z0-9]{4}$'`
323 )
324 )
317325 .orderBy(desc(repositories.updatedAt));
318326
319327 // "Open Issues" below used to sum repositories.issueCount, which is
667675 gap: var(--space-2);
668676 flex-wrap: wrap;
669677 }
678 /* Repo-card description — 2-line clamp so a marketing paragraph
679 (or leaked meta-text) can't blow up the row height. Full text
680 stays reachable via the title attribute. */
681 .dash-card-desc {
682 font-size: 12px;
683 color: var(--text-muted);
684 margin: 0;
685 line-height: 1.5;
686 /* fallback cap for engines without -webkit-box */
687 max-height: 3em;
688 overflow: hidden;
689 display: -webkit-box;
690 -webkit-line-clamp: 2;
691 -webkit-box-orient: vertical;
692 }
670693 @media (max-width: 720px) {
671694 .dash-hero-inner { flex-direction: column; align-items: flex-start; }
672695 .dash-hero-actions { width: 100%; }
782805 />
783806 <div style="padding: var(--space-4)">
784807 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px">
785 <div>
808 <div style="flex: 1; min-width: 0">
786809 <h3 style="font-size: 16px; margin-bottom: 2px">
787810 <a href={`/${user.username}/${repo.name}`}>{repo.name}</a>
788811 </h3>
789812 {repo.description && (
790 <p style="font-size: 12px; color: var(--text-muted); margin-bottom: 0">
813 <p class="dash-card-desc" title={repo.description}>
791814 {repo.description}
792815 </p>
793816 )}
878901 <div style="min-width: 0">
879902 <span style="font-size: 14px">
880903 {a.count && a.count > 1 ? (
881 <>
882 Created {a.count} repositories{" "}
883 <span style="color: var(--text-muted)">
884 ({a.title}
885 {a.count > 4 ? `, +${a.count - 4} more` : ""})
886 </span>
887 </>
904 a.action === "push" ? (
905 <>
906 Pushed code in <a href={repoUrl}>{a.repoName}</a>{" "}
907 <span style="color: var(--text-muted)">
908 {"×"}{a.count}
909 </span>
910 </>
911 ) : (
912 <>
913 Created {a.count} repositories{" "}
914 <span style="color: var(--text-muted)">
915 ({a.title}
916 {a.count > 4 ? `, +${a.count - 4} more` : ""})
917 </span>
918 </>
919 )
888920 ) : (
889921 <>
890922 {formatAction(a.action)}
Modifiedsrc/views/live-feed.tsx+17−2View fileUnifiedSplit
1717 const title = props.title ?? "Live activity";
1818 const listId = "live-feed";
1919
20 const emptyId = `${listId}-empty`;
21
2022 // formatFn is inlined client-side JS. It receives the parsed event payload
2123 // and returns an HTML string (an <li>). All interpolated values are HTML-
22 // escaped to avoid breakout.
24 // escaped to avoid breakout. The first real event removes the SSR'd
25 // empty-state row before its HTML is appended.
2326 const formatFn = `
2427 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
2528 var d = event && event.data ? event.data : event;
2629 if (!d) return '';
30 var ph = document.getElementById(${JSON.stringify(emptyId)});
31 if (ph) ph.remove();
2732 return '<li>' + esc(d.actor) + ' ' + esc(d.action) + ' ' + esc(d.target) + '</li>';
2833 `;
2934
4449 <ul
4550 id={listId}
4651 style="list-style: none; padding: 0; margin: 0; font-size: 13px; color: var(--text)"
47 />
52 >
53 {/* Honest empty state: without it the panel is a bare bordered box
54 that advertises liveness and proves deadness. Removed client-side
55 when the first real event arrives. */}
56 <li
57 id={`${listId}-empty`}
58 style="color: var(--text-muted); font-size: 13px"
59 >
60 No live activity right now — events appear here as they happen.
61 </li>
62 </ul>
4863 <script dangerouslySetInnerHTML={{ __html: script }} />
4964 </section>
5065 );
5166
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts