Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commite2e0279unknown_key

polish(dep-updater): 2026 cards + status pills

polish(dep-updater): 2026 cards + status pills

Picked up from parallel polish agent's worktree. Dep update tasks
rendered as polished cards with status pill, version mono pills,
tabular-nums for run counts, and empty-state with orb.

Scoped .dep-* CSS. All actions/queries preserved.
Claude committed on May 25, 2026Parent: 93812e4
1 file changed+647114e2e0279bef08662b4f1909c8bf582e9654349943
1 changed file+647−114
Modifiedsrc/routes/dep-updater.tsx+647−114View fileUnifiedSplit
55 * POST /:owner/:repo/settings/dep-updater/run — kicks off a run (fire & forget)
66 *
77 * Owner-only. See `src/lib/dep-updater.ts` for the orchestrator.
8 *
9 * 2026 polish: scoped `.dep-*` class system mirrors the gradient hero and
10 * card patterns from admin-integrations.tsx and admin-ops.tsx. Bump tables
11 * use tabular-nums + mono for versions; PR refs + branch names are mono.
12 * RepoHeader + IssueNav above are untouched — only the content beneath
13 * the nav is restyled.
814 */
915
1016import { Hono } from "hono";
2329
2430depUpdater.use("*", softAuth);
2531
32/* ─────────────────────────────────────────────────────────────────────────
33 * Scoped CSS — every class prefixed `.dep-*`. Hero with gradient hairline +
34 * orb, run cards with status pill / tabular-nums timing, mono version cells.
35 * ───────────────────────────────────────────────────────────────────── */
36const depStyles = `
37 .dep-wrap { max-width: 1100px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
38
39 /* ─── Hero ─── */
40 .dep-hero {
41 position: relative;
42 margin-bottom: var(--space-5);
43 padding: var(--space-5) var(--space-6);
44 background: var(--bg-elevated);
45 border: 1px solid var(--border);
46 border-radius: 16px;
47 overflow: hidden;
48 }
49 .dep-hero::before {
50 content: '';
51 position: absolute;
52 top: 0; left: 0; right: 0;
53 height: 2px;
54 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
55 opacity: 0.7;
56 pointer-events: none;
57 }
58 .dep-hero-orb {
59 position: absolute;
60 inset: -20% -10% auto auto;
61 width: 380px; height: 380px;
62 background: radial-gradient(circle, rgba(140,109,255,0.20), rgba(54,197,214,0.10) 45%, transparent 70%);
63 filter: blur(80px);
64 opacity: 0.7;
65 pointer-events: none;
66 z-index: 0;
67 }
68 .dep-hero-inner {
69 position: relative;
70 z-index: 1;
71 display: flex;
72 align-items: flex-end;
73 justify-content: space-between;
74 gap: var(--space-4);
75 flex-wrap: wrap;
76 }
77 .dep-hero-text { max-width: 720px; flex: 1; min-width: 240px; }
78 .dep-eyebrow {
79 display: inline-flex;
80 align-items: center;
81 gap: 8px;
82 text-transform: uppercase;
83 font-family: var(--font-mono);
84 font-size: 11px;
85 letter-spacing: 0.16em;
86 color: var(--text-muted);
87 font-weight: 600;
88 margin-bottom: 10px;
89 }
90 .dep-eyebrow-dot {
91 width: 8px; height: 8px;
92 border-radius: 9999px;
93 background: linear-gradient(135deg, #8c6dff, #36c5d6);
94 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
95 }
96 .dep-title {
97 font-family: var(--font-display);
98 font-size: clamp(28px, 4vw, 40px);
99 font-weight: 800;
100 letter-spacing: -0.028em;
101 line-height: 1.05;
102 margin: 0 0 var(--space-2);
103 color: var(--text-strong);
104 }
105 .dep-title-grad {
106 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
107 -webkit-background-clip: text;
108 background-clip: text;
109 -webkit-text-fill-color: transparent;
110 color: transparent;
111 }
112 .dep-sub {
113 font-size: 15px;
114 color: var(--text-muted);
115 margin: 0;
116 line-height: 1.5;
117 max-width: 620px;
118 }
119 .dep-sub code {
120 font-family: var(--font-mono);
121 font-size: 13px;
122 background: var(--bg-tertiary);
123 padding: 1px 5px;
124 border-radius: 4px;
125 }
126 .dep-hero-cta {
127 display: inline-flex;
128 align-items: center;
129 gap: 7px;
130 padding: 10px 18px;
131 font-size: 13.5px;
132 font-weight: 600;
133 border-radius: 10px;
134 border: 1px solid transparent;
135 cursor: pointer;
136 font: inherit;
137 line-height: 1;
138 white-space: nowrap;
139 text-decoration: none;
140 color: #ffffff;
141 background: linear-gradient(135deg, #8c6dff 0%, #36c5d6 100%);
142 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
143 transition: transform 120ms ease, box-shadow 120ms ease;
144 }
145 .dep-hero-cta:hover {
146 transform: translateY(-1px);
147 box-shadow: 0 10px 24px -8px rgba(140,109,255,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
148 text-decoration: none;
149 color: #ffffff;
150 }
151
152 /* ─── Section cards ─── */
153 .dep-section {
154 margin-bottom: var(--space-5);
155 background: var(--bg-elevated);
156 border: 1px solid var(--border);
157 border-radius: 14px;
158 overflow: hidden;
159 position: relative;
160 }
161 .dep-section::before {
162 content: '';
163 position: absolute;
164 top: 0; left: 0; right: 0;
165 height: 2px;
166 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
167 opacity: 0.45;
168 pointer-events: none;
169 }
170 .dep-section-head {
171 padding: var(--space-4) var(--space-5);
172 border-bottom: 1px solid var(--border);
173 display: flex;
174 align-items: flex-start;
175 justify-content: space-between;
176 gap: var(--space-3);
177 flex-wrap: wrap;
178 }
179 .dep-section-title {
180 margin: 0;
181 font-family: var(--font-display);
182 font-size: 16px;
183 font-weight: 700;
184 letter-spacing: -0.018em;
185 color: var(--text-strong);
186 display: flex;
187 align-items: center;
188 gap: 10px;
189 }
190 .dep-section-title-icon {
191 display: inline-flex;
192 align-items: center;
193 justify-content: center;
194 width: 26px; height: 26px;
195 border-radius: 8px;
196 background: rgba(140,109,255,0.12);
197 color: #b69dff;
198 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.28);
199 flex-shrink: 0;
200 }
201 .dep-section-sub {
202 margin: 6px 0 0 36px;
203 font-size: 12.5px;
204 color: var(--text-muted);
205 line-height: 1.45;
206 }
207 .dep-section-body { padding: var(--space-4) var(--space-5); }
208
209 /* ─── Run card list ─── */
210 .dep-run-list { display: flex; flex-direction: column; gap: 10px; }
211 .dep-run-card {
212 padding: 14px;
213 background: rgba(255,255,255,0.018);
214 border: 1px solid var(--border);
215 border-radius: 12px;
216 transition: border-color 120ms ease, background 120ms ease;
217 }
218 .dep-run-card:hover {
219 border-color: var(--border-strong);
220 background: rgba(255,255,255,0.03);
221 }
222 .dep-run-head {
223 display: flex;
224 align-items: center;
225 gap: 10px;
226 flex-wrap: wrap;
227 }
228 .dep-run-stamp {
229 font-family: var(--font-mono);
230 font-size: 12px;
231 color: var(--text-muted);
232 font-variant-numeric: tabular-nums;
233 }
234 .dep-run-pr {
235 font-family: var(--font-mono);
236 font-size: 12.5px;
237 color: var(--accent);
238 text-decoration: none;
239 padding: 2px 8px;
240 border-radius: 6px;
241 background: rgba(140,109,255,0.10);
242 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.28);
243 font-variant-numeric: tabular-nums;
244 }
245 .dep-run-pr:hover { text-decoration: none; background: rgba(140,109,255,0.18); }
246 .dep-run-branch {
247 font-family: var(--font-mono);
248 font-size: 12px;
249 color: var(--text);
250 background: var(--bg-tertiary);
251 padding: 2px 7px;
252 border-radius: 5px;
253 border: 1px solid var(--border);
254 }
255 .dep-run-err {
256 margin-top: 10px;
257 padding: 8px 10px;
258 background: rgba(248,113,113,0.06);
259 border: 1px solid rgba(248,113,113,0.28);
260 border-radius: 8px;
261 font-family: var(--font-mono);
262 font-size: 12px;
263 color: #fecaca;
264 line-height: 1.45;
265 }
266 .dep-bumps { margin-top: 10px; }
267 .dep-bumps > summary {
268 cursor: pointer;
269 font-size: 12.5px;
270 color: var(--text-muted);
271 display: inline-flex;
272 align-items: center;
273 gap: 6px;
274 padding: 4px 9px;
275 border-radius: 9999px;
276 background: rgba(255,255,255,0.025);
277 border: 1px solid var(--border);
278 user-select: none;
279 list-style: none;
280 }
281 .dep-bumps > summary::-webkit-details-marker { display: none; }
282 .dep-bumps > summary::before {
283 content: '▸';
284 font-size: 10px;
285 color: var(--text-muted);
286 transition: transform 120ms ease;
287 display: inline-block;
288 }
289 .dep-bumps[open] > summary::before { transform: rotate(90deg); }
290 .dep-bumps[open] > summary {
291 color: var(--text-strong);
292 border-color: var(--border-strong);
293 }
294
295 /* ─── Bump table ─── */
296 .dep-table {
297 width: 100%;
298 margin-top: 12px;
299 border-collapse: separate;
300 border-spacing: 0;
301 font-size: 12.5px;
302 border: 1px solid var(--border);
303 border-radius: 10px;
304 overflow: hidden;
305 }
306 .dep-table thead th {
307 text-align: left;
308 padding: 9px 12px;
309 font-size: 11px;
310 text-transform: uppercase;
311 letter-spacing: 0.06em;
312 color: var(--text-muted);
313 font-weight: 600;
314 background: rgba(255,255,255,0.025);
315 border-bottom: 1px solid var(--border);
316 }
317 .dep-table tbody td {
318 padding: 9px 12px;
319 border-bottom: 1px solid var(--border);
320 vertical-align: middle;
321 }
322 .dep-table tbody tr:last-child td { border-bottom: none; }
323 .dep-table tbody tr:hover td { background: rgba(255,255,255,0.02); }
324 .dep-pkg {
325 font-family: var(--font-mono);
326 font-size: 12px;
327 color: var(--text-strong);
328 }
329 .dep-ver {
330 font-family: var(--font-mono);
331 font-size: 12px;
332 font-variant-numeric: tabular-nums;
333 color: var(--text);
334 }
335 .dep-ver.is-from { color: var(--text-muted); }
336 .dep-kind-chip {
337 display: inline-flex;
338 align-items: center;
339 padding: 2px 8px;
340 border-radius: 6px;
341 font-size: 11px;
342 font-family: var(--font-mono);
343 background: var(--bg-tertiary);
344 color: var(--text);
345 border: 1px solid var(--border);
346 }
347 .dep-major {
348 display: inline-flex;
349 align-items: center;
350 gap: 4px;
351 padding: 2px 8px;
352 border-radius: 9999px;
353 font-size: 10.5px;
354 font-weight: 700;
355 letter-spacing: 0.04em;
356 text-transform: uppercase;
357 }
358 .dep-major.is-yes {
359 background: rgba(251,191,36,0.12);
360 color: #fde68a;
361 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
362 }
363 .dep-major.is-no {
364 background: rgba(148,163,184,0.14);
365 color: #cbd5e1;
366 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.28);
367 }
368
369 /* ─── Status pills ─── */
370 .dep-pill {
371 display: inline-flex;
372 align-items: center;
373 gap: 6px;
374 padding: 3px 9px;
375 border-radius: 9999px;
376 font-size: 11px;
377 font-weight: 600;
378 letter-spacing: 0.04em;
379 text-transform: uppercase;
380 }
381 .dep-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
382 .dep-pill.is-success {
383 background: rgba(52,211,153,0.14);
384 color: #6ee7b7;
385 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
386 }
387 .dep-pill.is-no-updates {
388 background: rgba(148,163,184,0.16);
389 color: #cbd5e1;
390 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
391 }
392 .dep-pill.is-failed {
393 background: rgba(248,113,113,0.14);
394 color: #fca5a5;
395 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
396 }
397 .dep-pill.is-running {
398 background: rgba(54,197,214,0.14);
399 color: #67e8f9;
400 box-shadow: inset 0 0 0 1px rgba(54,197,214,0.32);
401 }
402 .dep-pill.is-pending {
403 background: rgba(251,191,36,0.12);
404 color: #fde68a;
405 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
406 }
407
408 /* ─── Empty state ─── */
409 .dep-empty {
410 position: relative;
411 overflow: hidden;
412 padding: clamp(28px, 5vw, 48px) clamp(20px, 4vw, 36px);
413 text-align: center;
414 background: var(--bg-elevated);
415 border: 1px dashed var(--border-strong);
416 border-radius: 16px;
417 }
418 .dep-empty-orb {
419 position: absolute;
420 inset: -40% 30% auto 30%;
421 height: 280px;
422 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.10) 45%, transparent 70%);
423 filter: blur(70px);
424 opacity: 0.7;
425 pointer-events: none;
426 z-index: 0;
427 }
428 .dep-empty-inner { position: relative; z-index: 1; }
429 .dep-empty-icon {
430 width: 56px; height: 56px;
431 border-radius: 9999px;
432 background: linear-gradient(135deg, rgba(140,109,255,0.25), rgba(54,197,214,0.20));
433 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.40);
434 display: inline-flex;
435 align-items: center;
436 justify-content: center;
437 color: #c4b5fd;
438 margin-bottom: 14px;
439 }
440 .dep-empty-title {
441 font-family: var(--font-display);
442 font-size: 18px;
443 font-weight: 700;
444 margin: 0 0 6px;
445 color: var(--text-strong);
446 }
447 .dep-empty-sub {
448 margin: 0 auto 0;
449 font-size: 13.5px;
450 color: var(--text-muted);
451 max-width: 460px;
452 line-height: 1.5;
453 }
454
455 /* ─── 4xx-style card ─── */
456 .dep-notice {
457 max-width: 540px;
458 margin: var(--space-12) auto;
459 padding: var(--space-6);
460 text-align: center;
461 background: var(--bg-elevated);
462 border: 1px solid var(--border);
463 border-radius: 16px;
464 }
465 .dep-notice h2 {
466 font-family: var(--font-display);
467 font-size: 22px;
468 margin: 0 0 8px;
469 color: var(--text-strong);
470 }
471 .dep-notice p { color: var(--text-muted); margin: 0; font-size: 14px; }
472`;
473
474function IconPackage() {
475 return (
476 <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">
477 <line x1="16.5" y1="9.4" x2="7.5" y2="4.21" />
478 <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
479 <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
480 <line x1="12" y1="22.08" x2="12" y2="12" />
481 </svg>
482 );
483}
484function IconPlay() {
485 return (
486 <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
487 <polygon points="5 3 19 12 5 21 5 3" />
488 </svg>
489 );
490}
491function IconHistory() {
492 return (
493 <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">
494 <polyline points="1 4 1 10 7 10" />
495 <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
496 </svg>
497 );
498}
499
500function statusPill(status: string) {
501 const cls =
502 status === "success"
503 ? "is-success"
504 : status === "no_updates"
505 ? "is-no-updates"
506 : status === "failed"
507 ? "is-failed"
508 : status === "running"
509 ? "is-running"
510 : "is-pending";
511 const label =
512 status === "no_updates" ? "No updates" : status;
513 return (
514 <span class={`dep-pill ${cls}`}>
515 <span class="dot" aria-hidden="true" />
516 {label}
517 </span>
518 );
519}
520
26521/**
27522 * Resolve repo row + enforce owner-only access. Returns either a
28523 * rendered Response (when unauthorised / missing) or `{ repo }`.
55550 kind: "response",
56551 res: c.html(
57552 <Layout title="Unauthorized" user={user}>
58 <div class="empty-state">
59 <h2>Unauthorized</h2>
60 <p>Only the repository owner can configure the dependency updater.</p>
553 <div class="dep-wrap">
554 <div class="dep-notice">
555 <h2>Unauthorized</h2>
556 <p>Only the repository owner can configure the dependency updater.</p>
557 </div>
61558 </div>
559 <style dangerouslySetInnerHTML={{ __html: depStyles }} />
62560 </Layout>,
63561 403
64562 ),
82580 kind: "response",
83581 res: c.html(
84582 <Layout title="Error" user={user}>
85 <div class="empty-state">
86 <h2>Service unavailable</h2>
87 <p>The dependency updater is temporarily offline.</p>
583 <div class="dep-wrap">
584 <div class="dep-notice">
585 <h2>Service unavailable</h2>
586 <p>The dependency updater is temporarily offline.</p>
587 </div>
88588 </div>
589 <style dangerouslySetInnerHTML={{ __html: depStyles }} />
89590 </Layout>,
90591 503
91592 ),
93594 }
94595}
95596
96function statusChip(status: string) {
97 const colorMap: Record<string, string> = {
98 success: "badge-open",
99 no_updates: "badge-closed",
100 failed: "badge-closed",
101 running: "badge-open",
102 pending: "badge-closed",
103 };
104 const cls = colorMap[status] || "badge-closed";
105 return <span class={`issue-badge ${cls}`}>{status}</span>;
106}
107
108597function safeParseBumps(raw: string): Bump[] {
109598 try {
110599 const v = JSON.parse(raw || "[]");
141630 <Layout title={`Dep Updater — ${ownerName}/${repoName}`} user={user}>
142631 <RepoHeader owner={ownerName} repo={repoName} />
143632 <IssueNav owner={ownerName} repo={repoName} active="code" />
144 <div style="max-width: 900px">
145 <h2 style="margin-bottom: 8px">Dependency updater</h2>
146 <p style="color: var(--text-muted); margin-bottom: 20px">
147 Reads <code>package.json</code> on the default branch, queries the
148 npm registry for newer versions, and opens a pull request with the
149 bumped dependencies. Runs on-demand from this page; background
150 scheduling can be added later.
151 </p>
152
153 <form
154 method="post"
155 action={`/${ownerName}/${repoName}/settings/dep-updater/run`}
156 style="margin-bottom: 24px"
157 >
158 <button type="submit" class="btn btn-primary">
159 Run now
160 </button>
161 </form>
162
163 <h3 style="margin: 24px 0 8px; font-size: 16px">Recent runs</h3>
164 {runs.length === 0 ? (
165 <div class="empty-state">
166 <p>No runs yet. Click "Run now" to start your first scan.</p>
633 <div class="dep-wrap">
634 <section class="dep-hero">
635 <div class="dep-hero-orb" aria-hidden="true" />
636 <div class="dep-hero-inner">
637 <div class="dep-hero-text">
638 <div class="dep-eyebrow">
639 <span class="dep-eyebrow-dot" aria-hidden="true" />
640 Repository · Dependency updater
641 </div>
642 <h1 class="dep-title">
643 <span class="dep-title-grad">Keep deps fresh.</span>
644 </h1>
645 <p class="dep-sub">
646 Reads <code>package.json</code> on the default branch, queries
647 the npm registry, and opens a PR with the bumps. On-demand
648 for now — background scheduling lands later.
649 </p>
650 </div>
651 <form
652 method="post"
653 action={`/${ownerName}/${repoName}/settings/dep-updater/run`}
654 >
655 <button type="submit" class="dep-hero-cta">
656 <IconPlay />
657 Run now
658 </button>
659 </form>
167660 </div>
168 ) : (
169 <div class="issue-list">
170 {runs.map((r) => {
171 const applied = safeParseBumps(r.appliedBumps);
172 const attempted = safeParseBumps(r.attemptedBumps);
173 const bumps = applied.length > 0 ? applied : attempted;
174 const when = formatRelative(r.createdAt as unknown as string);
175 return (
176 <div
177 class="issue-row"
178 style="padding: 12px; border-bottom: 1px solid var(--border)"
179 >
180 <div style="display: flex; align-items: center; gap: 8px">
181 {statusChip(r.status)}
182 <span style="color: var(--text-muted); font-size: 13px">
183 {when}
184 </span>
185 {r.prNumber != null && (
186 <a
187 href={`/${ownerName}/${repoName}/pulls/${r.prNumber}`}
188 style="font-size: 13px"
189 >
190 PR #{r.prNumber}
191 </a>
192 )}
193 {r.branchName && (
194 <code style="font-size: 12px; color: var(--text-muted)">
195 {r.branchName}
196 </code>
197 )}
661 </section>
662
663 <section class="dep-section">
664 <header class="dep-section-head">
665 <div>
666 <h2 class="dep-section-title">
667 <span class="dep-section-title-icon" aria-hidden="true">
668 <IconHistory />
669 </span>
670 Recent runs
671 <span style="font-family:var(--font-mono);font-size:12px;color:var(--text-muted);font-weight:500;font-variant-numeric:tabular-nums">
672 {" "}({runs.length})
673 </span>
674 </h2>
675 <p class="dep-section-sub">
676 Latest 20 runs, newest first. Expand a run to inspect the
677 exact bumps it applied or attempted.
678 </p>
679 </div>
680 </header>
681 <div class="dep-section-body">
682 {runs.length === 0 ? (
683 <div class="dep-empty">
684 <div class="dep-empty-orb" aria-hidden="true" />
685 <div class="dep-empty-inner">
686 <div class="dep-empty-icon" aria-hidden="true">
687 <IconPackage />
198688 </div>
199 {r.errorMessage && (
200 <div
201 style="margin-top: 6px; font-size: 13px; color: var(--red)"
202 >
203 {r.errorMessage}
204 </div>
205 )}
206 {bumps.length > 0 && (
207 <details style="margin-top: 8px">
208 <summary style="cursor: pointer; font-size: 13px">
209 {bumps.length} bump{bumps.length === 1 ? "" : "s"}
210 </summary>
211 <table style="margin-top: 8px; font-size: 13px; width: 100%">
212 <thead>
213 <tr style="text-align: left; color: var(--text-muted)">
214 <th style="padding: 4px 8px">Package</th>
215 <th style="padding: 4px 8px">From</th>
216 <th style="padding: 4px 8px">To</th>
217 <th style="padding: 4px 8px">Kind</th>
218 <th style="padding: 4px 8px">Major?</th>
219 </tr>
220 </thead>
221 <tbody>
222 {bumps.map((b) => (
223 <tr>
224 <td style="padding: 4px 8px">
225 <code>{b.name}</code>
226 </td>
227 <td style="padding: 4px 8px">{b.from}</td>
228 <td style="padding: 4px 8px">{b.to}</td>
229 <td style="padding: 4px 8px">{b.kind}</td>
230 <td style="padding: 4px 8px">
231 {b.major ? "yes" : "no"}
232 </td>
233 </tr>
234 ))}
235 </tbody>
236 </table>
237 </details>
238 )}
689 <h3 class="dep-empty-title">No runs yet</h3>
690 <p class="dep-empty-sub">
691 Hit <em>Run now</em> above to start your first scan.
692 Results land here as soon as the orchestrator finishes.
693 </p>
239694 </div>
240 );
241 })}
695 </div>
696 ) : (
697 <div class="dep-run-list">
698 {runs.map((r) => {
699 const applied = safeParseBumps(r.appliedBumps);
700 const attempted = safeParseBumps(r.attemptedBumps);
701 const bumps = applied.length > 0 ? applied : attempted;
702 const when = formatRelative(r.createdAt as unknown as string);
703 return (
704 <div class="dep-run-card">
705 <div class="dep-run-head">
706 {statusPill(r.status)}
707 <span class="dep-run-stamp">{when}</span>
708 {r.prNumber != null && (
709 <a
710 href={`/${ownerName}/${repoName}/pulls/${r.prNumber}`}
711 class="dep-run-pr"
712 >
713 PR #{r.prNumber}
714 </a>
715 )}
716 {r.branchName && (
717 <code class="dep-run-branch">{r.branchName}</code>
718 )}
719 </div>
720 {r.errorMessage && (
721 <div class="dep-run-err">{r.errorMessage}</div>
722 )}
723 {bumps.length > 0 && (
724 <details class="dep-bumps">
725 <summary>
726 {bumps.length} bump{bumps.length === 1 ? "" : "s"}
727 </summary>
728 <table class="dep-table">
729 <thead>
730 <tr>
731 <th>Package</th>
732 <th>From</th>
733 <th>To</th>
734 <th>Kind</th>
735 <th>Major</th>
736 </tr>
737 </thead>
738 <tbody>
739 {bumps.map((b) => (
740 <tr>
741 <td>
742 <span class="dep-pkg">{b.name}</span>
743 </td>
744 <td>
745 <span class="dep-ver is-from">{b.from}</span>
746 </td>
747 <td>
748 <span class="dep-ver">{b.to}</span>
749 </td>
750 <td>
751 <span class="dep-kind-chip">{b.kind}</span>
752 </td>
753 <td>
754 <span
755 class={
756 "dep-major " +
757 (b.major ? "is-yes" : "is-no")
758 }
759 >
760 {b.major ? "yes" : "no"}
761 </span>
762 </td>
763 </tr>
764 ))}
765 </tbody>
766 </table>
767 </details>
768 )}
769 </div>
770 );
771 })}
772 </div>
773 )}
242774 </div>
243 )}
775 </section>
244776 </div>
777 <style dangerouslySetInnerHTML={{ __html: depStyles }} />
245778 </Layout>
246779 );
247780 }
248781