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

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

demo.tsxBlame946 lines · 1 contributor
52ad8b1Claude1/**
2 * Block L3 — public `/demo` landing page + companion JSON endpoints.
3 *
4 * Anonymous-friendly. Demonstrates Gluecron's AI features in real time by
5 * surfacing live counts off the audit log + `pr_comments` table for the
6 * seeded demo repos.
7 *
8 * Routes mounted from `src/app.tsx`. Must come BEFORE `routes/admin.tsx`
9 * in mount order so this handler wins over the legacy `/demo` redirect.
10 *
11 * GET /demo → SSR landing page (graceful no-JS,
12 * polls JSON endpoints every 30s
13 * when JS is available)
14 * GET /api/v2/demo/activity → combined activity feed JSON
15 * GET /api/v2/demo/queued → queued ai:build issues JSON
16 * GET /api/v2/demo/merges → recent auto-merges JSON
17 * GET /api/v2/demo/reviews → recent AI reviews JSON
18 *
19 * All JSON endpoints serve `Cache-Control: public, max-age=30` so the demo
20 * page is cheap to refresh and external dashboards can embed safely.
e3eb5ffClaude21 *
22 * 2026 polish: scoped `.demo-page-` CSS, four-step interactive walkthrough
23 * (mirrors /connect/claude), eyebrow + display headline + 1-line subtitle,
24 * 'Try this' prompts on each step, and a reset button on the live feed.
52ad8b1Claude25 */
26
27import { Hono } from "hono";
28import { Layout } from "../views/layout";
29import { softAuth } from "../middleware/auth";
30import type { AuthEnv } from "../middleware/auth";
31import { DEMO_USERNAME } from "../lib/demo-seed";
32import {
33 countAiReviewsSince,
34 listDemoActivityFeed,
35 listQueuedAiBuildIssues,
36 listRecentAiReviews,
37 listRecentAutoMerges,
38 type DemoActivityEntry,
39} from "../lib/demo-activity";
40
41const app = new Hono<AuthEnv>();
42
43const POLL_INTERVAL_MS = 30_000;
44
45function jsonCacheHeaders(): Record<string, string> {
46 return {
47 "Content-Type": "application/json; charset=utf-8",
48 "Cache-Control": "public, max-age=30",
49 };
50}
51
52// ───────────────────────────────────────────────────────────────────
53// JSON endpoints — cheap, public, cacheable.
54// ───────────────────────────────────────────────────────────────────
55
56app.get("/api/v2/demo/activity", async (c) => {
57 const feed = await listDemoActivityFeed(20);
58 return new Response(
59 JSON.stringify({
60 entries: feed.map((e) => ({
61 kind: e.kind,
62 repo: e.repo,
63 ref: e.ref,
64 at: e.at.toISOString(),
65 })),
66 }),
67 { status: 200, headers: jsonCacheHeaders() }
68 );
69});
70
71app.get("/api/v2/demo/queued", async (c) => {
72 const items = await listQueuedAiBuildIssues(5);
73 return new Response(
74 JSON.stringify({
75 items: items.map((i) => ({
76 repo: i.repo,
77 number: i.number,
78 title: i.title,
79 createdAt: i.createdAt.toISOString(),
80 })),
81 }),
82 { status: 200, headers: jsonCacheHeaders() }
83 );
84});
85
86app.get("/api/v2/demo/merges", async (c) => {
87 const items = await listRecentAutoMerges(5, 24);
88 return new Response(
89 JSON.stringify({
90 items: items.map((m) => ({
91 repo: m.repo,
92 number: m.number,
93 title: m.title,
94 mergedAt: m.mergedAt.toISOString(),
95 })),
96 }),
97 { status: 200, headers: jsonCacheHeaders() }
98 );
99});
100
101app.get("/api/v2/demo/reviews", async (c) => {
102 const [items, count] = await Promise.all([
103 listRecentAiReviews(5, 24),
104 countAiReviewsSince(24),
105 ]);
106 return new Response(
107 JSON.stringify({
108 count,
109 items: items.map((r) => ({
110 repo: r.repo,
111 prNumber: r.prNumber,
112 commentSnippet: r.commentSnippet,
113 createdAt: r.createdAt.toISOString(),
114 })),
115 }),
116 { status: 200, headers: jsonCacheHeaders() }
117 );
118});
119
120// ───────────────────────────────────────────────────────────────────
121// HTML landing page.
122// ───────────────────────────────────────────────────────────────────
123
124function relativeTime(d: Date): string {
125 const ms = Date.now() - d.getTime();
126 const s = Math.max(0, Math.floor(ms / 1000));
127 if (s < 60) return `${s}s ago`;
128 const m = Math.floor(s / 60);
129 if (m < 60) return `${m}m ago`;
130 const h = Math.floor(m / 60);
131 if (h < 48) return `${h}h ago`;
132 const days = Math.floor(h / 24);
133 return `${days}d ago`;
134}
135
136function activityLabel(kind: DemoActivityEntry["kind"]): string {
137 switch (kind) {
138 case "auto_merge.merged":
139 return "auto-merged";
140 case "ai_build.dispatched":
141 return "AI-build dispatched";
142 case "ai_review.posted":
143 return "AI review posted";
144 }
145}
146
147app.get("/demo", softAuth, async (c) => {
148 const user = c.get("user") ?? null;
149
150 // Server-side render the initial snapshot. The JS poller refreshes it
151 // every 30s but the no-JS experience still works.
152 const [queued, merges, reviewsCountAndList, feed] = await Promise.all([
153 listQueuedAiBuildIssues(5),
154 listRecentAutoMerges(5, 24),
155 Promise.all([listRecentAiReviews(5, 24), countAiReviewsSince(24)]),
156 listDemoActivityFeed(20),
157 ]);
158 const [reviews, reviewCount] = reviewsCountAndList;
159
160 const demoUserUrl = `/${DEMO_USERNAME}`;
161 const demoRepos: { name: string; tagline: string }[] = [
162 { name: "hello-python", tagline: "Tiny Python app — labels, issues, gates." },
163 { name: "todo-api", tagline: "Hono todo API — PRs, AI review, auto-merge." },
164 { name: "design-docs", tagline: "ADRs + architecture docs." },
165 ];
166
167 // Poller — refreshes the four tiles every 30s. Plain vanilla JS, no
e3eb5ffClaude168 // framework. Adds a reset action that re-fetches every tile immediately.
52ad8b1Claude169 const pollerScript = `
170(function(){try{
171 var INTERVAL=${POLL_INTERVAL_MS};
172 function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
173 function rel(iso){try{var ms=Date.now()-new Date(iso).getTime();var s=Math.max(0,Math.floor(ms/1000));if(s<60)return s+'s ago';var m=Math.floor(s/60);if(m<60)return m+'m ago';var h=Math.floor(m/60);if(h<48)return h+'h ago';return Math.floor(h/24)+'d ago';}catch(e){return '';}}
174 function pollQueued(){fetch('/api/v2/demo/queued').then(function(r){return r.json();}).then(function(d){
175 var el=document.getElementById('tile-queued-list');if(!el)return;
7d0c65eClaude176 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">Nothing building right now — tag an issue ai:build to watch it start.</li>';return;}
e3eb5ffClaude177 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/issues/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
52ad8b1Claude178 }).catch(function(){});}
179 function pollMerges(){fetch('/api/v2/demo/merges').then(function(r){return r.json();}).then(function(d){
180 var el=document.getElementById('tile-merges-list');if(!el)return;
7d0c65eClaude181 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No instant auto-merges yet — one fires the moment gates go green.</li>';return;}
e3eb5ffClaude182 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.number+'">#'+i.number+' '+esc(i.title)+'</a> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.mergedAt)+'</span></li>';}).join('');
52ad8b1Claude183 }).catch(function(){});}
184 function pollReviews(){fetch('/api/v2/demo/reviews').then(function(r){return r.json();}).then(function(d){
185 var c=document.getElementById('tile-reviews-count');if(c)c.textContent=String(d.count||0);
186 var el=document.getElementById('tile-reviews-list');if(!el)return;
e3eb5ffClaude187 if(!d.items||d.items.length===0){el.innerHTML='<li class="demo-page-empty">No AI reviews in the last 24h.</li>';return;}
188 el.innerHTML=d.items.map(function(i){return '<li><a href="/${DEMO_USERNAME}/'+esc(i.repo)+'/pulls/'+i.prNumber+'">#'+i.prNumber+'</a> <span class="demo-page-snippet">'+esc(i.commentSnippet)+'</span> <span class="demo-page-meta">'+esc(i.repo)+' · '+rel(i.createdAt)+'</span></li>';}).join('');
52ad8b1Claude189 }).catch(function(){});}
190 function pollFeed(){fetch('/api/v2/demo/activity').then(function(r){return r.json();}).then(function(d){
191 var el=document.getElementById('demo-feed-list');if(!el)return;
e3eb5ffClaude192 if(!d.entries||d.entries.length===0){el.innerHTML='<li class="demo-page-empty">Quiet right now — push a commit to a demo repo to see live updates.</li>';return;}
193 el.innerHTML=d.entries.map(function(e){var label=e.kind==='auto_merge.merged'?'auto-merged':(e.kind==='ai_build.dispatched'?'AI-build dispatched':'AI review posted');var path=e.ref.type==='pr'?'pulls':'issues';return '<li><span class="demo-page-kind demo-page-kind-'+esc(e.kind.replace(/\\./g,'-'))+'">'+esc(label)+'</span> <a href="/${DEMO_USERNAME}/'+esc(e.repo)+'/'+path+'/'+e.ref.number+'">'+esc(e.repo)+' #'+e.ref.number+'</a> <span class="demo-page-meta">'+rel(e.at)+'</span></li>';}).join('');
52ad8b1Claude194 }).catch(function(){});}
195 function tick(){pollQueued();pollMerges();pollReviews();pollFeed();}
e3eb5ffClaude196 // Reset button — re-fetch every tile immediately, then flash the button.
197 var reset=document.getElementById('demo-reset');
198 if(reset){reset.addEventListener('click',function(e){
199 e.preventDefault();
200 tick();
201 var orig=reset.textContent;
202 reset.textContent='Refreshed';
203 reset.classList.add('is-done');
204 setTimeout(function(){reset.textContent=orig;reset.classList.remove('is-done');},1500);
205 });}
206 // Background polling — initial SSR snapshot stays visible until the
207 // first tick fires, so the page doesn't "flash" identical content.
52ad8b1Claude208 setInterval(tick,INTERVAL);
209}catch(e){}})();
210`.trim();
211
212 return c.html(
5acce80Claude213 <Layout
214 title="Live demo"
215 user={user}
216 description="Watch Gluecron build and ship code in real time. AI writes the PR, reviews it, and merges it automatically."
217 ogTitle="Live demo — Gluecron"
218 ogDescription="Watch Gluecron build and ship code in real time. AI writes the PR, reviews it, and merges it automatically."
219 twitterCard="summary_large_image"
220 >
52ad8b1Claude221 <style dangerouslySetInnerHTML={{ __html: DEMO_CSS }} />
222 <div class="demo-page">
e3eb5ffClaude223 {/* ─── Hero ─── */}
224 <section class="demo-page-hero">
225 <div class="demo-page-hero-orb" aria-hidden="true" />
226 <div class="demo-page-hero-inner">
227 <div class="demo-page-eyebrow">
228 <span class="demo-page-pulse" aria-hidden="true" />
52ad8b1Claude229 Live · pulled from production · refreshes every 30s
230 </div>
e3eb5ffClaude231 <h1 class="demo-page-title">
232 Watch Claude{" "}
233 <span class="demo-page-title-grad">build software, live.</span>
52ad8b1Claude234 </h1>
e3eb5ffClaude235 <p class="demo-page-sub">
236 Every tile and step below is real audit data from the seeded
237 demo repos. No staging, no mocks.
52ad8b1Claude238 </p>
e3eb5ffClaude239 <div class="demo-page-hero-cta">
240 <a href="/register" class="demo-page-btn demo-page-btn-primary">
241 Sign up free <span aria-hidden="true">→</span>
242 </a>
243 <a href={demoUserUrl} class="demo-page-btn">
244 Browse the demo user
245 </a>
246 </div>
52ad8b1Claude247 </div>
e3eb5ffClaude248 </section>
249
250 {/* ─── 4-step interactive walkthrough ─── */}
251 <section class="demo-page-section" aria-labelledby="demo-walk-h">
252 <header class="demo-page-section-head">
253 <div>
254 <p class="demo-page-section-eyebrow">Walkthrough</p>
255 <h2 class="demo-page-section-title" id="demo-walk-h">
256 Four steps. Each one round-trips through production.
257 </h2>
258 <p class="demo-page-section-sub">
259 Click "Try this" on any step to jump into the live demo
260 surface that performs it.
261 </p>
262 </div>
263 </header>
264 <div class="demo-page-section-body">
265 <ol class="demo-page-walk">
266 {/* Step 1 — File an ai:build issue */}
267 <li class="demo-page-step">
268 <div class="demo-page-step-head">
269 <span class="demo-page-step-num">1</span>
270 <span class="demo-page-step-icon" aria-hidden="true">
271 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
272 <circle cx="12" cy="12" r="9" />
273 <line x1="12" y1="8" x2="12" y2="13" />
274 <line x1="12" y1="16" x2="12.01" y2="16" />
275 </svg>
276 </span>
277 <h3 class="demo-page-step-title">File an issue</h3>
278 </div>
279 <p class="demo-page-step-body">
280 Open a new issue on any demo repo and tag it{" "}
7d0c65eClaude281 <code>ai:build</code>. The autopilot picks it up in real
282 time — a draft PR appears within 90 seconds.
e3eb5ffClaude283 </p>
284 <p class="demo-page-step-try">
285 <span class="demo-page-try-label">Try this</span>
286 <a
287 class="demo-page-try-link"
288 href={`/${DEMO_USERNAME}/todo-api/issues/new`}
289 >
290 Open the new-issue page →
291 </a>
292 </p>
293 </li>
294
295 {/* Step 2 — Claude opens a PR */}
296 <li class="demo-page-step">
297 <div class="demo-page-step-head">
298 <span class="demo-page-step-num">2</span>
299 <span class="demo-page-step-icon" aria-hidden="true">
300 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
301 <circle cx="6" cy="6" r="3" />
302 <circle cx="6" cy="18" r="3" />
303 <line x1="6" y1="9" x2="6" y2="15" />
304 <path d="M18 9a9 9 0 0 0-9-9" />
305 <circle cx="18" cy="18" r="3" />
306 <line x1="18" y1="9" x2="18" y2="15" />
307 </svg>
308 </span>
309 <h3 class="demo-page-step-title">Claude opens a PR</h3>
310 </div>
311 <p class="demo-page-step-body">
312 The autopilot reads the issue, edits the repo, opens a
7d0c65eClaude313 branch, and pushes a PR linked back to the issue — all
314 happening right now. Watch it appear in the tile below.
e3eb5ffClaude315 </p>
316 <p class="demo-page-step-try">
317 <span class="demo-page-try-label">Try this</span>
318 <a class="demo-page-try-link" href="#tile-queued-h">
319 See the queue ↓
320 </a>
321 </p>
322 </li>
323
324 {/* Step 3 — AI review */}
325 <li class="demo-page-step">
326 <div class="demo-page-step-head">
327 <span class="demo-page-step-num">3</span>
328 <span class="demo-page-step-icon" aria-hidden="true">
329 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
330 <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
331 </svg>
332 </span>
333 <h3 class="demo-page-step-title">AI review lands</h3>
334 </div>
335 <p class="demo-page-step-body">
7d0c65eClaude336 Every PR gets a second-AI review pass within ~8 seconds
337 of opening — typed comments with line numbers, severity,
338 and a one-line summary at the top. No human needed.
e3eb5ffClaude339 </p>
340 <p class="demo-page-step-try">
341 <span class="demo-page-try-label">Try this</span>
342 <a class="demo-page-try-link" href="#tile-reviews-h">
7d0c65eClaude343 See reviews happening now ↓
e3eb5ffClaude344 </a>
345 </p>
346 </li>
347
348 {/* Step 4 — Auto-merge */}
349 <li class="demo-page-step">
350 <div class="demo-page-step-head">
351 <span class="demo-page-step-num">4</span>
352 <span class="demo-page-step-icon" aria-hidden="true">
353 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
354 <path d="M20 6L9 17l-5-5" />
355 </svg>
356 </span>
357 <h3 class="demo-page-step-title">Auto-merge when green</h3>
358 </div>
359 <p class="demo-page-step-body">
7d0c65eClaude360 The instant every gate goes green, the autopilot merges
361 the PR and closes the originating issue — no click, no
362 wait. Branch protection rules still apply.
e3eb5ffClaude363 </p>
364 <p class="demo-page-step-try">
365 <span class="demo-page-try-label">Try this</span>
366 <a class="demo-page-try-link" href="#tile-merges-h">
7d0c65eClaude367 Watch it merge in real time ↓
e3eb5ffClaude368 </a>
369 </p>
370 </li>
371 </ol>
52ad8b1Claude372 </div>
e3eb5ffClaude373 </section>
52ad8b1Claude374
e3eb5ffClaude375 {/* ─── Live tiles ─── */}
376 <div class="demo-page-tiles">
377 <section class="demo-page-tile" aria-labelledby="tile-queued-h">
378 <h2 id="tile-queued-h" class="demo-page-tile-title">
7d0c65eClaude379 Issues being built by AI right now
52ad8b1Claude380 </h2>
e3eb5ffClaude381 <ul id="tile-queued-list" class="demo-page-list">
52ad8b1Claude382 {queued.length === 0 ? (
7d0c65eClaude383 <li class="demo-page-empty">Nothing building right now — tag an issue ai:build to watch it start.</li>
52ad8b1Claude384 ) : (
385 queued.map((i) => (
386 <li>
387 <a href={`/${DEMO_USERNAME}/${i.repo}/issues/${i.number}`}>
388 #{i.number} {i.title}
389 </a>{" "}
e3eb5ffClaude390 <span class="demo-page-meta">
52ad8b1Claude391 {i.repo} · {relativeTime(i.createdAt)}
392 </span>
393 </li>
394 ))
395 )}
396 </ul>
397 </section>
398
e3eb5ffClaude399 <section class="demo-page-tile" aria-labelledby="tile-merges-h">
400 <h2 id="tile-merges-h" class="demo-page-tile-title">
7d0c65eClaude401 PRs auto-merged the instant gates passed
52ad8b1Claude402 </h2>
e3eb5ffClaude403 <ul id="tile-merges-list" class="demo-page-list">
52ad8b1Claude404 {merges.length === 0 ? (
e3eb5ffClaude405 <li class="demo-page-empty">
7d0c65eClaude406 No instant auto-merges yet — one fires the moment gates go green.
52ad8b1Claude407 </li>
408 ) : (
409 merges.map((m) => (
410 <li>
411 <a href={`/${DEMO_USERNAME}/${m.repo}/pulls/${m.number}`}>
412 #{m.number} {m.title}
413 </a>{" "}
e3eb5ffClaude414 <span class="demo-page-meta">
52ad8b1Claude415 {m.repo} · {relativeTime(m.mergedAt)}
416 </span>
417 </li>
418 ))
419 )}
420 </ul>
421 </section>
422
e3eb5ffClaude423 <section class="demo-page-tile" aria-labelledby="tile-reviews-h">
424 <h2 id="tile-reviews-h" class="demo-page-tile-title">
52ad8b1Claude425 AI reviews posted today
426 </h2>
e3eb5ffClaude427 <div class="demo-page-bigcount">
52ad8b1Claude428 <span id="tile-reviews-count">{reviewCount}</span>
e3eb5ffClaude429 <span class="demo-page-bigcount-label">in the last 24h</span>
52ad8b1Claude430 </div>
e3eb5ffClaude431 <ul
432 id="tile-reviews-list"
433 class="demo-page-list demo-page-list-small"
434 >
52ad8b1Claude435 {reviews.length === 0 ? (
e3eb5ffClaude436 <li class="demo-page-empty">
52ad8b1Claude437 No AI reviews in the last 24h.
438 </li>
439 ) : (
440 reviews.map((r) => (
441 <li>
442 <a href={`/${DEMO_USERNAME}/${r.repo}/pulls/${r.prNumber}`}>
443 #{r.prNumber}
444 </a>{" "}
e3eb5ffClaude445 <span class="demo-page-snippet">{r.commentSnippet}</span>{" "}
446 <span class="demo-page-meta">
52ad8b1Claude447 {r.repo} · {relativeTime(r.createdAt)}
448 </span>
449 </li>
450 ))
451 )}
452 </ul>
453 </section>
454 </div>
455
e3eb5ffClaude456 <section class="demo-page-section" aria-labelledby="demo-repos-h">
457 <header class="demo-page-section-head">
458 <div>
459 <p class="demo-page-section-eyebrow">Browse</p>
460 <h2 class="demo-page-section-title" id="demo-repos-h">
461 Dig into the demo repos
462 </h2>
463 </div>
464 </header>
465 <div class="demo-page-section-body">
466 <div class="demo-page-repos-grid">
467 {demoRepos.map((r) => (
468 <a class="demo-page-repo-card" href={`${demoUserUrl}/${r.name}`}>
469 <div class="demo-page-repo-name">
470 {DEMO_USERNAME}/{r.name}
471 </div>
472 <div class="demo-page-repo-tag">{r.tagline}</div>
473 </a>
474 ))}
475 </div>
52ad8b1Claude476 </div>
477 </section>
478
e3eb5ffClaude479 <section class="demo-page-section" aria-labelledby="demo-feed-h">
480 <header class="demo-page-section-head">
481 <div>
482 <p class="demo-page-section-eyebrow">Stream</p>
483 <h2 class="demo-page-section-title" id="demo-feed-h">
484 Live activity
485 </h2>
486 <p class="demo-page-section-sub">
7d0c65eClaude487 Happening right now — newest event first, auto-refreshes every 30s.
e3eb5ffClaude488 </p>
489 </div>
490 <button
491 type="button"
492 id="demo-reset"
493 class="demo-page-btn demo-page-reset"
494 aria-label="Refresh all tiles now"
495 >
496 Refresh now
497 </button>
498 </header>
499 <div class="demo-page-section-body">
500 <ul id="demo-feed-list" class="demo-page-feed-list">
501 {feed.length === 0 ? (
502 <li class="demo-page-empty">
503 Quiet right now — push a commit to a demo repo to see
504 live updates.
505 </li>
506 ) : (
507 feed.map((e) => {
508 const path = e.ref.type === "pr" ? "pulls" : "issues";
509 return (
510 <li>
511 <span
512 class={`demo-page-kind demo-page-kind-${e.kind.replace(/\./g, "-")}`}
513 >
514 {activityLabel(e.kind)}
515 </span>{" "}
516 <a href={`/${DEMO_USERNAME}/${e.repo}/${path}/${e.ref.number}`}>
517 {e.repo} #{e.ref.number}
518 </a>{" "}
519 <span class="demo-page-meta">{relativeTime(e.at)}</span>
520 </li>
521 );
522 })
523 )}
524 </ul>
525 </div>
52ad8b1Claude526 </section>
527 </div>
528 <script dangerouslySetInnerHTML={{ __html: pollerScript }} />
529 </Layout>
530 );
531});
532
e3eb5ffClaude533// Scoped CSS — every class prefixed `.demo-page-` so this surface can't
534// bleed into other views. Drop-in replacement for the legacy `.demo-*`
535// classes; the new prefix is wider so we can polish without collisions.
52ad8b1Claude536const DEMO_CSS = `
eed4684Claude537 .demo-page { max-width: 1680px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
e3eb5ffClaude538
539 /* ─── Hero ─── */
540 .demo-page-hero {
541 position: relative;
542 margin-bottom: var(--space-5);
543 padding: var(--space-6) var(--space-6);
544 background: var(--bg-elevated);
545 border: 1px solid var(--border);
546 border-radius: 18px;
547 overflow: hidden;
548 }
549 .demo-page-hero::before {
550 content: '';
551 position: absolute;
552 top: 0; left: 0; right: 0;
553 height: 2px;
6fd5915Claude554 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
e3eb5ffClaude555 opacity: 0.7;
556 pointer-events: none;
557 }
558 .demo-page-hero-orb {
559 position: absolute;
560 inset: -25% -10% auto auto;
561 width: 480px; height: 480px;
6fd5915Claude562 background: radial-gradient(circle, rgba(91,110,232,0.22), rgba(95,143,160,0.10) 45%, transparent 70%);
e3eb5ffClaude563 filter: blur(80px);
564 opacity: 0.7;
565 pointer-events: none;
566 z-index: 0;
567 }
568 .demo-page-hero-inner { position: relative; z-index: 1; max-width: 720px; }
569 .demo-page-eyebrow {
570 font-size: 12px;
571 color: var(--text-muted);
572 margin-bottom: var(--space-2);
573 letter-spacing: 0.02em;
574 display: inline-flex;
575 align-items: center;
576 gap: 8px;
577 }
578 .demo-page-pulse {
579 display: inline-block;
580 width: 8px;
581 height: 8px;
582 border-radius: 50%;
583 background: #34d399;
584 box-shadow: 0 0 8px rgba(52,211,153,0.7);
585 animation: demo-page-pulse 1.6s infinite;
586 }
587 @keyframes demo-page-pulse {
588 0%, 100% { opacity: 1; }
589 50% { opacity: 0.4; }
590 }
591 .demo-page-title {
592 font-size: clamp(32px, 5vw, 52px);
593 font-family: var(--font-display);
594 font-weight: 800;
595 letter-spacing: -0.028em;
596 line-height: 1.05;
597 margin: 0 0 var(--space-3);
598 color: var(--text-strong);
599 }
600 .demo-page-title-grad {
6fd5915Claude601 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
e3eb5ffClaude602 -webkit-background-clip: text;
603 background-clip: text;
604 -webkit-text-fill-color: transparent;
605 color: transparent;
606 }
607 .demo-page-sub {
608 font-size: 16px;
609 color: var(--text-muted);
610 margin: 0 0 var(--space-4);
611 line-height: 1.55;
612 max-width: 620px;
613 }
614 .demo-page-hero-cta { display: flex; gap: 10px; flex-wrap: wrap; }
615
616 /* ─── Buttons ─── */
617 .demo-page-btn {
618 appearance: none;
619 border: 1px solid var(--border-strong);
620 background: var(--bg-secondary);
621 color: var(--text);
622 padding: 10px 16px;
623 border-radius: 10px;
624 font-family: inherit;
625 font-size: 14px;
626 font-weight: 500;
627 cursor: pointer;
628 transition: border-color 150ms ease, background 150ms ease, transform 150ms ease, color 150ms ease;
629 text-decoration: none;
630 display: inline-flex; align-items: center; gap: 8px;
631 }
632 .demo-page-btn:hover {
633 border-color: var(--border-focus);
634 background: rgba(255,255,255,0.03);
635 transform: translateY(-1px);
636 text-decoration: none;
637 }
638 .demo-page-btn-primary {
6fd5915Claude639 border-color: rgba(91,110,232,0.45);
640 background: linear-gradient(135deg, rgba(91,110,232,0.20), rgba(95,143,160,0.14));
e3eb5ffClaude641 color: var(--text-strong);
642 }
643 .demo-page-btn-primary:hover {
6fd5915Claude644 border-color: rgba(91,110,232,0.65);
645 background: linear-gradient(135deg, rgba(91,110,232,0.28), rgba(95,143,160,0.20));
e3eb5ffClaude646 }
647 .demo-page-reset {
648 padding: 6px 12px;
649 font-size: 12.5px;
650 border-radius: 8px;
651 }
652 .demo-page-reset.is-done {
653 border-color: rgba(52,211,153,0.45);
654 background: rgba(52,211,153,0.10);
655 color: #6ee7b7;
656 }
657
658 /* ─── Section cards ─── */
659 .demo-page-section {
660 margin-bottom: var(--space-5);
661 background: var(--bg-elevated);
662 border: 1px solid var(--border);
663 border-radius: 14px;
664 overflow: hidden;
665 }
666 .demo-page-section-head {
667 padding: var(--space-4) var(--space-5);
668 border-bottom: 1px solid var(--border);
669 display: flex;
670 align-items: flex-start;
671 justify-content: space-between;
672 gap: var(--space-3);
673 flex-wrap: wrap;
674 }
675 .demo-page-section-eyebrow {
676 font-size: 11px;
677 font-weight: 600;
678 letter-spacing: 0.08em;
679 text-transform: uppercase;
680 color: var(--text-faint);
681 margin: 0 0 6px;
682 }
683 .demo-page-section-title {
684 margin: 0;
685 font-family: var(--font-display);
686 font-size: 17px;
687 font-weight: 700;
688 letter-spacing: -0.018em;
689 color: var(--text-strong);
690 }
691 .demo-page-section-sub {
692 margin: 6px 0 0;
693 font-size: 12.5px;
694 color: var(--text-muted);
695 line-height: 1.5;
696 }
697 .demo-page-section-body { padding: var(--space-5); }
698
699 /* ─── 4-step walkthrough ─── */
700 .demo-page-walk {
701 list-style: none;
702 margin: 0;
703 padding: 0;
704 display: grid;
705 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
706 gap: var(--space-3);
707 }
708 .demo-page-step {
709 padding: var(--space-4);
710 background: var(--bg-secondary);
711 border: 1px solid var(--border-subtle);
712 border-radius: 12px;
713 display: flex;
714 flex-direction: column;
715 gap: 10px;
716 transition: border-color 150ms ease, transform 150ms ease;
717 }
718 .demo-page-step:hover {
719 border-color: var(--border-strong);
720 transform: translateY(-1px);
721 }
722 .demo-page-step-head {
723 display: flex;
724 align-items: center;
725 gap: 10px;
726 }
727 .demo-page-step-num {
728 display: inline-flex;
729 align-items: center;
730 justify-content: center;
731 width: 28px; height: 28px;
732 border-radius: 50%;
6fd5915Claude733 background: linear-gradient(135deg, rgba(91,110,232,0.22), rgba(95,143,160,0.16));
e3eb5ffClaude734 color: #c5b3ff;
6fd5915Claude735 border: 1px solid rgba(91,110,232,0.40);
e3eb5ffClaude736 font-family: var(--font-display);
737 font-weight: 700;
738 font-size: 13px;
739 }
740 .demo-page-step-icon {
741 display: inline-flex;
742 align-items: center;
743 justify-content: center;
744 width: 26px; height: 26px;
745 border-radius: 8px;
6fd5915Claude746 background: rgba(95,143,160,0.10);
e3eb5ffClaude747 color: #5fd3e0;
6fd5915Claude748 box-shadow: inset 0 0 0 1px rgba(95,143,160,0.30);
e3eb5ffClaude749 }
750 .demo-page-step-title {
751 margin: 0;
752 font-family: var(--font-display);
753 font-size: 15px;
754 font-weight: 700;
755 color: var(--text-strong);
756 letter-spacing: -0.012em;
757 }
758 .demo-page-step-body {
759 margin: 0;
760 font-size: 13.5px;
761 color: var(--text-muted);
762 line-height: 1.55;
763 }
764 .demo-page-step-body code {
765 font-family: var(--font-mono);
766 font-size: 12px;
767 background: var(--bg-tertiary);
768 padding: 1px 5px;
769 border-radius: 4px;
770 color: var(--text-strong);
771 }
772 .demo-page-step-try {
773 margin: auto 0 0;
774 padding-top: 10px;
775 border-top: 1px dashed var(--border-subtle);
776 display: flex;
777 align-items: center;
778 justify-content: space-between;
779 gap: 8px;
780 font-size: 12.5px;
781 }
782 .demo-page-try-label {
783 font-size: 10.5px;
784 font-weight: 600;
785 letter-spacing: 0.08em;
786 text-transform: uppercase;
787 color: var(--text-faint);
788 }
789 .demo-page-try-link {
6fd5915Claude790 color: var(--accent, #5b6ee8);
e3eb5ffClaude791 text-decoration: none;
792 font-weight: 600;
793 }
794 .demo-page-try-link:hover { text-decoration: underline; }
795
796 /* ─── Live tiles ─── */
797 .demo-page-tiles {
798 display: grid;
799 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
800 gap: var(--space-3);
801 margin-bottom: var(--space-5);
802 }
803 .demo-page-tile {
804 background: var(--bg-elevated);
805 border: 1px solid var(--border);
806 border-radius: 14px;
807 padding: var(--space-4) var(--space-4);
808 }
809 .demo-page-tile-title {
810 font-size: 11px;
811 font-weight: 600;
812 text-transform: uppercase;
813 letter-spacing: 0.08em;
814 color: var(--text-faint);
815 margin: 0 0 12px;
816 }
817 .demo-page-list {
818 list-style: none;
819 margin: 0;
820 padding: 0;
821 font-size: 14px;
822 line-height: 1.5;
823 }
824 .demo-page-list li {
825 padding: 6px 0;
826 border-bottom: 1px solid var(--border-subtle);
827 }
828 .demo-page-list li:last-child { border-bottom: 0; }
829 .demo-page-list-small li { font-size: 13px; }
830 .demo-page-list a {
831 color: var(--text-strong);
832 text-decoration: none;
833 font-weight: 500;
834 }
835 .demo-page-list a:hover {
6fd5915Claude836 color: var(--accent, #5b6ee8);
e3eb5ffClaude837 text-decoration: underline;
838 }
839 .demo-page-meta { color: var(--text-muted); font-size: 12px; }
840 .demo-page-snippet { color: var(--text-muted); font-style: italic; font-size: 12px; }
841 .demo-page-empty { color: var(--text-muted); font-size: 13px; padding: 6px 0; }
842 .demo-page-bigcount {
843 display: flex;
844 align-items: baseline;
845 gap: 8px;
846 margin-bottom: 12px;
847 }
848 .demo-page-bigcount span:first-child {
849 font-size: 32px;
850 font-weight: 700;
851 color: var(--text-strong);
852 font-family: var(--font-display);
853 letter-spacing: -0.02em;
854 }
855 .demo-page-bigcount-label { color: var(--text-muted); font-size: 12px; }
856
857 /* ─── Demo repos grid ─── */
858 .demo-page-repos-grid {
859 display: grid;
860 grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
861 gap: var(--space-3);
862 }
863 .demo-page-repo-card {
864 display: block;
865 background: var(--bg-secondary);
866 border: 1px solid var(--border-subtle);
867 border-radius: 12px;
868 padding: var(--space-3) var(--space-4);
869 text-decoration: none;
870 color: inherit;
871 transition: border-color 150ms ease, transform 150ms ease;
872 }
873 .demo-page-repo-card:hover {
6fd5915Claude874 border-color: rgba(91,110,232,0.45);
e3eb5ffClaude875 transform: translateY(-1px);
876 text-decoration: none;
877 }
878 .demo-page-repo-name {
879 font-family: var(--font-mono);
880 font-weight: 600;
881 font-size: 13.5px;
882 color: var(--text-strong);
883 margin-bottom: 4px;
884 }
885 .demo-page-repo-tag {
886 font-size: 12.5px;
887 color: var(--text-muted);
888 line-height: 1.45;
889 }
890
891 /* ─── Live feed ─── */
892 .demo-page-feed-list {
893 list-style: none;
894 margin: 0;
895 padding: 0;
896 font-size: 13px;
897 line-height: 1.6;
898 }
899 .demo-page-feed-list li {
900 padding: 6px 0;
901 border-bottom: 1px solid var(--border-subtle);
902 display: flex;
903 align-items: center;
904 gap: 8px;
905 flex-wrap: wrap;
906 }
907 .demo-page-feed-list li:last-child { border-bottom: 0; }
908 .demo-page-feed-list a {
909 color: var(--text-strong);
910 text-decoration: none;
911 font-weight: 500;
912 }
913 .demo-page-feed-list a:hover {
6fd5915Claude914 color: var(--accent, #5b6ee8);
e3eb5ffClaude915 text-decoration: underline;
916 }
917 .demo-page-kind {
918 display: inline-flex;
919 align-items: center;
920 padding: 2px 9px;
921 border-radius: 9999px;
922 font-size: 11px;
923 font-weight: 600;
924 letter-spacing: 0.01em;
925 }
926 .demo-page-kind-auto_merge-merged,
927 .demo-page-kind-auto-merge-merged {
928 background: rgba(52,211,153,0.12);
929 color: #34d399;
930 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.28);
931 }
932 .demo-page-kind-ai_build-dispatched,
933 .demo-page-kind-ai-build-dispatched {
6fd5915Claude934 background: rgba(91,110,232,0.15);
935 color: #5b6ee8;
936 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.32);
e3eb5ffClaude937 }
938 .demo-page-kind-ai_review-posted,
939 .demo-page-kind-ai-review-posted {
6fd5915Claude940 background: rgba(95,143,160,0.15);
e3eb5ffClaude941 color: #5fd3e0;
6fd5915Claude942 box-shadow: inset 0 0 0 1px rgba(95,143,160,0.32);
e3eb5ffClaude943 }
52ad8b1Claude944`;
945
946export default app;