Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

pulse.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

pulse.tsxBlame367 lines · 1 contributor
7c975c6Claude1/**
2 * Block J18 — Repository pulse.
3 *
4 * GET /:owner/:repo/pulse[?window=1d|7d|30d|90d]
5 *
6 * Renders a GitHub-parity "Pulse" overview: commit activity, active PRs,
7 * recent merges/closes, issue throughput, top contributors — bucketed into
8 * a rolling window. Read-only. softAuth so public repos are accessible to
9 * logged-out visitors.
10 */
11
12import { Hono } from "hono";
13import { and, eq } from "drizzle-orm";
14import { db } from "../db";
15import { issues, pullRequests, repositories, users } from "../db/schema";
16import { Layout } from "../views/layout";
17import { RepoHeader, RepoNav } from "../views/components";
18import { softAuth } from "../middleware/auth";
19import type { AuthEnv } from "../middleware/auth";
20import { listCommits, getDefaultBranch } from "../git/repository";
21import {
22 PULSE_WINDOWS,
23 type PulseWindow,
24 parseWindow,
25 buildPulseReport,
26 windowDays,
27 type PulseCommit,
28 type PulsePr,
29 type PulseIssue,
30} from "../lib/repo-pulse";
31
32const pulseRoutes = new Hono<AuthEnv>();
33
34async function resolveRepo(ownerName: string, repoName: string) {
35 try {
36 const [owner] = await db
37 .select()
38 .from(users)
39 .where(eq(users.username, ownerName))
40 .limit(1);
41 if (!owner) return null;
42 const [repo] = await db
43 .select()
44 .from(repositories)
45 .where(
46 and(
47 eq(repositories.ownerId, owner.id),
48 eq(repositories.name, repoName)
49 )
50 )
51 .limit(1);
52 if (!repo) return null;
53 return { owner, repo };
54 } catch {
55 return null;
56 }
57}
58
59const WINDOW_LABEL: Record<PulseWindow, string> = {
60 "1d": "24 hours",
61 "7d": "7 days",
62 "30d": "30 days",
63 "90d": "90 days",
64};
65
66pulseRoutes.get("/:owner/:repo/pulse", softAuth, async (c) => {
67 const { owner: ownerName, repo: repoName } = c.req.param();
68 const user = c.get("user");
69 const w: PulseWindow = parseWindow(c.req.query("window"));
70
71 const resolved = await resolveRepo(ownerName, repoName);
72 if (!resolved) {
73 return c.html(
74 <Layout title="Not Found" user={user}>
75 <div class="empty-state">
76 <h2>Repository not found</h2>
77 </div>
78 </Layout>,
79 404
80 );
81 }
82
83 const { repo } = resolved;
84 if (repo.isPrivate && (!user || user.id !== resolved.owner.id)) {
85 return c.html(
86 <Layout title="Not Found" user={user}>
87 <div class="empty-state">
88 <h2>Repository not found</h2>
89 </div>
90 </Layout>,
91 404
92 );
93 }
94
95 // --- Fetch inputs in parallel -------------------------------------------
96 const [commitsRaw, prRows, issueRows, defaultBranch] = await Promise.all([
97 (async (): Promise<PulseCommit[]> => {
98 try {
99 const ref =
100 (await getDefaultBranch(ownerName, repoName)) || "HEAD";
101 const commits = await listCommits(ownerName, repoName, ref, 500, 0);
102 return commits.map((c) => ({
103 sha: c.sha,
104 author: c.author,
105 authorEmail: c.authorEmail,
106 date: c.date,
107 message: c.message,
108 }));
109 } catch {
110 return [];
111 }
112 })(),
113 db
114 .select({
115 id: pullRequests.id,
116 number: pullRequests.number,
117 title: pullRequests.title,
118 state: pullRequests.state,
119 isDraft: pullRequests.isDraft,
120 authorName: users.username,
121 createdAt: pullRequests.createdAt,
122 updatedAt: pullRequests.updatedAt,
123 closedAt: pullRequests.closedAt,
124 mergedAt: pullRequests.mergedAt,
125 })
126 .from(pullRequests)
127 .innerJoin(users, eq(pullRequests.authorId, users.id))
128 .where(eq(pullRequests.repositoryId, repo.id))
129 .limit(1000),
130 db
131 .select({
132 id: issues.id,
133 number: issues.number,
134 title: issues.title,
135 state: issues.state,
136 authorName: users.username,
137 createdAt: issues.createdAt,
138 updatedAt: issues.updatedAt,
139 closedAt: issues.closedAt,
140 })
141 .from(issues)
142 .innerJoin(users, eq(issues.authorId, users.id))
143 .where(eq(issues.repositoryId, repo.id))
144 .limit(1000),
145 getDefaultBranch(ownerName, repoName).catch(() => "HEAD"),
146 ]);
147
148 const now = new Date();
149 const report = buildPulseReport({
150 window: w,
151 now,
152 commits: commitsRaw,
153 prs: prRows as PulsePr[],
154 issues: issueRows as PulseIssue[],
155 });
156
157 const label = WINDOW_LABEL[w];
158 const days = windowDays(w);
159
160 return c.html(
161 <Layout title={`Pulse — ${ownerName}/${repoName}`} user={user}>
162 <RepoHeader owner={ownerName} repo={repoName} />
163 <RepoNav owner={ownerName} repo={repoName} active="insights" />
164 <div style="max-width: 960px; margin-top: 16px">
165 <div style="display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 16px">
166 <h2 style="margin: 0">Pulse</h2>
167 <div style="display: flex; gap: 6px">
168 {PULSE_WINDOWS.map((opt) => (
169 <a
170 href={`/${ownerName}/${repoName}/pulse?window=${opt}`}
171 class={`btn ${opt === w ? "btn-primary" : ""}`}
172 style="padding: 4px 10px; font-size: 12px"
173 >
174 {WINDOW_LABEL[opt]}
175 </a>
176 ))}
177 </div>
178 </div>
179
180 <p style="color: var(--text-muted); margin-bottom: 24px">
181 Activity in the last {label} on{" "}
182 <code>{defaultBranch || "HEAD"}</code> —{" "}
183 <strong>{report.commits.total}</strong> commit
184 {report.commits.total === 1 ? "" : "s"} from{" "}
185 <strong>{report.commits.byAuthor.length}</strong> contributor
186 {report.commits.byAuthor.length === 1 ? "" : "s"}.
187 </p>
188
189 <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px">
190 <PulseCard label="Opened PRs" value={report.prs.opened} tone="blue" />
191 <PulseCard
192 label="Merged PRs"
193 value={report.prs.mergedCount}
194 tone="green"
195 />
196 <PulseCard
197 label="Closed PRs"
198 value={report.prs.closed}
199 tone="red"
200 />
201 <PulseCard
202 label="Active PRs"
203 value={report.prs.active}
204 tone="grey"
205 />
206 <PulseCard
207 label="Opened issues"
208 value={report.issues.opened}
209 tone="blue"
210 />
211 <PulseCard
212 label="Closed issues"
213 value={report.issues.closed}
214 tone="red"
215 />
216 <PulseCard
217 label="Active issues"
218 value={report.issues.active}
219 tone="grey"
220 />
221 <PulseCard
222 label="Commits"
223 value={report.commits.total}
224 tone="green"
225 />
226 </div>
227
228 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px">
229 <section>
230 <h3 style="font-size: 14px; margin-bottom: 8px">
231 Top contributors
232 </h3>
233 {report.commits.byAuthor.length === 0 ? (
234 <p style="color: var(--text-muted); font-size: 13px">
235 No commits in the last {label}.
236 </p>
237 ) : (
238 <ul style="list-style: none; padding: 0; margin: 0">
239 {report.commits.byAuthor.slice(0, 10).map((a) => (
240 <li style="display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--border)">
241 <span style="font-weight: 500">{a.author}</span>
242 <span style="color: var(--text-muted); font-size: 12px">
243 {a.count} commit{a.count === 1 ? "" : "s"}
244 </span>
245 </li>
246 ))}
247 </ul>
248 )}
249 </section>
250
251 <section>
252 <h3 style="font-size: 14px; margin-bottom: 8px">
253 Recently merged PRs
254 </h3>
255 {report.prs.mergedList.length === 0 ? (
256 <p style="color: var(--text-muted); font-size: 13px">
257 No PRs merged in the last {label}.
258 </p>
259 ) : (
260 <ul style="list-style: none; padding: 0; margin: 0">
261 {report.prs.mergedList.slice(0, 10).map((p) => (
262 <li style="padding: 6px 0; border-bottom: 1px solid var(--border)">
263 <a
264 href={`/${ownerName}/${repoName}/pulls/${p.number}`}
265 style="font-weight: 500"
266 >
267 #{p.number}
268 </a>{" "}
269 {p.title}
270 {p.authorName && (
271 <span style="color: var(--text-muted); font-size: 12px">
272 {" "}
273 · {p.authorName}
274 </span>
275 )}
276 </li>
277 ))}
278 </ul>
279 )}
280 </section>
281 </div>
282
283 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 24px">
284 <section>
285 <h3 style="font-size: 14px; margin-bottom: 8px">
286 Newly opened issues
287 </h3>
288 {report.issues.openedList.length === 0 ? (
289 <p style="color: var(--text-muted); font-size: 13px">
290 No new issues in the last {label}.
291 </p>
292 ) : (
293 <ul style="list-style: none; padding: 0; margin: 0">
294 {report.issues.openedList.slice(0, 10).map((i) => (
295 <li style="padding: 6px 0; border-bottom: 1px solid var(--border)">
296 <a
297 href={`/${ownerName}/${repoName}/issues/${i.number}`}
298 style="font-weight: 500"
299 >
300 #{i.number}
301 </a>{" "}
302 {i.title}
303 </li>
304 ))}
305 </ul>
306 )}
307 </section>
308
309 <section>
310 <h3 style="font-size: 14px; margin-bottom: 8px">
311 Newly closed issues
312 </h3>
313 {report.issues.closedList.length === 0 ? (
314 <p style="color: var(--text-muted); font-size: 13px">
315 No issues closed in the last {label}.
316 </p>
317 ) : (
318 <ul style="list-style: none; padding: 0; margin: 0">
319 {report.issues.closedList.slice(0, 10).map((i) => (
320 <li style="padding: 6px 0; border-bottom: 1px solid var(--border)">
321 <a
322 href={`/${ownerName}/${repoName}/issues/${i.number}`}
323 style="font-weight: 500"
324 >
325 #{i.number}
326 </a>{" "}
327 {i.title}
328 </li>
329 ))}
330 </ul>
331 )}
332 </section>
333 </div>
334
335 <p style="color: var(--text-muted); font-size: 11px; margin-top: 24px">
336 Window: {days} day{days === 1 ? "" : "s"} · {report.start.slice(0, 10)}{" "}
337 → {report.end.slice(0, 10)}
338 </p>
339 </div>
340 </Layout>
341 );
342});
343
344const TONE_COLORS: Record<string, string> = {
345 green: "#2ea043",
346 red: "#f85149",
347 blue: "#58a6ff",
348 grey: "var(--text-muted)",
349};
350
351function PulseCard(props: { label: string; value: number; tone: string }) {
352 const colour = TONE_COLORS[props.tone] || TONE_COLORS.grey;
353 return (
354 <div style="border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; background: var(--bg-secondary)">
355 <div
356 style={`font-size: 22px; font-weight: 600; color: ${colour}; line-height: 1`}
357 >
358 {props.value}
359 </div>
360 <div style="color: var(--text-muted); font-size: 12px; margin-top: 4px">
361 {props.label}
362 </div>
363 </div>
364 );
365}
366
367export default pulseRoutes;