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

docs-tracking.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.

docs-tracking.tsxBlame501 lines · 1 contributor
d199847Claude1/**
2 * AI-tracked documentation sections — staleness dashboard.
3 *
4 * GET /:owner/:repo/docs/tracking
5 *
6 * Lists every `<!-- gluecron:doc-track src=... -->` region we know about
7 * in the repo, alongside its source hash, last-checked timestamp, and a
8 * "Stale / Fresh / Unseen" pill. Empty state when nothing is tracked yet.
9 *
10 * All page-local CSS is scoped under `.doctrk-*` so it cannot bleed into
11 * the shared layout (per CLAUDE.md: do NOT modify shared layout /
12 * components / ui). Mirrors the gradient hairline + orb pattern used by
13 * previews.tsx and environments.tsx.
14 */
15
16import { Hono } from "hono";
17import { and, eq } from "drizzle-orm";
18import { db } from "../db";
19import { docTracking, pullRequests, repositories, users } from "../db/schema";
20import { softAuth } from "../middleware/auth";
21import type { AuthEnv } from "../middleware/auth";
22import { Layout } from "../views/layout";
23import { RepoHeader, RepoNav } from "../views/components";
24import { getUnreadCount } from "../lib/unread";
25import { findTrackedDocs, type TrackedDoc } from "../lib/ai-doc-updater";
26
27const r = new Hono<AuthEnv>();
28r.use("*", softAuth);
29
30interface RepoRow {
31 id: string;
32 name: string;
33 defaultBranch: string;
34 starCount: number;
35 forkCount: number;
36}
37
38async function loadRepo(owner: string, repo: string): Promise<RepoRow | null> {
39 try {
40 const [row] = await db
41 .select({
42 id: repositories.id,
43 name: repositories.name,
44 defaultBranch: repositories.defaultBranch,
45 starCount: repositories.starCount,
46 forkCount: repositories.forkCount,
47 })
48 .from(repositories)
49 .innerJoin(users, eq(repositories.ownerId, users.id))
50 .where(and(eq(users.username, owner), eq(repositories.name, repo)))
51 .limit(1);
52 return row || null;
53 } catch (err) {
54 console.error("[docs-tracking] loadRepo failed:", err);
55 return null;
56 }
57}
58
59interface StoredRow {
60 docPath: string;
61 sectionMarker: string;
62 lastCheckedAt: Date;
63 lastPrId: string | null;
64 prNumber: number | null;
65 prState: string | null;
66}
67
68async function loadStored(repositoryId: string): Promise<StoredRow[]> {
69 try {
70 const rows = await db
71 .select({
72 docPath: docTracking.docPath,
73 sectionMarker: docTracking.sectionMarker,
74 lastCheckedAt: docTracking.lastCheckedAt,
75 lastPrId: docTracking.lastPrId,
76 prNumber: pullRequests.number,
77 prState: pullRequests.state,
78 })
79 .from(docTracking)
80 .leftJoin(pullRequests, eq(pullRequests.id, docTracking.lastPrId))
81 .where(eq(docTracking.repositoryId, repositoryId));
82 return rows.map((r) => ({
83 docPath: r.docPath,
84 sectionMarker: r.sectionMarker,
85 lastCheckedAt: r.lastCheckedAt,
86 lastPrId: r.lastPrId,
87 prNumber: r.prNumber,
88 prState: r.prState,
89 }));
90 } catch {
91 return [];
92 }
93}
94
95/* ─────────────────────────────────────────────────────────────────────────
96 * Scoped CSS — `.doctrk-*` only. Mirrors `.preview-*` styling so the
97 * dashboard feels at home alongside the other repo sub-pages.
98 * ───────────────────────────────────────────────────────────────────── */
99const docTrackingStyles = `
eed4684Claude100 .doctrk-wrap { max-width: 1680px; margin: 0 auto; padding: var(--space-5) var(--space-4) var(--space-8); }
d199847Claude101
102 .doctrk-head {
103 position: relative;
104 margin-bottom: var(--space-5);
105 padding: var(--space-4) var(--space-5);
106 background: var(--bg-elevated);
107 border: 1px solid var(--border);
108 border-radius: 14px;
109 overflow: hidden;
110 }
111 .doctrk-head::before {
112 content: '';
113 position: absolute;
114 top: 0; left: 0; right: 0;
115 height: 2px;
116 background: linear-gradient(90deg, transparent 0%, #36c5d6 30%, #8c6dff 70%, transparent 100%);
117 opacity: 0.7;
118 pointer-events: none;
119 }
120 .doctrk-head-orb {
121 position: absolute;
122 inset: -30% -10% auto auto;
123 width: 320px; height: 320px;
124 background: radial-gradient(circle, rgba(54,197,214,0.18), rgba(140,109,255,0.08) 45%, transparent 70%);
125 filter: blur(70px);
126 opacity: 0.7;
127 pointer-events: none;
128 z-index: 0;
129 }
130 .doctrk-head-inner { position: relative; z-index: 1; }
131
132 .doctrk-eyebrow {
133 display: inline-flex;
134 align-items: center;
135 gap: 8px;
136 text-transform: uppercase;
137 font-family: var(--font-mono);
138 font-size: 11px;
139 letter-spacing: 0.16em;
140 color: var(--text-muted);
141 font-weight: 600;
142 margin-bottom: 10px;
143 }
144 .doctrk-eyebrow-dot {
145 width: 8px; height: 8px;
146 border-radius: 9999px;
147 background: linear-gradient(135deg, #36c5d6, #8c6dff);
148 box-shadow: 0 0 0 3px rgba(54,197,214,0.18);
149 }
150 .doctrk-title {
151 font-family: var(--font-display);
152 font-size: 28px;
153 font-weight: 700;
154 color: var(--text-strong);
155 margin: 0 0 6px;
156 letter-spacing: -0.02em;
157 }
158 .doctrk-sub {
159 color: var(--text-muted);
160 margin: 0;
161 max-width: 70ch;
162 line-height: 1.5;
163 }
164
165 .doctrk-list {
166 display: flex;
167 flex-direction: column;
168 gap: var(--space-3);
169 }
170 .doctrk-doc {
171 border: 1px solid var(--border);
172 border-radius: 12px;
173 background: var(--bg-elevated);
174 padding: var(--space-3) var(--space-4);
175 }
176 .doctrk-doc-head {
177 display: flex;
178 justify-content: space-between;
179 align-items: center;
180 gap: var(--space-3);
181 margin-bottom: var(--space-2);
182 }
183 .doctrk-doc-path {
184 font-family: var(--font-mono);
185 font-size: 14px;
186 font-weight: 700;
187 color: var(--text-strong);
188 word-break: break-all;
189 }
190 .doctrk-section {
191 padding: 10px 12px;
192 border: 1px solid var(--border);
193 border-radius: 8px;
194 background: rgba(255,255,255,0.02);
195 margin-top: 8px;
196 }
197 .doctrk-section-head {
198 display: flex;
199 justify-content: space-between;
200 align-items: center;
201 gap: var(--space-2);
202 margin-bottom: 6px;
203 flex-wrap: wrap;
204 }
205 .doctrk-section-src {
206 font-family: var(--font-mono);
207 font-size: 12.5px;
208 color: var(--text);
209 }
210 .doctrk-section-claim {
211 font-size: 13px;
212 color: var(--text-muted);
213 line-height: 1.45;
214 white-space: pre-wrap;
215 max-height: 4.5em;
216 overflow: hidden;
217 text-overflow: ellipsis;
218 }
219 .doctrk-section-meta {
220 margin-top: 6px;
221 font-size: 11.5px;
222 color: var(--text-muted);
223 font-variant-numeric: tabular-nums;
224 display: flex;
225 gap: var(--space-3);
226 flex-wrap: wrap;
227 }
228 .doctrk-section-meta code {
229 font-family: var(--font-mono);
230 background: rgba(255,255,255,0.04);
231 padding: 1px 6px;
232 border-radius: 6px;
233 }
234 .doctrk-section-meta a {
235 color: #b69dff;
236 text-decoration: none;
237 }
238 .doctrk-section-meta a:hover { text-decoration: underline; }
239
240 /* ─── status pills ─── */
241 .doctrk-pill {
242 display: inline-flex;
243 align-items: center;
244 gap: 5px;
245 padding: 2px 9px;
246 border-radius: 9999px;
247 font-size: 10.5px;
248 font-weight: 600;
249 letter-spacing: 0.05em;
250 text-transform: uppercase;
251 font-family: var(--font-mono);
252 background: rgba(255,255,255,0.04);
253 color: var(--text-muted);
254 box-shadow: inset 0 0 0 1px var(--border);
255 }
256 .doctrk-pill.is-stale {
257 background: rgba(251,191,36,0.10);
258 color: #fde68a;
259 box-shadow: inset 0 0 0 1px rgba(251,191,36,0.30);
260 }
261 .doctrk-pill.is-fresh {
262 background: rgba(52,211,153,0.10);
263 color: #6ee7b7;
264 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.30);
265 }
266 .doctrk-pill.is-unseen {
267 background: rgba(148,163,184,0.10);
268 color: #cbd5e1;
269 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
270 }
271 .doctrk-pill.is-missing {
272 background: rgba(248,113,113,0.10);
273 color: #fecaca;
274 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.35);
275 }
276 .doctrk-pill-dot {
277 width: 6px; height: 6px;
278 border-radius: 9999px;
279 background: currentColor;
280 }
281
282 /* ─── empty state ─── */
283 .doctrk-empty {
284 position: relative;
285 padding: var(--space-6) var(--space-5);
286 background: var(--bg-elevated);
287 border: 1px dashed var(--border-strong, var(--border));
288 border-radius: 14px;
289 text-align: center;
290 overflow: hidden;
291 margin-bottom: var(--space-5);
292 }
293 .doctrk-empty-orb {
294 position: absolute;
295 inset: auto auto -40% 50%;
296 transform: translateX(-50%);
297 width: 320px; height: 320px;
298 background: radial-gradient(circle, rgba(54,197,214,0.18), rgba(140,109,255,0.08) 45%, transparent 70%);
299 filter: blur(70px);
300 opacity: 0.7;
301 pointer-events: none;
302 }
303 .doctrk-empty-inner { position: relative; z-index: 1; max-width: 540px; margin: 0 auto; }
304 .doctrk-empty-title {
305 font-family: var(--font-display);
306 font-size: 16px;
307 font-weight: 700;
308 color: var(--text-strong);
309 margin: 0 0 6px;
310 }
311 .doctrk-empty-body {
312 font-size: 13.5px;
313 color: var(--text-muted);
314 line-height: 1.5;
315 margin: 0 0 var(--space-3);
316 }
317 .doctrk-empty code, .doctrk-codeblock {
318 font-family: var(--font-mono);
319 font-size: 12px;
320 color: var(--text);
321 background: rgba(255,255,255,0.06);
322 padding: 1px 6px;
323 border-radius: 6px;
324 }
325 .doctrk-codeblock {
326 display: block;
327 padding: 10px 14px;
328 white-space: pre;
329 text-align: left;
330 margin: var(--space-3) auto;
331 max-width: 540px;
332 overflow-x: auto;
333 }
334`;
335
336function statusFor(
337 stored: StoredRow | undefined,
338 section: { stale: boolean; currentSrcHash: string }
339): { label: string; cls: string } {
340 if (section.currentSrcHash.startsWith("missing:")) {
341 return { label: "missing source", cls: "is-missing" };
342 }
343 if (!stored) {
344 return { label: "unseen", cls: "is-unseen" };
345 }
346 if (section.stale) {
347 return { label: "stale", cls: "is-stale" };
348 }
349 return { label: "fresh", cls: "is-fresh" };
350}
351
352r.get("/:owner/:repo/docs/tracking", async (c) => {
353 const user = c.get("user");
354 const { owner, repo } = c.req.param();
355 const repoRow = await loadRepo(owner, repo);
356 if (!repoRow) return c.notFound();
357
358 const [docs, stored, unread] = await Promise.all([
359 findTrackedDocs(repoRow.id).catch(() => [] as TrackedDoc[]),
360 loadStored(repoRow.id),
361 user ? getUnreadCount(user.id) : Promise.resolve(0),
362 ]);
363
364 const storedByKey = new Map<string, StoredRow>();
365 for (const row of stored) {
366 storedByKey.set(`${row.docPath}::${row.sectionMarker}`, row);
367 }
368
369 const totalSections = docs.reduce((a, d) => a + d.sections.length, 0);
370
371 return c.html(
372 <Layout
373 title={`Tracked docs — ${owner}/${repo}`}
374 user={user}
375 notificationCount={unread}
376 >
377 <RepoHeader
378 owner={owner}
379 repo={repo}
380 starCount={repoRow.starCount}
381 forkCount={repoRow.forkCount}
382 currentUser={user?.username}
383 />
384 <RepoNav owner={owner} repo={repo} active="code" />
385
386 <div class="doctrk-wrap">
387 <section class="doctrk-head">
388 <div class="doctrk-head-orb" aria-hidden="true" />
389 <div class="doctrk-head-inner">
390 <div class="doctrk-eyebrow">
391 <span class="doctrk-eyebrow-dot" aria-hidden="true" />
392 AI-tracked docs · {owner}/{repo}
393 </div>
394 <h2 class="doctrk-title">Documentation drift</h2>
395 <p class="doctrk-sub">
396 Every markdown region wrapped in{" "}
397 <code>&lt;!-- gluecron:doc-track src=... --&gt;</code> markers is
398 hashed against the source it claims to describe. When the
399 source changes, Claude opens a draft PR labelled{" "}
400 <code>ai:doc-update</code> with the refreshed prose.
401 </p>
402 </div>
403 </section>
404
405 {docs.length === 0 ? (
406 <div class="doctrk-empty">
407 <div class="doctrk-empty-orb" aria-hidden="true" />
408 <div class="doctrk-empty-inner">
409 <h3 class="doctrk-empty-title">No tracked sections yet</h3>
410 <p class="doctrk-empty-body">
411 Wrap a paragraph in a markdown file with the marker below
412 and Claude will keep it in sync with the source whenever
413 you push.
414 </p>
415 <code class="doctrk-codeblock">{`<!-- gluecron:doc-track src=src/lib/auth.ts -->
416This module exports \`signIn\` and \`signUp\` —
417see the source for details.
418<!-- /gluecron:doc-track -->`}</code>
419 </div>
420 </div>
421 ) : (
422 <>
423 <p class="doctrk-sub" style="margin-bottom: var(--space-3);">
424 {docs.length} doc{docs.length === 1 ? "" : "s"} · {totalSections}{" "}
425 tracked section{totalSections === 1 ? "" : "s"}
426 </p>
427 <div class="doctrk-list">
428 {docs.map((d) => (
429 <div class="doctrk-doc">
430 <div class="doctrk-doc-head">
431 <div class="doctrk-doc-path">{d.path}</div>
432 <div>
433 <span class="doctrk-pill">
434 <span class="doctrk-pill-dot" aria-hidden="true" />
435 {d.sections.length} section
436 {d.sections.length === 1 ? "" : "s"}
437 </span>
438 </div>
439 </div>
440 {d.sections.map((s) => {
441 const key = `${d.path}::${s.marker}`;
442 const storedRow = storedByKey.get(key);
443 const status = statusFor(storedRow, s);
444 const checkedLabel = storedRow
445 ? new Date(storedRow.lastCheckedAt).toISOString()
446 : "never";
447 return (
448 <div class="doctrk-section">
449 <div class="doctrk-section-head">
450 <div class="doctrk-section-src">
451 tracks <code>{s.claimedFor}</code>
452 </div>
453 <span class={`doctrk-pill ${status.cls}`}>
454 <span class="doctrk-pill-dot" aria-hidden="true" />
455 {status.label}
456 </span>
457 </div>
458 <div class="doctrk-section-claim">{s.claim}</div>
459 <div class="doctrk-section-meta">
460 <span>
461 marker <code>{s.marker}</code>
462 </span>
463 <span>
464 current{" "}
465 <code>{s.currentSrcHash.slice(0, 12)}</code>
466 </span>
467 <span>
468 stored{" "}
469 <code>
470 {(s.storedClaimedHash ?? "—").slice(0, 12)}
471 </code>
472 </span>
473 <span>last checked {checkedLabel}</span>
474 {storedRow?.prNumber ? (
475 <span>
476 PR{" "}
477 <a
478 href={`/${owner}/${repo}/pulls/${storedRow.prNumber}`}
479 >
480 #{storedRow.prNumber}
481 </a>{" "}
482 ({storedRow.prState ?? "?"})
483 </span>
484 ) : null}
485 </div>
486 </div>
487 );
488 })}
489 </div>
490 ))}
491 </div>
492 </>
493 )}
494 </div>
495
496 <style dangerouslySetInnerHTML={{ __html: docTrackingStyles }} />
497 </Layout>
498 );
499});
500
501export default r;