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

explain.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.

explain.tsxBlame923 lines · 1 contributor
0a69faaClaude1/**
2 * "Explain This Repo" — AI-powered codebase analysis dashboard.
3 *
4 * Routes:
5 * GET /:owner/:repo/explain — landing / cached result
6 * POST /:owner/:repo/explain — trigger analysis, redirect to job page
7 * GET /:owner/:repo/explain/:jobId — progress polling / result page
8 * GET /:owner/:repo/explain/:jobId/raw — JSON result
9 *
10 * Analysis runs asynchronously in the background. The result page
11 * polls with a meta-refresh every 3 seconds while status=running.
12 * Completed results are cached in `repo_explain_cache` (DB) and served
13 * directly on subsequent visits.
14 */
15
16import { Hono } from "hono";
17import { html } from "hono/html";
18import { eq, and } from "drizzle-orm";
19import { db } from "../db";
20import { repositories, users } from "../db/schema";
21import { Layout } from "../views/layout";
22import { RepoHeader } from "../views/components";
23import { softAuth, requireAuth } from "../middleware/auth";
24import type { AuthEnv } from "../middleware/auth";
25import { renderMarkdown } from "../lib/markdown";
26import {
27 explainJobs,
28 startExplainJob,
29 getCachedExplainResult,
30 resolveRepoForExplain,
31} from "../lib/repo-explainer";
32import type { ExplainJobResult, ExplainJob } from "../lib/repo-explainer";
33
34const explainRoutes = new Hono<AuthEnv>();
35
36// ---------------------------------------------------------------------------
37// Scoped CSS
38// ---------------------------------------------------------------------------
39
40const STYLES = `
41 .explain-wrap { max-width: 1040px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
42
43 /* ── Hero ── */
44 .explain-hero {
45 position: relative;
46 margin-bottom: var(--space-5);
47 padding: var(--space-5) var(--space-6);
48 background: var(--bg-elevated);
49 border: 1px solid var(--border);
50 border-radius: 16px;
51 overflow: hidden;
52 }
53 .explain-hero::before {
54 content: '';
55 position: absolute;
56 top: 0; left: 0; right: 0;
57 height: 2px;
6fd5915Claude58 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
0a69faaClaude59 opacity: 0.7;
60 pointer-events: none;
61 }
62 .explain-hero-orb {
63 position: absolute;
64 inset: -20% -10% auto auto;
65 width: 460px; height: 460px;
6fd5915Claude66 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.09) 45%, transparent 70%);
0a69faaClaude67 filter: blur(90px);
68 opacity: 0.7;
69 pointer-events: none;
70 z-index: 0;
71 }
72 .explain-hero-inner { position: relative; z-index: 1; }
73 .explain-eyebrow {
74 font-size: 12px;
75 color: var(--text-muted);
76 margin-bottom: var(--space-2);
77 letter-spacing: 0.02em;
78 display: inline-flex; align-items: center; gap: 8px;
79 }
80 .explain-eyebrow .pill {
81 display: inline-flex; align-items: center; justify-content: center;
82 width: 18px; height: 18px; border-radius: 6px;
6fd5915Claude83 background: rgba(91,110,232,0.14);
84 color: #5b6ee8;
85 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.35);
0a69faaClaude86 }
87 .explain-title {
88 font-size: clamp(28px, 4vw, 40px);
89 font-family: var(--font-display);
90 font-weight: 800;
91 letter-spacing: -0.028em;
92 line-height: 1.05;
93 margin: 0 0 var(--space-2);
94 color: var(--text-strong);
95 }
96 .explain-title-grad {
6fd5915Claude97 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
0a69faaClaude98 -webkit-background-clip: text;
99 background-clip: text;
100 -webkit-text-fill-color: transparent;
101 color: transparent;
102 }
103 .explain-sub {
104 font-size: 15px; color: var(--text-muted);
105 margin: 0 0 var(--space-4);
106 line-height: 1.55; max-width: 620px;
107 }
108 .explain-trigger-btn {
109 display: inline-flex; align-items: center; gap: 8px;
110 padding: 11px 22px;
6fd5915Claude111 background: linear-gradient(135deg, #5b6ee8 0%, #5f8fa0 100%);
0a69faaClaude112 color: #fff;
113 border: 1px solid transparent;
114 border-radius: 10px;
115 font-size: 14px; font-weight: 600;
116 text-decoration: none; cursor: pointer;
6fd5915Claude117 box-shadow: 0 6px 18px -4px rgba(91,110,232,0.45), inset 0 1px 0 rgba(255,255,255,0.16);
0a69faaClaude118 font-family: inherit;
119 transition: transform 120ms ease, box-shadow 120ms ease;
120 }
121 .explain-trigger-btn:hover {
122 transform: translateY(-1px);
6fd5915Claude123 box-shadow: 0 10px 24px -6px rgba(91,110,232,0.55), inset 0 1px 0 rgba(255,255,255,0.20);
0a69faaClaude124 }
125 .explain-trigger-btn svg { display: block; }
126
127 /* ── Progress / running state ── */
128 .explain-progress {
129 display: flex; align-items: flex-start; gap: var(--space-4);
130 padding: var(--space-5) var(--space-6);
131 background: var(--bg-elevated);
132 border: 1px solid var(--border);
133 border-radius: 14px;
134 margin-bottom: var(--space-5);
135 }
136 .explain-spinner {
137 width: 36px; height: 36px; flex-shrink: 0;
6fd5915Claude138 border: 3px solid rgba(91,110,232,0.18);
139 border-top-color: #5b6ee8;
0a69faaClaude140 border-radius: 50%;
141 animation: explain-spin 0.8s linear infinite;
142 }
143 @keyframes explain-spin {
144 to { transform: rotate(360deg); }
145 }
146 .explain-progress-text h3 {
147 margin: 0 0 4px;
148 font-size: 15px; font-weight: 700; color: var(--text-strong);
149 }
150 .explain-progress-text p {
151 margin: 0; font-size: 13px; color: var(--text-muted); line-height: 1.5;
152 }
153
154 /* ── Error state ── */
155 .explain-error {
156 padding: var(--space-5);
157 background: rgba(239,68,68,0.07);
158 border: 1px solid rgba(239,68,68,0.25);
159 border-radius: 14px;
160 color: #ef4444;
161 margin-bottom: var(--space-5);
162 }
163 .explain-error h3 { margin: 0 0 6px; font-size: 15px; }
164 .explain-error p { margin: 0; font-size: 13px; opacity: 0.9; }
165
166 /* ── Result dashboard ── */
167 .explain-dashboard {
168 display: grid;
169 grid-template-columns: 1fr 1fr;
170 gap: var(--space-4);
171 margin-bottom: var(--space-5);
172 }
173 @media (max-width: 680px) {
174 .explain-dashboard { grid-template-columns: 1fr; }
175 }
176
177 .explain-card {
178 background: var(--bg-elevated);
179 border: 1px solid var(--border);
180 border-radius: 14px;
181 overflow: hidden;
182 }
183 .explain-card-full { grid-column: 1 / -1; }
184 .explain-card-head {
185 display: flex; align-items: center; gap: 10px;
186 padding: 12px 16px;
187 background: var(--bg-tertiary);
188 border-bottom: 1px solid var(--border);
189 }
190 .explain-card-icon {
191 display: flex; align-items: center; justify-content: center;
192 width: 22px; height: 22px; border-radius: 6px;
6fd5915Claude193 background: rgba(91,110,232,0.12);
194 color: #5b6ee8;
0a69faaClaude195 flex-shrink: 0;
196 }
197 .explain-card-title {
198 font-size: 13px; font-weight: 700;
199 color: var(--text-strong);
200 margin: 0;
201 letter-spacing: -0.01em;
202 }
203 .explain-card-body { padding: 16px; }
204
205 /* Summary */
206 .explain-summary-text {
207 font-size: 14.5px; line-height: 1.65; color: var(--text);
208 margin: 0;
209 }
210
211 /* Health score */
212 .explain-health-score {
213 display: inline-flex; align-items: center; gap: 8px;
214 padding: 8px 14px;
215 border-radius: 10px;
216 font-size: 15px; font-weight: 700;
217 margin-bottom: 10px;
218 }
219 .explain-health-score.elite { background: rgba(52,211,153,0.12); color: #10b981; border: 1px solid rgba(52,211,153,0.28); }
220 .explain-health-score.strong { background: rgba(96,165,250,0.12); color: #3b82f6; border: 1px solid rgba(96,165,250,0.28); }
221 .explain-health-score.improving { background: rgba(251,191,36,0.12); color: #f59e0b; border: 1px solid rgba(251,191,36,0.28); }
222 .explain-health-score.needs-attention { background: rgba(239,68,68,0.12); color: #ef4444; border: 1px solid rgba(239,68,68,0.28); }
223 .explain-health-dot {
224 width: 8px; height: 8px; border-radius: 50%; background: currentColor;
225 }
226 .explain-health-desc { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; margin: 0; }
227
228 /* Tech stack chips */
229 .explain-chips {
230 display: flex; flex-wrap: wrap; gap: 8px;
231 }
232 .explain-chip {
233 display: inline-flex; align-items: center;
234 padding: 4px 10px;
235 background: var(--bg-tertiary);
236 border: 1px solid var(--border);
237 border-radius: 9999px;
238 font-size: 12px; font-weight: 600;
239 color: var(--text);
240 }
241
242 /* Architecture — rendered markdown inside dark card */
243 .explain-arch-body {
244 font-size: 13.5px; line-height: 1.65; color: var(--text);
245 }
246 .explain-arch-body .markdown-body {
247 color: var(--text);
248 background: transparent;
249 font-size: 13.5px;
250 }
251 .explain-arch-body .markdown-body h1,
252 .explain-arch-body .markdown-body h2,
253 .explain-arch-body .markdown-body h3 {
254 color: var(--text-strong);
255 border-bottom-color: var(--border);
256 }
257 .explain-arch-body .markdown-body a { color: var(--link); }
258 .explain-arch-body .markdown-body code {
259 background: var(--bg-tertiary);
260 color: var(--text);
261 padding: 1px 5px;
262 border-radius: 4px;
263 font-family: var(--font-mono);
264 font-size: 12px;
265 }
266 .explain-arch-body .markdown-body pre {
267 background: var(--bg);
268 border: 1px solid var(--border);
269 border-radius: 8px;
270 padding: 12px 14px;
271 overflow-x: auto;
272 }
273 .explain-arch-body .markdown-body pre code {
274 background: transparent; color: inherit; padding: 0;
275 }
276
277 /* Entry points table */
278 .explain-ep-table {
279 width: 100%; border-collapse: collapse;
280 font-size: 13px;
281 }
282 .explain-ep-table th {
283 text-align: left; padding: 6px 10px;
284 font-size: 11px; font-weight: 700;
285 color: var(--text-muted);
286 text-transform: uppercase; letter-spacing: 0.04em;
287 border-bottom: 1px solid var(--border);
288 }
289 .explain-ep-table td {
290 padding: 8px 10px;
291 border-bottom: 1px solid var(--border);
292 vertical-align: top;
293 color: var(--text);
294 }
295 .explain-ep-table tr:last-child td { border-bottom: none; }
296 .explain-ep-table td:first-child {
297 font-family: var(--font-mono); font-size: 12px;
298 color: var(--text-strong);
299 white-space: nowrap;
300 }
301 .explain-ep-table a { color: var(--link); text-decoration: none; }
302 .explain-ep-table a:hover { text-decoration: underline; }
303
304 /* Getting started — rendered markdown */
305 .explain-gs-body .markdown-body {
306 color: var(--text);
307 background: transparent;
308 font-size: 13.5px;
309 line-height: 1.65;
310 }
311 .explain-gs-body .markdown-body code {
312 background: var(--bg-tertiary);
313 color: var(--text);
314 padding: 1px 5px;
315 border-radius: 4px;
316 font-family: var(--font-mono);
317 font-size: 12px;
318 }
319 .explain-gs-body .markdown-body pre {
320 background: var(--bg);
321 border: 1px solid var(--border);
322 border-radius: 8px;
323 padding: 12px 14px;
324 overflow-x: auto;
325 }
326 .explain-gs-body .markdown-body pre code {
327 background: transparent; color: inherit; padding: 0;
328 }
329
330 /* Suggested issues cards */
331 .explain-issues { display: flex; flex-direction: column; gap: 10px; }
332 .explain-issue-card {
333 padding: 12px 14px;
334 background: var(--bg);
335 border: 1px solid var(--border);
336 border-radius: 10px;
337 }
338 .explain-issue-card h4 {
339 margin: 0 0 4px;
340 font-size: 13.5px; font-weight: 700; color: var(--text-strong);
341 }
342 .explain-issue-card p {
343 margin: 0 0 10px;
344 font-size: 12.5px; color: var(--text-muted); line-height: 1.55;
345 }
346 .explain-issue-btn {
347 display: inline-flex; align-items: center; gap: 6px;
348 padding: 5px 12px;
349 background: var(--bg-elevated);
350 border: 1px solid var(--border);
351 border-radius: 8px;
352 font-size: 12px; font-weight: 600;
353 color: var(--text);
354 text-decoration: none;
355 cursor: pointer; font-family: inherit;
356 transition: background 100ms ease, border-color 100ms ease;
357 }
358 .explain-issue-btn:hover {
359 background: var(--bg-tertiary);
360 border-color: var(--border-strong, var(--border));
361 }
362
363 /* Share + actions bar */
364 .explain-actions-bar {
365 display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
366 margin-bottom: var(--space-5);
367 }
368 .explain-share-btn {
369 display: inline-flex; align-items: center; gap: 6px;
370 padding: 8px 14px;
371 background: var(--bg-elevated);
372 border: 1px solid var(--border);
373 border-radius: 10px;
374 font-size: 13px; font-weight: 600;
375 color: var(--text); text-decoration: none;
376 cursor: pointer; font-family: inherit;
377 transition: background 100ms ease;
378 }
379 .explain-share-btn:hover { background: var(--bg-tertiary); }
380
381 /* Powered-by */
382 .explain-poweredby {
383 margin-top: var(--space-5);
384 text-align: center;
385 color: var(--text-muted);
386 font-size: 11.5px;
387 }
388 .explain-poweredby-pill {
389 display: inline-flex; align-items: center; gap: 6px;
390 padding: 4px 10px;
391 border-radius: 9999px;
6fd5915Claude392 background: rgba(91,110,232,0.08);
393 border: 1px solid rgba(91,110,232,0.22);
0a69faaClaude394 color: var(--text-muted);
395 font-size: 11px; letter-spacing: 0.04em;
396 text-transform: uppercase; font-weight: 600;
397 }
398 .explain-poweredby-pill .dot {
399 width: 6px; height: 6px; border-radius: 50%;
6fd5915Claude400 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
0a69faaClaude401 }
402
403 /* Cached badge */
404 .explain-cached-pill {
405 display: inline-flex; align-items: center; gap: 5px;
406 padding: 2px 8px; border-radius: 9999px;
407 font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em;
408 text-transform: uppercase;
409 background: rgba(52,211,153,0.12); color: #6ee7b7;
410 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.30);
411 }
412 .explain-cached-pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
413`;
414
415// ---------------------------------------------------------------------------
416// Shared repo resolution
417// ---------------------------------------------------------------------------
418
419async function resolveRepo(owner: string, repo: string) {
420 const [ownerRow] = await db
421 .select({ id: users.id, username: users.username })
422 .from(users)
423 .where(eq(users.username, owner))
424 .limit(1);
425 if (!ownerRow) return null;
426
427 const [repoRow] = await db
428 .select({ id: repositories.id, ownerId: repositories.ownerId })
429 .from(repositories)
430 .where(and(eq(repositories.ownerId, ownerRow.id), eq(repositories.name, repo)))
431 .limit(1);
432 if (!repoRow) return null;
433
434 return { repoId: repoRow.id, ownerId: repoRow.ownerId, ownerUsername: ownerRow.username };
435}
436
437// ---------------------------------------------------------------------------
438// Shared page scaffolding helpers
439// ---------------------------------------------------------------------------
440
441function HealthBadge({ score }: { score: string }) {
442 const cls = {
443 "Elite": "elite",
444 "Strong": "strong",
445 "Improving": "improving",
446 "Needs Attention": "needs-attention",
447 }[score] ?? "improving";
448 const emoji = {
449 "Elite": "★",
450 "Strong": "●",
451 "Improving": "◐",
452 "Needs Attention": "○",
453 }[score] ?? "●";
454 return (
455 <span class={`explain-health-score ${cls}`}>
456 <span class="explain-health-dot" aria-hidden="true" />
457 {emoji} {score}
458 </span>
459 );
460}
461
462function TechChips({ stack }: { stack: string[] }) {
463 return (
464 <div class="explain-chips">
465 {stack.map((t) => <span class="explain-chip">{t}</span>)}
466 </div>
467 );
468}
469
470function ResultDashboard({
471 result,
472 owner,
473 repo,
474 cached,
475}: {
476 result: ExplainJobResult;
477 owner: string;
478 repo: string;
479 cached?: boolean;
480}) {
481 const issueNewBase = `/${owner}/${repo}/issues/new`;
482 return (
483 <>
484 <div class="explain-actions-bar">
485 {cached && (
486 <span class="explain-cached-pill">
487 <span class="dot" />
488 cached
489 </span>
490 )}
491 <a
492 href={`/share/${owner}`}
493 class="explain-share-btn"
494 title="Share this analysis"
495 >
496 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
497 <circle cx="18" cy="5" r="3" />
498 <circle cx="6" cy="12" r="3" />
499 <circle cx="18" cy="19" r="3" />
500 <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
501 <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
502 </svg>
503 Share analysis
504 </a>
505 </div>
506
507 <div class="explain-dashboard">
508 {/* Summary */}
509 <div class="explain-card explain-card-full">
510 <div class="explain-card-head">
511 <span class="explain-card-icon" aria-hidden="true">
512 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
513 <path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
514 </svg>
515 </span>
516 <p class="explain-card-title">Summary</p>
517 </div>
518 <div class="explain-card-body">
519 <p class="explain-summary-text">{result.summary}</p>
520 </div>
521 </div>
522
523 {/* Health Score */}
524 <div class="explain-card">
525 <div class="explain-card-head">
526 <span class="explain-card-icon" aria-hidden="true">
527 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
528 <polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
529 </svg>
530 </span>
531 <p class="explain-card-title">Health Score</p>
532 </div>
533 <div class="explain-card-body">
534 <HealthBadge score={result.healthScore} />
535 <p class="explain-health-desc">
536 Based on code quality signals visible in the repository — tests, documentation, type coverage, and CI configuration.
537 </p>
538 </div>
539 </div>
540
541 {/* Tech Stack */}
542 <div class="explain-card">
543 <div class="explain-card-head">
544 <span class="explain-card-icon" aria-hidden="true">
545 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
546 <polygon points="12 2 2 7 12 12 22 7 12 2" />
547 <polyline points="2 17 12 22 22 17" />
548 <polyline points="2 12 12 17 22 12" />
549 </svg>
550 </span>
551 <p class="explain-card-title">Tech Stack</p>
552 </div>
553 <div class="explain-card-body">
554 {result.techStack.length > 0
555 ? <TechChips stack={result.techStack} />
556 : <p style="color:var(--text-muted);font-size:13px;margin:0">No tech stack detected.</p>
557 }
558 </div>
559 </div>
560
561 {/* Architecture */}
562 <div class="explain-card explain-card-full">
563 <div class="explain-card-head">
564 <span class="explain-card-icon" aria-hidden="true">
565 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
566 <rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" />
567 <rect x="14" y="14" width="7" height="7" /><rect x="3" y="14" width="7" height="7" />
568 </svg>
569 </span>
570 <p class="explain-card-title">Architecture</p>
571 </div>
572 <div class="explain-card-body">
573 <div class="explain-arch-body">
574 <div class="markdown-body">
575 {html([renderMarkdown(result.architecture)] as unknown as TemplateStringsArray)}
576 </div>
577 </div>
578 </div>
579 </div>
580
581 {/* Entry Points */}
582 <div class="explain-card explain-card-full">
583 <div class="explain-card-head">
584 <span class="explain-card-icon" aria-hidden="true">
585 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
586 <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
587 <polyline points="14 2 14 8 20 8" />
588 </svg>
589 </span>
590 <p class="explain-card-title">Key Entry Points</p>
591 </div>
592 <div class="explain-card-body">
593 {result.entryPoints.length > 0 ? (
594 <table class="explain-ep-table">
595 <thead>
596 <tr>
597 <th>File</th>
598 <th>Role</th>
599 </tr>
600 </thead>
601 <tbody>
602 {result.entryPoints.map((ep) => (
603 <tr>
604 <td>
605 <a href={`/${owner}/${repo}/blob/main/${ep.file}`}>
606 {ep.file}
607 </a>
608 </td>
609 <td>{ep.role}</td>
610 </tr>
611 ))}
612 </tbody>
613 </table>
614 ) : (
615 <p style="color:var(--text-muted);font-size:13px;margin:0">No entry points detected.</p>
616 )}
617 </div>
618 </div>
619
620 {/* Getting Started */}
621 <div class="explain-card explain-card-full">
622 <div class="explain-card-head">
623 <span class="explain-card-icon" aria-hidden="true">
624 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
625 <polyline points="4 17 10 11 4 5" /><line x1="12" y1="19" x2="20" y2="19" />
626 </svg>
627 </span>
628 <p class="explain-card-title">Getting Started</p>
629 </div>
630 <div class="explain-card-body">
631 <div class="explain-gs-body">
632 <div class="markdown-body">
633 {html([renderMarkdown(result.gettingStarted)] as unknown as TemplateStringsArray)}
634 </div>
635 </div>
636 </div>
637 </div>
638
639 {/* Suggested Issues */}
640 <div class="explain-card explain-card-full">
641 <div class="explain-card-head">
642 <span class="explain-card-icon" aria-hidden="true">
643 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
644 <circle cx="12" cy="12" r="10" />
645 <line x1="12" y1="8" x2="12" y2="12" />
646 <line x1="12" y1="16" x2="12.01" y2="16" />
647 </svg>
648 </span>
649 <p class="explain-card-title">Suggested First Tasks</p>
650 </div>
651 <div class="explain-card-body">
652 {result.suggestedIssues.length > 0 ? (
653 <div class="explain-issues">
654 {result.suggestedIssues.map((issue) => {
655 const params = new URLSearchParams({
656 title: issue.title,
657 body: issue.description,
658 });
659 return (
660 <div class="explain-issue-card">
661 <h4>{issue.title}</h4>
662 <p>{issue.description}</p>
663 <a
664 href={`${issueNewBase}?${params.toString()}`}
665 class="explain-issue-btn"
666 >
667 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
668 <circle cx="12" cy="12" r="10" />
669 <line x1="12" y1="8" x2="12" y2="16" />
670 <line x1="8" y1="12" x2="16" y2="12" />
671 </svg>
672 Open Issue
673 </a>
674 </div>
675 );
676 })}
677 </div>
678 ) : (
679 <p style="color:var(--text-muted);font-size:13px;margin:0">No suggestions generated.</p>
680 )}
681 </div>
682 </div>
683 </div>
684 </>
685 );
686}
687
688// ---------------------------------------------------------------------------
689// Route: GET /:owner/:repo/explain — landing page or cached result
690// ---------------------------------------------------------------------------
691
692explainRoutes.get("/:owner/:repo/explain", softAuth, async (c) => {
693 const { owner, repo } = c.req.param();
694 const user = c.get("user");
695
696 const resolved = await resolveRepo(owner, repo);
697 if (!resolved) {
698 return c.html(
699 <Layout title="Not Found" user={user}>
700 <div class="container" style="padding: var(--space-6);">
701 <h2>Repository not found</h2>
702 </div>
703 </Layout>,
704 404
705 );
706 }
707
708 const cached = await getCachedExplainResult(resolved.repoId);
709 const canTrigger = !!user;
710
711 return c.html(
712 <Layout title={`Explain — ${owner}/${repo}`} user={user}>
713 <RepoHeader owner={owner} repo={repo} />
714 <div class="explain-wrap">
715 <section class="explain-hero">
716 <div class="explain-hero-orb" aria-hidden="true" />
717 <div class="explain-hero-inner">
718 <div class="explain-eyebrow">
719 <span class="pill" aria-hidden="true">
720 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
721 <path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 1 1 7.072 0l-.548.547A3.374 3.374 0 0 0 14 18.469V19a2 2 0 1 1-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
722 </svg>
723 </span>
724 AI · gluecron · explain
725 </div>
726 <h1 class="explain-title">
727 <span class="explain-title-grad">Explain</span>{" "}
728 <span style="color:var(--text-strong)">{owner}/{repo}</span>
729 </h1>
730 <p class="explain-sub">
731 One click — AI reads the entire codebase and generates an architecture overview,
732 tech stack analysis, key entry points, onboarding guide, and suggested first tasks.
733 </p>
734
735 {!cached && (
736 canTrigger ? (
737 <form method="post" action={`/${owner}/${repo}/explain`} style="display:inline">
738 <button type="submit" class="explain-trigger-btn">
739 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
740 <path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 1 1 7.072 0l-.548.547A3.374 3.374 0 0 0 14 18.469V19a2 2 0 1 1-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
741 </svg>
742 Explain This Repo
743 </button>
744 </form>
745 ) : (
746 <a href="/login" class="explain-trigger-btn">
747 Sign in to explain this repo
748 </a>
749 )
750 )}
751
752 {cached && (
753 <form method="post" action={`/${owner}/${repo}/explain`} style="display:inline">
754 <button type="submit" class="explain-trigger-btn" style="background:var(--bg-elevated);color:var(--text);border:1px solid var(--border);box-shadow:none">
755 <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
756 <polyline points="23 4 23 10 17 10" />
757 <polyline points="1 20 1 14 7 14" />
758 <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
759 </svg>
760 Regenerate
761 </button>
762 </form>
763 )}
764 </div>
765 </section>
766
767 {cached ? (
768 <ResultDashboard result={cached} owner={owner} repo={repo} cached />
769 ) : (
770 <div style="padding:var(--space-6);text-align:center;color:var(--text-muted);font-size:14px;">
771 No analysis yet. Click "Explain This Repo" to generate one.
772 </div>
773 )}
774
775 <div class="explain-poweredby">
776 <span class="explain-poweredby-pill">
777 <span class="dot" aria-hidden="true" />
778 Powered by Claude
779 </span>
780 </div>
781 </div>
782 <style dangerouslySetInnerHTML={{ __html: STYLES }} />
783 </Layout>
784 );
785});
786
787// ---------------------------------------------------------------------------
788// Route: POST /:owner/:repo/explain — trigger analysis
789// ---------------------------------------------------------------------------
790
791explainRoutes.post("/:owner/:repo/explain", softAuth, async (c) => {
792 const { owner, repo } = c.req.param();
793 const user = c.get("user");
794
795 if (!user) {
796 return c.redirect(`/login`);
797 }
798
799 const resolved = await resolveRepo(owner, repo);
800 if (!resolved) return c.notFound();
801
802 const jobId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
803 startExplainJob(jobId, owner, repo, resolved.repoId);
804
805 return c.redirect(`/${owner}/${repo}/explain/${jobId}`);
806});
807
808// ---------------------------------------------------------------------------
809// Route: GET /:owner/:repo/explain/:jobId/raw — JSON result
810// ---------------------------------------------------------------------------
811
812explainRoutes.get("/:owner/:repo/explain/:jobId/raw", async (c) => {
813 const { jobId } = c.req.param();
814 const job = explainJobs.get(jobId);
815 if (!job) return c.json({ error: "Job not found" }, 404);
816 return c.json(job);
817});
818
819// ---------------------------------------------------------------------------
820// Route: GET /:owner/:repo/explain/:jobId — progress / result page
821// ---------------------------------------------------------------------------
822
823explainRoutes.get("/:owner/:repo/explain/:jobId", softAuth, async (c) => {
824 const { owner, repo, jobId } = c.req.param();
825 const user = c.get("user");
826
827 const job = explainJobs.get(jobId);
828 if (!job) {
829 return c.html(
830 <Layout title="Job not found" user={user}>
831 <div class="container" style="padding:var(--space-6)">
832 <h2>Analysis job not found</h2>
833 <p>
834 The job may have expired. <a href={`/${owner}/${repo}/explain`}>Start a new analysis</a>.
835 </p>
836 </div>
837 </Layout>,
838 404
839 );
840 }
841
842 const isRunning = job.status === "running";
843 const isFailed = job.status === "failed";
844 const isDone = job.status === "done";
845
846 return c.html(
847 <Layout title={`Explain — ${owner}/${repo}`} user={user}>
848 {/* Auto-refresh while running */}
849 {isRunning && (
850 <meta http-equiv="refresh" content={`3;url=/${owner}/${repo}/explain/${jobId}`} />
851 )}
852 <RepoHeader owner={owner} repo={repo} />
853 <div class="explain-wrap">
854 <section class="explain-hero">
855 <div class="explain-hero-orb" aria-hidden="true" />
856 <div class="explain-hero-inner">
857 <div class="explain-eyebrow">
858 <span class="pill" aria-hidden="true">
859 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
860 <path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 1 1 7.072 0l-.548.547A3.374 3.374 0 0 0 14 18.469V19a2 2 0 1 1-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
861 </svg>
862 </span>
863 AI · gluecron · explain
864 </div>
865 <h1 class="explain-title">
866 <span class="explain-title-grad">Explain</span>{" "}
867 <span style="color:var(--text-strong)">{owner}/{repo}</span>
868 </h1>
869 </div>
870 </section>
871
872 {isRunning && (
873 <div class="explain-progress">
874 <div class="explain-spinner" aria-label="Analyzing…" />
875 <div class="explain-progress-text">
876 <h3>Analyzing codebase…</h3>
877 <p>
878 Claude is reading the file tree and key source files.
879 This usually takes 10–30 seconds. This page refreshes automatically.
880 </p>
881 </div>
882 </div>
883 )}
884
885 {isFailed && (
886 <div class="explain-error">
887 <h3>Analysis failed</h3>
888 <p>{job.error ?? "An unexpected error occurred. Please try again."}</p>
889 </div>
890 )}
891
892 {isDone && job.result && (
893 <ResultDashboard result={job.result} owner={owner} repo={repo} />
894 )}
895
896 <div style="margin-top:var(--space-4);font-size:13px;color:var(--text-muted);">
897 <a href={`/${owner}/${repo}/explain`} style="color:var(--link)">
898 ← Back to explain page
899 </a>
900 {" · "}
901 <a
902 href={`/${owner}/${repo}/explain/${jobId}/raw`}
903 style="color:var(--link)"
904 target="_blank"
905 rel="noopener noreferrer"
906 >
907 View raw JSON
908 </a>
909 </div>
910
911 <div class="explain-poweredby">
912 <span class="explain-poweredby-pill">
913 <span class="dot" aria-hidden="true" />
914 Powered by Claude
915 </span>
916 </div>
917 </div>
918 <style dangerouslySetInnerHTML={{ __html: STYLES }} />
919 </Layout>
920 );
921});
922
923export default explainRoutes;