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.tsxBlame274 lines · 1 contributor
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.
8 */
9
10import { Hono } from "hono";
11import { eq, and, desc } from "drizzle-orm";
12import { db } from "../db";
13import { depUpdateRuns, repositories, users } from "../db/schema";
14import { Layout } from "../views/layout";
15import { RepoHeader } from "../views/components";
16import { IssueNav } from "./issues";
17import { softAuth, requireAuth } from "../middleware/auth";
18import type { AuthEnv } from "../middleware/auth";
19import { runDepUpdateRun, type Bump } from "../lib/dep-updater";
20
21const depUpdater = new Hono<AuthEnv>();
22
23depUpdater.use("*", softAuth);
24
25/**
26 * Resolve repo row + enforce owner-only access. Returns either a
27 * rendered Response (when unauthorised / missing) or `{ repo }`.
28 */
29async function resolveOwnerRepo(
30 c: any,
31 ownerName: string,
32 repoName: string
33): Promise<
34 | { kind: "ok"; repo: typeof repositories.$inferSelect }
35 | { kind: "response"; res: Response }
36> {
37 const user = c.get("user");
38 if (!user) {
39 return {
40 kind: "response",
41 res: c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`),
42 };
43 }
44
45 try {
46 const [owner] = await db
47 .select()
48 .from(users)
49 .where(eq(users.username, ownerName))
50 .limit(1);
51 if (!owner) return { kind: "response", res: c.notFound() };
52 if (owner.id !== user.id) {
53 return {
54 kind: "response",
55 res: c.html(
56 <Layout title="Unauthorized" user={user}>
57 <div class="empty-state">
58 <h2>Unauthorized</h2>
59 <p>Only the repository owner can configure the dependency updater.</p>
60 </div>
61 </Layout>,
62 403
63 ),
64 };
65 }
66 const [repo] = await db
67 .select()
68 .from(repositories)
69 .where(
70 and(
71 eq(repositories.ownerId, owner.id),
72 eq(repositories.name, repoName)
73 )
74 )
75 .limit(1);
76 if (!repo) return { kind: "response", res: c.notFound() };
77 return { kind: "ok", repo };
78 } catch (err) {
79 // DB unreachable — let the global 503 / 500 handler cope.
80 return {
81 kind: "response",
82 res: c.html(
83 <Layout title="Error" user={user}>
84 <div class="empty-state">
85 <h2>Service unavailable</h2>
86 <p>The dependency updater is temporarily offline.</p>
87 </div>
88 </Layout>,
89 503
90 ),
91 };
92 }
93}
94
95function statusChip(status: string) {
96 const colorMap: Record<string, string> = {
97 success: "badge-open",
98 no_updates: "badge-closed",
99 failed: "badge-closed",
100 running: "badge-open",
101 pending: "badge-closed",
102 };
103 const cls = colorMap[status] || "badge-closed";
104 return <span class={`issue-badge ${cls}`}>{status}</span>;
105}
106
107function safeParseBumps(raw: string): Bump[] {
108 try {
109 const v = JSON.parse(raw || "[]");
110 return Array.isArray(v) ? v : [];
111 } catch {
112 return [];
113 }
114}
115
116// GET — run history + "Run now"
117depUpdater.get(
118 "/:owner/:repo/settings/dep-updater",
119 requireAuth,
120 async (c) => {
121 const { owner: ownerName, repo: repoName } = c.req.param();
122 const resolved = await resolveOwnerRepo(c, ownerName, repoName);
123 if (resolved.kind === "response") return resolved.res;
124 const { repo } = resolved;
125 const user = c.get("user")!;
126
127 let runs: Array<typeof depUpdateRuns.$inferSelect> = [];
128 try {
129 runs = await db
130 .select()
131 .from(depUpdateRuns)
132 .where(eq(depUpdateRuns.repositoryId, repo.id))
133 .orderBy(desc(depUpdateRuns.createdAt))
134 .limit(20);
135 } catch {
136 runs = [];
137 }
138
139 return c.html(
140 <Layout title={`Dep Updater — ${ownerName}/${repoName}`} user={user}>
141 <RepoHeader owner={ownerName} repo={repoName} />
142 <IssueNav owner={ownerName} repo={repoName} active="code" />
143 <div style="max-width: 900px">
144 <h2 style="margin-bottom: 8px">Dependency updater</h2>
145 <p style="color: var(--text-muted); margin-bottom: 20px">
146 Reads <code>package.json</code> on the default branch, queries the
147 npm registry for newer versions, and opens a pull request with the
148 bumped dependencies. Runs on-demand from this page; background
149 scheduling can be added later.
150 </p>
151
152 <form
9e2c6dfClaude153 method="post"
3cbe3d6Claude154 action={`/${ownerName}/${repoName}/settings/dep-updater/run`}
155 style="margin-bottom: 24px"
156 >
157 <button type="submit" class="btn btn-primary">
158 Run now
159 </button>
160 </form>
161
162 <h3 style="margin: 24px 0 8px; font-size: 16px">Recent runs</h3>
163 {runs.length === 0 ? (
164 <div class="empty-state">
165 <p>No runs yet. Click "Run now" to start your first scan.</p>
166 </div>
167 ) : (
168 <div class="issue-list">
169 {runs.map((r) => {
170 const applied = safeParseBumps(r.appliedBumps);
171 const attempted = safeParseBumps(r.attemptedBumps);
172 const bumps = applied.length > 0 ? applied : attempted;
173 const when = new Date(r.createdAt).toLocaleString();
174 return (
175 <div
176 class="issue-row"
177 style="padding: 12px; border-bottom: 1px solid var(--border)"
178 >
179 <div style="display: flex; align-items: center; gap: 8px">
180 {statusChip(r.status)}
181 <span style="color: var(--text-muted); font-size: 13px">
182 {when}
183 </span>
184 {r.prNumber != null && (
185 <a
186 href={`/${ownerName}/${repoName}/pulls/${r.prNumber}`}
187 style="font-size: 13px"
188 >
189 PR #{r.prNumber}
190 </a>
191 )}
192 {r.branchName && (
193 <code style="font-size: 12px; color: var(--text-muted)">
194 {r.branchName}
195 </code>
196 )}
197 </div>
198 {r.errorMessage && (
199 <div
200 style="margin-top: 6px; font-size: 13px; color: var(--red)"
201 >
202 {r.errorMessage}
203 </div>
204 )}
205 {bumps.length > 0 && (
206 <details style="margin-top: 8px">
207 <summary style="cursor: pointer; font-size: 13px">
208 {bumps.length} bump{bumps.length === 1 ? "" : "s"}
209 </summary>
210 <table style="margin-top: 8px; font-size: 13px; width: 100%">
211 <thead>
212 <tr style="text-align: left; color: var(--text-muted)">
213 <th style="padding: 4px 8px">Package</th>
214 <th style="padding: 4px 8px">From</th>
215 <th style="padding: 4px 8px">To</th>
216 <th style="padding: 4px 8px">Kind</th>
217 <th style="padding: 4px 8px">Major?</th>
218 </tr>
219 </thead>
220 <tbody>
221 {bumps.map((b) => (
222 <tr>
223 <td style="padding: 4px 8px">
224 <code>{b.name}</code>
225 </td>
226 <td style="padding: 4px 8px">{b.from}</td>
227 <td style="padding: 4px 8px">{b.to}</td>
228 <td style="padding: 4px 8px">{b.kind}</td>
229 <td style="padding: 4px 8px">
230 {b.major ? "yes" : "no"}
231 </td>
232 </tr>
233 ))}
234 </tbody>
235 </table>
236 </details>
237 )}
238 </div>
239 );
240 })}
241 </div>
242 )}
243 </div>
244 </Layout>
245 );
246 }
247);
248
249// POST — fire and forget
250depUpdater.post(
251 "/:owner/:repo/settings/dep-updater/run",
252 requireAuth,
253 async (c) => {
254 const { owner: ownerName, repo: repoName } = c.req.param();
255 const resolved = await resolveOwnerRepo(c, ownerName, repoName);
256 if (resolved.kind === "response") return resolved.res;
257 const { repo } = resolved;
258 const user = c.get("user")!;
259
260 // Fire-and-forget. The run records its own failures to `dep_update_runs`.
261 runDepUpdateRun({
262 repositoryId: repo.id,
263 owner: ownerName,
264 repo: repoName,
265 userId: user.id,
266 }).catch((err) => {
267 console.error("[dep-updater] run failed:", err);
268 });
269
270 return c.redirect(`/${ownerName}/${repoName}/settings/dep-updater`);
271 }
272);
273
274export default depUpdater;