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

engineering-insights.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.

engineering-insights.tsxBlame1826 lines · 1 contributor
8286942Claude1/**
2 * Engineering Intelligence Dashboard
3 *
4 * An executive-level analytics surface for CTOs and VPs of Engineering.
5 * Answers: "Which team is shipping the most? Where is technical debt
6 * accumulating? Which developers are blocked? What's the velocity trend?"
7 *
8 * Routes:
9 * GET /insights — org-level dashboard (all repos the user has access to)
10 * GET /:owner/:repo/insights/engineering — per-repo analytics
11 * GET /insights/api/summary — JSON endpoint for the main KPIs
12 *
13 * No new migrations needed — queries existing tables only.
14 */
15
16import { Hono } from "hono";
17import { db } from "../db";
18import {
19 repositories,
20 pullRequests,
21 issues,
22 issueComments,
23 activityFeed,
24 users,
25 prComments,
26 gateRuns,
27 prReviews,
28} from "../db/schema";
29import {
30 eq,
31 and,
32 gte,
33 lte,
34 sql,
35 desc,
36 isNotNull,
37 inArray,
38 ilike,
39 or,
40 count,
41 avg,
42} from "drizzle-orm";
43import { Layout } from "../views/layout";
44import { RepoHeader, RepoNav } from "../views/components";
45import { softAuth, requireAuth } from "../middleware/auth";
46import type { AuthEnv } from "../middleware/auth";
47import { getUnreadCount } from "../lib/unread";
48
49const engineeringInsights = new Hono<AuthEnv>();
50engineeringInsights.use("*", softAuth);
51
52// ─── CSS ──────────────────────────────────────────────────────────────────────
53
54const styles = `
55 .ei-wrap {
56 max-width: 1280px;
57 margin: 0 auto;
58 padding: var(--space-5) var(--space-4);
59 }
60
61 /* Hero */
62 .ei-hero {
63 position: relative;
64 margin-bottom: var(--space-5);
65 padding: var(--space-5) var(--space-6);
66 background: var(--bg-elevated);
67 border: 1px solid var(--border);
68 border-radius: 16px;
69 overflow: hidden;
70 }
71 .ei-hero::before {
72 content: '';
73 position: absolute;
74 top: 0; left: 0; right: 0;
75 height: 2px;
76 background: linear-gradient(90deg, transparent 0%, #6366f1 25%, #8b5cf6 50%, #06b6d4 75%, transparent 100%);
77 opacity: 0.8;
78 pointer-events: none;
79 }
80 .ei-hero-orb {
81 position: absolute;
82 inset: -30% -15% auto auto;
83 width: 500px; height: 500px;
84 background: radial-gradient(circle, rgba(99,102,241,0.18), rgba(6,182,212,0.08) 45%, transparent 70%);
85 filter: blur(90px);
86 opacity: 0.7;
87 pointer-events: none;
88 z-index: 0;
89 }
90 .ei-hero-inner { position: relative; z-index: 1; }
91 .ei-hero-top {
92 display: flex;
93 align-items: flex-start;
94 justify-content: space-between;
95 gap: var(--space-4);
96 flex-wrap: wrap;
97 margin-bottom: var(--space-4);
98 }
99 .ei-eyebrow {
100 display: inline-flex;
101 align-items: center;
102 gap: 8px;
103 text-transform: uppercase;
104 font-family: var(--font-mono);
105 font-size: 11px;
106 letter-spacing: 0.18em;
107 color: var(--text-muted);
108 font-weight: 600;
109 margin-bottom: 12px;
110 }
111 .ei-eyebrow-dot {
112 width: 8px; height: 8px;
113 border-radius: 9999px;
114 background: linear-gradient(135deg, #6366f1, #06b6d4);
115 box-shadow: 0 0 0 3px rgba(99,102,241,0.18);
116 }
117 .ei-hero-title {
118 font-family: var(--font-display);
119 font-size: clamp(26px, 3.5vw, 36px);
120 font-weight: 800;
121 letter-spacing: -0.025em;
122 color: var(--text-strong);
123 margin: 0 0 var(--space-2);
124 line-height: 1.1;
125 }
126 .ei-hero-title-grad {
127 background-image: linear-gradient(135deg, #818cf8 0%, #6366f1 40%, #06b6d4 100%);
128 -webkit-background-clip: text;
129 background-clip: text;
130 -webkit-text-fill-color: transparent;
131 color: transparent;
132 }
133 .ei-hero-sub {
134 font-size: 14px;
135 color: var(--text-muted);
136 margin: 0;
137 line-height: 1.6;
138 }
139
140 /* Time range selector */
141 .ei-range-bar {
142 display: flex;
143 align-items: center;
144 gap: 6px;
145 flex-wrap: wrap;
146 }
147 .ei-range-label {
148 font-size: 12px;
149 color: var(--text-muted);
150 margin-right: 4px;
151 }
152 .ei-range-btn {
153 display: inline-block;
154 padding: 5px 13px;
155 border-radius: 7px;
156 font-size: 12px;
157 font-weight: 500;
158 text-decoration: none;
159 border: 1px solid var(--border);
160 color: var(--text-muted);
161 background: var(--bg);
162 transition: border-color 120ms ease, color 120ms ease, background 120ms ease;
163 white-space: nowrap;
164 }
165 .ei-range-btn:hover { color: var(--text); border-color: var(--border-strong, var(--border)); }
166 .ei-range-btn.active {
167 background: #6366f1;
168 border-color: #6366f1;
169 color: #fff;
170 }
171
172 /* KPI cards row */
173 .ei-kpi-row {
174 display: grid;
175 grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
176 gap: var(--space-3);
177 margin-bottom: var(--space-5);
178 }
179 .ei-kpi {
180 position: relative;
181 background: var(--bg-elevated);
182 border: 1px solid var(--border);
183 border-radius: 14px;
184 padding: var(--space-4);
185 transition: border-color 120ms ease, transform 120ms ease;
186 overflow: hidden;
187 }
188 .ei-kpi:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
189 .ei-kpi-icon {
190 width: 32px; height: 32px;
191 border-radius: 8px;
192 display: flex;
193 align-items: center;
194 justify-content: center;
195 font-size: 16px;
196 margin-bottom: var(--space-3);
197 }
198 .ei-kpi-icon-blue { background: rgba(99,102,241,0.12); }
199 .ei-kpi-icon-green { background: rgba(52,211,153,0.12); }
200 .ei-kpi-icon-amber { background: rgba(251,191,36,0.12); }
201 .ei-kpi-icon-purple { background: rgba(139,92,246,0.12); }
202 .ei-kpi-label {
203 font-size: 11px;
204 letter-spacing: 0.1em;
205 text-transform: uppercase;
206 color: var(--text-muted);
207 font-weight: 700;
208 margin-bottom: 6px;
209 }
210 .ei-kpi-value {
211 font-family: var(--font-display);
212 font-size: 32px;
213 font-weight: 800;
214 letter-spacing: -0.022em;
215 color: var(--text-strong);
216 font-variant-numeric: tabular-nums;
217 line-height: 1;
218 margin-bottom: 8px;
219 }
220 .ei-kpi-value.na { color: var(--text-muted); font-size: 22px; }
221 .ei-kpi-delta {
222 display: inline-flex;
223 align-items: center;
224 gap: 4px;
225 font-size: 12px;
226 font-variant-numeric: tabular-nums;
227 color: var(--text-muted);
228 margin-bottom: 4px;
229 }
230 .ei-kpi-delta.up { color: #34d399; }
231 .ei-kpi-delta.down { color: #f87171; }
232 .ei-kpi-sub {
233 font-size: 12px;
234 color: var(--text-muted);
235 line-height: 1.4;
236 }
237
238 /* Section */
239 .ei-section {
240 margin-bottom: var(--space-6);
241 }
242 .ei-section-head {
243 display: flex;
244 align-items: baseline;
245 justify-content: space-between;
246 gap: var(--space-3);
247 flex-wrap: wrap;
248 margin-bottom: var(--space-3);
249 }
250 .ei-section-title {
251 font-family: var(--font-display);
252 font-size: 16px;
253 font-weight: 700;
254 letter-spacing: -0.015em;
255 color: var(--text-strong);
256 margin: 0;
257 }
258 .ei-section-sub {
259 font-size: 12.5px;
260 color: var(--text-muted);
261 }
262
263 /* Velocity chart */
264 .ei-chart-wrap {
265 background: var(--bg-elevated);
266 border: 1px solid var(--border);
267 border-radius: 12px;
268 padding: var(--space-4);
269 overflow-x: auto;
270 }
271 .ei-chart-bars {
272 display: flex;
273 align-items: flex-end;
274 gap: 8px;
275 height: 120px;
276 padding-top: 8px;
277 }
278 .ei-chart-col {
279 display: flex;
280 flex-direction: column;
281 align-items: center;
282 flex: 1;
283 min-width: 40px;
284 }
285 .ei-chart-bar-wrap {
286 flex: 1;
287 width: 100%;
288 display: flex;
289 align-items: flex-end;
290 justify-content: center;
291 }
292 .ei-chart-bar {
293 width: 70%;
294 border-radius: 4px 4px 0 0;
295 min-height: 4px;
296 transition: opacity 120ms ease;
297 position: relative;
298 }
299 .ei-chart-bar.pr-bar { background: linear-gradient(180deg, #818cf8, #6366f1); }
300 .ei-chart-bar.issue-bar { background: linear-gradient(180deg, #34d399, #10b981); opacity: 0.6; }
301 .ei-chart-bar:hover { opacity: 0.8; }
302 .ei-chart-label {
303 font-family: var(--font-mono);
304 font-size: 10px;
305 color: var(--text-muted);
306 margin-top: 6px;
307 white-space: nowrap;
308 }
309 .ei-chart-val {
310 font-family: var(--font-mono);
311 font-size: 10px;
312 color: var(--text-muted);
313 margin-bottom: 4px;
314 }
315 .ei-chart-legend {
316 display: flex;
317 gap: var(--space-4);
318 margin-top: var(--space-3);
319 padding-top: var(--space-3);
320 border-top: 1px solid var(--border);
321 }
322 .ei-chart-legend-item {
323 display: flex;
324 align-items: center;
325 gap: 6px;
326 font-size: 12px;
327 color: var(--text-muted);
328 }
329 .ei-legend-dot {
330 width: 10px; height: 10px;
331 border-radius: 2px;
332 }
333
334 /* Contributors table */
335 .ei-table-wrap {
336 background: var(--bg-elevated);
337 border: 1px solid var(--border);
338 border-radius: 12px;
339 overflow: hidden;
340 }
341 .ei-table {
342 width: 100%;
343 border-collapse: collapse;
344 font-size: 13px;
345 }
346 .ei-table th {
347 padding: 10px 16px;
348 text-align: left;
349 font-size: 11px;
350 font-weight: 700;
351 letter-spacing: 0.08em;
352 text-transform: uppercase;
353 color: var(--text-muted);
354 border-bottom: 1px solid var(--border);
355 background: var(--bg);
356 white-space: nowrap;
357 }
358 .ei-table th:not(:first-child) { text-align: right; }
359 .ei-table td {
360 padding: 10px 16px;
361 border-bottom: 1px solid var(--border);
362 color: var(--text);
363 vertical-align: middle;
364 }
365 .ei-table td:not(:first-child) {
366 text-align: right;
367 font-variant-numeric: tabular-nums;
368 font-family: var(--font-mono);
369 font-size: 12px;
370 }
371 .ei-table tr:last-child td { border-bottom: none; }
372 .ei-table tr:hover td { background: var(--bg-hover); }
373 .ei-contributor-rank {
374 display: inline-flex;
375 align-items: center;
376 justify-content: center;
377 width: 20px; height: 20px;
378 border-radius: 50%;
379 font-size: 10px;
380 font-weight: 700;
381 background: rgba(99,102,241,0.12);
382 color: #818cf8;
383 margin-right: 8px;
384 flex-shrink: 0;
385 }
386 .ei-contributor-rank.top1 { background: rgba(251,191,36,0.15); color: #fbbf24; }
387 .ei-contributor-rank.top2 { background: rgba(156,163,175,0.15); color: #9ca3af; }
388 .ei-contributor-rank.top3 { background: rgba(217,119,6,0.15); color: #d97706; }
389 .ei-avatar {
390 display: inline-flex;
391 align-items: center;
392 justify-content: center;
393 width: 24px; height: 24px;
394 border-radius: 50%;
395 font-size: 11px;
396 font-weight: 700;
397 background: rgba(99,102,241,0.18);
398 color: #818cf8;
399 margin-right: 8px;
400 flex-shrink: 0;
401 }
402 .ei-contributor-cell {
403 display: flex;
404 align-items: center;
405 }
406
407 /* Alert banners */
408 .ei-alerts {
409 display: flex;
410 flex-direction: column;
411 gap: var(--space-2);
412 margin-bottom: var(--space-5);
413 }
414 .ei-alert {
415 display: flex;
416 align-items: flex-start;
417 gap: 12px;
418 padding: 12px 16px;
419 border-radius: 10px;
420 font-size: 13px;
421 line-height: 1.55;
422 }
423 .ei-alert.red {
424 border: 1px solid rgba(248,113,113,0.3);
425 background: rgba(248,113,113,0.07);
426 color: #fca5a5;
427 }
428 .ei-alert.amber {
429 border: 1px solid rgba(251,191,36,0.3);
430 background: rgba(251,191,36,0.07);
431 color: #fcd34d;
432 }
433 .ei-alert.green {
434 border: 1px solid rgba(52,211,153,0.3);
435 background: rgba(52,211,153,0.07);
436 color: #6ee7b7;
437 }
438 .ei-alert-icon { font-size: 16px; line-height: 1.4; flex-shrink: 0; }
439 .ei-alert strong { font-weight: 700; }
440
441 /* AI impact box */
442 .ei-ai-box {
443 background: var(--bg-elevated);
444 border: 1px solid rgba(99,102,241,0.25);
445 border-radius: 14px;
446 padding: var(--space-4) var(--space-5);
447 position: relative;
448 overflow: hidden;
449 }
450 .ei-ai-box::before {
451 content: '';
452 position: absolute;
453 top: 0; left: 0; right: 0;
454 height: 2px;
455 background: linear-gradient(90deg, transparent, #6366f1 30%, #8b5cf6 60%, transparent);
456 opacity: 0.6;
457 }
458 .ei-ai-grid {
459 display: grid;
460 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
461 gap: var(--space-4);
462 margin-top: var(--space-4);
463 }
464 .ei-ai-stat {
465 display: flex;
466 flex-direction: column;
467 gap: 4px;
468 }
469 .ei-ai-stat-val {
470 font-family: var(--font-display);
471 font-size: 26px;
472 font-weight: 800;
473 letter-spacing: -0.02em;
474 font-variant-numeric: tabular-nums;
475 color: var(--text-strong);
476 }
477 .ei-ai-stat-label {
478 font-size: 12px;
479 color: var(--text-muted);
480 line-height: 1.4;
481 }
482 .ei-ai-summary-line {
483 font-size: 13.5px;
484 color: var(--text-muted);
485 margin-top: var(--space-3);
486 padding-top: var(--space-3);
487 border-top: 1px solid var(--border);
488 line-height: 1.55;
489 }
490 .ei-ai-summary-line strong { color: var(--text); font-weight: 600; }
491
492 /* Health indicators grid */
493 .ei-health-grid {
494 display: grid;
495 grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
496 gap: var(--space-3);
497 }
498 .ei-health-card {
499 background: var(--bg-elevated);
500 border: 1px solid var(--border);
501 border-radius: 12px;
502 padding: var(--space-4);
503 }
504 .ei-health-card.red { border-color: rgba(248,113,113,0.3); }
505 .ei-health-card.amber { border-color: rgba(251,191,36,0.3); }
506 .ei-health-card.green { border-color: rgba(52,211,153,0.3); }
507 .ei-health-badge {
508 display: inline-flex;
509 align-items: center;
510 gap: 5px;
511 padding: 3px 9px;
512 border-radius: 9999px;
513 font-size: 11px;
514 font-weight: 700;
515 text-transform: uppercase;
516 letter-spacing: 0.08em;
517 margin-bottom: var(--space-3);
518 }
519 .ei-health-badge.red { background: rgba(248,113,113,0.12); color: #f87171; }
520 .ei-health-badge.amber { background: rgba(251,191,36,0.12); color: #fbbf24; }
521 .ei-health-badge.green { background: rgba(52,211,153,0.12); color: #34d399; }
522 .ei-health-num {
523 font-family: var(--font-display);
524 font-size: 28px;
525 font-weight: 800;
526 letter-spacing: -0.02em;
527 font-variant-numeric: tabular-nums;
528 color: var(--text-strong);
529 line-height: 1;
530 margin-bottom: 4px;
531 }
532 .ei-health-label {
533 font-size: 13px;
534 color: var(--text-muted);
535 line-height: 1.4;
536 }
537
538 /* Quality table */
539 .ei-quality-row {
540 display: flex;
541 align-items: center;
542 gap: var(--space-3);
543 padding: 10px 0;
544 border-bottom: 1px solid var(--border);
545 }
546 .ei-quality-row:last-child { border-bottom: none; }
547 .ei-quality-name {
548 flex: 1;
549 font-size: 13px;
550 color: var(--text);
551 font-weight: 500;
552 }
553 .ei-quality-bar-wrap {
554 width: 120px;
555 height: 6px;
556 background: var(--bg-hover);
557 border-radius: 9999px;
558 overflow: hidden;
559 }
560 .ei-quality-bar {
561 height: 100%;
562 border-radius: 9999px;
563 background: linear-gradient(90deg, #6366f1, #06b6d4);
564 }
565 .ei-quality-val {
566 font-family: var(--font-mono);
567 font-size: 12px;
568 color: var(--text-muted);
569 font-variant-numeric: tabular-nums;
570 width: 40px;
571 text-align: right;
572 }
573
574 /* Empty state */
575 .ei-empty {
576 text-align: center;
577 padding: var(--space-6) var(--space-4);
578 color: var(--text-muted);
579 font-size: 13px;
580 border: 1px dashed var(--border);
581 border-radius: 12px;
582 }
583 .ei-empty strong {
584 display: block;
585 font-size: 14px;
586 font-weight: 600;
587 color: var(--text);
588 margin-bottom: 4px;
589 }
590
591 /* Two-column layout */
592 .ei-two-col {
593 display: grid;
594 grid-template-columns: 1fr 1fr;
595 gap: var(--space-4);
596 }
597 @media (max-width: 768px) {
598 .ei-two-col { grid-template-columns: 1fr; }
599 }
600`;
601
602// ─── Helpers ──────────────────────────────────────────────────────────────────
603
604function weeksAgo(n: number): Date {
605 const d = new Date();
606 d.setDate(d.getDate() - n * 7);
607 return d;
608}
609
610function daysAgo(n: number): Date {
611 const d = new Date();
612 d.setDate(d.getDate() - n);
613 return d;
614}
615
616function fmtHours(hours: number | null): string {
617 if (hours === null || isNaN(hours)) return "—";
618 if (hours < 1) return `${Math.round(hours * 60)}m`;
619 if (hours < 24) return `${hours.toFixed(1)}h`;
620 return `${(hours / 24).toFixed(1)}d`;
621}
622
623function shortWeekLabel(weekStart: Date): string {
624 return weekStart.toLocaleDateString("en-US", { month: "short", day: "numeric" });
625}
626
627// ─── Data queries ─────────────────────────────────────────────────────────────
628
629async function getAccessibleRepoIds(userId: string): Promise<string[]> {
630 // Owned repos (public + private) + public repos
631 const owned = await db
632 .select({ id: repositories.id })
633 .from(repositories)
634 .where(eq(repositories.ownerId, userId));
635 return owned.map((r) => r.id);
636}
637
638interface WeeklyBucket {
639 weekStart: Date;
640 prsMerged: number;
641 issuesClosed: number;
642}
643
644async function getWeeklyVelocity(
645 repoIds: string[],
646 weeks: number
647): Promise<WeeklyBucket[]> {
648 if (repoIds.length === 0) return [];
649
650 const since = weeksAgo(weeks);
651 const buckets: WeeklyBucket[] = [];
652
653 // Build week buckets
654 for (let i = weeks - 1; i >= 0; i--) {
655 const start = weeksAgo(i + 1);
656 start.setHours(0, 0, 0, 0);
657 const end = weeksAgo(i);
658 end.setHours(23, 59, 59, 999);
659 buckets.push({ weekStart: start, prsMerged: 0, issuesClosed: 0 });
660
661 // PRs merged in this week
662 const prs = await db
663 .select({ id: pullRequests.id })
664 .from(pullRequests)
665 .where(
666 and(
667 inArray(pullRequests.repositoryId, repoIds),
668 eq(pullRequests.state, "merged"),
669 gte(pullRequests.mergedAt, start),
670 lte(pullRequests.mergedAt, end)
671 )
672 );
673 buckets[buckets.length - 1].prsMerged = prs.length;
674
675 // Issues closed in this week
676 const closedIssues = await db
677 .select({ id: issues.id })
678 .from(issues)
679 .where(
680 and(
681 inArray(issues.repositoryId, repoIds),
682 eq(issues.state, "closed"),
683 gte(issues.closedAt, start),
684 lte(issues.closedAt, end)
685 )
686 );
687 buckets[buckets.length - 1].issuesClosed = closedIssues.length;
688 }
689
690 return buckets;
691}
692
693interface PrStats {
694 totalMerged: number;
695 avgMergeHours: number | null;
696 mergedThisPeriod: number;
697 mergedLastPeriod: number;
698}
699
700async function getPrStats(repoIds: string[], days: number): Promise<PrStats> {
701 if (repoIds.length === 0) {
702 return { totalMerged: 0, avgMergeHours: null, mergedThisPeriod: 0, mergedLastPeriod: 0 };
703 }
704
705 const periodStart = daysAgo(days);
706 const prevPeriodStart = daysAgo(days * 2);
707
708 const merged = await db
709 .select({
710 id: pullRequests.id,
711 createdAt: pullRequests.createdAt,
712 mergedAt: pullRequests.mergedAt,
713 })
714 .from(pullRequests)
715 .where(
716 and(
717 inArray(pullRequests.repositoryId, repoIds),
718 eq(pullRequests.state, "merged"),
719 isNotNull(pullRequests.mergedAt)
720 )
721 );
722
723 const thisPeriod = merged.filter(
724 (p) => p.mergedAt && new Date(p.mergedAt) >= periodStart
725 );
726 const lastPeriod = merged.filter(
727 (p) =>
728 p.mergedAt &&
729 new Date(p.mergedAt) >= prevPeriodStart &&
730 new Date(p.mergedAt) < periodStart
731 );
732
733 // avg merge time (hours) from created→merged
734 const timesHours = thisPeriod
735 .filter((p) => p.mergedAt)
736 .map((p) => {
737 const diff =
738 new Date(p.mergedAt!).getTime() - new Date(p.createdAt).getTime();
739 return diff / 1000 / 3600;
740 })
741 .filter((h) => h >= 0 && h < 10000);
742
743 const avgMergeHours =
744 timesHours.length > 0
745 ? timesHours.reduce((a, b) => a + b, 0) / timesHours.length
746 : null;
747
748 return {
749 totalMerged: merged.length,
750 avgMergeHours,
751 mergedThisPeriod: thisPeriod.length,
752 mergedLastPeriod: lastPeriod.length,
753 };
754}
755
756interface GateStats {
757 passRate: number | null;
758 totalRuns: number;
759 passed: number;
760 failed: number;
761}
762
763async function getGateStats(repoIds: string[], days: number): Promise<GateStats> {
764 if (repoIds.length === 0) return { passRate: null, totalRuns: 0, passed: 0, failed: 0 };
765
766 const since = daysAgo(days);
767 const runs = await db
768 .select({ status: gateRuns.status })
769 .from(gateRuns)
770 .where(
771 and(
772 inArray(gateRuns.repositoryId, repoIds),
773 gte(gateRuns.createdAt, since)
774 )
775 );
776
777 const passed = runs.filter((r) => r.status === "passed").length;
778 const failed = runs.filter((r) => r.status === "failed").length;
779 const total = runs.length;
780 const passRate = total > 0 ? (passed / total) * 100 : null;
781
782 return { passRate, totalRuns: total, passed, failed };
783}
784
785interface ContributorRow {
786 username: string;
787 displayName: string | null;
788 prsMerged: number;
789 prsOpened: number;
790 reviewsGiven: number;
791 avgMergeHours: number | null;
792}
793
794async function getTopContributors(
795 repoIds: string[],
796 days: number
797): Promise<ContributorRow[]> {
798 if (repoIds.length === 0) return [];
799
800 const since = daysAgo(days);
801
802 // PRs opened per user
803 const opened = await db
804 .select({
805 authorId: pullRequests.authorId,
806 username: users.username,
807 displayName: users.displayName,
808 createdAt: pullRequests.createdAt,
809 })
810 .from(pullRequests)
811 .innerJoin(users, eq(pullRequests.authorId, users.id))
812 .where(
813 and(
814 inArray(pullRequests.repositoryId, repoIds),
815 gte(pullRequests.createdAt, since)
816 )
817 );
818
819 // PRs merged per user
820 const mergedRows = await db
821 .select({
822 authorId: pullRequests.authorId,
823 createdAt: pullRequests.createdAt,
824 mergedAt: pullRequests.mergedAt,
825 })
826 .from(pullRequests)
827 .where(
828 and(
829 inArray(pullRequests.repositoryId, repoIds),
830 eq(pullRequests.state, "merged"),
831 isNotNull(pullRequests.mergedAt),
832 gte(pullRequests.mergedAt, since)
833 )
834 );
835
836 // Reviews (non-AI comments on PRs)
837 const reviews = await db
838 .select({ authorId: prComments.authorId })
839 .from(prComments)
840 .innerJoin(pullRequests, eq(prComments.pullRequestId, pullRequests.id))
841 .where(
842 and(
843 inArray(pullRequests.repositoryId, repoIds),
844 eq(prComments.isAiReview, false),
845 gte(prComments.createdAt, since)
846 )
847 );
848
849 // Aggregate by user
850 const byUser = new Map<
851 string,
852 {
853 username: string;
854 displayName: string | null;
855 opened: number;
856 merged: number;
857 reviews: number;
858 mergeTimes: number[];
859 }
860 >();
861
862 for (const row of opened) {
863 const key = row.authorId;
864 if (!byUser.has(key)) {
865 byUser.set(key, {
866 username: row.username,
867 displayName: row.displayName,
868 opened: 0,
869 merged: 0,
870 reviews: 0,
871 mergeTimes: [],
872 });
873 }
874 byUser.get(key)!.opened++;
875 }
876
877 for (const row of mergedRows) {
878 const key = row.authorId;
879 if (!byUser.has(key)) continue;
880 byUser.get(key)!.merged++;
881 if (row.mergedAt) {
882 const hours =
883 (new Date(row.mergedAt).getTime() - new Date(row.createdAt).getTime()) /
884 1000 /
885 3600;
886 if (hours >= 0 && hours < 10000) {
887 byUser.get(key)!.mergeTimes.push(hours);
888 }
889 }
890 }
891
892 for (const row of reviews) {
893 const key = row.authorId;
894 if (!byUser.has(key)) continue;
895 byUser.get(key)!.reviews++;
896 }
897
898 // Sort by merged desc, then opened
899 return Array.from(byUser.entries())
900 .map(([, v]) => ({
901 username: v.username,
902 displayName: v.displayName,
903 prsMerged: v.merged,
904 prsOpened: v.opened,
905 reviewsGiven: v.reviews,
906 avgMergeHours:
907 v.mergeTimes.length > 0
908 ? v.mergeTimes.reduce((a, b) => a + b, 0) / v.mergeTimes.length
909 : null,
910 }))
911 .sort((a, b) => b.prsMerged - a.prsMerged || b.prsOpened - a.prsOpened)
912 .slice(0, 10);
913}
914
915interface HealthIndicators {
916 staleRepos: number;
917 stuckPrs: number;
918 abandonedIssues: number;
919}
920
921async function getHealthIndicators(
922 repoIds: string[]
923): Promise<HealthIndicators> {
924 if (repoIds.length === 0)
925 return { staleRepos: 0, stuckPrs: 0, abandonedIssues: 0 };
926
927 const thirtyDaysAgo = daysAgo(30);
928 const sevenDaysAgo = daysAgo(7);
929
930 // Stale repos: no pushAt update in 30+ days
931 const staleReposRows = await db
932 .select({ id: repositories.id })
933 .from(repositories)
934 .where(
935 and(
936 inArray(repositories.id, repoIds),
937 or(
938 lte(repositories.pushedAt, thirtyDaysAgo),
939 sql`${repositories.pushedAt} IS NULL`
940 )
941 )
942 );
943
944 // Stuck PRs: open for > 7 days
945 const stuckPrsRows = await db
946 .select({ id: pullRequests.id })
947 .from(pullRequests)
948 .where(
949 and(
950 inArray(pullRequests.repositoryId, repoIds),
951 eq(pullRequests.state, "open"),
952 lte(pullRequests.createdAt, sevenDaysAgo)
953 )
954 );
955
956 // Abandoned issues: open > 30 days with no comments
957 const openOldIssues = await db
958 .select({ id: issues.id })
959 .from(issues)
960 .where(
961 and(
962 inArray(issues.repositoryId, repoIds),
963 eq(issues.state, "open"),
964 lte(issues.createdAt, thirtyDaysAgo)
965 )
966 );
967
968 // Count which have no comments
969 let abandonedCount = 0;
970 if (openOldIssues.length > 0) {
971 const issueIds = openOldIssues.map((i) => i.id);
972 const withComments = await db
973 .select({ issueId: issueComments.issueId })
974 .from(issueComments)
975 .where(inArray(issueComments.issueId, issueIds));
976 const commentedSet = new Set(withComments.map((c) => c.issueId));
977 abandonedCount = openOldIssues.filter((i) => !commentedSet.has(i.id)).length;
978 }
979
980 return {
981 staleRepos: staleReposRows.length,
982 stuckPrs: stuckPrsRows.length,
983 abandonedIssues: abandonedCount,
984 };
985}
986
987interface AiImpact {
988 aiMergedPrs: number;
989 aiSecurityIssues: number;
990 aiReviewComments: number;
991 estimatedHoursSaved: number;
992}
993
994async function getAiImpact(repoIds: string[], days: number): Promise<AiImpact> {
995 if (repoIds.length === 0)
996 return { aiMergedPrs: 0, aiSecurityIssues: 0, aiReviewComments: 0, estimatedHoursSaved: 0 };
997
998 const since = daysAgo(days);
999
1000 // AI-merged PRs: merged via mergedBy where the user has "ai" or "bot" in name,
1001 // or from activity_feed with action='pr_merge' and metadata containing AI hint.
1002 // Simpler: PRs merged where title or body contains "[AI]" or "auto-merged"
1003 const aiMergedRows = await db
1004 .select({ id: pullRequests.id })
1005 .from(pullRequests)
1006 .where(
1007 and(
1008 inArray(pullRequests.repositoryId, repoIds),
1009 eq(pullRequests.state, "merged"),
1010 isNotNull(pullRequests.mergedAt),
1011 gte(pullRequests.mergedAt, since),
1012 or(
1013 ilike(pullRequests.title, "%[ai]%"),
1014 ilike(pullRequests.title, "%auto-merged%"),
1015 ilike(pullRequests.body, "%auto-merged by gluecron%")
1016 )
1017 )
1018 );
1019
1020 // AI security issues: issues with [CVE] or [AI] in title
1021 const aiSecurityRows = await db
1022 .select({ id: issues.id })
1023 .from(issues)
1024 .where(
1025 and(
1026 inArray(issues.repositoryId, repoIds),
1027 gte(issues.createdAt, since),
1028 or(
1029 ilike(issues.title, "%[cve]%"),
1030 ilike(issues.title, "%[ai]%"),
1031 ilike(issues.title, "%[security]%")
1032 )
1033 )
1034 );
1035
1036 // AI review comments
1037 const aiCommentsRows = await db
1038 .select({ id: prComments.id })
1039 .from(prComments)
1040 .innerJoin(pullRequests, eq(prComments.pullRequestId, pullRequests.id))
1041 .where(
1042 and(
1043 inArray(pullRequests.repositoryId, repoIds),
1044 eq(prComments.isAiReview, true),
1045 gte(prComments.createdAt, since)
1046 )
1047 );
1048
1049 // Estimate: avg review takes 4h; each AI-merged PR saves that
1050 const estimatedHoursSaved = aiMergedRows.length * 4;
1051
1052 return {
1053 aiMergedPrs: aiMergedRows.length,
1054 aiSecurityIssues: aiSecurityRows.length,
1055 aiReviewComments: aiCommentsRows.length,
1056 estimatedHoursSaved,
1057 };
1058}
1059
1060// ─── SVG Chart ────────────────────────────────────────────────────────────────
1061
1062function VelocityChart({ buckets }: { buckets: WeeklyBucket[] }) {
1063 const maxVal = Math.max(...buckets.map((b) => Math.max(b.prsMerged, b.issuesClosed)), 1);
1064 const chartHeight = 100;
1065
1066 return (
1067 <div class="ei-chart-wrap">
1068 <div class="ei-chart-bars">
1069 {buckets.map((b) => {
1070 const prHeight = Math.max(4, (b.prsMerged / maxVal) * chartHeight);
1071 const issueHeight = Math.max(4, (b.issuesClosed / maxVal) * chartHeight);
1072 return (
1073 <div class="ei-chart-col">
1074 <div class="ei-chart-val">{b.prsMerged}</div>
1075 <div class="ei-chart-bar-wrap">
1076 <div
1077 class="ei-chart-bar pr-bar"
1078 style={`height:${prHeight}px`}
1079 title={`${b.prsMerged} PRs merged`}
1080 />
1081 </div>
1082 <div class="ei-chart-label">{shortWeekLabel(b.weekStart)}</div>
1083 </div>
1084 );
1085 })}
1086 </div>
1087 <div class="ei-chart-legend">
1088 <div class="ei-chart-legend-item">
1089 <div class="ei-legend-dot" style="background: linear-gradient(90deg,#818cf8,#6366f1)" />
1090 PRs merged per week
1091 </div>
1092 </div>
1093 </div>
1094 );
1095}
1096
1097// ─── Global /insights dashboard ───────────────────────────────────────────────
1098
1099engineeringInsights.get("/insights", requireAuth, async (c) => {
1100 const user = c.get("user")!;
1101 const rangeParam = c.req.query("range") || "30";
1102 const days = rangeParam === "7" ? 7 : rangeParam === "90" ? 90 : rangeParam === "365" ? 365 : 30;
1103 const notifCount = await getUnreadCount(user.id);
1104
1105 const repoIds = await getAccessibleRepoIds(user.id);
1106
1107 const [prStats, gateStats, weeklyVelocity, contributors, health, aiImpact] =
1108 await Promise.all([
1109 getPrStats(repoIds, days),
1110 getGateStats(repoIds, days),
1111 getWeeklyVelocity(repoIds, 8),
1112 getTopContributors(repoIds, days),
1113 getHealthIndicators(repoIds),
1114 getAiImpact(repoIds, days),
1115 ]);
1116
1117 const rangeLabel =
1118 days === 7 ? "7d" : days === 90 ? "90d" : days === 365 ? "12mo" : "30d";
1119
1120 // Deltas
1121 const prDeltaPct =
1122 prStats.mergedLastPeriod > 0
1123 ? Math.round(
1124 ((prStats.mergedThisPeriod - prStats.mergedLastPeriod) /
1125 prStats.mergedLastPeriod) *
1126 100
1127 )
1128 : 0;
1129
1130 return c.html(
1131 <Layout
1132 title="Engineering Intelligence — Insights"
1133 user={user}
1134 notificationCount={notifCount}
1135 description="Org-level engineering metrics: velocity, quality, AI impact, and team health."
1136 >
1137 <style dangerouslySetInnerHTML={{ __html: styles }} />
1138 <div class="ei-wrap">
1139 {/* Hero */}
1140 <section class="ei-hero">
1141 <div class="ei-hero-orb" aria-hidden="true" />
1142 <div class="ei-hero-inner">
1143 <div class="ei-hero-top">
1144 <div>
1145 <div class="ei-eyebrow">
1146 <span class="ei-eyebrow-dot" aria-hidden="true" />
1147 Engineering Intelligence
1148 </div>
1149 <h1 class="ei-hero-title">
1150 <span class="ei-hero-title-grad">Team Performance.</span>{" "}
1151 Org-wide.
1152 </h1>
1153 <p class="ei-hero-sub">
1154 Velocity, quality, AI impact, and developer health across all
1155 your repositories — in one view.
1156 </p>
1157 </div>
1158 {/* Time range selector */}
1159 <div>
1160 <div class="ei-range-label">Time range</div>
1161 <div class="ei-range-bar">
1162 {(["7", "30", "90", "365"] as const).map((r) => (
1163 <a
1164 href={`/insights?range=${r}`}
1165 class={`ei-range-btn${rangeParam === r ? " active" : ""}`}
1166 >
1167 {r === "365" ? "12mo" : `${r}d`}
1168 </a>
1169 ))}
1170 </div>
1171 </div>
1172 </div>
1173 </div>
1174 </section>
1175
1176 {/* KPI Row */}
1177 <div class="ei-kpi-row">
1178 <div class="ei-kpi">
1179 <div class="ei-kpi-icon ei-kpi-icon-blue">{"⬆"}</div>
1180 <div class="ei-kpi-label">PRs Merged</div>
1181 <div class="ei-kpi-value">{prStats.mergedThisPeriod}</div>
1182 {prDeltaPct !== 0 && (
1183 <div class={`ei-kpi-delta ${prDeltaPct > 0 ? "up" : "down"}`}>
1184 <span>{prDeltaPct > 0 ? "▲" : "▼"}</span>
1185 {Math.abs(prDeltaPct)}% vs last {rangeLabel}
1186 </div>
1187 )}
1188 <div class="ei-kpi-sub">This {rangeLabel}</div>
1189 </div>
1190
1191 <div class="ei-kpi">
1192 <div class="ei-kpi-icon ei-kpi-icon-green">{"⏱"}</div>
1193 <div class="ei-kpi-label">Avg Merge Time</div>
1194 <div class={`ei-kpi-value${prStats.avgMergeHours === null ? " na" : ""}`}>
1195 {fmtHours(prStats.avgMergeHours)}
1196 </div>
1197 <div class="ei-kpi-sub">Open → merge, merged PRs</div>
1198 </div>
1199
1200 <div class="ei-kpi">
1201 <div class="ei-kpi-icon ei-kpi-icon-amber">{"✓"}</div>
1202 <div class="ei-kpi-label">Gate Pass Rate</div>
1203 <div class={`ei-kpi-value${gateStats.passRate === null ? " na" : ""}`}>
1204 {gateStats.passRate !== null
1205 ? `${Math.round(gateStats.passRate)}%`
1206 : "—"}
1207 </div>
1208 <div class="ei-kpi-sub">
1209 {gateStats.passed}/{gateStats.totalRuns} runs passed
1210 </div>
1211 </div>
1212
1213 <div class="ei-kpi">
1214 <div class="ei-kpi-icon ei-kpi-icon-purple">{"✨"}</div>
1215 <div class="ei-kpi-label">AI Impact</div>
1216 <div class="ei-kpi-value">{aiImpact.aiMergedPrs}</div>
1217 <div class="ei-kpi-sub">
1218 PRs auto-merged · ~{aiImpact.estimatedHoursSaved}h saved
1219 </div>
1220 </div>
1221 </div>
1222
1223 {/* At-risk alerts */}
1224 {(health.stuckPrs > 0 || health.staleRepos > 0 || health.abandonedIssues > 0) && (
1225 <div class="ei-alerts">
1226 {health.stuckPrs > 0 && (
1227 <div class="ei-alert red">
1228 <span class="ei-alert-icon">{"⚠"}</span>
1229 <div>
1230 <strong>
1231 {health.stuckPrs} PR{health.stuckPrs !== 1 ? "s" : ""} open for
1232 {" "}{">"} 7 days
1233 </strong>{" "}
1234 — these need attention. Unreviewed PRs slow down the whole team.
1235 </div>
1236 </div>
1237 )}
1238 {health.staleRepos > 0 && (
1239 <div class="ei-alert amber">
1240 <span class="ei-alert-icon">{"⚡"}</span>
1241 <div>
1242 <strong>
1243 {health.staleRepos} repo{health.staleRepos !== 1 ? "s" : ""} with no
1244 commits in 30+ days
1245 </strong>{" "}
1246 — consider archiving or reassigning.
1247 </div>
1248 </div>
1249 )}
1250 {health.abandonedIssues > 0 && (
1251 <div class="ei-alert amber">
1252 <span class="ei-alert-icon">{"!"}</span>
1253 <div>
1254 <strong>
1255 {health.abandonedIssues} issue{health.abandonedIssues !== 1 ? "s" : ""} open
1256 {">"} 30 days with no comments
1257 </strong>{" "}
1258 — abandoned backlog items inflate your issue count artificially.
1259 </div>
1260 </div>
1261 )}
1262 {gateStats.passRate !== null && gateStats.passRate >= 90 && (
1263 <div class="ei-alert green">
1264 <span class="ei-alert-icon">{"✓"}</span>
1265 <div>
1266 Gate pass rate is{" "}
1267 <strong>{Math.round(gateStats.passRate)}%</strong> — solid
1268 quality signal across your repos.
1269 </div>
1270 </div>
1271 )}
1272 </div>
1273 )}
1274
1275 {/* Velocity chart */}
1276 <section class="ei-section">
1277 <div class="ei-section-head">
1278 <h2 class="ei-section-title">PR Velocity</h2>
1279 <span class="ei-section-sub">PRs merged per week, last 8 weeks</span>
1280 </div>
1281 {repoIds.length === 0 ? (
1282 <div class="ei-empty">
1283 <strong>No repositories yet</strong>
1284 Create or import a repository to see velocity metrics.
1285 </div>
1286 ) : (
1287 <VelocityChart buckets={weeklyVelocity} />
1288 )}
1289 </section>
1290
1291 <div class="ei-two-col">
1292 {/* Top contributors */}
1293 <section class="ei-section">
1294 <div class="ei-section-head">
1295 <h2 class="ei-section-title">Top Contributors</h2>
1296 <span class="ei-section-sub">Last {rangeLabel}</span>
1297 </div>
1298 {contributors.length === 0 ? (
1299 <div class="ei-empty">
1300 <strong>No contributor data yet</strong>
1301 Merge some PRs to see rankings.
1302 </div>
1303 ) : (
1304 <div class="ei-table-wrap">
1305 <table class="ei-table">
1306 <thead>
1307 <tr>
1308 <th>Developer</th>
1309 <th>Merged</th>
1310 <th>Opened</th>
1311 <th>Reviews</th>
1312 <th>Avg TTM</th>
1313 </tr>
1314 </thead>
1315 <tbody>
1316 {contributors.map((c, i) => (
1317 <tr>
1318 <td>
1319 <div class="ei-contributor-cell">
1320 <span
1321 class={`ei-contributor-rank${i === 0 ? " top1" : i === 1 ? " top2" : i === 2 ? " top3" : ""}`}
1322 >
1323 {i + 1}
1324 </span>
1325 <div class="ei-avatar">
1326 {(c.displayName || c.username).charAt(0).toUpperCase()}
1327 </div>
1328 <a href={`/${c.username}`} style="color:var(--text);text-decoration:none;font-weight:500">
1329 {c.displayName || c.username}
1330 </a>
1331 </div>
1332 </td>
1333 <td>{c.prsMerged}</td>
1334 <td>{c.prsOpened}</td>
1335 <td>{c.reviewsGiven}</td>
1336 <td>{fmtHours(c.avgMergeHours)}</td>
1337 </tr>
1338 ))}
1339 </tbody>
1340 </table>
1341 </div>
1342 )}
1343 </section>
1344
1345 {/* Health indicators */}
1346 <section class="ei-section">
1347 <div class="ei-section-head">
1348 <h2 class="ei-section-title">Health Indicators</h2>
1349 <span class="ei-section-sub">Across all repos</span>
1350 </div>
1351 <div class="ei-health-grid">
1352 <div class={`ei-health-card ${health.stuckPrs > 0 ? "red" : "green"}`}>
1353 <div class={`ei-health-badge ${health.stuckPrs > 0 ? "red" : "green"}`}>
1354 {health.stuckPrs > 0 ? "⚠ Stuck" : "✓ Clear"}
1355 </div>
1356 <div class="ei-health-num">{health.stuckPrs}</div>
1357 <div class="ei-health-label">PRs open {">"} 7 days</div>
1358 </div>
1359 <div class={`ei-health-card ${health.staleRepos > 0 ? "amber" : "green"}`}>
1360 <div class={`ei-health-badge ${health.staleRepos > 0 ? "amber" : "green"}`}>
1361 {health.staleRepos > 0 ? "! Stale" : "✓ Active"}
1362 </div>
1363 <div class="ei-health-num">{health.staleRepos}</div>
1364 <div class="ei-health-label">Repos with no commits in 30d</div>
1365 </div>
1366 <div class={`ei-health-card ${health.abandonedIssues > 0 ? "amber" : "green"}`}>
1367 <div class={`ei-health-badge ${health.abandonedIssues > 0 ? "amber" : "green"}`}>
1368 {health.abandonedIssues > 0 ? "! Backlog" : "✓ Engaged"}
1369 </div>
1370 <div class="ei-health-num">{health.abandonedIssues}</div>
1371 <div class="ei-health-label">Issues {">"} 30d with no comments</div>
1372 </div>
1373 <div class={`ei-health-card ${gateStats.passRate !== null && gateStats.passRate < 80 ? "red" : gateStats.passRate !== null && gateStats.passRate < 95 ? "amber" : "green"}`}>
1374 <div class={`ei-health-badge ${gateStats.passRate !== null && gateStats.passRate < 80 ? "red" : gateStats.passRate !== null && gateStats.passRate < 95 ? "amber" : "green"}`}>
1375 {gateStats.passRate !== null && gateStats.passRate >= 95 ? "✓ Healthy" : gateStats.passRate !== null && gateStats.passRate < 80 ? "⚠ Critical" : "! Watch"}
1376 </div>
1377 <div class="ei-health-num">
1378 {gateStats.passRate !== null ? `${Math.round(gateStats.passRate)}%` : "—"}
1379 </div>
1380 <div class="ei-health-label">Gate pass rate</div>
1381 </div>
1382 </div>
1383 </section>
1384 </div>
1385
1386 {/* AI Impact */}
1387 <section class="ei-section">
1388 <div class="ei-section-head">
1389 <h2 class="ei-section-title">{"✨ "}AI Impact</h2>
1390 <span class="ei-section-sub">Last {rangeLabel}</span>
1391 </div>
1392 <div class="ei-ai-box">
1393 <div class="ei-ai-grid">
1394 <div class="ei-ai-stat">
1395 <div class="ei-ai-stat-val">{aiImpact.aiMergedPrs}</div>
1396 <div class="ei-ai-stat-label">PRs auto-merged by AI</div>
1397 </div>
1398 <div class="ei-ai-stat">
1399 <div class="ei-ai-stat-val">~{aiImpact.estimatedHoursSaved}h</div>
1400 <div class="ei-ai-stat-label">Estimated hours saved (4h/PR avg)</div>
1401 </div>
1402 <div class="ei-ai-stat">
1403 <div class="ei-ai-stat-val">{aiImpact.aiSecurityIssues}</div>
1404 <div class="ei-ai-stat-label">Security issues opened by AI</div>
1405 </div>
1406 <div class="ei-ai-stat">
1407 <div class="ei-ai-stat-val">{aiImpact.aiReviewComments}</div>
1408 <div class="ei-ai-stat-label">AI review comments left</div>
1409 </div>
1410 </div>
1411 {aiImpact.aiMergedPrs > 0 && (
1412 <div class="ei-ai-summary-line">
1413 AI has auto-merged{" "}
1414 <strong>{aiImpact.aiMergedPrs} PRs</strong> saving an
1415 estimated{" "}
1416 <strong>~{aiImpact.estimatedHoursSaved} hours</strong> of
1417 review time.{" "}
1418 {aiImpact.aiSecurityIssues > 0 && (
1419 <>
1420 AI also opened{" "}
1421 <strong>{aiImpact.aiSecurityIssues} security issues</strong>{" "}
1422 and left{" "}
1423 <strong>{aiImpact.aiReviewComments} review comments</strong>.
1424 </>
1425 )}
1426 </div>
1427 )}
1428 </div>
1429 </section>
1430 </div>
1431 </Layout>
1432 );
1433});
1434
1435// ─── JSON API summary ─────────────────────────────────────────────────────────
1436
1437engineeringInsights.get("/insights/api/summary", requireAuth, async (c) => {
1438 const user = c.get("user")!;
1439 const days = parseInt(c.req.query("days") || "30", 10);
1440
1441 const repoIds = await getAccessibleRepoIds(user.id);
1442
1443 const [prStats, gateStats, health, aiImpact, contributors] = await Promise.all([
1444 getPrStats(repoIds, days),
1445 getGateStats(repoIds, days),
1446 getHealthIndicators(repoIds),
1447 getAiImpact(repoIds, days),
1448 getTopContributors(repoIds, days),
1449 ]);
1450
1451 return c.json({
1452 ok: true,
1453 period: { days },
1454 repos: repoIds.length,
1455 velocity: {
1456 prsMergedThisPeriod: prStats.mergedThisPeriod,
1457 prsMergedLastPeriod: prStats.mergedLastPeriod,
1458 avgMergeHours: prStats.avgMergeHours,
1459 },
1460 quality: {
1461 gatePassRate: gateStats.passRate,
1462 gateRunsTotal: gateStats.totalRuns,
1463 gatePassed: gateStats.passed,
1464 gateFailed: gateStats.failed,
1465 },
1466 health: {
1467 stuckPrs: health.stuckPrs,
1468 staleRepos: health.staleRepos,
1469 abandonedIssues: health.abandonedIssues,
1470 },
1471 ai: {
1472 prsMergedByAi: aiImpact.aiMergedPrs,
1473 securityIssuesOpened: aiImpact.aiSecurityIssues,
1474 reviewCommentsLeft: aiImpact.aiReviewComments,
1475 estimatedHoursSaved: aiImpact.estimatedHoursSaved,
1476 },
1477 topContributors: contributors.slice(0, 5).map((c) => ({
1478 username: c.username,
1479 prsMerged: c.prsMerged,
1480 prsOpened: c.prsOpened,
1481 reviewsGiven: c.reviewsGiven,
1482 avgMergeHours: c.avgMergeHours,
1483 })),
1484 });
1485});
1486
1487// ─── Per-repo engineering insights ───────────────────────────────────────────
1488
1489engineeringInsights.get(
1490 "/:owner/:repo/insights/engineering",
1491 async (c) => {
1492 const { owner, repo } = c.req.param();
1493 const user = c.get("user");
1494 const rangeParam = c.req.query("range") || "30";
1495 const days =
1496 rangeParam === "7" ? 7 : rangeParam === "90" ? 90 : rangeParam === "365" ? 365 : 30;
1497 const rangeLabel = days === 7 ? "7d" : days === 90 ? "90d" : days === 365 ? "12mo" : "30d";
1498 const notifCount = user ? await getUnreadCount(user.id) : 0;
1499
1500 // Resolve repo
1501 const repoRows = await db
1502 .select({ id: repositories.id, ownerId: repositories.ownerId })
1503 .from(repositories)
1504 .innerJoin(users, eq(repositories.ownerId, users.id))
1505 .where(
1506 and(
1507 eq(users.username, owner),
1508 eq(repositories.name, repo)
1509 )
1510 )
1511 .limit(1);
1512
1513 if (repoRows.length === 0) return c.notFound();
1514 const repoId = repoRows[0].id;
1515 const repoIds = [repoId];
1516
1517 const [prStats, gateStats, weeklyVelocity, contributors, health, aiImpact] =
1518 await Promise.all([
1519 getPrStats(repoIds, days),
1520 getGateStats(repoIds, days),
1521 getWeeklyVelocity(repoIds, 8),
1522 getTopContributors(repoIds, days),
1523 getHealthIndicators(repoIds),
1524 getAiImpact(repoIds, days),
1525 ]);
1526
1527 const prDeltaPct =
1528 prStats.mergedLastPeriod > 0
1529 ? Math.round(
1530 ((prStats.mergedThisPeriod - prStats.mergedLastPeriod) /
1531 prStats.mergedLastPeriod) *
1532 100
1533 )
1534 : 0;
1535
1536 return c.html(
1537 <Layout
1538 title={`Engineering Insights — ${owner}/${repo}`}
1539 user={user}
1540 notificationCount={notifCount}
1541 >
1542 <style dangerouslySetInnerHTML={{ __html: styles }} />
1543 <RepoHeader owner={owner} repo={repo} />
1544 <RepoNav owner={owner} repo={repo} active="insights" />
1545
1546 <div class="ei-wrap">
1547 {/* Hero */}
1548 <section class="ei-hero">
1549 <div class="ei-hero-orb" aria-hidden="true" />
1550 <div class="ei-hero-inner">
1551 <div class="ei-hero-top">
1552 <div>
1553 <div class="ei-eyebrow">
1554 <span class="ei-eyebrow-dot" aria-hidden="true" />
1555 Engineering Intelligence · {owner}/{repo}
1556 </div>
1557 <h1 class="ei-hero-title">
1558 <span class="ei-hero-title-grad">Velocity.</span>{" "}
1559 Quality. AI Impact.
1560 </h1>
1561 <p class="ei-hero-sub">
1562 Shipping velocity, gate health, contributor rankings, and AI
1563 impact — all scoped to this repository.
1564 </p>
1565 </div>
1566 {/* Time range + sub-nav */}
1567 <div>
1568 <div class="ei-range-label">Time range</div>
1569 <div class="ei-range-bar">
1570 {(["7", "30", "90", "365"] as const).map((r) => (
1571 <a
1572 href={`/${owner}/${repo}/insights/engineering?range=${r}`}
1573 class={`ei-range-btn${rangeParam === r ? " active" : ""}`}
1574 >
1575 {r === "365" ? "12mo" : `${r}d`}
1576 </a>
1577 ))}
1578 </div>
1579 </div>
1580 </div>
1581 </div>
1582 </section>
1583
1584 {/* Insights sub-nav */}
1585 <div style="display:flex;gap:4px;margin-bottom:var(--space-5);border-bottom:1px solid var(--border);padding-bottom:0">
1586 <a href={`/${owner}/${repo}/insights`} style="padding:8px 14px;font-size:13px;font-weight:500;color:var(--text-muted);text-decoration:none;border-bottom:2px solid transparent;margin-bottom:-1px">
1587 Code Intelligence
1588 </a>
1589 <a href={`/${owner}/${repo}/insights/velocity`} style="padding:8px 14px;font-size:13px;font-weight:500;color:var(--text-muted);text-decoration:none;border-bottom:2px solid transparent;margin-bottom:-1px">
1590 Velocity
1591 </a>
1592 <a href={`/${owner}/${repo}/insights/dora`} style="padding:8px 14px;font-size:13px;font-weight:500;color:var(--text-muted);text-decoration:none;border-bottom:2px solid transparent;margin-bottom:-1px">
1593 DORA
1594 </a>
1595 <a href={`/${owner}/${repo}/insights/health`} style="padding:8px 14px;font-size:13px;font-weight:500;color:var(--text-muted);text-decoration:none;border-bottom:2px solid transparent;margin-bottom:-1px">
1596 Health
1597 </a>
1598 <a href={`/${owner}/${repo}/insights/engineering`}
1599 style="padding:8px 14px;font-size:13px;font-weight:500;color:var(--accent,#5865f2);text-decoration:none;border-bottom:2px solid var(--accent,#5865f2);margin-bottom:-1px">
1600 Engineering
1601 </a>
1602 </div>
1603
1604 {/* KPI Row */}
1605 <div class="ei-kpi-row">
1606 <div class="ei-kpi">
1607 <div class="ei-kpi-icon ei-kpi-icon-blue">{"⬆"}</div>
1608 <div class="ei-kpi-label">PRs Merged</div>
1609 <div class="ei-kpi-value">{prStats.mergedThisPeriod}</div>
1610 {prDeltaPct !== 0 && (
1611 <div class={`ei-kpi-delta ${prDeltaPct > 0 ? "up" : "down"}`}>
1612 <span>{prDeltaPct > 0 ? "▲" : "▼"}</span>
1613 {Math.abs(prDeltaPct)}% vs last {rangeLabel}
1614 </div>
1615 )}
1616 <div class="ei-kpi-sub">This {rangeLabel}</div>
1617 </div>
1618
1619 <div class="ei-kpi">
1620 <div class="ei-kpi-icon ei-kpi-icon-green">{"⏱"}</div>
1621 <div class="ei-kpi-label">Avg Merge Time</div>
1622 <div class={`ei-kpi-value${prStats.avgMergeHours === null ? " na" : ""}`}>
1623 {fmtHours(prStats.avgMergeHours)}
1624 </div>
1625 <div class="ei-kpi-sub">Open → merge, merged PRs</div>
1626 </div>
1627
1628 <div class="ei-kpi">
1629 <div class="ei-kpi-icon ei-kpi-icon-amber">{"✓"}</div>
1630 <div class="ei-kpi-label">Gate Pass Rate</div>
1631 <div class={`ei-kpi-value${gateStats.passRate === null ? " na" : ""}`}>
1632 {gateStats.passRate !== null
1633 ? `${Math.round(gateStats.passRate)}%`
1634 : "—"}
1635 </div>
1636 <div class="ei-kpi-sub">
1637 {gateStats.passed}/{gateStats.totalRuns} passed this {rangeLabel}
1638 </div>
1639 </div>
1640
1641 <div class="ei-kpi">
1642 <div class="ei-kpi-icon ei-kpi-icon-purple">{"✨"}</div>
1643 <div class="ei-kpi-label">AI Impact</div>
1644 <div class="ei-kpi-value">{aiImpact.aiMergedPrs}</div>
1645 <div class="ei-kpi-sub">
1646 PRs auto-merged · ~{aiImpact.estimatedHoursSaved}h saved
1647 </div>
1648 </div>
1649 </div>
1650
1651 {/* Alerts */}
1652 {(health.stuckPrs > 0 || health.staleRepos > 0 || health.abandonedIssues > 0) && (
1653 <div class="ei-alerts">
1654 {health.stuckPrs > 0 && (
1655 <div class="ei-alert red">
1656 <span class="ei-alert-icon">{"⚠"}</span>
1657 <div>
1658 <strong>
1659 {health.stuckPrs} PR{health.stuckPrs !== 1 ? "s" : ""} open
1660 {" "}{">"} 7 days in this repo
1661 </strong>{" "}
1662 — unblock them to maintain team velocity.
1663 </div>
1664 </div>
1665 )}
1666 {health.abandonedIssues > 0 && (
1667 <div class="ei-alert amber">
1668 <span class="ei-alert-icon">{"!"}</span>
1669 <div>
1670 <strong>
1671 {health.abandonedIssues} issue
1672 {health.abandonedIssues !== 1 ? "s" : ""} open {">"} 30 days
1673 without comments
1674 </strong>{" "}
1675 — consider triaging or closing these.
1676 </div>
1677 </div>
1678 )}
1679 </div>
1680 )}
1681
1682 {/* Velocity chart */}
1683 <section class="ei-section">
1684 <div class="ei-section-head">
1685 <h2 class="ei-section-title">PR Velocity</h2>
1686 <span class="ei-section-sub">PRs merged per week, last 8 weeks</span>
1687 </div>
1688 <VelocityChart buckets={weeklyVelocity} />
1689 </section>
1690
1691 <div class="ei-two-col">
1692 {/* Contributors */}
1693 <section class="ei-section">
1694 <div class="ei-section-head">
1695 <h2 class="ei-section-title">Top Contributors</h2>
1696 <span class="ei-section-sub">
1697 <a href={`/${owner}/${repo}/contributors`} style="color:var(--accent);text-decoration:none">
1698 View all →
1699 </a>
1700 </span>
1701 </div>
1702 {contributors.length === 0 ? (
1703 <div class="ei-empty">
1704 <strong>No contributor data yet</strong>
1705 Merge some PRs to see rankings.
1706 </div>
1707 ) : (
1708 <div class="ei-table-wrap">
1709 <table class="ei-table">
1710 <thead>
1711 <tr>
1712 <th>Developer</th>
1713 <th>Merged</th>
1714 <th>Reviews</th>
1715 <th>Avg TTM</th>
1716 </tr>
1717 </thead>
1718 <tbody>
1719 {contributors.slice(0, 7).map((c, i) => (
1720 <tr>
1721 <td>
1722 <div class="ei-contributor-cell">
1723 <span
1724 class={`ei-contributor-rank${i === 0 ? " top1" : i === 1 ? " top2" : i === 2 ? " top3" : ""}`}
1725 >
1726 {i + 1}
1727 </span>
1728 <a href={`/${c.username}`} style="color:var(--text);text-decoration:none;font-weight:500">
1729 {c.displayName || c.username}
1730 </a>
1731 </div>
1732 </td>
1733 <td>{c.prsMerged}</td>
1734 <td>{c.reviewsGiven}</td>
1735 <td>{fmtHours(c.avgMergeHours)}</td>
1736 </tr>
1737 ))}
1738 </tbody>
1739 </table>
1740 </div>
1741 )}
1742 </section>
1743
1744 {/* Health indicators */}
1745 <section class="ei-section">
1746 <div class="ei-section-head">
1747 <h2 class="ei-section-title">Health Snapshot</h2>
1748 <span class="ei-section-sub">
1749 <a href={`/${owner}/${repo}/insights/health`} style="color:var(--accent);text-decoration:none">
1750 Full report →
1751 </a>
1752 </span>
1753 </div>
1754 <div class="ei-health-grid">
1755 <div class={`ei-health-card ${health.stuckPrs > 0 ? "red" : "green"}`}>
1756 <div class={`ei-health-badge ${health.stuckPrs > 0 ? "red" : "green"}`}>
1757 {health.stuckPrs > 0 ? "⚠ Stuck" : "✓ Clear"}
1758 </div>
1759 <div class="ei-health-num">{health.stuckPrs}</div>
1760 <div class="ei-health-label">PRs open {">"} 7 days</div>
1761 </div>
1762 <div class={`ei-health-card ${health.abandonedIssues > 0 ? "amber" : "green"}`}>
1763 <div class={`ei-health-badge ${health.abandonedIssues > 0 ? "amber" : "green"}`}>
1764 {health.abandonedIssues > 0 ? "! Backlog" : "✓ Active"}
1765 </div>
1766 <div class="ei-health-num">{health.abandonedIssues}</div>
1767 <div class="ei-health-label">Abandoned issues</div>
1768 </div>
1769 </div>
1770 </section>
1771 </div>
1772
1773 {/* AI Impact */}
1774 <section class="ei-section">
1775 <div class="ei-section-head">
1776 <h2 class="ei-section-title">{"✨ "}AI Impact</h2>
1777 <span class="ei-section-sub">Last {rangeLabel}</span>
1778 </div>
1779 <div class="ei-ai-box">
1780 <div class="ei-ai-grid">
1781 <div class="ei-ai-stat">
1782 <div class="ei-ai-stat-val">{aiImpact.aiMergedPrs}</div>
1783 <div class="ei-ai-stat-label">PRs auto-merged by AI</div>
1784 </div>
1785 <div class="ei-ai-stat">
1786 <div class="ei-ai-stat-val">~{aiImpact.estimatedHoursSaved}h</div>
1787 <div class="ei-ai-stat-label">Estimated hours saved</div>
1788 </div>
1789 <div class="ei-ai-stat">
1790 <div class="ei-ai-stat-val">{aiImpact.aiSecurityIssues}</div>
1791 <div class="ei-ai-stat-label">Security issues opened by AI</div>
1792 </div>
1793 <div class="ei-ai-stat">
1794 <div class="ei-ai-stat-val">{aiImpact.aiReviewComments}</div>
1795 <div class="ei-ai-stat-label">AI review comments</div>
1796 </div>
1797 </div>
1798 {(aiImpact.aiMergedPrs > 0 || aiImpact.aiReviewComments > 0) && (
1799 <div class="ei-ai-summary-line">
1800 {aiImpact.aiMergedPrs > 0 && (
1801 <>
1802 AI auto-merged{" "}
1803 <strong>{aiImpact.aiMergedPrs} PRs</strong>, saving
1804 ~<strong>{aiImpact.estimatedHoursSaved} hours</strong> of
1805 review time.{" "}
1806 </>
1807 )}
1808 {aiImpact.aiReviewComments > 0 && (
1809 <>
1810 AI left{" "}
1811 <strong>{aiImpact.aiReviewComments} review comments</strong>{" "}
1812 and opened{" "}
1813 <strong>{aiImpact.aiSecurityIssues} security issues</strong>.
1814 </>
1815 )}
1816 </div>
1817 )}
1818 </div>
1819 </section>
1820 </div>
1821 </Layout>
1822 );
1823 }
1824);
1825
1826export default engineeringInsights;