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

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

traffic.tsxBlame195 lines · 1 contributor
8f50ed0Claude1/**
2 * Block F1 — Traffic analytics UI.
3 *
4 * GET /:owner/:repo/traffic — owner-only 14-day views/clones chart,
5 * unique visitors, top paths + referers.
6 */
7
8import { Hono } from "hono";
9import { and, eq } from "drizzle-orm";
10import { db } from "../db";
11import { repositories, users } from "../db/schema";
12import { Layout } from "../views/layout";
13import { RepoHeader, RepoNav } from "../views/components";
14import { softAuth, requireAuth } from "../middleware/auth";
15import type { AuthEnv } from "../middleware/auth";
16import { summarise } from "../lib/traffic";
17
18const traffic = new Hono<AuthEnv>();
19traffic.use("*", softAuth);
20
21async function loadRepo(owner: string, repo: string) {
22 try {
23 const [row] = await db
24 .select({
25 id: repositories.id,
26 name: repositories.name,
27 ownerId: repositories.ownerId,
28 starCount: repositories.starCount,
29 forkCount: repositories.forkCount,
30 })
31 .from(repositories)
32 .innerJoin(users, eq(repositories.ownerId, users.id))
33 .where(and(eq(users.username, owner), eq(repositories.name, repo)))
34 .limit(1);
35 return row || null;
36 } catch {
37 return null;
38 }
39}
40
41traffic.get("/:owner/:repo/traffic", requireAuth, async (c) => {
42 const user = c.get("user")!;
43 const { owner, repo } = c.req.param();
44 const repoRow = await loadRepo(owner, repo);
45 if (!repoRow) return c.notFound();
46 if (repoRow.ownerId !== user.id) {
47 return c.redirect(`/${owner}/${repo}`);
48 }
49
50 const windowDays = Math.max(
51 1,
52 Math.min(90, parseInt(c.req.query("days") || "14", 10) || 14)
53 );
54 const summary = await summarise(repoRow.id, windowDays);
55
56 // Simple ascii-bar chart scaled to the max day.
57 const maxN = Math.max(
58 1,
59 ...summary.daily.map((d) => d.views + d.clones)
60 );
61
62 return c.html(
63 <Layout title={`Traffic — ${owner}/${repo}`} user={user}>
64 <RepoHeader
65 owner={owner}
66 repo={repo}
67 starCount={repoRow.starCount}
68 forkCount={repoRow.forkCount}
69 currentUser={user.username}
70 />
71 <RepoNav owner={owner} repo={repo} active="insights" />
72
73 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
74 <h3>Traffic ({windowDays}d)</h3>
75 <div>
76 {[7, 14, 30, 90].map((d) => (
77 <a
78 href={`/${owner}/${repo}/traffic?days=${d}`}
79 class={`btn btn-sm ${d === windowDays ? "btn-primary" : ""}`}
80 style="margin-left:4px"
81 >
82 {d}d
83 </a>
84 ))}
85 </div>
86 </div>
87
88 <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px">
89 <div class="panel" style="padding:12px;text-align:center">
90 <div style="font-size:22px;font-weight:700;color:#79c0ff">
91 {summary.totalViews}
92 </div>
93 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
94 Views
95 </div>
96 </div>
97 <div class="panel" style="padding:12px;text-align:center">
98 <div style="font-size:22px;font-weight:700;color:#d2a8ff">
99 {summary.totalClones}
100 </div>
101 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
102 Clones
103 </div>
104 </div>
105 <div class="panel" style="padding:12px;text-align:center">
106 <div style="font-size:22px;font-weight:700;color:var(--green)">
107 {summary.uniqueVisitorsApprox}
108 </div>
109 <div style="font-size:11px;color:var(--text-muted);text-transform:uppercase">
110 Unique (approx)
111 </div>
112 </div>
113 </div>
114
115 <h4>Daily</h4>
116 <div class="panel" style="margin-bottom:20px;padding:12px">
117 {summary.daily.length === 0 ? (
118 <p style="color:var(--text-muted);font-size:13px">
119 No traffic recorded yet. Views are tracked automatically as people
120 visit this repo; clones + API hits are tracked on git-http access.
121 </p>
122 ) : (
123 summary.daily.map((d) => {
124 const total = d.views + d.clones;
125 const pct = Math.round((total / maxN) * 100);
126 return (
127 <div style="display:flex;align-items:center;gap:8px;font-size:12px;padding:3px 0">
128 <span
129 style="font-family:var(--font-mono);color:var(--text-muted);width:88px"
130 >
131 {d.day}
132 </span>
133 <div
134 style={`flex:1;height:14px;background:var(--bg-tertiary);border-radius:3px;position:relative;overflow:hidden`}
135 >
136 <div
137 style={`position:absolute;left:0;top:0;bottom:0;width:${pct}%;background:linear-gradient(90deg,#79c0ff ${d.views / Math.max(1, total) * 100}%,#d2a8ff ${d.views / Math.max(1, total) * 100}%)`}
138 />
139 </div>
140 <span style="font-family:var(--font-mono);width:56px;text-align:right">
141 {d.views}v / {d.clones}c
142 </span>
143 </div>
144 );
145 })
146 )}
147 </div>
148
149 <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
150 <div>
151 <h4>Top paths</h4>
152 <div class="panel">
153 {summary.topPaths.length === 0 ? (
154 <div class="panel-empty">No paths recorded.</div>
155 ) : (
156 summary.topPaths.map((p) => (
157 <div class="panel-item" style="justify-content:space-between">
158 <code style="font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block">
159 {p.path}
160 </code>
161 <span style="font-family:var(--font-mono);color:var(--text-muted)">
162 {p.n}
163 </span>
164 </div>
165 ))
166 )}
167 </div>
168 </div>
169 <div>
170 <h4>Top referers</h4>
171 <div class="panel">
172 {summary.topReferers.length === 0 ? (
173 <div class="panel-empty">No external referers.</div>
174 ) : (
175 summary.topReferers.map((r) => (
176 <div class="panel-item" style="justify-content:space-between">
177 <span
178 style="font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block"
179 >
180 {r.referer}
181 </span>
182 <span style="font-family:var(--font-mono);color:var(--text-muted)">
183 {r.n}
184 </span>
185 </div>
186 ))
187 )}
188 </div>
189 </div>
190 </div>
191 </Layout>
192 );
193});
194
195export default traffic;