Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

dep-updater.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.

dep-updater.tsxBlame809 lines · 3 contributors
3cbe3d6Claude1/**
2 * Block D2 — AI dependency updater UI.
3 *
4 * GET /:owner/:repo/settings/dep-updater — run history + "Run now"
5 * POST /:owner/:repo/settings/dep-updater/run — kicks off a run (fire & forget)
6 *
7 * Owner-only. See `src/lib/dep-updater.ts` for the orchestrator.
e2e0279Claude8 *
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.
ae2a071ccanty labs12 * RepoHeader + RepoNav above are untouched — only the content beneath
13 * the nav is restyled. (Was rendering the wrong nav component, IssueNav
14 * with active="code" — fixed to RepoNav active="settings".)
3cbe3d6Claude15 */
16
17import { Hono } from "hono";
18import { eq, and, desc } from "drizzle-orm";
19import { db } from "../db";
20import { depUpdateRuns, repositories, users } from "../db/schema";
21import { Layout } from "../views/layout";
22import { RepoHeader } from "../views/components";
4127ecfClaude23import { formatRelative } from "../views/ui";
ae2a071ccanty labs24import { RepoNav } from "../views/components";
3cbe3d6Claude25import { softAuth, requireAuth } from "../middleware/auth";
26import type { AuthEnv } from "../middleware/auth";
27import { runDepUpdateRun, type Bump } from "../lib/dep-updater";
28
29const depUpdater = new Hono<AuthEnv>();
30
31depUpdater.use("*", softAuth);
32
e2e0279Claude33/* ─────────────────────────────────────────────────────────────────────────
34 * Scoped CSS — every class prefixed `.dep-*`. Hero with gradient hairline +
35 * orb, run cards with status pill / tabular-nums timing, mono version cells.
36 * ───────────────────────────────────────────────────────────────────── */
37const depStyles = `
eed4684Claude38 .dep-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
e2e0279Claude39
40 /* ─── Hero ─── */
41 .dep-hero {
42 position: relative;
43 margin-bottom: var(--space-5);
44 padding: var(--space-5) var(--space-6);
45 background: var(--bg-elevated);
46 border: 1px solid var(--border);
47 border-radius: 16px;
48 overflow: hidden;
49 }
50 .dep-hero::before {
51 content: '';
52 position: absolute;
53 top: 0; left: 0; right: 0;
54 height: 2px;
6fd5915Claude55 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
e2e0279Claude56 opacity: 0.7;
57 pointer-events: none;
58 }
59 .dep-hero-orb {
60 position: absolute;
61 inset: -20% -10% auto auto;
62 width: 380px; height: 380px;
6fd5915Claude63 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
e2e0279Claude64 filter: blur(80px);
65 opacity: 0.7;
66 pointer-events: none;
67 z-index: 0;
68 }
69 .dep-hero-inner {
70 position: relative;
71 z-index: 1;
72 display: flex;
73 align-items: flex-end;
74 justify-content: space-between;
75 gap: var(--space-4);
76 flex-wrap: wrap;
77 }
78 .dep-hero-text { max-width: 720px; flex: 1; min-width: 240px; }
79 .dep-eyebrow {
80 display: inline-flex;
81 align-items: center;
82 gap: 8px;
83 text-transform: uppercase;
84 font-family: var(--font-mono);
85 font-size: 11px;
86 letter-spacing: 0.16em;
87 color: var(--text-muted);
88 font-weight: 600;
89 margin-bottom: 10px;
90 }
91 .dep-eyebrow-dot {
92 width: 8px; height: 8px;
93 border-radius: 9999px;
6fd5915Claude94 background: linear-gradient(135deg, #5b6ee8, #5f8fa0);
95 box-shadow: 0 0 0 3px rgba(91,110,232,0.18);
e2e0279Claude96 }
97 .dep-title {
98 font-family: var(--font-display);
99 font-size: clamp(28px, 4vw, 40px);
100 font-weight: 800;
101 letter-spacing: -0.028em;
102 line-height: 1.05;
103 margin: 0 0 var(--space-2);
104 color: var(--text-strong);
105 }
106 .dep-title-grad {
6fd5915Claude107 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
e2e0279Claude108 -webkit-background-clip: text;
109 background-clip: text;
110 -webkit-text-fill-color: transparent;
111 color: transparent;
112 }
113 .dep-sub {
114 font-size: 15px;
115 color: var(--text-muted);
116 margin: 0;
117 line-height: 1.5;
118 max-width: 620px;
119 }
120 .dep-sub code {
121 font-family: var(--font-mono);
122 font-size: 13px;
123 background: var(--bg-tertiary);
124 padding: 1px 5px;
125 border-radius: 4px;
126 }
127 .dep-hero-cta {
128 display: inline-flex;
129 align-items: center;
130 gap: 7px;
131 padding: 10px 18px;
132 font-size: 13.5px;
133 font-weight: 600;
134 border-radius: 10px;
135 border: 1px solid transparent;
136 cursor: pointer;
137 font: inherit;
138 line-height: 1;
139 white-space: nowrap;
140 text-decoration: none;
141 color: #ffffff;
6fd5915Claude142 background: linear-gradient(135deg, #5b6ee8 0%, #5f8fa0 100%);
143 box-shadow: 0 6px 18px -6px rgba(91,110,232,0.50), inset 0 1px 0 rgba(255,255,255,0.16);
e2e0279Claude144 transition: transform 120ms ease, box-shadow 120ms ease;
145 }
146 .dep-hero-cta:hover {
147 transform: translateY(-1px);
6fd5915Claude148 box-shadow: 0 10px 24px -8px rgba(91,110,232,0.60), inset 0 1px 0 rgba(255,255,255,0.20);
e2e0279Claude149 text-decoration: none;
150 color: #ffffff;
151 }
152
153 /* ─── Section cards ─── */
154 .dep-section {
155 margin-bottom: var(--space-5);
156 background: var(--bg-elevated);
157 border: 1px solid var(--border);
158 border-radius: 14px;
159 overflow: hidden;
160 position: relative;
161 }
162 .dep-section::before {
163 content: '';
164 position: absolute;
165 top: 0; left: 0; right: 0;
166 height: 2px;
6fd5915Claude167 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
e2e0279Claude168 opacity: 0.45;
169 pointer-events: none;
170 }
171 .dep-section-head {
172 padding: var(--space-4) var(--space-5);
173 border-bottom: 1px solid var(--border);
174 display: flex;
175 align-items: flex-start;
176 justify-content: space-between;
177 gap: var(--space-3);
178 flex-wrap: wrap;
179 }
180 .dep-section-title {
181 margin: 0;
182 font-family: var(--font-display);
183 font-size: 16px;
184 font-weight: 700;
185 letter-spacing: -0.018em;
186 color: var(--text-strong);
187 display: flex;
188 align-items: center;
189 gap: 10px;
190 }
191 .dep-section-title-icon {
192 display: inline-flex;
193 align-items: center;
194 justify-content: center;
195 width: 26px; height: 26px;
196 border-radius: 8px;
6fd5915Claude197 background: rgba(91,110,232,0.12);
e589f77ccantynz-alt198 color: var(--accent);
6fd5915Claude199 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.28);
e2e0279Claude200 flex-shrink: 0;
201 }
202 .dep-section-sub {
203 margin: 6px 0 0 36px;
204 font-size: 12.5px;
205 color: var(--text-muted);
206 line-height: 1.45;
207 }
208 .dep-section-body { padding: var(--space-4) var(--space-5); }
209
210 /* ─── Run card list ─── */
211 .dep-run-list { display: flex; flex-direction: column; gap: 10px; }
212 .dep-run-card {
213 padding: 14px;
214 background: rgba(255,255,255,0.018);
215 border: 1px solid var(--border);
216 border-radius: 12px;
217 transition: border-color 120ms ease, background 120ms ease;
218 }
219 .dep-run-card:hover {
220 border-color: var(--border-strong);
221 background: rgba(255,255,255,0.03);
222 }
223 .dep-run-head {
224 display: flex;
225 align-items: center;
226 gap: 10px;
227 flex-wrap: wrap;
228 }
229 .dep-run-stamp {
230 font-family: var(--font-mono);
231 font-size: 12px;
232 color: var(--text-muted);
233 font-variant-numeric: tabular-nums;
234 }
235 .dep-run-pr {
236 font-family: var(--font-mono);
237 font-size: 12.5px;
238 color: var(--accent);
239 text-decoration: none;
240 padding: 2px 8px;
241 border-radius: 6px;
6fd5915Claude242 background: rgba(91,110,232,0.10);
243 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.28);
e2e0279Claude244 font-variant-numeric: tabular-nums;
245 }
6fd5915Claude246 .dep-run-pr:hover { text-decoration: none; background: rgba(91,110,232,0.18); }
e2e0279Claude247 .dep-run-branch {
248 font-family: var(--font-mono);
249 font-size: 12px;
250 color: var(--text);
251 background: var(--bg-tertiary);
252 padding: 2px 7px;
253 border-radius: 5px;
254 border: 1px solid var(--border);
255 }
256 .dep-run-err {
257 margin-top: 10px;
258 padding: 8px 10px;
259 background: rgba(248,113,113,0.06);
260 border: 1px solid rgba(248,113,113,0.28);
261 border-radius: 8px;
262 font-family: var(--font-mono);
263 font-size: 12px;
264 color: #fecaca;
265 line-height: 1.45;
266 }
267 .dep-bumps { margin-top: 10px; }
268 .dep-bumps > summary {
269 cursor: pointer;
270 font-size: 12.5px;
271 color: var(--text-muted);
272 display: inline-flex;
273 align-items: center;
274 gap: 6px;
275 padding: 4px 9px;
276 border-radius: 9999px;
277 background: rgba(255,255,255,0.025);
278 border: 1px solid var(--border);
279 user-select: none;
280 list-style: none;
281 }
282 .dep-bumps > summary::-webkit-details-marker { display: none; }
283 .dep-bumps > summary::before {
284 content: '▸';
285 font-size: 10px;
286 color: var(--text-muted);
287 transition: transform 120ms ease;
288 display: inline-block;
289 }
290 .dep-bumps[open] > summary::before { transform: rotate(90deg); }
291 .dep-bumps[open] > summary {
292 color: var(--text-strong);
293 border-color: var(--border-strong);
294 }
295
296 /* ─── Bump table ─── */
297 .dep-table {
298 width: 100%;
299 margin-top: 12px;
300 border-collapse: separate;
301 border-spacing: 0;
302 font-size: 12.5px;
303 border: 1px solid var(--border);
304 border-radius: 10px;
305 overflow: hidden;
306 }
307 .dep-table thead th {
308 text-align: left;
309 padding: 9px 12px;
310 font-size: 11px;
311 text-transform: uppercase;
312 letter-spacing: 0.06em;
313 color: var(--text-muted);
314 font-weight: 600;
315 background: rgba(255,255,255,0.025);
316 border-bottom: 1px solid var(--border);
317 }
318 .dep-table tbody td {
319 padding: 9px 12px;
320 border-bottom: 1px solid var(--border);
321 vertical-align: middle;
322 }
323 .dep-table tbody tr:last-child td { border-bottom: none; }
324 .dep-table tbody tr:hover td { background: rgba(255,255,255,0.02); }
325 .dep-pkg {
326 font-family: var(--font-mono);
327 font-size: 12px;
328 color: var(--text-strong);
329 }
330 .dep-ver {
331 font-family: var(--font-mono);
332 font-size: 12px;
333 font-variant-numeric: tabular-nums;
334 color: var(--text);
335 }
336 .dep-ver.is-from { color: var(--text-muted); }
337 .dep-kind-chip {
338 display: inline-flex;
339 align-items: center;
340 padding: 2px 8px;
341 border-radius: 6px;
342 font-size: 11px;
343 font-family: var(--font-mono);
344 background: var(--bg-tertiary);
345 color: var(--text);
346 border: 1px solid var(--border);
347 }
348 .dep-major {
349 display: inline-flex;
350 align-items: center;
351 gap: 4px;
352 padding: 2px 8px;
353 border-radius: 9999px;
354 font-size: 10.5px;
355 font-weight: 700;
356 letter-spacing: 0.04em;
357 text-transform: uppercase;
358 }
359 .dep-major.is-yes {
360 background: rgba(251,191,36,0.12);
361 color: #fde68a;
362 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
363 }
364 .dep-major.is-no {
365 background: rgba(148,163,184,0.14);
366 color: #cbd5e1;
367 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.28);
368 }
369
370 /* ─── Status pills ─── */
371 .dep-pill {
372 display: inline-flex;
373 align-items: center;
374 gap: 6px;
375 padding: 3px 9px;
376 border-radius: 9999px;
377 font-size: 11px;
378 font-weight: 600;
379 letter-spacing: 0.04em;
380 text-transform: uppercase;
381 }
382 .dep-pill .dot { width: 6px; height: 6px; border-radius: 9999px; background: currentColor; }
383 .dep-pill.is-success {
384 background: rgba(52,211,153,0.14);
e589f77ccantynz-alt385 color: var(--green);
e2e0279Claude386 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.32);
387 }
388 .dep-pill.is-no-updates {
389 background: rgba(148,163,184,0.16);
390 color: #cbd5e1;
391 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
392 }
393 .dep-pill.is-failed {
394 background: rgba(248,113,113,0.14);
e589f77ccantynz-alt395 color: var(--red);
e2e0279Claude396 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.32);
397 }
398 .dep-pill.is-running {
6fd5915Claude399 background: rgba(95,143,160,0.14);
e2e0279Claude400 color: #67e8f9;
6fd5915Claude401 box-shadow: inset 0 0 0 1px rgba(95,143,160,0.32);
e2e0279Claude402 }
403 .dep-pill.is-pending {
404 background: rgba(251,191,36,0.12);
405 color: #fde68a;
406 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.32);
407 }
408
409 /* ─── Empty state ─── */
410 .dep-empty {
411 position: relative;
412 overflow: hidden;
413 padding: clamp(28px, 5vw, 48px) clamp(20px, 4vw, 36px);
414 text-align: center;
415 background: var(--bg-elevated);
416 border: 1px dashed var(--border-strong);
417 border-radius: 16px;
418 }
419 .dep-empty-orb {
420 position: absolute;
421 inset: -40% 30% auto 30%;
422 height: 280px;
6fd5915Claude423 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.10) 45%, transparent 70%);
e2e0279Claude424 filter: blur(70px);
425 opacity: 0.7;
426 pointer-events: none;
427 z-index: 0;
428 }
429 .dep-empty-inner { position: relative; z-index: 1; }
430 .dep-empty-icon {
431 width: 56px; height: 56px;
432 border-radius: 9999px;
6fd5915Claude433 background: linear-gradient(135deg, rgba(91,110,232,0.25), rgba(95,143,160,0.20));
434 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.40);
e2e0279Claude435 display: inline-flex;
436 align-items: center;
437 justify-content: center;
438 color: #c4b5fd;
439 margin-bottom: 14px;
440 }
441 .dep-empty-title {
442 font-family: var(--font-display);
443 font-size: 18px;
444 font-weight: 700;
445 margin: 0 0 6px;
446 color: var(--text-strong);
447 }
448 .dep-empty-sub {
449 margin: 0 auto 0;
450 font-size: 13.5px;
451 color: var(--text-muted);
452 max-width: 460px;
453 line-height: 1.5;
454 }
455
456 /* ─── 4xx-style card ─── */
457 .dep-notice {
458 max-width: 540px;
459 margin: var(--space-12) auto;
460 padding: var(--space-6);
461 text-align: center;
462 background: var(--bg-elevated);
463 border: 1px solid var(--border);
464 border-radius: 16px;
465 }
466 .dep-notice h2 {
467 font-family: var(--font-display);
468 font-size: 22px;
469 margin: 0 0 8px;
470 color: var(--text-strong);
471 }
472 .dep-notice p { color: var(--text-muted); margin: 0; font-size: 14px; }
473`;
474
475function IconPackage() {
476 return (
477 <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">
478 <line x1="16.5" y1="9.4" x2="7.5" y2="4.21" />
479 <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" />
480 <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
481 <line x1="12" y1="22.08" x2="12" y2="12" />
482 </svg>
483 );
484}
485function IconPlay() {
486 return (
487 <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
488 <polygon points="5 3 19 12 5 21 5 3" />
489 </svg>
490 );
491}
492function IconHistory() {
493 return (
494 <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">
495 <polyline points="1 4 1 10 7 10" />
496 <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
497 </svg>
498 );
499}
500
501function statusPill(status: string) {
502 const cls =
503 status === "success"
504 ? "is-success"
505 : status === "no_updates"
506 ? "is-no-updates"
507 : status === "failed"
508 ? "is-failed"
509 : status === "running"
510 ? "is-running"
511 : "is-pending";
512 const label =
513 status === "no_updates" ? "No updates" : status;
514 return (
515 <span class={`dep-pill ${cls}`}>
516 <span class="dot" aria-hidden="true" />
517 {label}
518 </span>
519 );
520}
521
3cbe3d6Claude522/**
523 * Resolve repo row + enforce owner-only access. Returns either a
524 * rendered Response (when unauthorised / missing) or `{ repo }`.
525 */
526async function resolveOwnerRepo(
527 c: any,
528 ownerName: string,
529 repoName: string
530): Promise<
531 | { kind: "ok"; repo: typeof repositories.$inferSelect }
532 | { kind: "response"; res: Response }
533> {
534 const user = c.get("user");
535 if (!user) {
536 return {
537 kind: "response",
538 res: c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`),
539 };
540 }
541
542 try {
543 const [owner] = await db
544 .select()
545 .from(users)
546 .where(eq(users.username, ownerName))
547 .limit(1);
548 if (!owner) return { kind: "response", res: c.notFound() };
549 if (owner.id !== user.id) {
550 return {
551 kind: "response",
552 res: c.html(
553 <Layout title="Unauthorized" user={user}>
e2e0279Claude554 <div class="dep-wrap">
555 <div class="dep-notice">
556 <h2>Unauthorized</h2>
557 <p>Only the repository owner can configure the dependency updater.</p>
558 </div>
3cbe3d6Claude559 </div>
e2e0279Claude560 <style dangerouslySetInnerHTML={{ __html: depStyles }} />
3cbe3d6Claude561 </Layout>,
562 403
563 ),
564 };
565 }
566 const [repo] = await db
567 .select()
568 .from(repositories)
569 .where(
570 and(
571 eq(repositories.ownerId, owner.id),
572 eq(repositories.name, repoName)
573 )
574 )
575 .limit(1);
576 if (!repo) return { kind: "response", res: c.notFound() };
577 return { kind: "ok", repo };
578 } catch (err) {
579 // DB unreachable — let the global 503 / 500 handler cope.
580 return {
581 kind: "response",
582 res: c.html(
583 <Layout title="Error" user={user}>
e2e0279Claude584 <div class="dep-wrap">
585 <div class="dep-notice">
586 <h2>Service unavailable</h2>
587 <p>The dependency updater is temporarily offline.</p>
588 </div>
3cbe3d6Claude589 </div>
e2e0279Claude590 <style dangerouslySetInnerHTML={{ __html: depStyles }} />
3cbe3d6Claude591 </Layout>,
592 503
593 ),
594 };
595 }
596}
597
598function safeParseBumps(raw: string): Bump[] {
599 try {
600 const v = JSON.parse(raw || "[]");
601 return Array.isArray(v) ? v : [];
602 } catch {
603 return [];
604 }
605}
606
607// GET — run history + "Run now"
608depUpdater.get(
609 "/:owner/:repo/settings/dep-updater",
610 requireAuth,
611 async (c) => {
612 const { owner: ownerName, repo: repoName } = c.req.param();
613 const resolved = await resolveOwnerRepo(c, ownerName, repoName);
614 if (resolved.kind === "response") return resolved.res;
615 const { repo } = resolved;
616 const user = c.get("user")!;
617
618 let runs: Array<typeof depUpdateRuns.$inferSelect> = [];
619 try {
620 runs = await db
621 .select()
622 .from(depUpdateRuns)
623 .where(eq(depUpdateRuns.repositoryId, repo.id))
624 .orderBy(desc(depUpdateRuns.createdAt))
625 .limit(20);
626 } catch {
627 runs = [];
628 }
629
630 return c.html(
631 <Layout title={`Dep Updater — ${ownerName}/${repoName}`} user={user}>
632 <RepoHeader owner={ownerName} repo={repoName} />
ae2a071ccanty labs633 <RepoNav owner={ownerName} repo={repoName} active="settings" />
e2e0279Claude634 <div class="dep-wrap">
635 <section class="dep-hero">
636 <div class="dep-hero-orb" aria-hidden="true" />
637 <div class="dep-hero-inner">
638 <div class="dep-hero-text">
639 <div class="dep-eyebrow">
640 <span class="dep-eyebrow-dot" aria-hidden="true" />
641 Repository · Dependency updater
642 </div>
643 <h1 class="dep-title">
644 <span class="dep-title-grad">Keep deps fresh.</span>
645 </h1>
646 <p class="dep-sub">
647 Reads <code>package.json</code> on the default branch, queries
648 the npm registry, and opens a PR with the bumps. On-demand
649 for now — background scheduling lands later.
650 </p>
651 </div>
652 <form
653 method="post"
654 action={`/${ownerName}/${repoName}/settings/dep-updater/run`}
655 >
656 <button type="submit" class="dep-hero-cta">
657 <IconPlay />
658 Run now
659 </button>
660 </form>
3cbe3d6Claude661 </div>
e2e0279Claude662 </section>
663
664 <section class="dep-section">
665 <header class="dep-section-head">
666 <div>
667 <h2 class="dep-section-title">
668 <span class="dep-section-title-icon" aria-hidden="true">
669 <IconHistory />
670 </span>
671 Recent runs
672 <span style="font-family:var(--font-mono);font-size:12px;color:var(--text-muted);font-weight:500;font-variant-numeric:tabular-nums">
673 {" "}({runs.length})
674 </span>
675 </h2>
676 <p class="dep-section-sub">
677 Latest 20 runs, newest first. Expand a run to inspect the
678 exact bumps it applied or attempted.
679 </p>
680 </div>
681 </header>
682 <div class="dep-section-body">
683 {runs.length === 0 ? (
684 <div class="dep-empty">
685 <div class="dep-empty-orb" aria-hidden="true" />
686 <div class="dep-empty-inner">
687 <div class="dep-empty-icon" aria-hidden="true">
688 <IconPackage />
3cbe3d6Claude689 </div>
e2e0279Claude690 <h3 class="dep-empty-title">No runs yet</h3>
691 <p class="dep-empty-sub">
692 Hit <em>Run now</em> above to start your first scan.
693 Results land here as soon as the orchestrator finishes.
694 </p>
3cbe3d6Claude695 </div>
e2e0279Claude696 </div>
697 ) : (
698 <div class="dep-run-list">
699 {runs.map((r) => {
700 const applied = safeParseBumps(r.appliedBumps);
701 const attempted = safeParseBumps(r.attemptedBumps);
702 const bumps = applied.length > 0 ? applied : attempted;
703 const when = formatRelative(r.createdAt as unknown as string);
704 return (
705 <div class="dep-run-card">
706 <div class="dep-run-head">
707 {statusPill(r.status)}
708 <span class="dep-run-stamp">{when}</span>
709 {r.prNumber != null && (
710 <a
711 href={`/${ownerName}/${repoName}/pulls/${r.prNumber}`}
712 class="dep-run-pr"
713 >
714 PR #{r.prNumber}
715 </a>
716 )}
717 {r.branchName && (
718 <code class="dep-run-branch">{r.branchName}</code>
719 )}
720 </div>
721 {r.errorMessage && (
722 <div class="dep-run-err">{r.errorMessage}</div>
723 )}
724 {bumps.length > 0 && (
725 <details class="dep-bumps">
726 <summary>
727 {bumps.length} bump{bumps.length === 1 ? "" : "s"}
728 </summary>
729 <table class="dep-table">
730 <thead>
731 <tr>
732 <th>Package</th>
733 <th>From</th>
734 <th>To</th>
735 <th>Kind</th>
736 <th>Major</th>
737 </tr>
738 </thead>
739 <tbody>
740 {bumps.map((b) => (
741 <tr>
742 <td>
743 <span class="dep-pkg">{b.name}</span>
744 </td>
745 <td>
746 <span class="dep-ver is-from">{b.from}</span>
747 </td>
748 <td>
749 <span class="dep-ver">{b.to}</span>
750 </td>
751 <td>
752 <span class="dep-kind-chip">{b.kind}</span>
753 </td>
754 <td>
755 <span
756 class={
757 "dep-major " +
758 (b.major ? "is-yes" : "is-no")
759 }
760 >
761 {b.major ? "yes" : "no"}
762 </span>
763 </td>
764 </tr>
765 ))}
766 </tbody>
767 </table>
768 </details>
769 )}
770 </div>
771 );
772 })}
773 </div>
774 )}
3cbe3d6Claude775 </div>
e2e0279Claude776 </section>
3cbe3d6Claude777 </div>
e2e0279Claude778 <style dangerouslySetInnerHTML={{ __html: depStyles }} />
3cbe3d6Claude779 </Layout>
780 );
781 }
782);
783
784// POST — fire and forget
785depUpdater.post(
786 "/:owner/:repo/settings/dep-updater/run",
787 requireAuth,
788 async (c) => {
789 const { owner: ownerName, repo: repoName } = c.req.param();
790 const resolved = await resolveOwnerRepo(c, ownerName, repoName);
791 if (resolved.kind === "response") return resolved.res;
792 const { repo } = resolved;
793 const user = c.get("user")!;
794
795 // Fire-and-forget. The run records its own failures to `dep_update_runs`.
796 runDepUpdateRun({
797 repositoryId: repo.id,
798 owner: ownerName,
799 repo: repoName,
800 userId: user.id,
801 }).catch((err) => {
802 console.error("[dep-updater] run failed:", err);
803 });
804
805 return c.redirect(`/${ownerName}/${repoName}/settings/dep-updater`);
806 }
807);
808
809export default depUpdater;