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.tsxBlame291 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.
6 */
7
8import { Hono } from "hono";
9import { Layout } from "../views/layout";
10import { RepoHeader, RepoNav } from "../views/components";
11import {
12 getFileTimeline,
13 getFunctionTimeline,
14 detectCoupledFiles,
15 getRepoStory,
16} from "../lib/timetravel";
17import {
18 buildImportGraph,
19 analyzeUpgradeImpact,
20 findUnusedDeps,
21} from "../lib/depimpact";
22import { findRollbackTarget, executeRollback } from "../lib/rollback";
23import {
24 repoExists,
25 getDefaultBranch,
26 listBranches,
27} from "../git/repository";
28import { softAuth, requireAuth } from "../middleware/auth";
29import type { AuthEnv } from "../middleware/auth";
30
31const insights = new Hono<AuthEnv>();
32
33insights.use("*", softAuth);
34
35// ─── TIME TRAVEL ─────────────────────────────────────────────
36
37// File evolution timeline
38insights.get("/:owner/:repo/timeline/:ref{.+$}", async (c) => {
39 const { owner, repo } = c.req.param();
40 const user = c.get("user");
41 const refAndPath = c.req.param("ref");
42
43 const branches = await listBranches(owner, repo);
44 let ref = "";
45 let filePath = "";
46
47 for (const branch of branches) {
48 if (refAndPath.startsWith(branch + "/")) {
49 ref = branch;
50 filePath = refAndPath.slice(branch.length + 1);
51 break;
52 }
53 }
54 if (!ref) {
55 const idx = refAndPath.indexOf("/");
56 if (idx === -1) return c.notFound();
57 ref = refAndPath.slice(0, idx);
58 filePath = refAndPath.slice(idx + 1);
59 }
60
61 const timeline = await getFileTimeline(owner, repo, ref, filePath);
62 if (!timeline) return c.notFound();
63
64 return c.html(
65 <Layout title={`Timeline: ${filePath} — ${owner}/${repo}`} user={user}>
66 <RepoHeader owner={owner} repo={repo} />
67 <RepoNav owner={owner} repo={repo} active="code" />
68 <h2 style="margin-bottom: 4px">
69 Time Travel: {filePath}
70 </h2>
71 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 20px">
72 {timeline.totalRevisions} revision{timeline.totalRevisions !== 1 ? "s" : ""} | First seen{" "}
73 {new Date(timeline.firstSeen.date).toLocaleDateString()} by {timeline.firstSeen.author}
74 </p>
75
76 <div class="timeline">
77 {timeline.revisions.map((rev, i) => (
78 <div class="timeline-item">
79 <div class="timeline-dot" />
80 <div class="timeline-content">
81 <div style="display: flex; justify-content: space-between; align-items: start">
82 <div>
83 <a
84 href={`/${owner}/${repo}/commit/${rev.sha}`}
85 style="font-weight: 600; font-size: 14px"
86 >
87 {rev.message}
88 </a>
89 <div style="font-size: 12px; color: var(--text-muted); margin-top: 2px">
90 {rev.author} — {new Date(rev.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
91 </div>
92 </div>
93 <div style="text-align: right; font-size: 12px; white-space: nowrap">
94 <span class="stat-add">+{rev.linesAdded}</span>{" "}
95 <span class="stat-del">-{rev.linesRemoved}</span>
96 <div style="color: var(--text-muted)">{rev.sizeAfter} bytes</div>
97 </div>
98 </div>
99 </div>
100 </div>
101 ))}
102 </div>
103 </Layout>
104 );
105});
106
107// Coupled files analysis
108insights.get("/:owner/:repo/coupling", async (c) => {
109 const { owner, repo } = c.req.param();
110 const user = c.get("user");
111
112 if (!(await repoExists(owner, repo))) return c.notFound();
113 const ref = (await getDefaultBranch(owner, repo)) || "main";
114
115 const coupled = await detectCoupledFiles(owner, repo, ref);
116 const story = await getRepoStory(owner, repo, ref);
117 const milestones = story.filter((s) => s.significance !== "normal").slice(0, 20);
118
119 return c.html(
120 <Layout title={`Insights — ${owner}/${repo}`} user={user}>
121 <RepoHeader owner={owner} repo={repo} />
122 <RepoNav owner={owner} repo={repo} active="code" />
123 <h2 style="margin-bottom: 20px">Code Insights</h2>
124
125 <h3 style="margin-bottom: 12px">Coupled Files</h3>
126 <p style="color: var(--text-muted); font-size: 13px; margin-bottom: 12px">
127 Files that change together frequently — potential architectural coupling
128 </p>
129 {coupled.length === 0 ? (
130 <p style="color: var(--text-muted)">No strong coupling detected.</p>
131 ) : (
132 <div class="issue-list" style="margin-bottom: 32px">
133 {coupled.map((pair) => (
134 <div class="issue-item">
135 <div style="font-size: 13px; font-family: var(--font-mono)">
136 <a href={`/${owner}/${repo}/blob/${ref}/${pair.files[0]}`}>
137 {pair.files[0]}
138 </a>
139 <span style="color: var(--text-muted); margin: 0 8px">+</span>
140 <a href={`/${owner}/${repo}/blob/${ref}/${pair.files[1]}`}>
141 {pair.files[1]}
142 </a>
143 </div>
144 <div style="font-size: 12px; color: var(--text-muted); white-space: nowrap">
145 {pair.cochanges} co-changes ({pair.percentage}%)
146 </div>
147 </div>
148 ))}
149 </div>
150 )}
151
152 <h3 style="margin-bottom: 12px">Project Milestones</h3>
153 {milestones.length === 0 ? (
154 <p style="color: var(--text-muted)">No milestones detected yet.</p>
155 ) : (
156 <div class="timeline">
157 {milestones.map((m) => (
158 <div class="timeline-item">
159 <div
160 class="timeline-dot"
161 style={m.significance === "milestone" ? "background: var(--green); width: 12px; height: 12px" : ""}
162 />
163 <div class="timeline-content">
164 <a href={`/${owner}/${repo}/commit/${m.sha}`} style="font-weight: 600; font-size: 14px">
165 {m.message}
166 </a>
167 <div style="font-size: 12px; color: var(--text-muted)">
168 {m.author} — {new Date(m.date).toLocaleDateString()} |{" "}
169 <span class="stat-add">+{m.stats.additions}</span>{" "}
170 <span class="stat-del">-{m.stats.deletions}</span> in {m.stats.files} files
171 </div>
172 </div>
173 </div>
174 ))}
175 </div>
176 )}
177 </Layout>
178 );
179});
180
181// ─── DEPENDENCY INSIGHTS ─────────────────────────────────────
182
183insights.get("/:owner/:repo/dependencies", async (c) => {
184 const { owner, repo } = c.req.param();
185 const user = c.get("user");
186
187 if (!(await repoExists(owner, repo))) return c.notFound();
188 const ref = (await getDefaultBranch(owner, repo)) || "main";
189
190 const graph = await buildImportGraph(owner, repo, ref);
191 const unused = findUnusedDeps(graph);
192
193 return c.html(
194 <Layout title={`Dependencies — ${owner}/${repo}`} user={user}>
195 <RepoHeader owner={owner} repo={repo} />
196 <RepoNav owner={owner} repo={repo} active="code" />
197 <h2 style="margin-bottom: 4px">Dependency Intelligence</h2>
198 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 20px">
199 {graph.externalDependencies} dependencies | {graph.internalModules} source files
200 {graph.circularDeps.length > 0 && (
201 <span style="color: var(--red)">
202 {" "}| {graph.circularDeps.length} circular dependency chain{graph.circularDeps.length !== 1 ? "s" : ""}
203 </span>
204 )}
205 </p>
206
207 {unused.length > 0 && (
208 <div style="background: rgba(248, 81, 73, 0.1); border: 1px solid var(--red); border-radius: var(--radius); padding: 12px 16px; margin-bottom: 20px">
209 <strong style="color: var(--red)">Unused dependencies:</strong>{" "}
210 <span style="font-family: var(--font-mono); font-size: 13px">
211 {unused.join(", ")}
212 </span>
213 <div style="font-size: 12px; color: var(--text-muted); margin-top: 4px">
214 These are installed but never imported. Removing them reduces install time and attack surface.
215 </div>
216 </div>
217 )}
218
219 <div class="issue-list">
220 {graph.dependencies.map((dep) => (
221 <div class="issue-item" style="flex-direction: column; align-items: stretch">
222 <div style="display: flex; justify-content: space-between; align-items: center">
223 <div>
224 <strong style="font-size: 14px">{dep.name}</strong>
225 <span style="margin-left: 8px; font-size: 12px; color: var(--text-muted)">
226 {dep.version}
227 </span>
228 {dep.isDevDep && (
229 <span class="badge" style="margin-left: 8px; font-size: 10px">
230 dev
231 </span>
232 )}
233 </div>
234 <span style="font-size: 13px; color: var(--text-muted)">
235 {dep.totalImports === 0 ? (
236 <span style="color: var(--red)">unused</span>
237 ) : (
238 `${dep.totalImports} import${dep.totalImports !== 1 ? "s" : ""}`
239 )}
240 </span>
241 </div>
242 {dep.usedIn.length > 0 && (
243 <div style="margin-top: 8px; font-size: 12px">
244 {dep.usedIn.slice(0, 3).map((usage) => (
245 <div style="color: var(--text-muted); font-family: var(--font-mono); margin-top: 2px">
246 <a href={`/${owner}/${repo}/blob/${ref}/${usage.file}`}>
247 {usage.file}:{usage.line}
248 </a>
249 <span style="margin-left: 8px">
250 {"{"}
251 {usage.importedSymbols.join(", ")}
252 {"}"}
253 </span>
254 </div>
255 ))}
256 {dep.usedIn.length > 3 && (
257 <div style="color: var(--text-muted); margin-top: 2px">
258 +{dep.usedIn.length - 3} more
259 </div>
260 )}
261 </div>
262 )}
263 </div>
264 ))}
265 </div>
266 </Layout>
267 );
268});
269
270// ─── ROLLBACK ────────────────────────────────────────────────
271
272insights.post("/:owner/:repo/rollback", requireAuth, async (c) => {
273 const { owner, repo } = c.req.param();
274 const user = c.get("user")!;
275 const body = await c.req.parseBody();
276 const branch = String(body.branch || "main");
277 const targetSha = String(body.target_sha || "");
278
279 if (!targetSha) {
280 return c.redirect(`/${owner}/${repo}`);
281 }
282
283 const result = await executeRollback(owner, repo, branch, targetSha);
284 if (!result.success) {
285 return c.redirect(`/${owner}/${repo}?error=${encodeURIComponent(result.error || "Rollback failed")}`);
286 }
287
288 return c.redirect(`/${owner}/${repo}/commit/${result.newSha}`);
289});
290
291export default insights;