Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

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.

insights.tsxBlame943 lines · 2 contributors
16b325cClaude1/**
2 * Insight routes — time-travel, dependency analysis, rollback.
3 *
4 * These are the pages that don't exist on GitHub.
5 * This is why developers will switch.
c9188afClaude6 *
7 * 2026 polish: gradient-hairline hero + radial orb + stat cards + polished
8 * timeline. Every class prefixed `.insights-` so this surface doesn't
9 * bleed into the wider repo polish. All data fetches, queries, and
10 * actions preserved exactly.
16b325cClaude11 */
12
13import { Hono } from "hono";
14import { Layout } from "../views/layout";
15import { RepoHeader, RepoNav } from "../views/components";
16import {
17 getFileTimeline,
18 getFunctionTimeline,
19 detectCoupledFiles,
20 getRepoStory,
21} from "../lib/timetravel";
22import {
23 buildImportGraph,
24 analyzeUpgradeImpact,
25 findUnusedDeps,
26} from "../lib/depimpact";
27import { findRollbackTarget, executeRollback } from "../lib/rollback";
28import {
29 repoExists,
30 getDefaultBranch,
31 listBranches,
32} from "../git/repository";
33import { softAuth, requireAuth } from "../middleware/auth";
34import type { AuthEnv } from "../middleware/auth";
35
36const insights = new Hono<AuthEnv>();
37
38insights.use("*", softAuth);
39
c9188afClaude40/* ─────────────────────────────────────────────────────────────────────────
41 * Scoped CSS — every class prefixed `.insights-`. Mirrors the
42 * gradient-hairline hero + radial orb + per-card pattern from
43 * `admin-integrations` and `admin-diagnose`. Stat cards use
44 * `font-variant-numeric: tabular-nums` so columns of numbers line up.
45 * ───────────────────────────────────────────────────────────────────── */
46const styles = `
eed4684Claude47 .insights-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4); }
c9188afClaude48
49 .insights-hero {
50 position: relative;
51 margin-bottom: var(--space-5);
52 padding: var(--space-5) var(--space-6);
53 background: var(--bg-elevated);
54 border: 1px solid var(--border);
55 border-radius: 16px;
56 overflow: hidden;
57 }
58 .insights-hero::before {
59 content: '';
60 position: absolute;
61 top: 0; left: 0; right: 0;
62 height: 2px;
6fd5915Claude63 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
c9188afClaude64 opacity: 0.75;
65 pointer-events: none;
66 }
67 .insights-hero-orb {
68 position: absolute;
69 inset: -30% -15% auto auto;
70 width: 460px; height: 460px;
6fd5915Claude71 background: radial-gradient(circle, rgba(91,110,232,0.22), rgba(95,143,160,0.10) 45%, transparent 70%);
c9188afClaude72 filter: blur(80px);
73 opacity: 0.75;
74 pointer-events: none;
75 z-index: 0;
76 }
77 .insights-hero-inner { position: relative; z-index: 1; max-width: 760px; }
78 .insights-eyebrow {
79 display: inline-flex;
80 align-items: center;
81 gap: 8px;
82 text-transform: uppercase;
83 font-family: var(--font-mono);
84 font-size: 11px;
85 letter-spacing: 0.18em;
86 color: var(--text-muted);
87 font-weight: 600;
88 margin-bottom: 14px;
89 }
90 .insights-eyebrow-dot {
91 width: 8px; height: 8px;
92 border-radius: 9999px;
6fd5915Claude93 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
94 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
c9188afClaude95 }
96 .insights-title {
97 font-family: var(--font-display);
98 font-size: clamp(28px, 4vw, 40px);
99 font-weight: 800;
100 letter-spacing: -0.028em;
101 line-height: 1.05;
102 margin: 0 0 var(--space-2);
103 color: var(--text-strong);
104 }
105 .insights-title-grad {
6fd5915Claude106 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
c9188afClaude107 -webkit-background-clip: text;
108 background-clip: text;
109 -webkit-text-fill-color: transparent;
110 color: transparent;
111 }
112 .insights-sub {
113 font-size: 15px;
114 color: var(--text-muted);
115 margin: 0;
116 line-height: 1.55;
117 }
118
119 /* Stat-card grid */
120 .insights-stats {
121 display: grid;
122 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
123 gap: var(--space-3);
124 margin-bottom: var(--space-5);
125 }
126 .insights-stat {
127 position: relative;
128 background: var(--bg-elevated);
129 border: 1px solid var(--border);
130 border-radius: 14px;
131 padding: var(--space-4);
132 transition: border-color 120ms ease, transform 120ms ease;
133 }
134 .insights-stat:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
135 .insights-stat-label {
136 font-size: 10.5px;
137 letter-spacing: 0.14em;
138 text-transform: uppercase;
139 color: var(--text-muted);
140 font-weight: 700;
141 margin-bottom: 6px;
142 }
143 .insights-stat-value {
144 font-family: var(--font-display);
145 font-size: 32px;
146 font-weight: 800;
147 letter-spacing: -0.022em;
148 color: var(--text-strong);
149 font-variant-numeric: tabular-nums;
150 line-height: 1;
151 }
152 .insights-stat-value.is-warn { color: #fca5a5; }
153 .insights-stat-trend {
154 margin-top: 6px;
155 display: inline-flex;
156 align-items: center;
157 gap: 4px;
158 font-size: 12px;
159 font-variant-numeric: tabular-nums;
160 color: var(--text-muted);
161 }
162 .insights-stat-trend.is-up { color: #6ee7b7; }
163 .insights-stat-trend.is-down { color: #fca5a5; }
164 .insights-stat-trend .arrow { font-size: 11px; line-height: 1; }
165 .insights-stat-hint {
166 margin-top: 6px;
167 font-size: 12px;
168 color: var(--text-muted);
169 }
170
171 /* Section heading */
172 .insights-section-head {
173 margin: 0 0 var(--space-3);
174 display: flex;
175 align-items: baseline;
176 justify-content: space-between;
177 gap: var(--space-3);
178 flex-wrap: wrap;
179 }
180 .insights-section-title {
181 margin: 0;
182 font-family: var(--font-display);
183 font-size: 18px;
184 font-weight: 700;
185 letter-spacing: -0.018em;
186 color: var(--text-strong);
187 }
188 .insights-section-sub { font-size: 12.5px; color: var(--text-muted); }
189
190 .insights-blurb {
191 margin: 0 0 var(--space-3);
192 font-size: 13px;
193 color: var(--text-muted);
194 line-height: 1.55;
195 }
196
197 /* Generic card list */
198 .insights-list {
199 display: flex;
200 flex-direction: column;
201 gap: var(--space-2);
202 margin-bottom: var(--space-5);
203 }
204 .insights-card {
205 position: relative;
206 background: var(--bg-elevated);
207 border: 1px solid var(--border);
208 border-radius: 12px;
209 padding: var(--space-3) var(--space-4);
210 transition: border-color 120ms ease, transform 120ms ease;
211 }
212 .insights-card:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
213 .insights-card.is-warn { border-color: rgba(248,113,113,0.30); }
214
215 .insights-card-row {
216 display: flex;
217 justify-content: space-between;
218 align-items: flex-start;
219 gap: 16px;
220 flex-wrap: wrap;
221 }
222 .insights-card-main { min-width: 0; flex: 1; }
223 .insights-card-title {
224 font-family: var(--font-display);
225 font-size: 14.5px;
226 font-weight: 700;
227 color: var(--text-strong);
228 letter-spacing: -0.005em;
229 line-height: 1.3;
230 margin: 0 0 4px;
231 word-break: break-word;
232 }
233 .insights-card-title a { color: inherit; text-decoration: none; }
234 .insights-card-title a:hover { color: var(--accent); }
235 .insights-card-sub {
236 font-size: 12.5px;
237 color: var(--text-muted);
238 line-height: 1.5;
239 margin: 0;
240 }
241 .insights-card-meta {
242 font-family: var(--font-mono);
243 font-size: 12px;
244 color: var(--text-muted);
245 text-align: right;
246 white-space: nowrap;
247 font-variant-numeric: tabular-nums;
248 }
249 .insights-card-meta .add { color: #6ee7b7; }
250 .insights-card-meta .del { color: #fca5a5; }
251
252 .insights-coupled {
253 font-family: var(--font-mono);
254 font-size: 13px;
255 color: var(--text);
256 word-break: break-word;
257 }
258 .insights-coupled a { color: var(--accent); text-decoration: none; }
259 .insights-coupled a:hover { text-decoration: underline; }
260 .insights-coupled .plus { color: var(--text-muted); margin: 0 8px; }
261
262 /* Timeline (revisions / milestones) */
263 .insights-timeline {
264 position: relative;
265 padding-left: 22px;
266 margin: 0;
267 list-style: none;
268 }
269 .insights-timeline::before {
270 content: '';
271 position: absolute;
272 left: 6px;
273 top: 6px;
274 bottom: 6px;
275 width: 2px;
6fd5915Claude276 background: linear-gradient(180deg, rgba(91,110,232,0.22), rgba(95,143,160,0.06));
c9188afClaude277 border-radius: 9999px;
278 }
279 .insights-timeline-item {
280 position: relative;
281 padding: 0 0 var(--space-3) 0;
282 }
283 .insights-timeline-dot {
284 position: absolute;
285 left: -22px;
286 top: 6px;
287 width: 12px; height: 12px;
288 border-radius: 9999px;
289 background: var(--text-muted);
290 box-shadow: 0 0 0 3px rgba(255,255,255,0.04);
291 }
292 .insights-timeline-dot.is-milestone {
6fd5915Claude293 background: linear-gradient(135deg, #34d399, #5f8fa0);
c9188afClaude294 box-shadow: 0 0 0 4px rgba(52,211,153,0.18);
295 width: 14px; height: 14px;
296 left: -23px;
297 }
298 .insights-timeline-card {
299 background: var(--bg-elevated);
300 border: 1px solid var(--border);
301 border-radius: 12px;
302 padding: var(--space-3) var(--space-4);
303 transition: border-color 120ms ease, transform 120ms ease;
304 }
305 .insights-timeline-card:hover { border-color: var(--border-strong, var(--border)); transform: translateY(-1px); }
306
307 /* Unused-deps banner */
308 .insights-banner {
309 margin-bottom: var(--space-4);
310 padding: 12px 16px;
311 border-radius: 12px;
312 border: 1px solid rgba(248,113,113,0.34);
313 background: rgba(248,113,113,0.08);
314 color: #fecaca;
315 font-size: 13px;
316 line-height: 1.55;
317 }
318 .insights-banner strong { color: #fecaca; font-weight: 700; }
319 .insights-banner code {
320 font-family: var(--font-mono);
321 font-size: 12.5px;
322 background: rgba(255,255,255,0.04);
323 border: 1px solid rgba(248,113,113,0.22);
324 padding: 1px 6px;
325 border-radius: 5px;
326 }
327 .insights-banner-hint {
328 margin-top: 6px;
329 font-size: 12px;
330 color: rgba(254,202,202,0.78);
331 }
332
333 /* Dep usage block */
334 .insights-dep-uses {
335 margin-top: 10px;
336 padding-top: 10px;
337 border-top: 1px solid var(--border);
338 font-size: 12px;
339 color: var(--text-muted);
340 display: flex;
341 flex-direction: column;
342 gap: 4px;
343 }
344 .insights-dep-uses code {
345 font-family: var(--font-mono);
346 font-size: 12px;
347 color: var(--text);
348 background: rgba(255,255,255,0.04);
349 border: 1px solid var(--border);
350 padding: 1px 6px;
351 border-radius: 5px;
352 }
353 .insights-dep-uses a { color: var(--accent); text-decoration: none; }
354 .insights-dep-uses a:hover { text-decoration: underline; }
355 .insights-dep-badge {
356 display: inline-flex;
357 align-items: center;
358 gap: 4px;
359 padding: 1px 7px;
360 border-radius: 9999px;
361 font-size: 10.5px;
362 font-weight: 700;
363 letter-spacing: 0.06em;
364 text-transform: uppercase;
365 background: rgba(96,165,250,0.12);
366 color: #93c5fd;
367 box-shadow: inset 0 0 0 1px rgba(96,165,250,0.30);
368 }
369 .insights-dep-unused {
370 color: #fca5a5;
371 font-weight: 600;
372 }
373
374 /* Empty state — dashed orb card */
375 .insights-empty {
376 position: relative;
377 overflow: hidden;
378 text-align: center;
379 padding: var(--space-6) var(--space-4);
380 border: 1px dashed var(--border-strong, var(--border));
381 border-radius: 16px;
382 background: rgba(255,255,255,0.012);
383 color: var(--text-muted);
384 margin-bottom: var(--space-5);
385 }
386 .insights-empty::before {
387 content: '';
388 position: absolute;
389 inset: -40% -20% auto auto;
390 width: 320px; height: 320px;
6fd5915Claude391 background: radial-gradient(circle, rgba(91,110,232,0.14), rgba(95,143,160,0.06) 45%, transparent 70%);
c9188afClaude392 filter: blur(60px);
393 pointer-events: none;
394 }
395 .insights-empty-inner { position: relative; z-index: 1; }
396 .insights-empty strong {
397 display: block;
398 font-family: var(--font-display);
399 font-size: 16px;
400 font-weight: 700;
401 color: var(--text-strong);
402 margin-bottom: 4px;
403 }
404 .insights-empty span { font-size: 13px; }
405`;
406
16b325cClaude407// ─── TIME TRAVEL ─────────────────────────────────────────────
408
409// File evolution timeline
410insights.get("/:owner/:repo/timeline/:ref{.+$}", async (c) => {
411 const { owner, repo } = c.req.param();
412 const user = c.get("user");
413 const refAndPath = c.req.param("ref");
414
415 const branches = await listBranches(owner, repo);
416 let ref = "";
417 let filePath = "";
418
419 for (const branch of branches) {
420 if (refAndPath.startsWith(branch + "/")) {
421 ref = branch;
422 filePath = refAndPath.slice(branch.length + 1);
423 break;
424 }
425 }
426 if (!ref) {
427 const idx = refAndPath.indexOf("/");
428 if (idx === -1) return c.notFound();
429 ref = refAndPath.slice(0, idx);
430 filePath = refAndPath.slice(idx + 1);
431 }
432
433 const timeline = await getFileTimeline(owner, repo, ref, filePath);
434 if (!timeline) return c.notFound();
435
436 return c.html(
437 <Layout title={`Timeline: ${filePath} — ${owner}/${repo}`} user={user}>
438 <RepoHeader owner={owner} repo={repo} />
439 <RepoNav owner={owner} repo={repo} active="code" />
c9188afClaude440
441 <div class="insights-wrap">
442 <section class="insights-hero">
443 <div class="insights-hero-orb" aria-hidden="true" />
444 <div class="insights-hero-inner">
445 <div class="insights-eyebrow">
446 <span class="insights-eyebrow-dot" aria-hidden="true" />
447 Time travel · {owner}/{repo}
448 </div>
449 <h2 class="insights-title">
450 <span class="insights-title-grad">{filePath}</span>
451 </h2>
452 <p class="insights-sub">
453 {timeline.totalRevisions} revision
454 {timeline.totalRevisions !== 1 ? "s" : ""} · First seen{" "}
455 {new Date(timeline.firstSeen.date).toLocaleDateString()} by{" "}
456 {timeline.firstSeen.author}
457 </p>
458 </div>
459 </section>
460
461 <ul class="insights-timeline">
462 {timeline.revisions.map((rev) => (
463 <li class="insights-timeline-item">
464 <span class="insights-timeline-dot" aria-hidden="true" />
465 <div class="insights-timeline-card">
466 <div class="insights-card-row">
467 <div class="insights-card-main">
468 <h4 class="insights-card-title">
469 <a href={`/${owner}/${repo}/commit/${rev.sha}`}>
470 {rev.message}
471 </a>
472 </h4>
473 <p class="insights-card-sub">
474 {rev.author} —{" "}
475 {new Date(rev.date).toLocaleDateString("en-US", {
476 month: "short",
477 day: "numeric",
478 year: "numeric",
479 })}
480 </p>
481 </div>
482 <div class="insights-card-meta">
483 <span class="add">+{rev.linesAdded}</span>{" "}
484 <span class="del">-{rev.linesRemoved}</span>
485 <div style="color: var(--text-muted)">{rev.sizeAfter} bytes</div>
16b325cClaude486 </div>
487 </div>
488 </div>
c9188afClaude489 </li>
490 ))}
491 </ul>
16b325cClaude492 </div>
c9188afClaude493 <style dangerouslySetInnerHTML={{ __html: styles }} />
16b325cClaude494 </Layout>
495 );
496});
497
c9188afClaude498// Coupled files analysis (the canonical "Insights" landing surface)
6a645f0ccantynz-alt499// ─── INSIGHTS HUB (the tab landing page) ─────────────────────────────
500// The repo-nav "Insights" tab links here. Previously NOTHING served
501// /:owner/:repo/insights, so the tab 404'd on every repo — and the seven
502// insight sub-reports (DORA, velocity, health, hot files, bus factor, test
503// gaps, engineering) were orphaned with no entry point. This hub is the
504// "Overview" that every sub-page's subnav already links back to.
505insights.get("/:owner/:repo/insights", async (c) => {
506 const { owner, repo } = c.req.param();
507 const user = c.get("user");
508 if (!(await repoExists(owner, repo))) return c.notFound();
509
510 const base = `/${owner}/${repo}`;
511 const groups: Array<{
512 heading: string;
513 cards: Array<{ title: string; desc: string; href: string; icon: string }>;
514 }> = [
515 {
516 heading: "Delivery",
517 cards: [
518 { title: "DORA metrics", icon: "🚀", href: `${base}/insights/dora`, desc: "Deploy frequency, lead time, change-fail rate, and time-to-restore." },
519 { title: "Velocity", icon: "📈", href: `${base}/insights/velocity`, desc: "Throughput, cycle time, and PR flow over time." },
520 { title: "Engineering metrics", icon: "🧭", href: `${base}/insights/engineering`, desc: "Team-level engineering signals and trends." },
521 ],
522 },
523 {
524 heading: "Quality & risk",
525 cards: [
526 { title: "Reliability & health", icon: "❤️", href: `${base}/insights/health`, desc: "Build health, stability trends, and reliability signals." },
527 { title: "Test gaps", icon: "🧪", href: `${base}/insights/test-gaps`, desc: "Untested code ranked by risk — where to write tests first." },
528 { title: "Bus factor", icon: "🚌", href: `${base}/insights/bus-factor`, desc: "Knowledge concentration and single-maintainer risk." },
529 ],
530 },
531 {
532 heading: "Code intelligence",
533 cards: [
534 { title: "Hot files", icon: "🔥", href: `${base}/insights/hotfiles`, desc: "Highest-churn files — where change concentrates." },
535 { title: "File coupling & history", icon: "🕸️", href: `${base}/coupling`, desc: "Files that change together, plus milestone time-travel." },
536 { title: "Dependencies", icon: "📦", href: `${base}/dependencies`, desc: "Import graph and upgrade-impact analysis." },
537 ],
538 },
539 {
540 heading: "People & activity",
541 cards: [
542 { title: "Contributors", icon: "👥", href: `${base}/contributors`, desc: "Who's contributing, and how much." },
543 { title: "Pulse", icon: "💓", href: `${base}/pulse`, desc: "Recent activity at a glance." },
544 ],
545 },
546 ];
547
548 return c.html(
549 <Layout title={`Insights — ${owner}/${repo}`} user={user}>
550 <RepoHeader owner={owner} repo={repo} />
551 <RepoNav owner={owner} repo={repo} active="insights" />
552 <div class="insights-wrap">
553 <section class="insights-hero">
554 <div class="insights-hero-orb" aria-hidden="true" />
555 <div class="insights-hero-inner">
556 <div class="insights-eyebrow">
557 <span class="insights-eyebrow-dot" aria-hidden="true" />
558 Insights · {owner}/{repo}
559 </div>
560 <h2 class="insights-title">
561 <span class="insights-title-grad">Code intelligence.</span>
562 </h2>
563 <p class="insights-sub">
564 Delivery, quality, code-health, and people signals for this
565 repository — the reports GitHub doesn't ship.
566 </p>
567 </div>
568 </section>
569
570 {groups.map((g) => (
571 <>
572 <div class="insights-section-head">
573 <h3 class="insights-section-title">{g.heading}</h3>
574 </div>
575 <div
576 style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3);margin-bottom:var(--space-5)"
577 >
578 {g.cards.map((card) => (
579 <a
580 href={card.href}
581 style="display:block;padding:16px 18px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:12px;text-decoration:none;transition:border-color 120ms ease,transform 120ms ease"
582 >
583 <div style="display:flex;align-items:center;gap:9px;margin-bottom:6px">
584 <span aria-hidden="true" style="font-size:18px">{card.icon}</span>
585 <span style="font-size:15px;font-weight:600;color:var(--text-strong)">{card.title}</span>
586 <span style="margin-left:auto;color:var(--text-muted)">→</span>
587 </div>
588 <div style="font-size:13px;color:var(--text-muted);line-height:1.5">{card.desc}</div>
589 </a>
590 ))}
591 </div>
592 </>
593 ))}
594 </div>
595 <style dangerouslySetInnerHTML={{ __html: styles }} />
596 </Layout>
597 );
598});
599
16b325cClaude600insights.get("/:owner/:repo/coupling", async (c) => {
601 const { owner, repo } = c.req.param();
602 const user = c.get("user");
603
604 if (!(await repoExists(owner, repo))) return c.notFound();
605 const ref = (await getDefaultBranch(owner, repo)) || "main";
606
607 const coupled = await detectCoupledFiles(owner, repo, ref);
608 const story = await getRepoStory(owner, repo, ref);
609 const milestones = story.filter((s) => s.significance !== "normal").slice(0, 20);
610
c9188afClaude611 // Stat-card values derived from the data we already fetched. These line up
612 // with the spec (commits, files touched, milestones, coupled-pairs) so the
613 // grid renders even when the repo is empty.
614 const totalCommits = story.length;
615 const totalAdditions = story.reduce(
616 (n, s) => n + (s.stats?.additions || 0),
617 0
618 );
619 const totalDeletions = story.reduce(
620 (n, s) => n + (s.stats?.deletions || 0),
621 0
622 );
623
16b325cClaude624 return c.html(
625 <Layout title={`Insights — ${owner}/${repo}`} user={user}>
626 <RepoHeader owner={owner} repo={repo} />
c9188afClaude627 <RepoNav owner={owner} repo={repo} active="insights" />
628
629 <div class="insights-wrap">
630 <section class="insights-hero">
631 <div class="insights-hero-orb" aria-hidden="true" />
632 <div class="insights-hero-inner">
633 <div class="insights-eyebrow">
634 <span class="insights-eyebrow-dot" aria-hidden="true" />
635 Insights · {owner}/{repo}
16b325cClaude636 </div>
c9188afClaude637 <h2 class="insights-title">
638 <span class="insights-title-grad">Code intelligence.</span>
639 </h2>
640 <p class="insights-sub">
641 File coupling, milestone history, and contributor signals — the
642 kind of intelligence GitHub doesn't ship.
643 </p>
644 </div>
645 </section>
646
647 <div class="insights-stats">
648 <div class="insights-stat">
649 <div class="insights-stat-label">Commits indexed</div>
650 <div class="insights-stat-value">{totalCommits.toLocaleString()}</div>
651 <div class="insights-stat-hint">On {ref}</div>
652 </div>
653 <div class="insights-stat">
654 <div class="insights-stat-label">Lines added</div>
655 <div class="insights-stat-value">{totalAdditions.toLocaleString()}</div>
656 <div class="insights-stat-trend is-up">
657 <span class="arrow" aria-hidden="true">▲</span>
658 across history
659 </div>
660 </div>
661 <div class="insights-stat">
662 <div class="insights-stat-label">Lines removed</div>
663 <div class="insights-stat-value">{totalDeletions.toLocaleString()}</div>
664 <div class="insights-stat-trend is-down">
665 <span class="arrow" aria-hidden="true">▼</span>
666 across history
667 </div>
668 </div>
669 <div class="insights-stat">
670 <div class="insights-stat-label">Milestones</div>
671 <div class="insights-stat-value">{milestones.length}</div>
672 <div class="insights-stat-hint">Significant commits</div>
673 </div>
16b325cClaude674 </div>
c9188afClaude675
676 <div class="insights-section-head">
677 <h3 class="insights-section-title">Coupled files</h3>
678 <span class="insights-section-sub">{coupled.length} pair{coupled.length === 1 ? "" : "s"}</span>
679 </div>
680 <p class="insights-blurb">
681 Files that change together frequently — potential architectural
682 coupling worth refactoring.
683 </p>
684 {coupled.length === 0 ? (
685 <div class="insights-empty">
686 <div class="insights-empty-inner">
687 <strong>No strong coupling detected</strong>
688 <span>Push more code to see relationships emerge.</span>
689 </div>
690 </div>
691 ) : (
692 <div class="insights-list">
693 {coupled.map((pair) => (
694 <div class="insights-card">
695 <div class="insights-card-row">
696 <div class="insights-card-main">
697 <div class="insights-coupled">
698 <a href={`/${owner}/${repo}/blob/${ref}/${pair.files[0]}`}>
699 {pair.files[0]}
700 </a>
701 <span class="plus" aria-hidden="true">+</span>
702 <a href={`/${owner}/${repo}/blob/${ref}/${pair.files[1]}`}>
703 {pair.files[1]}
704 </a>
705 </div>
706 </div>
707 <div class="insights-card-meta">
708 {pair.cochanges} co-changes ({pair.percentage}%)
709 </div>
16b325cClaude710 </div>
711 </div>
c9188afClaude712 ))}
713 </div>
714 )}
715
716 <div class="insights-section-head">
717 <h3 class="insights-section-title">Project milestones</h3>
718 <span class="insights-section-sub">{milestones.length} milestone{milestones.length === 1 ? "" : "s"}</span>
16b325cClaude719 </div>
c9188afClaude720 {milestones.length === 0 ? (
721 <div class="insights-empty">
722 <div class="insights-empty-inner">
723 <strong>No milestones detected yet</strong>
724 <span>Push code to see insights emerge.</span>
725 </div>
726 </div>
727 ) : (
728 <ul class="insights-timeline">
729 {milestones.map((m) => (
730 <li class="insights-timeline-item">
731 <span
732 class={
733 "insights-timeline-dot" +
734 (m.significance === "milestone" ? " is-milestone" : "")
735 }
736 aria-hidden="true"
737 />
738 <div class="insights-timeline-card">
739 <div class="insights-card-row">
740 <div class="insights-card-main">
741 <h4 class="insights-card-title">
742 <a href={`/${owner}/${repo}/commit/${m.sha}`}>
743 {m.message}
744 </a>
745 </h4>
746 <p class="insights-card-sub">
747 {m.author} —{" "}
748 {new Date(m.date).toLocaleDateString()}
749 </p>
750 </div>
751 <div class="insights-card-meta">
752 <span class="add">+{m.stats.additions}</span>{" "}
753 <span class="del">-{m.stats.deletions}</span>
754 <div style="color: var(--text-muted)">
755 {m.stats.files} file{m.stats.files === 1 ? "" : "s"}
756 </div>
757 </div>
758 </div>
759 </div>
760 </li>
761 ))}
762 </ul>
763 )}
764 </div>
765 <style dangerouslySetInnerHTML={{ __html: styles }} />
16b325cClaude766 </Layout>
767 );
768});
769
770// ─── DEPENDENCY INSIGHTS ─────────────────────────────────────
771
772insights.get("/:owner/:repo/dependencies", async (c) => {
773 const { owner, repo } = c.req.param();
774 const user = c.get("user");
775
776 if (!(await repoExists(owner, repo))) return c.notFound();
777 const ref = (await getDefaultBranch(owner, repo)) || "main";
778
779 const graph = await buildImportGraph(owner, repo, ref);
780 const unused = findUnusedDeps(graph);
781
782 return c.html(
783 <Layout title={`Dependencies — ${owner}/${repo}`} user={user}>
784 <RepoHeader owner={owner} repo={repo} />
785 <RepoNav owner={owner} repo={repo} active="code" />
c9188afClaude786
787 <div class="insights-wrap">
788 <section class="insights-hero">
789 <div class="insights-hero-orb" aria-hidden="true" />
790 <div class="insights-hero-inner">
791 <div class="insights-eyebrow">
792 <span class="insights-eyebrow-dot" aria-hidden="true" />
793 Dependency intelligence · {owner}/{repo}
794 </div>
795 <h2 class="insights-title">
796 <span class="insights-title-grad">What you depend on.</span>
797 </h2>
798 <p class="insights-sub">
799 Static import graph across the repo — every package, how it's
800 used, and which ones are dead weight.
801 </p>
802 </div>
803 </section>
804
805 <div class="insights-stats">
806 <div class="insights-stat">
807 <div class="insights-stat-label">Dependencies</div>
808 <div class="insights-stat-value">{graph.externalDependencies}</div>
809 <div class="insights-stat-hint">External packages</div>
810 </div>
811 <div class="insights-stat">
812 <div class="insights-stat-label">Source files</div>
813 <div class="insights-stat-value">{graph.internalModules}</div>
814 <div class="insights-stat-hint">Internal modules</div>
815 </div>
816 <div class={"insights-stat"}>
817 <div class="insights-stat-label">Unused</div>
818 <div
819 class={
820 "insights-stat-value" + (unused.length > 0 ? " is-warn" : "")
821 }
822 >
823 {unused.length}
824 </div>
825 <div class="insights-stat-hint">Installed but never imported</div>
826 </div>
827 <div class="insights-stat">
828 <div class="insights-stat-label">Circular chains</div>
829 <div
830 class={
831 "insights-stat-value" +
832 (graph.circularDeps.length > 0 ? " is-warn" : "")
833 }
834 >
835 {graph.circularDeps.length}
836 </div>
837 <div class="insights-stat-hint">Cycles in the import graph</div>
16b325cClaude838 </div>
839 </div>
c9188afClaude840
841 {unused.length > 0 && (
842 <div class="insights-banner">
843 <strong>Unused dependencies:</strong>{" "}
844 <code>{unused.join(", ")}</code>
845 <div class="insights-banner-hint">
846 These are installed but never imported. Removing them reduces
847 install time and attack surface.
16b325cClaude848 </div>
c9188afClaude849 </div>
850 )}
851
852 <div class="insights-section-head">
853 <h3 class="insights-section-title">Packages</h3>
854 <span class="insights-section-sub">{graph.dependencies.length} total</span>
855 </div>
856 {graph.dependencies.length === 0 ? (
857 <div class="insights-empty">
858 <div class="insights-empty-inner">
859 <strong>No dependencies detected</strong>
860 <span>Push code with a manifest to see insights.</span>
861 </div>
862 </div>
863 ) : (
864 <div class="insights-list">
865 {graph.dependencies.map((dep) => (
866 <div
867 class={"insights-card" + (dep.totalImports === 0 ? " is-warn" : "")}
868 >
869 <div class="insights-card-row">
870 <div class="insights-card-main">
871 <h4 class="insights-card-title">
872 {dep.name}{" "}
873 <span
874 style="font-size:12px;font-weight:500;color:var(--text-muted);font-family:var(--font-mono);margin-left:6px"
875 >
876 {dep.version}
877 </span>
878 {dep.isDevDep && (
879 <span class="insights-dep-badge" style="margin-left:8px">
880 dev
881 </span>
882 )}
883 </h4>
16b325cClaude884 </div>
c9188afClaude885 <div class="insights-card-meta">
886 {dep.totalImports === 0 ? (
887 <span class="insights-dep-unused">unused</span>
888 ) : (
889 `${dep.totalImports} import${dep.totalImports !== 1 ? "s" : ""}`
890 )}
891 </div>
892 </div>
893 {dep.usedIn.length > 0 && (
894 <div class="insights-dep-uses">
895 {dep.usedIn.slice(0, 3).map((usage) => (
896 <div>
897 <a href={`/${owner}/${repo}/blob/${ref}/${usage.file}`}>
898 {usage.file}:{usage.line}
899 </a>
900 <code style="margin-left:8px">
901 {"{ "}
902 {usage.importedSymbols.join(", ")}
903 {" }"}
904 </code>
905 </div>
906 ))}
907 {dep.usedIn.length > 3 && (
908 <div>+{dep.usedIn.length - 3} more</div>
909 )}
16b325cClaude910 </div>
911 )}
912 </div>
c9188afClaude913 ))}
16b325cClaude914 </div>
c9188afClaude915 )}
16b325cClaude916 </div>
c9188afClaude917 <style dangerouslySetInnerHTML={{ __html: styles }} />
16b325cClaude918 </Layout>
919 );
920});
921
922// ─── ROLLBACK ────────────────────────────────────────────────
923
924insights.post("/:owner/:repo/rollback", requireAuth, async (c) => {
925 const { owner, repo } = c.req.param();
926 const user = c.get("user")!;
927 const body = await c.req.parseBody();
928 const branch = String(body.branch || "main");
929 const targetSha = String(body.target_sha || "");
930
931 if (!targetSha) {
932 return c.redirect(`/${owner}/${repo}`);
933 }
934
935 const result = await executeRollback(owner, repo, branch, targetSha);
936 if (!result.success) {
937 return c.redirect(`/${owner}/${repo}?error=${encodeURIComponent(result.error || "Rollback failed")}`);
938 }
939
940 return c.redirect(`/${owner}/${repo}/commit/${result.newSha}`);
941});
942
943export default insights;