Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

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

web.tsxBlame337 lines · 1 contributor
fc1817aClaude1/**
2 * Web UI routes — browse repositories, code, commits, diffs.
3 */
4
5import { Hono } from "hono";
6import { Layout } from "../views/layout";
7import {
8 RepoHeader,
9 RepoNav,
10 Breadcrumb,
11 FileTable,
12 CommitList,
13 DiffView,
14} from "../views/components";
15import {
16 getTree,
17 getBlob,
18 listCommits,
19 getCommit,
20 getCommitFullMessage,
21 getDiff,
22 getReadme,
23 getDefaultBranch,
24 listBranches,
25 repoExists,
26} from "../git/repository";
27
28const web = new Hono();
29
30// Home page
31web.get("/", (c) => {
32 return c.html(
33 <Layout>
34 <div class="empty-state">
35 <h2>gluecron</h2>
36 <p>AI-native code intelligence platform</p>
37 <pre>{`# Quick start
38curl -X POST http://localhost:3000/api/setup \\
39 -H 'Content-Type: application/json' \\
40 -d '{"username":"you","email":"you@dev.com","repoName":"hello"}'
41
42git remote add gluecron http://localhost:3000/you/hello.git
43git push gluecron main`}</pre>
44 </div>
45 </Layout>
46 );
47});
48
49// User profile (list repos) — placeholder
50web.get("/:owner", async (c) => {
51 const { owner } = c.req.param();
52 return c.html(
53 <Layout title={owner}>
54 <h2 style="margin-bottom: 16px">{owner}</h2>
55 <p style="color: var(--text-muted)">
56 Repository listing coming soon. Use the API to browse repos.
57 </p>
58 </Layout>
59 );
60});
61
62// Repository overview — file tree at HEAD
63web.get("/:owner/:repo", async (c) => {
64 const { owner, repo } = c.req.param();
65
66 if (!(await repoExists(owner, repo))) {
67 return c.html(
68 <Layout title="Not Found">
69 <div class="empty-state">
70 <h2>Repository not found</h2>
71 <p>
72 {owner}/{repo} does not exist.
73 </p>
74 </div>
75 </Layout>,
76 404
77 );
78 }
79
80 const defaultBranch = (await getDefaultBranch(owner, repo)) || "main";
81 const tree = await getTree(owner, repo, defaultBranch);
82
83 if (tree.length === 0) {
84 return c.html(
85 <Layout title={`${owner}/${repo}`}>
86 <RepoHeader owner={owner} repo={repo} />
87 <RepoNav owner={owner} repo={repo} active="code" />
88 <div class="empty-state">
89 <h2>Empty repository</h2>
90 <p>Get started by pushing code:</p>
91 <pre>{`git remote add gluecron http://localhost:3000/${owner}/${repo}.git
92git push -u gluecron main`}</pre>
93 </div>
94 </Layout>
95 );
96 }
97
98 const readme = await getReadme(owner, repo, defaultBranch);
99
100 return c.html(
101 <Layout title={`${owner}/${repo}`}>
102 <RepoHeader owner={owner} repo={repo} />
103 <RepoNav owner={owner} repo={repo} active="code" />
104 <div class="branch-selector">{defaultBranch}</div>
105 <FileTable
106 entries={tree}
107 owner={owner}
108 repo={repo}
109 ref={defaultBranch}
110 path=""
111 />
112 {readme && (
113 <div
114 class="blob-view"
115 style="margin-top: 20px"
116 >
117 <div class="blob-header">README.md</div>
118 <div style="padding: 16px; white-space: pre-wrap; font-size: 14px;">
119 {readme}
120 </div>
121 </div>
122 )}
123 </Layout>
124 );
125});
126
127// Browse tree at ref/path
128web.get("/:owner/:repo/tree/:ref{.+$}", async (c) => {
129 const { owner, repo } = c.req.param();
130 const refAndPath = c.req.param("ref");
131
132 // Parse ref from path — try known branches first, fallback to first segment
133 const branches = await listBranches(owner, repo);
134 let ref = "";
135 let treePath = "";
136
137 for (const branch of branches) {
138 if (refAndPath === branch || refAndPath.startsWith(branch + "/")) {
139 ref = branch;
140 treePath = refAndPath.slice(branch.length + 1);
141 break;
142 }
143 }
144
145 if (!ref) {
146 // Assume first path segment is the ref
147 const slashIdx = refAndPath.indexOf("/");
148 if (slashIdx === -1) {
149 ref = refAndPath;
150 } else {
151 ref = refAndPath.slice(0, slashIdx);
152 treePath = refAndPath.slice(slashIdx + 1);
153 }
154 }
155
156 const tree = await getTree(owner, repo, ref, treePath);
157
158 return c.html(
159 <Layout title={`${treePath || "/"} — ${owner}/${repo}`}>
160 <RepoHeader owner={owner} repo={repo} />
161 <RepoNav owner={owner} repo={repo} active="code" />
162 <div class="branch-selector">{ref}</div>
163 <Breadcrumb owner={owner} repo={repo} ref={ref} path={treePath} />
164 <FileTable
165 entries={tree}
166 owner={owner}
167 repo={repo}
168 ref={ref}
169 path={treePath}
170 />
171 </Layout>
172 );
173});
174
175// View file blob
176web.get("/:owner/:repo/blob/:ref{.+$}", async (c) => {
177 const { owner, repo } = c.req.param();
178 const refAndPath = c.req.param("ref");
179
180 const branches = await listBranches(owner, repo);
181 let ref = "";
182 let filePath = "";
183
184 for (const branch of branches) {
185 if (refAndPath.startsWith(branch + "/")) {
186 ref = branch;
187 filePath = refAndPath.slice(branch.length + 1);
188 break;
189 }
190 }
191
192 if (!ref) {
193 const slashIdx = refAndPath.indexOf("/");
194 if (slashIdx === -1) return c.text("Not found", 404);
195 ref = refAndPath.slice(0, slashIdx);
196 filePath = refAndPath.slice(slashIdx + 1);
197 }
198
199 const blob = await getBlob(owner, repo, ref, filePath);
200 if (!blob) {
201 return c.html(
202 <Layout title="Not Found">
203 <div class="empty-state">
204 <h2>File not found</h2>
205 </div>
206 </Layout>,
207 404
208 );
209 }
210
211 const lines = blob.content.split("\n");
212 // Remove trailing empty line from split
213 if (lines[lines.length - 1] === "") lines.pop();
214
215 return c.html(
216 <Layout title={`${filePath} — ${owner}/${repo}`}>
217 <RepoHeader owner={owner} repo={repo} />
218 <RepoNav owner={owner} repo={repo} active="code" />
219 <div class="branch-selector">{ref}</div>
220 <Breadcrumb owner={owner} repo={repo} ref={ref} path={filePath} />
221 <div class="blob-view">
222 <div class="blob-header">
223 {filePath.split("/").pop()} — {blob.size} bytes
224 </div>
225 {blob.isBinary ? (
226 <div style="padding: 16px; color: var(--text-muted)">
227 Binary file not shown.
228 </div>
229 ) : (
230 <div class="blob-code">
231 <table>
232 <tbody>
233 {lines.map((line, i) => (
234 <tr>
235 <td class="line-num">{i + 1}</td>
236 <td class="line-content">{line}</td>
237 </tr>
238 ))}
239 </tbody>
240 </table>
241 </div>
242 )}
243 </div>
244 </Layout>
245 );
246});
247
248// Commit log
249web.get("/:owner/:repo/commits/:ref?", async (c) => {
250 const { owner, repo } = c.req.param();
251 const ref =
252 c.req.param("ref") || (await getDefaultBranch(owner, repo)) || "main";
253
254 const commits = await listCommits(owner, repo, ref, 50);
255
256 return c.html(
257 <Layout title={`Commits — ${owner}/${repo}`}>
258 <RepoHeader owner={owner} repo={repo} />
259 <RepoNav owner={owner} repo={repo} active="commits" />
260 <div class="branch-selector">{ref}</div>
261 {commits.length === 0 ? (
262 <div class="empty-state">
263 <p>No commits yet.</p>
264 </div>
265 ) : (
266 <CommitList commits={commits} owner={owner} repo={repo} />
267 )}
268 </Layout>
269 );
270});
271
272// Single commit with diff
273web.get("/:owner/:repo/commit/:sha", async (c) => {
274 const { owner, repo, sha } = c.req.param();
275
276 const commit = await getCommit(owner, repo, sha);
277 if (!commit) {
278 return c.html(
279 <Layout title="Not Found">
280 <div class="empty-state">
281 <h2>Commit not found</h2>
282 </div>
283 </Layout>,
284 404
285 );
286 }
287
288 const fullMessage = await getCommitFullMessage(owner, repo, sha);
289 const { files, raw } = await getDiff(owner, repo, sha);
290
291 return c.html(
292 <Layout title={`${commit.message} — ${owner}/${repo}`}>
293 <RepoHeader owner={owner} repo={repo} />
294 <div
295 style="background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; margin-bottom: 20px"
296 >
297 <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px">
298 {commit.message}
299 </div>
300 {fullMessage !== commit.message && (
301 <div style="white-space: pre-wrap; color: var(--text-muted); font-size: 14px; margin-bottom: 12px">
302 {fullMessage}
303 </div>
304 )}
305 <div style="font-size: 13px; color: var(--text-muted)">
306 <strong style="color: var(--text)">{commit.author}</strong>{" "}
307 committed on{" "}
308 {new Date(commit.date).toLocaleDateString("en-US", {
309 month: "long",
310 day: "numeric",
311 year: "numeric",
312 })}
313 </div>
314 <div style="margin-top: 8px">
315 <span class="commit-sha">{commit.sha}</span>
316 {commit.parentShas.length > 0 && (
317 <span style="margin-left: 12px; font-size: 13px; color: var(--text-muted)">
318 Parent:{" "}
319 {commit.parentShas.map((p) => (
320 <a
321 href={`/${owner}/${repo}/commit/${p}`}
322 class="commit-sha"
323 style="margin-left: 4px"
324 >
325 {p.slice(0, 7)}
326 </a>
327 ))}
328 </span>
329 )}
330 </div>
331 </div>
332 <DiffView raw={raw} files={files} />
333 </Layout>
334 );
335});
336
337export default web;