Commit7a99d47unknown_key
polish(import): 2026 hero + repo picker cards + progress steps (parallel session 3.G)
2 files changed+1200−2387a99d47d9d6087def85ca83b2114b96ac309dcaa
2 changed files+1200−238
Modifiedsrc/routes/import-bulk.tsx+645−135View fileUnifiedSplit
@@ -43,6 +43,434 @@ interface GitHubRepo {
4343
4444type Visibility = "public" | "private" | "both";
4545
46// ─── PAGE-SCOPED CSS ─────────────────────────────────────────
47// All classes prefixed with .import-bulk- so the block cannot bleed
48// into neighbouring routes. Mirrors the /import polish.
49const importBulkStyles = `
50 .import-bulk-container { max-width: 880px; margin: 0 auto; }
51
52 /* ─── Hero ─── */
53 .import-bulk-hero {
54 position: relative;
55 margin-bottom: var(--space-6);
56 padding: var(--space-5) var(--space-6);
57 background: var(--bg-elevated);
58 border: 1px solid var(--border);
59 border-radius: 16px;
60 overflow: hidden;
61 }
62 .import-bulk-hero::before {
63 content: '';
64 position: absolute;
65 top: 0; left: 0; right: 0;
66 height: 2px;
67 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
68 opacity: 0.75;
69 pointer-events: none;
70 }
71 .import-bulk-hero-bg {
72 position: absolute;
73 inset: -20% -10% auto auto;
74 width: 360px; height: 360px;
75 pointer-events: none;
76 z-index: 0;
77 }
78 .import-bulk-hero-orb {
79 position: absolute;
80 inset: 0;
81 background: radial-gradient(circle, rgba(140,109,255,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
82 filter: blur(80px);
83 opacity: 0.7;
84 animation: importBulkHeroOrb 14s ease-in-out infinite;
85 }
86 @keyframes importBulkHeroOrb {
87 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
88 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
89 }
90 @media (prefers-reduced-motion: reduce) {
91 .import-bulk-hero-orb { animation: none; }
92 }
93 .import-bulk-hero-inner {
94 position: relative;
95 z-index: 1;
96 max-width: 640px;
97 }
98 .import-bulk-hero-eyebrow {
99 font-size: 13px;
100 color: var(--text-muted);
101 margin-bottom: var(--space-2);
102 letter-spacing: -0.005em;
103 }
104 .import-bulk-hero-eyebrow-dot {
105 display: inline-block;
106 width: 6px; height: 6px;
107 border-radius: 50%;
108 background: var(--accent);
109 box-shadow: 0 0 8px rgba(140,109,255,0.6);
110 margin-right: 8px;
111 vertical-align: 1px;
112 }
113 .import-bulk-hero-title {
114 font-size: clamp(28px, 4vw, 40px);
115 font-family: var(--font-display);
116 font-weight: 800;
117 letter-spacing: -0.028em;
118 line-height: 1.05;
119 margin: 0 0 var(--space-2);
120 color: var(--text-strong);
121 }
122 .import-bulk-hero-title .gradient-text {
123 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
124 -webkit-background-clip: text;
125 background-clip: text;
126 -webkit-text-fill-color: transparent;
127 color: transparent;
128 }
129 .import-bulk-hero-sub {
130 font-size: 15px;
131 color: var(--text-muted);
132 margin: 0;
133 line-height: 1.55;
134 }
135
136 /* ─── Banners ─── */
137 .import-bulk-banner {
138 position: relative;
139 padding: 14px 16px 14px 44px;
140 margin-bottom: var(--space-5);
141 border-radius: 12px;
142 border: 1px solid var(--border);
143 background: var(--bg-elevated);
144 font-size: 14px;
145 line-height: 1.5;
146 }
147 .import-bulk-banner::before {
148 content: '';
149 position: absolute;
150 left: 14px; top: 18px;
151 width: 14px; height: 14px;
152 border-radius: 50%;
153 }
154 .import-bulk-banner-error {
155 border-color: rgba(248, 81, 73, 0.32);
156 background: linear-gradient(180deg, rgba(248,81,73,0.06) 0%, var(--bg-elevated) 100%);
157 }
158 .import-bulk-banner-error::before {
159 background: radial-gradient(circle, #f85149 30%, transparent 70%);
160 box-shadow: 0 0 10px rgba(248,81,73,0.5);
161 }
162 .import-bulk-banner-title { font-weight: 600; color: var(--text-strong); }
163 .import-bulk-banner-detail { color: var(--text-muted); margin-top: 4px; font-size: 13.5px; }
164
165 /* ─── Section cards ─── */
166 .import-bulk-section {
167 background: var(--bg-elevated);
168 border: 1px solid var(--border);
169 border-radius: 14px;
170 margin-bottom: var(--space-5);
171 overflow: hidden;
172 }
173 .import-bulk-section-head {
174 padding: var(--space-4) var(--space-5) var(--space-3);
175 border-bottom: 1px solid var(--border);
176 }
177 .import-bulk-section-eyebrow {
178 font-size: 11px;
179 font-weight: 600;
180 letter-spacing: 0.08em;
181 text-transform: uppercase;
182 color: var(--accent);
183 margin-bottom: 6px;
184 }
185 .import-bulk-section-title {
186 font-family: var(--font-display);
187 font-size: 18px;
188 font-weight: 700;
189 letter-spacing: -0.018em;
190 margin: 0 0 4px;
191 color: var(--text-strong);
192 }
193 .import-bulk-section-desc {
194 font-size: 13.5px;
195 color: var(--text-muted);
196 margin: 0;
197 line-height: 1.5;
198 }
199 .import-bulk-section-body { padding: var(--space-4) var(--space-5); }
200
201 /* ─── Form ─── */
202 .import-bulk-field { margin-bottom: var(--space-4); }
203 .import-bulk-field:last-child { margin-bottom: 0; }
204 .import-bulk-field-label {
205 display: block;
206 font-size: 13px;
207 font-weight: 600;
208 color: var(--text-strong);
209 margin-bottom: 6px;
210 letter-spacing: -0.005em;
211 }
212 .import-bulk-field-label code {
213 font-size: 12px;
214 background: var(--bg-tertiary, var(--bg-secondary));
215 padding: 1px 5px;
216 border-radius: 4px;
217 font-weight: 500;
218 color: var(--text-muted);
219 }
220 .import-bulk-input,
221 .import-bulk-select {
222 width: 100%;
223 padding: 9px 12px;
224 font-size: 14px;
225 color: var(--text);
226 background: var(--bg);
227 border: 1px solid var(--border-strong);
228 border-radius: 8px;
229 outline: none;
230 transition: border-color 120ms ease, box-shadow 120ms ease;
231 font-family: var(--font-sans);
232 }
233 .import-bulk-input.is-mono { font-family: var(--font-mono); font-size: 13px; }
234 .import-bulk-input:focus,
235 .import-bulk-select:focus {
236 border-color: var(--accent);
237 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
238 }
239
240 /* ─── Toggle row (dry-run checkbox) ─── */
241 .import-bulk-toggle {
242 display: flex;
243 align-items: flex-start;
244 gap: 12px;
245 padding: 12px 14px;
246 border-radius: 10px;
247 border: 1px solid var(--border-subtle);
248 background: var(--bg-secondary);
249 transition: border-color 120ms ease;
250 }
251 .import-bulk-toggle:hover { border-color: var(--border); }
252 .import-bulk-toggle input[type="checkbox"] {
253 margin-top: 2px;
254 width: 16px; height: 16px;
255 accent-color: var(--accent);
256 cursor: pointer;
257 }
258 .import-bulk-toggle-text {
259 font-size: 13.5px;
260 color: var(--text);
261 line-height: 1.45;
262 }
263 .import-bulk-toggle-hint {
264 display: block;
265 margin-top: 3px;
266 font-size: 12.5px;
267 color: var(--text-muted);
268 }
269
270 .import-bulk-actions {
271 display: flex;
272 gap: 8px;
273 flex-wrap: wrap;
274 align-items: center;
275 margin-top: var(--space-4);
276 }
277
278 /* ─── Info / "what this does" panel ─── */
279 .import-bulk-info {
280 position: relative;
281 padding: 14px 16px 14px 18px;
282 margin-bottom: var(--space-5);
283 background: var(--bg-elevated);
284 border: 1px solid var(--border);
285 border-radius: 12px;
286 overflow: hidden;
287 }
288 .import-bulk-info::before {
289 content: '';
290 position: absolute;
291 left: 0; top: 0; bottom: 0;
292 width: 3px;
293 background: linear-gradient(180deg, #8c6dff 0%, #36c5d6 100%);
294 }
295 .import-bulk-info strong {
296 display: block;
297 font-family: var(--font-display);
298 font-weight: 700;
299 font-size: 14px;
300 color: var(--text-strong);
301 letter-spacing: -0.012em;
302 margin-bottom: 6px;
303 }
304 .import-bulk-info ul {
305 margin: 0;
306 padding-left: 18px;
307 font-size: 13px;
308 color: var(--text-muted);
309 line-height: 1.6;
310 }
311 .import-bulk-info code {
312 font-size: 12px;
313 background: var(--bg-secondary);
314 padding: 1px 5px;
315 border-radius: 4px;
316 color: var(--text);
317 }
318
319 /* ─── Summary strip (counts) ─── */
320 .import-bulk-summary {
321 display: flex;
322 align-items: center;
323 gap: var(--space-3);
324 flex-wrap: wrap;
325 padding: 12px 16px;
326 margin-bottom: var(--space-4);
327 background: var(--bg-elevated);
328 border: 1px solid var(--border);
329 border-radius: 12px;
330 font-size: 13.5px;
331 color: var(--text-muted);
332 }
333 .import-bulk-summary code {
334 font-size: 12.5px;
335 background: var(--bg-secondary);
336 padding: 1px 6px;
337 border-radius: 4px;
338 color: var(--text);
339 }
340 .import-bulk-summary-stat {
341 display: inline-flex;
342 align-items: center;
343 gap: 6px;
344 padding: 3px 10px;
345 border-radius: 9999px;
346 background: var(--bg-secondary);
347 border: 1px solid var(--border-subtle);
348 font-weight: 500;
349 color: var(--text-strong);
350 font-size: 12.5px;
351 }
352 .import-bulk-summary-stat .num { color: var(--accent); font-weight: 700; }
353
354 /* ─── Results table ─── */
355 .import-bulk-table-wrap {
356 background: var(--bg-elevated);
357 border: 1px solid var(--border);
358 border-radius: 14px;
359 overflow: hidden;
360 }
361 .import-bulk-table {
362 width: 100%;
363 border-collapse: collapse;
364 font-size: 13.5px;
365 }
366 .import-bulk-table thead tr {
367 background: var(--bg-secondary);
368 text-align: left;
369 }
370 .import-bulk-table th {
371 padding: 10px 14px;
372 border-bottom: 1px solid var(--border);
373 font-size: 11.5px;
374 font-weight: 600;
375 letter-spacing: 0.06em;
376 text-transform: uppercase;
377 color: var(--text-muted);
378 }
379 .import-bulk-table tbody tr {
380 transition: background 120ms ease;
381 }
382 .import-bulk-table tbody tr:hover {
383 background: rgba(255,255,255,0.018);
384 }
385 .import-bulk-table td {
386 padding: 9px 14px;
387 border-bottom: 1px solid var(--border-subtle);
388 vertical-align: middle;
389 }
390 .import-bulk-table tbody tr:last-child td { border-bottom: 0; }
391 .import-bulk-table-name {
392 font-family: var(--font-mono);
393 color: var(--text-strong);
394 font-size: 13px;
395 }
396 .import-bulk-table-notes {
397 color: var(--text-muted);
398 font-size: 12.5px;
399 }
400
401 /* ─── Status badge ─── */
402 .import-bulk-badge {
403 display: inline-flex;
404 align-items: center;
405 gap: 6px;
406 padding: 2px 9px;
407 border-radius: 9999px;
408 font-size: 12px;
409 font-weight: 600;
410 letter-spacing: -0.005em;
411 }
412 .import-bulk-badge-dot {
413 width: 6px; height: 6px;
414 border-radius: 50%;
415 flex-shrink: 0;
416 }
417 .import-bulk-badge-success {
418 color: #3fb950;
419 background: rgba(63,185,80,0.13);
420 border: 1px solid rgba(63,185,80,0.28);
421 }
422 .import-bulk-badge-success .import-bulk-badge-dot {
423 background: #3fb950;
424 box-shadow: 0 0 6px rgba(63,185,80,0.7);
425 }
426 .import-bulk-badge-warn {
427 color: #f0b429;
428 background: rgba(240,180,41,0.12);
429 border: 1px solid rgba(240,180,41,0.28);
430 }
431 .import-bulk-badge-warn .import-bulk-badge-dot { background: #f0b429; }
432 .import-bulk-badge-info {
433 color: #58a6ff;
434 background: rgba(88,166,255,0.12);
435 border: 1px solid rgba(88,166,255,0.28);
436 }
437 .import-bulk-badge-info .import-bulk-badge-dot { background: #58a6ff; }
438 .import-bulk-badge-error {
439 color: #f85149;
440 background: rgba(248,81,73,0.12);
441 border: 1px solid rgba(248,81,73,0.28);
442 }
443 .import-bulk-badge-error .import-bulk-badge-dot { background: #f85149; }
444
445 .import-bulk-empty {
446 padding: 18px;
447 text-align: center;
448 color: var(--text-muted);
449 font-size: 13.5px;
450 }
451
452 .import-bulk-callout {
453 margin-top: var(--space-4);
454 padding: 12px 14px;
455 background: var(--bg-elevated);
456 border: 1px solid var(--border);
457 border-left: 3px solid var(--accent);
458 border-radius: 10px;
459 font-size: 13px;
460 color: var(--text-muted);
461 }
462 .import-bulk-callout em { color: var(--text-strong); font-style: normal; font-weight: 600; }
463
464 .import-bulk-subhead {
465 font-family: var(--font-display);
466 font-size: 16px;
467 font-weight: 700;
468 letter-spacing: -0.014em;
469 color: var(--text-strong);
470 margin: var(--space-5) 0 var(--space-3);
471 }
472`;
473
46474/**
47475 * Paginate the GitHub "list org repos" endpoint. Caps at MAX_REPOS so a
48476 * single request can't fan out indefinitely. Throws on non-2xx so the
@@ -95,18 +523,43 @@ importBulkRoutes.get("/import/bulk", requireAuth, async (c) => {
95523
96524 return c.html(
97525 <Layout title="Bulk import from GitHub" user={user}>
98 <div style="max-width: 720px">
99 <h2 style="margin-bottom: 4px">Bulk import from GitHub</h2>
100 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 20px">
101 Paste a GitHub org + personal access token. Gluecron will clone
102 every repo into your namespace as a mirror.
103 </p>
104
105 {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
106
107 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; margin-bottom: 20px; font-size: 13px; color: var(--text-muted)">
108 <strong style="color: var(--text)">What this does</strong>
109 <ul style="margin: 8px 0 0 18px; line-height: 1.6">
526 <style dangerouslySetInnerHTML={{ __html: importBulkStyles }} />
527 <div class="import-bulk-container">
528 {/* ─── Hero ─── */}
529 <div class="import-bulk-hero">
530 <div class="import-bulk-hero-bg" aria-hidden="true">
531 <div class="import-bulk-hero-orb" />
532 </div>
533 <div class="import-bulk-hero-inner">
534 <div class="import-bulk-hero-eyebrow">
535 <span class="import-bulk-hero-eyebrow-dot" aria-hidden="true" />
536 Bulk migration
537 </div>
538 <h1 class="import-bulk-hero-title">
539 Import{" "}
540 <span class="gradient-text">many at once</span>.
541 </h1>
542 <p class="import-bulk-hero-sub">
543 Paste a GitHub org + personal access token. Gluecron clones every
544 repo into your namespace as a mirror — sequentially, with per-repo
545 status so one failure can't abort the batch.
546 </p>
547 </div>
548 </div>
549
550 {error && (
551 <div class="import-bulk-banner import-bulk-banner-error" role="alert">
552 <div class="import-bulk-banner-title">Bulk import didn't run</div>
553 <div class="import-bulk-banner-detail">
554 {decodeURIComponent(error)}
555 </div>
556 </div>
557 )}
558
559 {/* ─── What this does (info panel) ─── */}
560 <div class="import-bulk-info">
561 <strong>What this does</strong>
562 <ul>
110563 <li>
111564 Lists every repo in the org via the GitHub API
112565 (<code>/orgs/{"{org}"}/repos</code>, paginated).
@@ -120,70 +573,86 @@ importBulkRoutes.get("/import/bulk", requireAuth, async (c) => {
120573 the end. One failure does not abort the batch.
121574 </li>
122575 <li>
123 Hard caps: {MAX_REPOS} repos per run, 500MB per repo.
576 Hard caps: <code>{MAX_REPOS}</code> repos per run, 500MB per repo.
124577 </li>
125578 </ul>
126579 </div>
127580
128 <form method="post" action="/import/bulk">
129 <div class="form-group">
130 <label style="display:block; margin-bottom:4px; font-size:13px">
131 GitHub org
132 </label>
133 <input
134 type="text"
135 name="githubOrg"
136 required
137 placeholder="my-company"
138 aria-label="GitHub org"
139 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; width: 100%"
140 />
581 {/* ─── Form section ─── */}
582 <div class="import-bulk-section">
583 <div class="import-bulk-section-head">
584 <div class="import-bulk-section-eyebrow">Configure</div>
585 <h2 class="import-bulk-section-title">Org + token</h2>
586 <p class="import-bulk-section-desc">
587 Token is used only in this request — never stored.
588 </p>
141589 </div>
142
143 <div class="form-group">
144 <label style="display:block; margin-bottom:4px; font-size:13px">
145 GitHub personal access token (<code>repo:read</code> scope)
146 </label>
147 <input
148 type="password"
149 name="githubToken"
150 required
151 placeholder="ghp_xxxxxxxxxxxx"
152 autocomplete="off"
153 aria-label="GitHub personal access token"
154 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; font-family: var(--font-mono); width: 100%"
155 />
156 </div>
157
158 <div class="form-group">
159 <label style="display:block; margin-bottom:4px; font-size:13px">
160 Visibility filter
161 </label>
162 <select
163 name="visibility"
164 aria-label="Visibility filter"
165 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; width: 100%"
166 >
167 <option value="both" selected>Both (public + private)</option>
168 <option value="public">Public only</option>
169 <option value="private">Private only</option>
170 </select>
171 </div>
172
173 <div class="form-group" style="margin: 12px 0">
174 <label style="display:flex; align-items:center; gap:8px; font-size:13px">
175 <input type="checkbox" name="dryRun" value="1" checked />
176 Dry run — preview the list without cloning
177 </label>
590 <div class="import-bulk-section-body">
591 <form method="post" action="/import/bulk">
592 <div class="import-bulk-field">
593 <label class="import-bulk-field-label">GitHub org</label>
594 <input
595 type="text"
596 name="githubOrg"
597 required
598 placeholder="my-company"
599 aria-label="GitHub org"
600 class="import-bulk-input"
601 />
602 </div>
603
604 <div class="import-bulk-field">
605 <label class="import-bulk-field-label">
606 Personal access token <code>repo:read</code> scope
607 </label>
608 <input
609 type="password"
610 name="githubToken"
611 required
612 placeholder="ghp_xxxxxxxxxxxx"
613 autocomplete="off"
614 aria-label="GitHub personal access token"
615 class="import-bulk-input is-mono"
616 />
617 </div>
618
619 <div class="import-bulk-field">
620 <label class="import-bulk-field-label">Visibility filter</label>
621 <select
622 name="visibility"
623 aria-label="Visibility filter"
624 class="import-bulk-select"
625 >
626 <option value="both" selected>Both (public + private)</option>
627 <option value="public">Public only</option>
628 <option value="private">Private only</option>
629 </select>
630 </div>
631
632 <div class="import-bulk-field">
633 <label class="import-bulk-toggle">
634 <input type="checkbox" name="dryRun" value="1" checked />
635 <span class="import-bulk-toggle-text">
636 Dry run — preview the list without cloning
637 <span class="import-bulk-toggle-hint">
638 Recommended for the first pass. Uncheck once the preview
639 looks right.
640 </span>
641 </span>
642 </label>
643 </div>
644
645 <div class="import-bulk-actions">
646 <button type="submit" class="btn btn-primary">
647 Run bulk import
648 </button>
649 <a href="/import" class="btn">
650 Back to /import
651 </a>
652 </div>
653 </form>
178654 </div>
179
180 <button type="submit" class="btn btn-primary">
181 Run bulk import
182 </button>
183 <a href="/import" class="btn" style="margin-left: 8px">
184 Back to /import
185 </a>
186 </form>
655 </div>
187656 </div>
188657 </Layout>
189658 );
@@ -286,15 +755,44 @@ importBulkRoutes.post("/import/bulk", requireAuth, async (c) => {
286755 if (dryRun) {
287756 return c.html(
288757 <Layout title="Bulk import preview" user={user}>
289 <div style="max-width: 820px">
290 <h2 style="margin-bottom: 4px">Bulk import — dry-run preview</h2>
291 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 16px">
292 Org <code>{githubOrg}</code> · visibility <code>{visibility}</code>
293 {" · "}
294 {candidates.length} repo(s) would be imported
295 {oversized.length > 0 ? `, ${oversized.length} skipped (>500MB)` : ""}
296 .
297 </p>
758 <style dangerouslySetInnerHTML={{ __html: importBulkStyles }} />
759 <div class="import-bulk-container">
760 <div class="import-bulk-hero">
761 <div class="import-bulk-hero-bg" aria-hidden="true">
762 <div class="import-bulk-hero-orb" />
763 </div>
764 <div class="import-bulk-hero-inner">
765 <div class="import-bulk-hero-eyebrow">
766 <span class="import-bulk-hero-eyebrow-dot" aria-hidden="true" />
767 Preview only — nothing imported yet
768 </div>
769 <h1 class="import-bulk-hero-title">
770 Bulk import{" "}
771 <span class="gradient-text">dry run</span>.
772 </h1>
773 <p class="import-bulk-hero-sub">
774 This is what gluecron would clone from{" "}
775 <code>{githubOrg}</code> when you uncheck the dry-run box.
776 </p>
777 </div>
778 </div>
779
780 <div class="import-bulk-summary">
781 <span>
782 Org <code>{githubOrg}</code>
783 </span>
784 <span>
785 Visibility <code>{visibility}</code>
786 </span>
787 <span class="import-bulk-summary-stat">
788 <span class="num">{candidates.length}</span> to import
789 </span>
790 {oversized.length > 0 && (
791 <span class="import-bulk-summary-stat">
792 <span class="num">{oversized.length}</span> skipped (>500MB)
793 </span>
794 )}
795 </div>
298796
299797 <ResultsTable
300798 rows={candidates.map((r) => ({
@@ -308,7 +806,7 @@ importBulkRoutes.post("/import/bulk", requireAuth, async (c) => {
308806
309807 {oversized.length > 0 && (
310808 <>
311 <h3 style="margin-top:24px">Skipped — over 500MB</h3>
809 <h3 class="import-bulk-subhead">Skipped — over 500MB</h3>
312810 <ResultsTable
313811 rows={oversized.map((r) => ({
314812 name: sanitizeRepoName(r.name),
@@ -319,12 +817,11 @@ importBulkRoutes.post("/import/bulk", requireAuth, async (c) => {
319817 </>
320818 )}
321819
322 <div style="margin-top:20px; padding:12px 14px; background:var(--bg-secondary); border:1px solid var(--border); border-radius:var(--radius); font-size:13px">
323 Looks good? Go back and uncheck <em>Dry run</em> to actually
324 import.
820 <div class="import-bulk-callout">
821 Looks good? Go back and uncheck <em>Dry run</em> to actually import.
325822 </div>
326823
327 <div style="margin-top:16px">
824 <div class="import-bulk-actions">
328825 <a href="/import/bulk" class="btn btn-primary">
329826 Back to form
330827 </a>
@@ -369,19 +866,43 @@ importBulkRoutes.post("/import/bulk", requireAuth, async (c) => {
369866
370867 return c.html(
371868 <Layout title="Bulk import results" user={user}>
372 <div style="max-width: 820px">
373 <h2 style="margin-bottom: 4px">Bulk import — results</h2>
374 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 16px">
375 Org <code>{githubOrg}</code>: {counts["success"] || 0} imported,
376 {" "}
377 {counts["skipped-exists"] || 0} skipped (exists),
378 {" "}
379 {counts["failed"] || 0} failed.
380 </p>
869 <style dangerouslySetInnerHTML={{ __html: importBulkStyles }} />
870 <div class="import-bulk-container">
871 <div class="import-bulk-hero">
872 <div class="import-bulk-hero-bg" aria-hidden="true">
873 <div class="import-bulk-hero-orb" />
874 </div>
875 <div class="import-bulk-hero-inner">
876 <div class="import-bulk-hero-eyebrow">
877 <span class="import-bulk-hero-eyebrow-dot" aria-hidden="true" />
878 Bulk migration complete
879 </div>
880 <h1 class="import-bulk-hero-title">
881 Bulk import{" "}
882 <span class="gradient-text">results</span>.
883 </h1>
884 <p class="import-bulk-hero-sub">
885 From <code>{githubOrg}</code> into{" "}
886 <code>{user.username}</code>.
887 </p>
888 </div>
889 </div>
890
891 <div class="import-bulk-summary">
892 <span class="import-bulk-summary-stat">
893 <span class="num">{counts["success"] || 0}</span> imported
894 </span>
895 <span class="import-bulk-summary-stat">
896 <span class="num">{counts["skipped-exists"] || 0}</span> skipped
897 </span>
898 <span class="import-bulk-summary-stat">
899 <span class="num">{counts["failed"] || 0}</span> failed
900 </span>
901 </div>
381902
382903 <ResultsTable rows={results} />
383904
384 <div style="margin-top:20px; display:flex; gap:8px">
905 <div class="import-bulk-actions">
385906 <a href={`/${user.username}`} class="btn btn-primary">
386907 View my repositories
387908 </a>
@@ -403,62 +924,51 @@ function ResultsTable({
403924}) {
404925 if (rows.length === 0) {
405926 return (
406 <div style="padding:14px; background:var(--bg-secondary); border:1px solid var(--border); border-radius:var(--radius); font-size:13px; color:var(--text-muted)">
407 No rows.
927 <div class="import-bulk-table-wrap">
928 <div class="import-bulk-empty">No rows.</div>
408929 </div>
409930 );
410931 }
411932 return (
412 <table
413 style="width:100%; border-collapse:collapse; font-size:13px; background:var(--bg-secondary); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden"
414 >
415 <thead>
416 <tr style="background:var(--bg); text-align:left">
417 <th style="padding:8px 12px; border-bottom:1px solid var(--border)">
418 Name
419 </th>
420 <th style="padding:8px 12px; border-bottom:1px solid var(--border)">
421 Status
422 </th>
423 <th style="padding:8px 12px; border-bottom:1px solid var(--border)">
424 Notes
425 </th>
426 </tr>
427 </thead>
428 <tbody>
429 {rows.map((r) => (
933 <div class="import-bulk-table-wrap">
934 <table class="import-bulk-table">
935 <thead>
430936 <tr>
431 <td style="padding:6px 12px; border-bottom:1px solid var(--border); font-family:var(--font-mono)">
432 {r.name}
433 </td>
434 <td style="padding:6px 12px; border-bottom:1px solid var(--border)">
435 <StatusBadge status={r.status} />
436 </td>
437 <td style="padding:6px 12px; border-bottom:1px solid var(--border); color:var(--text-muted)">
438 {r.notes}
439 </td>
937 <th>Name</th>
938 <th>Status</th>
939 <th>Notes</th>
440940 </tr>
441 ))}
442 </tbody>
443 </table>
941 </thead>
942 <tbody>
943 {rows.map((r) => (
944 <tr>
945 <td class="import-bulk-table-name">{r.name}</td>
946 <td>
947 <StatusBadge status={r.status} />
948 </td>
949 <td class="import-bulk-table-notes">{r.notes}</td>
950 </tr>
951 ))}
952 </tbody>
953 </table>
954 </div>
444955 );
445956}
446957
447958function StatusBadge({ status }: { status: string }) {
448 const color =
959 const cls =
449960 status === "success"
450 ? "#3fb950"
961 ? "import-bulk-badge-success"
451962 : status === "skipped-exists"
452 ? "#f0b429"
963 ? "import-bulk-badge-warn"
453964 : status === "dry-run"
454 ? "#58a6ff"
965 ? "import-bulk-badge-info"
455966 : status === "too-large"
456 ? "#f0b429"
457 : "#f85149";
967 ? "import-bulk-badge-warn"
968 : "import-bulk-badge-error";
458969 return (
459 <span
460 style={`display:inline-block; padding:2px 8px; border-radius:10px; background:${color}22; color:${color}; font-size:12px; font-weight:600`}
461 >
970 <span class={`import-bulk-badge ${cls}`}>
971 <span class="import-bulk-badge-dot" aria-hidden="true" />
462972 {status}
463973 </span>
464974 );
Modifiedsrc/routes/import.tsx+555−103View fileUnifiedSplit
@@ -39,6 +39,362 @@ interface GitHubRepo {
3939 language: string | null;
4040}
4141
42// ─── PAGE-SCOPED CSS ─────────────────────────────────────────
43// All classes prefixed with .import- so the block cannot bleed into
44// neighbouring routes. Mirrors the dashboard-hero + settings polish.
45const importStyles = `
46 .import-container { max-width: 880px; margin: 0 auto; }
47
48 /* ─── Hero ─── */
49 .import-hero {
50 position: relative;
51 margin-bottom: var(--space-6);
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 .import-hero::before {
59 content: '';
60 position: absolute;
61 top: 0; left: 0; right: 0;
62 height: 2px;
63 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
64 opacity: 0.75;
65 pointer-events: none;
66 }
67 .import-hero-bg {
68 position: absolute;
69 inset: -20% -10% auto auto;
70 width: 360px; height: 360px;
71 pointer-events: none;
72 z-index: 0;
73 }
74 .import-hero-orb {
75 position: absolute;
76 inset: 0;
77 background: radial-gradient(circle, rgba(140,109,255,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
78 filter: blur(80px);
79 opacity: 0.7;
80 animation: importHeroOrb 14s ease-in-out infinite;
81 }
82 @keyframes importHeroOrb {
83 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
84 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
85 }
86 @media (prefers-reduced-motion: reduce) {
87 .import-hero-orb { animation: none; }
88 }
89 .import-hero-inner {
90 position: relative;
91 z-index: 1;
92 max-width: 640px;
93 }
94 .import-hero-eyebrow {
95 font-size: 13px;
96 color: var(--text-muted);
97 margin-bottom: var(--space-2);
98 letter-spacing: -0.005em;
99 }
100 .import-hero-eyebrow-dot {
101 display: inline-block;
102 width: 6px; height: 6px;
103 border-radius: 50%;
104 background: var(--accent);
105 box-shadow: 0 0 8px rgba(140,109,255,0.6);
106 margin-right: 8px;
107 vertical-align: 1px;
108 }
109 .import-hero-title {
110 font-size: clamp(28px, 4vw, 40px);
111 font-family: var(--font-display);
112 font-weight: 800;
113 letter-spacing: -0.028em;
114 line-height: 1.05;
115 margin: 0 0 var(--space-2);
116 color: var(--text-strong);
117 }
118 .import-hero-title .gradient-text {
119 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
120 -webkit-background-clip: text;
121 background-clip: text;
122 -webkit-text-fill-color: transparent;
123 color: transparent;
124 }
125 .import-hero-sub {
126 font-size: 15px;
127 color: var(--text-muted);
128 margin: 0;
129 line-height: 1.55;
130 }
131
132 /* ─── Bulk CTA strip (between hero and options) ─── */
133 .import-bulk-cta {
134 position: relative;
135 display: flex;
136 align-items: center;
137 justify-content: space-between;
138 gap: var(--space-4);
139 padding: 14px 18px;
140 margin-bottom: var(--space-5);
141 background: var(--bg-elevated);
142 border: 1px solid var(--border);
143 border-radius: 14px;
144 text-decoration: none;
145 color: inherit;
146 overflow: hidden;
147 transition: border-color 140ms ease, transform 140ms ease;
148 }
149 .import-bulk-cta:hover {
150 border-color: rgba(140,109,255,0.45);
151 transform: translateY(-1px);
152 }
153 .import-bulk-cta::before {
154 content: '';
155 position: absolute;
156 left: 0; top: 0; bottom: 0;
157 width: 3px;
158 background: linear-gradient(180deg, #8c6dff 0%, #36c5d6 100%);
159 }
160 .import-bulk-cta-text strong {
161 display: block;
162 font-family: var(--font-display);
163 font-weight: 700;
164 font-size: 15px;
165 color: var(--text-strong);
166 letter-spacing: -0.012em;
167 }
168 .import-bulk-cta-text .import-bulk-cta-sub {
169 font-size: 13px;
170 color: var(--text-muted);
171 margin-top: 3px;
172 line-height: 1.4;
173 }
174 .import-bulk-cta-arrow {
175 flex-shrink: 0;
176 font-size: 18px;
177 color: var(--accent);
178 transition: transform 140ms ease;
179 }
180 .import-bulk-cta:hover .import-bulk-cta-arrow { transform: translateX(3px); }
181
182 /* ─── Banners ─── */
183 .import-banner {
184 position: relative;
185 padding: 14px 16px 14px 44px;
186 margin-bottom: var(--space-5);
187 border-radius: 12px;
188 border: 1px solid var(--border);
189 background: var(--bg-elevated);
190 font-size: 14px;
191 line-height: 1.5;
192 }
193 .import-banner::before {
194 content: '';
195 position: absolute;
196 left: 14px; top: 18px;
197 width: 14px; height: 14px;
198 border-radius: 50%;
199 }
200 .import-banner-success {
201 border-color: rgba(63, 185, 80, 0.32);
202 background: linear-gradient(180deg, rgba(63,185,80,0.06) 0%, var(--bg-elevated) 100%);
203 }
204 .import-banner-success::before {
205 background: radial-gradient(circle, #3fb950 30%, transparent 70%);
206 box-shadow: 0 0 10px rgba(63,185,80,0.5);
207 }
208 .import-banner-error {
209 border-color: rgba(248, 81, 73, 0.32);
210 background: linear-gradient(180deg, rgba(248,81,73,0.06) 0%, var(--bg-elevated) 100%);
211 }
212 .import-banner-error::before {
213 background: radial-gradient(circle, #f85149 30%, transparent 70%);
214 box-shadow: 0 0 10px rgba(248,81,73,0.5);
215 }
216 .import-banner-title { font-weight: 600; color: var(--text-strong); }
217 .import-banner-detail { color: var(--text-muted); margin-top: 4px; font-size: 13.5px; }
218 .import-banner-actions { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; }
219
220 /* ─── Progress card with step pipeline ─── */
221 .import-progress-card {
222 position: relative;
223 margin-bottom: var(--space-5);
224 padding: 18px 18px 16px;
225 background: var(--bg-elevated);
226 border: 1px solid var(--border);
227 border-radius: 14px;
228 overflow: hidden;
229 }
230 .import-progress-card::before {
231 content: '';
232 position: absolute;
233 top: 0; left: 0; right: 0;
234 height: 2px;
235 background: linear-gradient(90deg, #8c6dff 0%, #36c5d6 50%, #8c6dff 100%);
236 background-size: 200% 100%;
237 animation: importProgressShimmer 2.2s linear infinite;
238 pointer-events: none;
239 }
240 @keyframes importProgressShimmer {
241 0% { background-position: 0% 0%; }
242 100% { background-position: 200% 0%; }
243 }
244 @media (prefers-reduced-motion: reduce) {
245 .import-progress-card::before { animation: none; }
246 }
247 .import-progress-title {
248 font-family: var(--font-display);
249 font-weight: 700;
250 font-size: 16px;
251 letter-spacing: -0.012em;
252 color: var(--text-strong);
253 margin: 0 0 4px;
254 }
255 .import-progress-sub {
256 font-size: 13px;
257 color: var(--text-muted);
258 margin: 0 0 14px;
259 }
260 .import-steps {
261 display: grid;
262 grid-template-columns: repeat(4, 1fr);
263 gap: 8px;
264 }
265 .import-step {
266 position: relative;
267 padding: 10px 12px;
268 background: var(--bg-secondary);
269 border: 1px solid var(--border-subtle);
270 border-radius: 10px;
271 font-size: 12.5px;
272 color: var(--text-muted);
273 display: flex;
274 align-items: center;
275 gap: 8px;
276 }
277 .import-step-dot {
278 width: 16px; height: 16px;
279 border-radius: 50%;
280 background: var(--bg-tertiary, #1a1d2a);
281 border: 1.5px solid var(--border-strong);
282 flex-shrink: 0;
283 position: relative;
284 }
285 .import-step.is-active {
286 border-color: rgba(140,109,255,0.45);
287 color: var(--text-strong);
288 background: rgba(140,109,255,0.07);
289 }
290 .import-step.is-active .import-step-dot {
291 background: var(--accent);
292 border-color: var(--accent);
293 box-shadow: 0 0 10px rgba(140,109,255,0.6);
294 animation: importStepPulse 1.4s ease-in-out infinite;
295 }
296 @keyframes importStepPulse {
297 0%, 100% { box-shadow: 0 0 8px rgba(140,109,255,0.4); }
298 50% { box-shadow: 0 0 14px rgba(140,109,255,0.85); }
299 }
300 @media (prefers-reduced-motion: reduce) {
301 .import-step.is-active .import-step-dot { animation: none; }
302 }
303 .import-step-label { font-weight: 500; }
304 @media (max-width: 640px) {
305 .import-steps { grid-template-columns: repeat(2, 1fr); }
306 }
307
308 /* ─── Option cards ─── */
309 .import-options {
310 display: grid;
311 gap: var(--space-4);
312 }
313 .import-option {
314 position: relative;
315 background: var(--bg-elevated);
316 border: 1px solid var(--border);
317 border-radius: 14px;
318 padding: var(--space-4) var(--space-5);
319 transition: border-color 140ms ease, box-shadow 140ms ease;
320 }
321 .import-option:hover { border-color: var(--border-strong); }
322 .import-option-head {
323 display: flex;
324 align-items: center;
325 gap: 10px;
326 margin-bottom: 4px;
327 }
328 .import-option-badge {
329 display: inline-flex;
330 align-items: center;
331 justify-content: center;
332 width: 24px; height: 24px;
333 border-radius: 8px;
334 background: linear-gradient(135deg, rgba(140,109,255,0.18) 0%, rgba(54,197,214,0.18) 100%);
335 color: var(--accent);
336 font-size: 12px;
337 font-weight: 700;
338 border: 1px solid rgba(140,109,255,0.28);
339 }
340 .import-option-title {
341 font-family: var(--font-display);
342 font-size: 16px;
343 font-weight: 700;
344 letter-spacing: -0.014em;
345 color: var(--text-strong);
346 margin: 0;
347 }
348 .import-option-desc {
349 font-size: 13.5px;
350 color: var(--text-muted);
351 line-height: 1.5;
352 margin: 0 0 14px;
353 }
354 .import-option-form .import-row {
355 display: flex;
356 gap: 8px;
357 }
358 .import-input {
359 flex: 1;
360 width: 100%;
361 padding: 9px 12px;
362 background: var(--bg);
363 border: 1px solid var(--border-strong);
364 border-radius: 8px;
365 color: var(--text);
366 font-size: 14px;
367 font-family: var(--font-sans);
368 outline: none;
369 transition: border-color 120ms ease, box-shadow 120ms ease;
370 }
371 .import-input:focus {
372 border-color: var(--accent);
373 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
374 }
375 .import-input.is-mono { font-family: var(--font-mono); font-size: 13px; }
376 .import-field { margin-top: 10px; }
377 .import-field-label {
378 display: block;
379 font-size: 12.5px;
380 font-weight: 600;
381 color: var(--text-strong);
382 margin-bottom: 4px;
383 letter-spacing: -0.005em;
384 }
385 .import-field-hint {
386 font-size: 12px;
387 color: var(--text-muted);
388 margin-top: 4px;
389 line-height: 1.45;
390 }
391
392 @media (max-width: 600px) {
393 .import-option-form .import-row { flex-direction: column; }
394 .import-option-form .import-row .btn { width: 100%; }
395 }
396`;
397
42398// ─── IMPORT PAGE ─────────────────────────────────────────────
43399
44400importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
@@ -50,11 +406,19 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
50406 // Inline progress banner: the clone subprocess can take 30+s for big
51407 // repos, so give the user visible feedback while the POST is in flight.
52408 // Pure client-side — no extra routes, no websockets, no polling.
409 // Polished step pipeline: clone → analyze → enable gates → done.
53410 const progressScript = `
54411 (function () {
55412 var forms = document.querySelectorAll('form[data-import-form]');
56413 var banner = document.getElementById('import-progress');
57414 if (!banner) return;
415 var steps = banner.querySelectorAll('[data-step]');
416 function setStep(idx) {
417 steps.forEach(function (el, i) {
418 if (i <= idx) el.classList.add('is-active');
419 else el.classList.remove('is-active');
420 });
421 }
58422 forms.forEach(function (form) {
59423 form.addEventListener('submit', function () {
60424 // Validate non-empty required fields before showing progress.
@@ -63,6 +427,11 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
63427 if (!req[i].value || !req[i].value.trim()) return;
64428 }
65429 banner.style.display = 'block';
430 setStep(0);
431 // Walk through 'analyze' and 'gates' steps on a timer so the
432 // pipeline visually advances even before the redirect lands.
433 setTimeout(function () { setStep(1); }, 1800);
434 setTimeout(function () { setStep(2); }, 4200);
66435 var btns = form.querySelectorAll('button[type="submit"]');
67436 btns.forEach(function (b) {
68437 b.disabled = true;
@@ -77,31 +446,52 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
77446
78447 return c.html(
79448 <Layout title="Import from GitHub" user={user}>
80 <div style="max-width: 700px">
81 <h2 style="margin-bottom: 4px">Import from GitHub</h2>
82 <p style="color: var(--text-muted); font-size: 14px; margin-bottom: 20px">
83 Migrate your repositories from GitHub to gluecron automatically.
84 All branches, all history, all code — one click.
85 </p>
86 <a
87 href="/import/bulk"
88 style="display: block; background: var(--bg-secondary); border: 1px solid var(--border); border-left: 3px solid var(--green); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 20px; font-size: 14px; text-decoration: none; color: inherit"
89 >
90 <strong>Migrating a whole org? Try the bulk importer →</strong>
91 <div style="color: var(--text-muted); margin-top: 4px">
92 Paste a GitHub org name + token and clone every repo in one shot.
449 <style dangerouslySetInnerHTML={{ __html: importStyles }} />
450 <div class="import-container">
451 {/* ─── Hero ─── */}
452 <div class="import-hero">
453 <div class="import-hero-bg" aria-hidden="true">
454 <div class="import-hero-orb" />
455 </div>
456 <div class="import-hero-inner">
457 <div class="import-hero-eyebrow">
458 <span class="import-hero-eyebrow-dot" aria-hidden="true" />
459 GitHub migration
460 </div>
461 <h1 class="import-hero-title">
462 Import from{" "}
463 <span class="gradient-text">GitHub</span>.
464 </h1>
465 <p class="import-hero-sub">
466 Migrate your repositories from GitHub to gluecron automatically —
467 all branches, all history, all code. One click, walk away, come
468 back to everything migrated.
469 </p>
470 </div>
471 </div>
472
473 {/* ─── Bulk-import shortcut ─── */}
474 <a href="/import/bulk" class="import-bulk-cta">
475 <div class="import-bulk-cta-text">
476 <strong>Migrating a whole org? Try the bulk importer.</strong>
477 <div class="import-bulk-cta-sub">
478 Paste a GitHub org name + token and clone every repo in one shot.
479 </div>
93480 </div>
481 <div class="import-bulk-cta-arrow" aria-hidden="true">→</div>
94482 </a>
483
484 {/* ─── Success / error banners ─── */}
95485 {success && (
96 <div class="auth-success">
97 {decodeURIComponent(success)}
486 <div class="import-banner import-banner-success" role="status">
487 <div class="import-banner-title">{decodeURIComponent(success)}</div>
98488 {imported && (
99 <div style="margin-top: 8px">
489 <div class="import-banner-detail">
100490 Successfully imported {decodeURIComponent(imported)} repositories.
101491 </div>
102492 )}
103 <div style="margin-top: 10px">
104 <a href={`/${user.username}`} class="btn btn-primary" style="margin-right: 8px">
493 <div class="import-banner-actions">
494 <a href={`/${user.username}`} class="btn btn-primary">
105495 View my repositories
106496 </a>
107497 <a href="/explore" class="btn">Explore</a>
@@ -109,106 +499,168 @@ importRoutes.get("/import", requireAuth, requireAdmin, async (c) => {
109499 </div>
110500 )}
111501 {error && (
112 <div class="auth-error">{decodeURIComponent(error)}</div>
502 <div class="import-banner import-banner-error" role="alert">
503 <div class="import-banner-title">Import didn't complete</div>
504 <div class="import-banner-detail">{decodeURIComponent(error)}</div>
505 </div>
113506 )}
114507
508 {/* ─── In-flight progress pipeline ─── */}
115509 <div
116510 id="import-progress"
511 class="import-progress-card"
117512 role="status"
118513 aria-live="polite"
119 style="display: none; background: var(--bg-secondary); border: 1px solid var(--border); border-left: 3px solid var(--yellow); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 20px; font-size: 14px"
514 style="display: none"
120515 >
121 <strong>Import in progress…</strong>
122 <div style="color: var(--text-muted); margin-top: 4px">
123 Cloning from GitHub. Large repositories can take 30+ seconds — don't close this tab.
516 <div class="import-progress-title">Import in progress</div>
517 <div class="import-progress-sub">
518 Large repositories can take 30+ seconds — don't close this tab.
124519 </div>
125 </div>
126
127 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 24px">
128 <h3 style="margin-bottom: 12px">Option 1: Import by username</h3>
129 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
130 Import all public repositories from a GitHub user or organization.
131 </p>
132 <form method="post" action="/import/github/user" data-import-form>
133 <div style="display: flex; gap: 8px">
134 <input
135 type="text"
136 name="github_username"
137 required
138 placeholder="GitHub username or org"
139 aria-label="GitHub username or org"
140 style="flex: 1; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
141 />
142 <button type="submit" class="btn btn-primary">
143 Import all repos
144 </button>
520 <div class="import-steps">
521 <div class="import-step" data-step="0">
522 <span class="import-step-dot" aria-hidden="true" />
523 <span class="import-step-label">Clone</span>
145524 </div>
146 </form>
147 </div>
148
149 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 24px">
150 <h3 style="margin-bottom: 12px">Option 2: Import single repo</h3>
151 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
152 Import a specific repository by URL (https, ssh, or owner/repo).
153 </p>
154 <form method="post" action="/import/github/repo" data-import-form>
155 <div style="display: flex; gap: 8px">
156 <input
157 type="text"
158 name="repo_url"
159 required
160 placeholder="https://github.com/owner/repo"
161 aria-label="Repository URL"
162 style="flex: 1; padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px"
163 />
164 <button type="submit" class="btn btn-primary">
165 Import
166 </button>
525 <div class="import-step" data-step="1">
526 <span class="import-step-dot" aria-hidden="true" />
527 <span class="import-step-label">Analyze</span>
528 </div>
529 <div class="import-step" data-step="2">
530 <span class="import-step-dot" aria-hidden="true" />
531 <span class="import-step-label">Enable gates</span>
167532 </div>
168 <div style="margin-top: 8px">
169 <input
170 type="password"
171 name="github_token"
172 placeholder="Optional: GitHub PAT to also migrate Actions secret names"
173 aria-label="Optional GitHub personal access token for secrets migration"
174 autocomplete="off"
175 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 13px; font-family: var(--font-mono); width: 100%"
176 />
533 <div class="import-step" data-step="3">
534 <span class="import-step-dot" aria-hidden="true" />
535 <span class="import-step-label">Done</span>
177536 </div>
178 </form>
537 </div>
179538 </div>
180539
181 <div style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px">
182 <h3 style="margin-bottom: 12px">Option 3: Import with token (private repos)</h3>
183 <p style="font-size: 13px; color: var(--text-muted); margin-bottom: 12px">
184 Use a GitHub personal access token to import private repositories too.
185 Generate one at github.com → Settings → Developer settings → Personal access tokens.
186 </p>
187 <form method="post" action="/import/github/user" data-import-form>
188 <div class="form-group">
189 <input
190 type="text"
191 name="github_username"
192 required
193 placeholder="GitHub username"
194 aria-label="GitHub username"
195 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; width: 100%"
196 />
540 {/* ─── Option cards ─── */}
541 <div class="import-options">
542 <div class="import-option">
543 <div class="import-option-head">
544 <span class="import-option-badge" aria-hidden="true">1</span>
545 <h3 class="import-option-title">Import by username</h3>
546 </div>
547 <p class="import-option-desc">
548 Import all public repositories from a GitHub user or organization.
549 </p>
550 <form
551 class="import-option-form"
552 method="post"
553 action="/import/github/user"
554 data-import-form
555 >
556 <div class="import-row">
557 <input
558 type="text"
559 name="github_username"
560 required
561 placeholder="GitHub username or org"
562 aria-label="GitHub username or org"
563 class="import-input"
564 />
565 <button type="submit" class="btn btn-primary">
566 Import all repos
567 </button>
568 </div>
569 </form>
570 </div>
571
572 <div class="import-option">
573 <div class="import-option-head">
574 <span class="import-option-badge" aria-hidden="true">2</span>
575 <h3 class="import-option-title">Import a single repo</h3>
197576 </div>
198 <div class="form-group">
199 <input
200 type="password"
201 name="github_token"
202 required
203 placeholder="ghp_xxxxxxxxxxxx (GitHub personal access token)"
204 aria-label="GitHub personal access token"
205 style="padding: 8px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; font-family: var(--font-mono); width: 100%"
206 />
577 <p class="import-option-desc">
578 Import one specific repository by URL (https, ssh, or owner/repo).
579 </p>
580 <form
581 class="import-option-form"
582 method="post"
583 action="/import/github/repo"
584 data-import-form
585 >
586 <div class="import-row">
587 <input
588 type="text"
589 name="repo_url"
590 required
591 placeholder="https://github.com/owner/repo"
592 aria-label="Repository URL"
593 class="import-input"
594 />
595 <button type="submit" class="btn btn-primary">
596 Import
597 </button>
598 </div>
599 <div class="import-field">
600 <input
601 type="password"
602 name="github_token"
603 placeholder="Optional: GitHub PAT to also migrate Actions secret names"
604 aria-label="Optional GitHub personal access token for secrets migration"
605 autocomplete="off"
606 class="import-input is-mono"
607 />
608 <div class="import-field-hint">
609 Token is only used in this request — never stored.
610 </div>
611 </div>
612 </form>
613 </div>
614
615 <div class="import-option">
616 <div class="import-option-head">
617 <span class="import-option-badge" aria-hidden="true">3</span>
618 <h3 class="import-option-title">
619 Import with token (private repos)
620 </h3>
207621 </div>
208 <button type="submit" class="btn btn-primary">
209 Import all repos (public + private)
210 </button>
211 </form>
622 <p class="import-option-desc">
623 Use a GitHub personal access token to import private repositories
624 too. Generate one at{" "}
625 <strong>github.com → Settings → Developer settings → Personal access tokens</strong>.
626 </p>
627 <form
628 class="import-option-form"
629 method="post"
630 action="/import/github/user"
631 data-import-form
632 >
633 <div class="import-field">
634 <label class="import-field-label">GitHub username</label>
635 <input
636 type="text"
637 name="github_username"
638 required
639 placeholder="GitHub username"
640 aria-label="GitHub username"
641 class="import-input"
642 />
643 </div>
644 <div class="import-field">
645 <label class="import-field-label">
646 Personal access token (repo scope)
647 </label>
648 <input
649 type="password"
650 name="github_token"
651 required
652 placeholder="ghp_xxxxxxxxxxxx"
653 aria-label="GitHub personal access token"
654 class="import-input is-mono"
655 />
656 </div>
657 <div class="import-field">
658 <button type="submit" class="btn btn-primary">
659 Import all repos (public + private)
660 </button>
661 </div>
662 </form>
663 </div>
212664 </div>
213665 </div>
214666 <script dangerouslySetInnerHTML={{ __html: progressScript }} />
215667