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

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.tsxBlame842 lines · 1 contributor
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)
16b325cClaude499insights.get("/:owner/:repo/coupling", async (c) => {
500 const { owner, repo } = c.req.param();
501 const user = c.get("user");
502
503 if (!(await repoExists(owner, repo))) return c.notFound();
504 const ref = (await getDefaultBranch(owner, repo)) || "main";
505
506 const coupled = await detectCoupledFiles(owner, repo, ref);
507 const story = await getRepoStory(owner, repo, ref);
508 const milestones = story.filter((s) => s.significance !== "normal").slice(0, 20);
509
c9188afClaude510 // Stat-card values derived from the data we already fetched. These line up
511 // with the spec (commits, files touched, milestones, coupled-pairs) so the
512 // grid renders even when the repo is empty.
513 const totalCommits = story.length;
514 const totalAdditions = story.reduce(
515 (n, s) => n + (s.stats?.additions || 0),
516 0
517 );
518 const totalDeletions = story.reduce(
519 (n, s) => n + (s.stats?.deletions || 0),
520 0
521 );
522
16b325cClaude523 return c.html(
524 <Layout title={`Insights — ${owner}/${repo}`} user={user}>
525 <RepoHeader owner={owner} repo={repo} />
c9188afClaude526 <RepoNav owner={owner} repo={repo} active="insights" />
527
528 <div class="insights-wrap">
529 <section class="insights-hero">
530 <div class="insights-hero-orb" aria-hidden="true" />
531 <div class="insights-hero-inner">
532 <div class="insights-eyebrow">
533 <span class="insights-eyebrow-dot" aria-hidden="true" />
534 Insights · {owner}/{repo}
16b325cClaude535 </div>
c9188afClaude536 <h2 class="insights-title">
537 <span class="insights-title-grad">Code intelligence.</span>
538 </h2>
539 <p class="insights-sub">
540 File coupling, milestone history, and contributor signals — the
541 kind of intelligence GitHub doesn't ship.
542 </p>
543 </div>
544 </section>
545
546 <div class="insights-stats">
547 <div class="insights-stat">
548 <div class="insights-stat-label">Commits indexed</div>
549 <div class="insights-stat-value">{totalCommits.toLocaleString()}</div>
550 <div class="insights-stat-hint">On {ref}</div>
551 </div>
552 <div class="insights-stat">
553 <div class="insights-stat-label">Lines added</div>
554 <div class="insights-stat-value">{totalAdditions.toLocaleString()}</div>
555 <div class="insights-stat-trend is-up">
556 <span class="arrow" aria-hidden="true">▲</span>
557 across history
558 </div>
559 </div>
560 <div class="insights-stat">
561 <div class="insights-stat-label">Lines removed</div>
562 <div class="insights-stat-value">{totalDeletions.toLocaleString()}</div>
563 <div class="insights-stat-trend is-down">
564 <span class="arrow" aria-hidden="true">▼</span>
565 across history
566 </div>
567 </div>
568 <div class="insights-stat">
569 <div class="insights-stat-label">Milestones</div>
570 <div class="insights-stat-value">{milestones.length}</div>
571 <div class="insights-stat-hint">Significant commits</div>
572 </div>
16b325cClaude573 </div>
c9188afClaude574
575 <div class="insights-section-head">
576 <h3 class="insights-section-title">Coupled files</h3>
577 <span class="insights-section-sub">{coupled.length} pair{coupled.length === 1 ? "" : "s"}</span>
578 </div>
579 <p class="insights-blurb">
580 Files that change together frequently — potential architectural
581 coupling worth refactoring.
582 </p>
583 {coupled.length === 0 ? (
584 <div class="insights-empty">
585 <div class="insights-empty-inner">
586 <strong>No strong coupling detected</strong>
587 <span>Push more code to see relationships emerge.</span>
588 </div>
589 </div>
590 ) : (
591 <div class="insights-list">
592 {coupled.map((pair) => (
593 <div class="insights-card">
594 <div class="insights-card-row">
595 <div class="insights-card-main">
596 <div class="insights-coupled">
597 <a href={`/${owner}/${repo}/blob/${ref}/${pair.files[0]}`}>
598 {pair.files[0]}
599 </a>
600 <span class="plus" aria-hidden="true">+</span>
601 <a href={`/${owner}/${repo}/blob/${ref}/${pair.files[1]}`}>
602 {pair.files[1]}
603 </a>
604 </div>
605 </div>
606 <div class="insights-card-meta">
607 {pair.cochanges} co-changes ({pair.percentage}%)
608 </div>
16b325cClaude609 </div>
610 </div>
c9188afClaude611 ))}
612 </div>
613 )}
614
615 <div class="insights-section-head">
616 <h3 class="insights-section-title">Project milestones</h3>
617 <span class="insights-section-sub">{milestones.length} milestone{milestones.length === 1 ? "" : "s"}</span>
16b325cClaude618 </div>
c9188afClaude619 {milestones.length === 0 ? (
620 <div class="insights-empty">
621 <div class="insights-empty-inner">
622 <strong>No milestones detected yet</strong>
623 <span>Push code to see insights emerge.</span>
624 </div>
625 </div>
626 ) : (
627 <ul class="insights-timeline">
628 {milestones.map((m) => (
629 <li class="insights-timeline-item">
630 <span
631 class={
632 "insights-timeline-dot" +
633 (m.significance === "milestone" ? " is-milestone" : "")
634 }
635 aria-hidden="true"
636 />
637 <div class="insights-timeline-card">
638 <div class="insights-card-row">
639 <div class="insights-card-main">
640 <h4 class="insights-card-title">
641 <a href={`/${owner}/${repo}/commit/${m.sha}`}>
642 {m.message}
643 </a>
644 </h4>
645 <p class="insights-card-sub">
646 {m.author} —{" "}
647 {new Date(m.date).toLocaleDateString()}
648 </p>
649 </div>
650 <div class="insights-card-meta">
651 <span class="add">+{m.stats.additions}</span>{" "}
652 <span class="del">-{m.stats.deletions}</span>
653 <div style="color: var(--text-muted)">
654 {m.stats.files} file{m.stats.files === 1 ? "" : "s"}
655 </div>
656 </div>
657 </div>
658 </div>
659 </li>
660 ))}
661 </ul>
662 )}
663 </div>
664 <style dangerouslySetInnerHTML={{ __html: styles }} />
16b325cClaude665 </Layout>
666 );
667});
668
669// ─── DEPENDENCY INSIGHTS ─────────────────────────────────────
670
671insights.get("/:owner/:repo/dependencies", async (c) => {
672 const { owner, repo } = c.req.param();
673 const user = c.get("user");
674
675 if (!(await repoExists(owner, repo))) return c.notFound();
676 const ref = (await getDefaultBranch(owner, repo)) || "main";
677
678 const graph = await buildImportGraph(owner, repo, ref);
679 const unused = findUnusedDeps(graph);
680
681 return c.html(
682 <Layout title={`Dependencies — ${owner}/${repo}`} user={user}>
683 <RepoHeader owner={owner} repo={repo} />
684 <RepoNav owner={owner} repo={repo} active="code" />
c9188afClaude685
686 <div class="insights-wrap">
687 <section class="insights-hero">
688 <div class="insights-hero-orb" aria-hidden="true" />
689 <div class="insights-hero-inner">
690 <div class="insights-eyebrow">
691 <span class="insights-eyebrow-dot" aria-hidden="true" />
692 Dependency intelligence · {owner}/{repo}
693 </div>
694 <h2 class="insights-title">
695 <span class="insights-title-grad">What you depend on.</span>
696 </h2>
697 <p class="insights-sub">
698 Static import graph across the repo — every package, how it's
699 used, and which ones are dead weight.
700 </p>
701 </div>
702 </section>
703
704 <div class="insights-stats">
705 <div class="insights-stat">
706 <div class="insights-stat-label">Dependencies</div>
707 <div class="insights-stat-value">{graph.externalDependencies}</div>
708 <div class="insights-stat-hint">External packages</div>
709 </div>
710 <div class="insights-stat">
711 <div class="insights-stat-label">Source files</div>
712 <div class="insights-stat-value">{graph.internalModules}</div>
713 <div class="insights-stat-hint">Internal modules</div>
714 </div>
715 <div class={"insights-stat"}>
716 <div class="insights-stat-label">Unused</div>
717 <div
718 class={
719 "insights-stat-value" + (unused.length > 0 ? " is-warn" : "")
720 }
721 >
722 {unused.length}
723 </div>
724 <div class="insights-stat-hint">Installed but never imported</div>
725 </div>
726 <div class="insights-stat">
727 <div class="insights-stat-label">Circular chains</div>
728 <div
729 class={
730 "insights-stat-value" +
731 (graph.circularDeps.length > 0 ? " is-warn" : "")
732 }
733 >
734 {graph.circularDeps.length}
735 </div>
736 <div class="insights-stat-hint">Cycles in the import graph</div>
16b325cClaude737 </div>
738 </div>
c9188afClaude739
740 {unused.length > 0 && (
741 <div class="insights-banner">
742 <strong>Unused dependencies:</strong>{" "}
743 <code>{unused.join(", ")}</code>
744 <div class="insights-banner-hint">
745 These are installed but never imported. Removing them reduces
746 install time and attack surface.
16b325cClaude747 </div>
c9188afClaude748 </div>
749 )}
750
751 <div class="insights-section-head">
752 <h3 class="insights-section-title">Packages</h3>
753 <span class="insights-section-sub">{graph.dependencies.length} total</span>
754 </div>
755 {graph.dependencies.length === 0 ? (
756 <div class="insights-empty">
757 <div class="insights-empty-inner">
758 <strong>No dependencies detected</strong>
759 <span>Push code with a manifest to see insights.</span>
760 </div>
761 </div>
762 ) : (
763 <div class="insights-list">
764 {graph.dependencies.map((dep) => (
765 <div
766 class={"insights-card" + (dep.totalImports === 0 ? " is-warn" : "")}
767 >
768 <div class="insights-card-row">
769 <div class="insights-card-main">
770 <h4 class="insights-card-title">
771 {dep.name}{" "}
772 <span
773 style="font-size:12px;font-weight:500;color:var(--text-muted);font-family:var(--font-mono);margin-left:6px"
774 >
775 {dep.version}
776 </span>
777 {dep.isDevDep && (
778 <span class="insights-dep-badge" style="margin-left:8px">
779 dev
780 </span>
781 )}
782 </h4>
16b325cClaude783 </div>
c9188afClaude784 <div class="insights-card-meta">
785 {dep.totalImports === 0 ? (
786 <span class="insights-dep-unused">unused</span>
787 ) : (
788 `${dep.totalImports} import${dep.totalImports !== 1 ? "s" : ""}`
789 )}
790 </div>
791 </div>
792 {dep.usedIn.length > 0 && (
793 <div class="insights-dep-uses">
794 {dep.usedIn.slice(0, 3).map((usage) => (
795 <div>
796 <a href={`/${owner}/${repo}/blob/${ref}/${usage.file}`}>
797 {usage.file}:{usage.line}
798 </a>
799 <code style="margin-left:8px">
800 {"{ "}
801 {usage.importedSymbols.join(", ")}
802 {" }"}
803 </code>
804 </div>
805 ))}
806 {dep.usedIn.length > 3 && (
807 <div>+{dep.usedIn.length - 3} more</div>
808 )}
16b325cClaude809 </div>
810 )}
811 </div>
c9188afClaude812 ))}
16b325cClaude813 </div>
c9188afClaude814 )}
16b325cClaude815 </div>
c9188afClaude816 <style dangerouslySetInnerHTML={{ __html: styles }} />
16b325cClaude817 </Layout>
818 );
819});
820
821// ─── ROLLBACK ────────────────────────────────────────────────
822
823insights.post("/:owner/:repo/rollback", requireAuth, async (c) => {
824 const { owner, repo } = c.req.param();
825 const user = c.get("user")!;
826 const body = await c.req.parseBody();
827 const branch = String(body.branch || "main");
828 const targetSha = String(body.target_sha || "");
829
830 if (!targetSha) {
831 return c.redirect(`/${owner}/${repo}`);
832 }
833
834 const result = await executeRollback(owner, repo, branch, targetSha);
835 if (!result.success) {
836 return c.redirect(`/${owner}/${repo}?error=${encodeURIComponent(result.error || "Rollback failed")}`);
837 }
838
839 return c.redirect(`/${owner}/${repo}/commit/${result.newSha}`);
840});
841
842export default insights;