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

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

migrations.tsxBlame771 lines · 1 contributor
14c3cc8Claude1/**
2 * Migration history — tracks repos imported from GitHub (bulk org import + single
3 * repo import) and lets owners re-run the post-migration verifier on demand.
4 *
5 * The `repositories` table does NOT currently carry an `importedAt` /
6 * `importSource` / `mirrorUpstreamUrl` column (see `src/db/schema.ts`), so
7 * we fall back to a best-effort derivation: list every repo owned by the
8 * current user and surface `createdAt` as the "imported at" timestamp. When
9 * the schema eventually grows an `importedAt` column we can switch the
10 * filter to `isNotNull(repositories.importedAt)` without changing the UI.
11 *
12 * The verifier itself lives in `src/lib/import-verify.ts` and is being
13 * supplied by a parallel agent. We load it via dynamic import inside a
14 * try/catch so a missing module produces a helpful "verifier not available"
15 * note instead of a 500.
54eab24Claude16 *
17 * 2026 polish:
18 * - Scoped `.mig-*` CSS — no bleed into the global layout.
19 * - Eyebrow + display headline + 1-line subtitle.
20 * - Each migration row is a card: run name, applied/pending/failed pill,
21 * timestamp, duration (best-effort).
22 * - Dashed empty-state with orb + CTA when no migrations exist.
23 * - All query params + form actions preserved verbatim.
14c3cc8Claude24 */
25
26import { Hono } from "hono";
27import { desc, eq } from "drizzle-orm";
28import { db } from "../db";
29import { repositories } from "../db/schema";
30import { Layout } from "../views/layout";
4127ecfClaude31import { formatRelative } from "../views/ui";
14c3cc8Claude32import { requireAuth } from "../middleware/auth";
33import type { AuthEnv } from "../middleware/auth";
34
35const migrations = new Hono<AuthEnv>();
36
37migrations.use("/migrations", requireAuth);
38migrations.use("/migrations/*", requireAuth);
39
54eab24Claude40// ─── Scoped CSS (.mig-*) ────────────────────────────────────────────────
41const migStyles = `
eed4684Claude42 .mig-wrap { max-width: 1320px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
54eab24Claude43
44 .mig-head {
45 margin-bottom: var(--space-5);
46 display: flex;
47 align-items: flex-end;
48 justify-content: space-between;
49 gap: var(--space-4);
50 flex-wrap: wrap;
51 }
52 .mig-head-text { flex: 1; min-width: 280px; }
53 .mig-eyebrow {
54 display: inline-flex;
55 align-items: center;
56 gap: 8px;
57 text-transform: uppercase;
58 font-family: var(--font-mono);
59 font-size: 11px;
60 letter-spacing: 0.16em;
61 color: var(--text-muted);
62 font-weight: 600;
63 margin-bottom: 10px;
64 }
65 .mig-eyebrow-dot {
66 width: 8px; height: 8px;
67 border-radius: 9999px;
6fd5915Claude68 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
69 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
54eab24Claude70 }
71 .mig-title {
72 font-family: var(--font-display);
73 font-size: clamp(24px, 3.4vw, 36px);
74 font-weight: 800;
75 letter-spacing: -0.028em;
76 line-height: 1.1;
77 margin: 0 0 6px;
78 color: var(--text-strong);
79 }
80 .mig-title-grad {
6fd5915Claude81 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
54eab24Claude82 -webkit-background-clip: text;
83 background-clip: text;
84 -webkit-text-fill-color: transparent;
85 color: transparent;
86 }
87 .mig-sub {
88 margin: 0;
89 font-size: 14px;
90 color: var(--text-muted);
91 line-height: 1.5;
92 max-width: 720px;
93 }
94
95 .mig-actions { display: flex; gap: 8px; flex-wrap: wrap; }
96 .mig-btn {
97 display: inline-flex;
98 align-items: center;
99 justify-content: center;
100 gap: 6px;
101 padding: 9px 16px;
102 border-radius: 10px;
103 font-size: 13px;
104 font-weight: 600;
105 text-decoration: none;
106 border: 1px solid transparent;
107 cursor: pointer;
108 font: inherit;
109 transition: transform 120ms ease, box-shadow 120ms ease, background 120ms ease, border-color 120ms ease;
110 line-height: 1;
111 white-space: nowrap;
112 }
113 .mig-btn-primary {
6fd5915Claude114 background: linear-gradient(135deg, #5b6ee8 0%, #5f8fa0 100%);
54eab24Claude115 color: #ffffff;
6fd5915Claude116 box-shadow: 0 6px 18px -6px rgba(91,110,232,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
54eab24Claude117 }
118 .mig-btn-primary:hover {
119 transform: translateY(-1px);
6fd5915Claude120 box-shadow: 0 10px 24px -8px rgba(91,110,232,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
54eab24Claude121 text-decoration: none;
122 color: #ffffff;
123 }
124 .mig-btn-ghost {
125 background: transparent;
126 color: var(--text);
127 border-color: var(--border-strong);
128 }
129 .mig-btn-ghost:hover {
6fd5915Claude130 background: rgba(91,110,232,0.06);
131 border-color: rgba(91,110,232,0.45);
54eab24Claude132 color: var(--text-strong);
133 text-decoration: none;
134 }
135
136 /* ─── Migration row cards ─── */
137 .mig-list {
138 display: flex;
139 flex-direction: column;
140 gap: 10px;
141 }
142 .mig-row {
143 position: relative;
144 display: grid;
145 grid-template-columns: 1fr auto;
146 gap: var(--space-3);
147 padding: 14px 16px;
148 background: var(--bg-elevated);
149 border: 1px solid var(--border);
150 border-radius: 12px;
151 transition: border-color 120ms ease, background 120ms ease;
152 overflow: hidden;
153 }
154 .mig-row::before {
155 content: '';
156 position: absolute;
157 top: 0; left: 0; right: 0;
158 height: 1.5px;
6fd5915Claude159 background: linear-gradient(90deg, transparent 0%, #5b6ee8 50%, #5f8fa0 100%);
54eab24Claude160 opacity: 0.35;
161 pointer-events: none;
162 }
163 .mig-row:hover {
164 border-color: var(--border-strong);
165 background: rgba(255,255,255,0.025);
166 }
167 .mig-row-main { min-width: 0; }
168 .mig-row-name-line {
169 display: flex;
170 align-items: center;
171 gap: 8px;
172 flex-wrap: wrap;
173 margin-bottom: 6px;
174 }
175 .mig-row-name {
176 font-family: var(--font-display);
177 font-size: 15.5px;
178 font-weight: 700;
179 color: var(--text-strong);
180 text-decoration: none;
181 letter-spacing: -0.012em;
182 }
183 .mig-row-name:hover { text-decoration: underline; }
184 .mig-row-desc {
185 font-size: 12.5px;
186 color: var(--text-muted);
187 line-height: 1.5;
188 margin: 0;
189 overflow: hidden;
190 text-overflow: ellipsis;
191 white-space: nowrap;
192 }
193 .mig-row-meta {
194 display: flex;
195 align-items: center;
196 gap: 8px;
197 flex-wrap: wrap;
198 margin-top: 8px;
199 font-size: 11.5px;
200 color: var(--text-muted);
201 font-variant-numeric: tabular-nums;
202 }
203 .mig-row-meta .sep { opacity: 0.4; }
204 .mig-row-actions {
205 display: flex;
206 align-items: center;
207 gap: 6px;
208 flex-shrink: 0;
209 }
210
211 /* ─── Status pills ─── */
212 .mig-pill {
213 display: inline-flex;
214 align-items: center;
215 gap: 6px;
216 padding: 3px 9px;
217 border-radius: 9999px;
218 font-size: 11px;
219 font-weight: 600;
220 letter-spacing: 0.02em;
221 text-transform: capitalize;
222 }
223 .mig-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
224 .mig-pill.is-applied {
225 background: rgba(52,211,153,0.14);
226 color: #6ee7b7;
227 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
228 }
229 .mig-pill.is-pending {
230 background: rgba(251,191,36,0.12);
231 color: #fde68a;
232 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
233 }
234 .mig-pill.is-failed {
235 background: rgba(248,113,113,0.12);
236 color: #fecaca;
237 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
238 }
239
240 /* ─── Banners (used on verify page) ─── */
241 .mig-banner {
242 margin-bottom: var(--space-4);
243 padding: 10px 14px;
244 border-radius: 10px;
245 font-size: 13.5px;
246 border: 1px solid var(--border);
247 background: rgba(255,255,255,0.025);
248 color: var(--text);
249 display: flex;
250 align-items: center;
251 gap: 10px;
252 }
253 .mig-banner.is-ok { border-color: rgba(52,211,153,0.40); background: rgba(52,211,153,0.08); color: #bbf7d0; }
254 .mig-banner.is-error { border-color: rgba(248,113,113,0.40); background: rgba(248,113,113,0.08); color: #fecaca; }
255 .mig-banner.is-warn { border-color: rgba(251,191,36,0.32); background: rgba(251,191,36,0.06); color: #fde68a; }
256 .mig-banner-dot { width: 8px; height: 8px; border-radius: 9999px; background: currentColor; flex-shrink: 0; }
257
258 .mig-checks { display: flex; flex-direction: column; gap: 10px; }
259 .mig-check {
260 display: flex;
261 align-items: center;
262 justify-content: space-between;
263 gap: 12px;
264 padding: 12px 14px;
265 background: var(--bg-elevated);
266 border: 1px solid var(--border);
267 border-radius: 10px;
268 }
269 .mig-check-label {
270 display: flex;
271 align-items: center;
272 gap: 10px;
273 font-family: var(--font-display);
274 font-weight: 700;
275 color: var(--text-strong);
276 font-size: 13.5px;
277 }
278 .mig-check-dot {
279 width: 10px; height: 10px;
280 border-radius: 9999px;
281 flex-shrink: 0;
282 }
283 .mig-check-dot.is-ok { background: #34d399; box-shadow: 0 0 0 3px rgba(52,211,153,0.18); }
284 .mig-check-dot.is-fail { background: #f87171; box-shadow: 0 0 0 3px rgba(248,113,113,0.18); }
285 .mig-check-detail {
286 font-size: 12px;
287 color: var(--text-muted);
288 }
289 .mig-issues-list {
290 margin: 0;
291 padding-left: 20px;
292 color: var(--text-muted);
293 font-size: 13px;
294 line-height: 1.6;
295 }
296
297 /* ─── Empty state ─── */
298 .mig-empty {
299 position: relative;
300 overflow: hidden;
301 padding: clamp(28px, 5vw, 52px) clamp(20px, 4vw, 40px);
302 text-align: center;
303 background: var(--bg-elevated);
304 border: 1px dashed var(--border-strong);
305 border-radius: 16px;
306 }
307 .mig-empty-orb {
308 position: absolute;
309 inset: -40% 25% auto 25%;
310 height: 300px;
6fd5915Claude311 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.10) 45%, transparent 70%);
54eab24Claude312 filter: blur(72px);
313 opacity: 0.7;
314 pointer-events: none;
315 z-index: 0;
316 }
317 .mig-empty-inner { position: relative; z-index: 1; }
318 .mig-empty-icon {
319 width: 56px; height: 56px;
320 margin: 0 auto 14px;
321 border-radius: 9999px;
6fd5915Claude322 background: linear-gradient(135deg, rgba(91,110,232,0.25), rgba(95,143,160,0.20));
323 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.40);
54eab24Claude324 display: inline-flex;
325 align-items: center;
326 justify-content: center;
327 color: #c4b5fd;
328 }
329 .mig-empty-title {
330 font-family: var(--font-display);
331 font-size: 18px;
332 font-weight: 700;
333 margin: 0 0 6px;
334 color: var(--text-strong);
335 }
336 .mig-empty-sub {
337 margin: 0 auto 16px;
338 font-size: 13.5px;
339 color: var(--text-muted);
340 max-width: 440px;
341 line-height: 1.5;
342 }
343 .mig-crumbs {
344 margin-bottom: var(--space-4);
345 font-size: 12.5px;
346 color: var(--text-muted);
347 }
348 .mig-crumbs a {
349 display: inline-flex;
350 align-items: center;
351 gap: 5px;
352 padding: 6px 11px;
353 background: rgba(255,255,255,0.025);
354 border: 1px solid var(--border);
355 border-radius: 8px;
356 color: var(--text-muted);
357 text-decoration: none;
358 font-weight: 500;
359 }
360 .mig-crumbs a:hover { border-color: var(--border-strong); color: var(--text-strong); text-decoration: none; }
361`;
362
363function IconImport() {
364 return (
365 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
366 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
367 <polyline points="7 10 12 15 17 10" />
368 <line x1="12" y1="15" x2="12" y2="3" />
369 </svg>
370 );
371}
372function IconShield() {
373 return (
374 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
375 <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
376 </svg>
377 );
378}
379
14c3cc8Claude380// ─── Verifier loader ─────────────────────────────────────────
381type VerifyResult = {
382 repoId: number;
383 clonable: boolean;
384 hasDefaultBranch: boolean;
385 commitCount: number;
386 issues: string[];
387};
388
389async function loadVerifier(): Promise<
390 ((repoId: number) => Promise<VerifyResult>) | null
391> {
392 try {
393 const mod: any = await import("../lib/import-verify");
394 if (mod && typeof mod.verifyMigration === "function") {
395 return mod.verifyMigration as (id: number) => Promise<VerifyResult>;
396 }
397 return null;
398 } catch {
399 return null;
400 }
401}
402
403// ─── GET /migrations ─────────────────────────────────────────
404migrations.get("/migrations", async (c) => {
405 const user = c.get("user")!;
406
407 let rows: Array<{
408 id: string;
409 name: string;
410 createdAt: Date;
411 description: string | null;
412 }> = [];
413 try {
414 const result = await db
415 .select({
416 id: repositories.id,
417 name: repositories.name,
418 createdAt: repositories.createdAt,
419 description: repositories.description,
420 })
421 .from(repositories)
422 .where(eq(repositories.ownerId, user.id))
423 .orderBy(desc(repositories.createdAt));
424 rows = result as any;
425 } catch {
426 rows = [];
427 }
428
429 return c.html(
430 <Layout title="Migration history" user={user}>
54eab24Claude431 <div class="mig-wrap">
432 <header class="mig-head">
433 <div class="mig-head-text">
434 <div class="mig-eyebrow">
435 <span class="mig-eyebrow-dot" aria-hidden="true" />
436 Account · Migration history
437 </div>
438 <h1 class="mig-title">
439 <span class="mig-title-grad">Every repo you brought over.</span>
440 </h1>
441 <p class="mig-sub">
442 Re-run the post-migration verifier any time — it confirms the
443 repo is clonable, the default branch resolves, and commits are
444 present.
445 </p>
446 </div>
447 <div class="mig-actions">
448 <a class="mig-btn mig-btn-ghost" href="/import">
449 <IconImport />
14c3cc8Claude450 Import
451 </a>
54eab24Claude452 <a class="mig-btn mig-btn-primary" href="/import/bulk">
453 <IconImport />
14c3cc8Claude454 Bulk import
455 </a>
456 </div>
54eab24Claude457 </header>
14c3cc8Claude458
459 {rows.length === 0 ? (
54eab24Claude460 <div class="mig-empty">
461 <div class="mig-empty-orb" aria-hidden="true" />
462 <div class="mig-empty-inner">
463 <div class="mig-empty-icon" aria-hidden="true">
464 <IconImport />
465 </div>
466 <h3 class="mig-empty-title">No migrations yet</h3>
467 <p class="mig-empty-sub">
468 Pull a single repo from GitHub or kick off a bulk org import to
469 see migration runs land here.
470 </p>
471 <div class="mig-actions" style="justify-content:center">
472 <a class="mig-btn mig-btn-ghost" href="/import">
473 <IconImport />
474 Single repo
475 </a>
476 <a class="mig-btn mig-btn-primary" href="/import/bulk">
477 <IconImport />
478 Bulk import
479 </a>
480 </div>
481 </div>
14c3cc8Claude482 </div>
483 ) : (
54eab24Claude484 <div class="mig-list">
485 {rows.map((r) => {
486 // The schema has no migration-status column yet — every repo
487 // that exists has, by definition, been "applied". We expose the
488 // pill shape so when status lands later we can swap the value.
489 const status: "applied" | "pending" | "failed" = "applied";
490 const pillClass =
491 status === "applied"
492 ? "mig-pill is-applied"
493 : status === "pending"
494 ? "mig-pill is-pending"
495 : "mig-pill is-failed";
496 const tsText = r.createdAt
497 ? formatRelative(r.createdAt as unknown as string)
498 : "—";
499 // Duration not tracked in the schema; render an em-dash so the
500 // visual treatment is in place for when it lands.
501 return (
502 <div class="mig-row">
503 <div class="mig-row-main">
504 <div class="mig-row-name-line">
505 <a
506 href={`/${user.username}/${r.name}`}
507 class="mig-row-name"
508 >
509 {r.name}
510 </a>
511 <span class={pillClass}>
512 <span class="dot" aria-hidden="true" />
513 {status}
514 </span>
14c3cc8Claude515 </div>
54eab24Claude516 {r.description && (
517 <p class="mig-row-desc">{r.description}</p>
518 )}
519 <div class="mig-row-meta">
520 <span>Imported {tsText}</span>
521 <span class="sep">·</span>
522 <span>duration —</span>
523 </div>
524 </div>
525 <div class="mig-row-actions">
526 <a
527 class="mig-btn mig-btn-primary"
528 href={`/migrations/verify/${r.id}`}
529 >
530 <IconShield />
531 Verify
532 </a>
533 </div>
14c3cc8Claude534 </div>
54eab24Claude535 );
536 })}
14c3cc8Claude537 </div>
538 )}
539 </div>
54eab24Claude540 <style dangerouslySetInnerHTML={{ __html: migStyles }} />
14c3cc8Claude541 </Layout>
542 );
543});
544
545// ─── GET /migrations/verify/:repoId ──────────────────────────
546migrations.get("/migrations/verify/:repoId", async (c) => {
547 const user = c.get("user")!;
548 const repoId = c.req.param("repoId");
549
550 let repo:
551 | {
552 id: string;
553 name: string;
554 ownerId: string;
555 defaultBranch: string;
556 }
557 | null = null;
558 try {
559 const [row] = await db
560 .select({
561 id: repositories.id,
562 name: repositories.name,
563 ownerId: repositories.ownerId,
564 defaultBranch: repositories.defaultBranch,
565 })
566 .from(repositories)
567 .where(eq(repositories.id, repoId))
568 .limit(1);
569 repo = (row as any) || null;
570 } catch {
571 repo = null;
572 }
573
574 if (!repo) {
575 return c.html(
576 <Layout title="Verify migration" user={user}>
54eab24Claude577 <div class="mig-wrap">
578 <div class="mig-crumbs">
579 <a href="/migrations">← Back to migration history</a>
580 </div>
581 <header class="mig-head">
582 <div class="mig-head-text">
583 <div class="mig-eyebrow">
584 <span class="mig-eyebrow-dot" aria-hidden="true" />
585 Verify migration
586 </div>
587 <h1 class="mig-title">
588 <span class="mig-title-grad">Repository not found.</span>
589 </h1>
590 <p class="mig-sub">
591 The repo ID didn't resolve — it may have been deleted or the
592 URL is stale.
593 </p>
594 </div>
595 </header>
14c3cc8Claude596 </div>
54eab24Claude597 <style dangerouslySetInnerHTML={{ __html: migStyles }} />
14c3cc8Claude598 </Layout>,
599 404
600 );
601 }
602
603 if (repo.ownerId !== user.id) {
604 return c.html(
605 <Layout title="Verify migration" user={user}>
54eab24Claude606 <div class="mig-wrap">
607 <div class="mig-crumbs">
608 <a href="/migrations">← Back to migration history</a>
609 </div>
610 <header class="mig-head">
611 <div class="mig-head-text">
612 <div class="mig-eyebrow">
613 <span class="mig-eyebrow-dot" aria-hidden="true" />
614 Forbidden
615 </div>
616 <h1 class="mig-title">
617 <span class="mig-title-grad">Not your repo.</span>
618 </h1>
619 <p class="mig-sub">
620 You can only verify repositories you own.
621 </p>
622 </div>
623 </header>
14c3cc8Claude624 </div>
54eab24Claude625 <style dangerouslySetInnerHTML={{ __html: migStyles }} />
14c3cc8Claude626 </Layout>,
627 403
628 );
629 }
630
631 const verify = await loadVerifier();
632 let result: VerifyResult | null = null;
633 let verifierError: string | null = null;
634 if (!verify) {
635 verifierError =
636 "Verifier not available. The import-verify module is not installed yet.";
637 } else {
638 try {
639 result = await verify(repo.id as unknown as number);
640 } catch (err: any) {
641 verifierError =
642 "Verifier failed: " + (err && err.message ? err.message : String(err));
643 }
644 }
645
646 return c.html(
647 <Layout title={`Verify ${repo.name}`} user={user}>
54eab24Claude648 <div class="mig-wrap">
649 <div class="mig-crumbs">
650 <a href="/migrations">← Back to migration history</a>
14c3cc8Claude651 </div>
54eab24Claude652 <header class="mig-head">
653 <div class="mig-head-text">
654 <div class="mig-eyebrow">
655 <span class="mig-eyebrow-dot" aria-hidden="true" />
656 Migration · Verify run
657 </div>
658 <h1 class="mig-title">
659 <span class="mig-title-grad">
660 {user.username}/{repo.name}
661 </span>
662 </h1>
663 <p class="mig-sub">
664 Re-runs the post-migration verifier. We confirm clonability, the
665 default branch, and the commit count.
666 </p>
667 </div>
668 <div class="mig-actions">
669 <a
670 class="mig-btn mig-btn-primary"
671 href={`/migrations/verify/${repo.id}`}
672 >
673 <IconShield />
674 Re-run
675 </a>
676 <a class="mig-btn mig-btn-ghost" href="/migrations">
677 Back
678 </a>
679 </div>
680 </header>
14c3cc8Claude681
682 {verifierError && (
54eab24Claude683 <div class="mig-banner is-warn" role="status">
684 <span class="mig-banner-dot" aria-hidden="true" />
14c3cc8Claude685 {verifierError}
686 </div>
687 )}
688
689 {result && (
54eab24Claude690 <div class="mig-checks">
691 <div class="mig-check">
692 <div class="mig-check-label">
693 <span
694 class={
695 "mig-check-dot " +
696 (result.clonable ? "is-ok" : "is-fail")
697 }
698 aria-hidden="true"
699 />
700 Clonable
14c3cc8Claude701 </div>
54eab24Claude702 <div class="mig-check-detail">
703 {result.clonable
704 ? "Repository responds to git clone"
705 : "Clone failed"}
14c3cc8Claude706 </div>
707 </div>
54eab24Claude708 <div class="mig-check">
709 <div class="mig-check-label">
710 <span
711 class={
712 "mig-check-dot " +
713 (result.hasDefaultBranch ? "is-ok" : "is-fail")
714 }
715 aria-hidden="true"
716 />
717 Default branch
14c3cc8Claude718 </div>
54eab24Claude719 <div class="mig-check-detail">
14c3cc8Claude720 {result.hasDefaultBranch
721 ? `Found ${repo.defaultBranch}`
722 : `Missing ${repo.defaultBranch}`}
723 </div>
724 </div>
54eab24Claude725 <div class="mig-check">
726 <div class="mig-check-label">
727 <span
728 class={
729 "mig-check-dot " +
730 (result.commitCount > 0 ? "is-ok" : "is-fail")
731 }
732 aria-hidden="true"
733 />
734 Commits
14c3cc8Claude735 </div>
54eab24Claude736 <div class="mig-check-detail">
14c3cc8Claude737 {result.commitCount} commit
738 {result.commitCount === 1 ? "" : "s"}
739 </div>
740 </div>
741 {result.issues && result.issues.length > 0 && (
54eab24Claude742 <div class="mig-check" style="flex-direction:column;align-items:stretch;gap:8px">
743 <div class="mig-check-label">
744 <span class="mig-check-dot is-fail" aria-hidden="true" />
745 Issues
14c3cc8Claude746 </div>
54eab24Claude747 <ul class="mig-issues-list">
14c3cc8Claude748 {result.issues.map((i) => (
749 <li>{i}</li>
750 ))}
751 </ul>
752 </div>
753 )}
754 {(!result.issues || result.issues.length === 0) &&
755 result.clonable &&
756 result.hasDefaultBranch &&
757 result.commitCount > 0 && (
54eab24Claude758 <div class="mig-banner is-ok" role="status">
759 <span class="mig-banner-dot" aria-hidden="true" />
760 All checks passed.
14c3cc8Claude761 </div>
762 )}
763 </div>
764 )}
765 </div>
54eab24Claude766 <style dangerouslySetInnerHTML={{ __html: migStyles }} />
14c3cc8Claude767 </Layout>
768 );
769});
770
771export default migrations;