Commit11c3ab6
feat(redesign): Quiet Intelligence platform redesign — 14 new Hono JSX views
feat(redesign): Quiet Intelligence platform redesign — 14 new Hono JSX views
Complete visual redesign replacing the old dark-glow UI with the "Quiet
Intelligence" design language: light #fcfcfd backgrounds, hairline borders,
Inter Tight headings, JetBrains Mono labels, indigo #4353c9 accent (links
only), black #16181d primary buttons, 7–8px status dots, gcPulse animation.
New view files (src/views/):
- landing-v2.tsx — replaces / route; live demo window, activity ticker,
6-stage loop cards cycling at 2.2s, sovereignty band
- signin-v2.tsx — split-screen auth with dark proof panel (wiring pending)
- onboarding-v2.tsx — 4-situation adaptive wizard with terminal copy/test
- agent-workspace.tsx — flagship AI workspace; 5-step timeline + sticky rail
- pr-redesign.tsx — PR view with AI Trio review + sandbox box
- daily-brief.tsx — editorial /brief page; needs-you + shipped overnight
- plan-approval.tsx — spec-to-PR plan review with manual step claims
- org-memory.tsx — ask-your-codebase search at /:owner/:repo/memory
- fleet-refactor.tsx — fleet table with per-row status timers
- trust-report.tsx — public /trust page with live quality metrics
- admin-console.tsx — 224px sidebar admin with Overview/Diagnostics/Support/Customers
- production-layers.tsx — 13 expandable layer rows at /layers
- ide-multiplayer.tsx — VS Code-style collab window
- distribution.tsx — 5-channel distribution cards at /distribute
Route wiring (src/routes/web.tsx):
- / now imports LandingPage from landing-v2 (compat alias preserved)
- GET /brief, /trust, /layers, /distribute — new standalone routes
- GET /:owner/:repo/workspace — agent workspace view
- GET /:owner/:repo/memory — org memory search view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>15 files changed+12766−011c3ab60cef971ade94cd954bf279380b1679808
15 changed files+12766−0
Modifiedsrc/routes/web.tsx+43−0View fileUnifiedSplit
@@ -72,6 +72,12 @@ import {
7272 countAiReviewsSince,
7373 listDemoActivityFeed,
7474} from "../lib/demo-activity";
75import { AgentWorkspace } from "../views/agent-workspace";
76import { DailyBrief } from "../views/daily-brief";
77import { TrustReport } from "../views/trust-report";
78import { ProductionLayers } from "../views/production-layers";
79import { DistributionView } from "../views/distribution";
80import { OrgMemory } from "../views/org-memory";
7581
7682const web = new Hono<AuthEnv>();
7783
@@ -2102,6 +2108,29 @@ web.post("/new", requireAuth, async (c) => {
21022108 return c.redirect(`/${user.username}/${name}`);
21032109});
21042110
2111// Daily brief — GET /brief
2112web.get("/brief", (c) => {
2113 const user = c.get("user");
2114 return c.html(<DailyBrief user={user} />);
2115});
2116
2117// Trust report — GET /trust (public)
2118web.get("/trust", (c) => {
2119 return c.html(<TrustReport />);
2120});
2121
2122// Production layers — GET /layers
2123web.get("/layers", (c) => {
2124 const user = c.get("user");
2125 return c.html(<ProductionLayers user={user} />);
2126});
2127
2128// Distribution — GET /distribute
2129web.get("/distribute", (c) => {
2130 const user = c.get("user");
2131 return c.html(<DistributionView user={user} />);
2132});
2133
21052134// User profile
21062135web.get("/:owner", async (c) => {
21072136 const { owner: ownerName } = c.req.param();
@@ -2727,6 +2756,20 @@ web.post("/:owner/:repo/setup/dismiss", softAuth, requireAuth, async (c) => {
27272756 return c.redirect(`/${owner}/${repo}`);
27282757});
27292758
2759// Agent workspace — GET /:owner/:repo/workspace
2760web.get("/:owner/:repo/workspace", (c) => {
2761 const { owner, repo } = c.req.param();
2762 const user = c.get("user");
2763 return c.html(<AgentWorkspace owner={owner} repo={repo} user={user} />);
2764});
2765
2766// Org memory — GET /:owner/:repo/memory
2767web.get("/:owner/:repo/memory", (c) => {
2768 const { owner, repo } = c.req.param();
2769 const user = c.get("user");
2770 return c.html(<OrgMemory owner={owner} repo={repo} user={user} />);
2771});
2772
27302773// Repository overview — file tree at HEAD
27312774web.get("/:owner/:repo", async (c) => {
27322775 const { owner, repo } = c.req.param();
Addedsrc/views/admin-console.tsx+1484−0View fileUnifiedSplit
Large file (1,484 lines). Load full file
Addedsrc/views/agent-workspace.tsx+1237−0View fileUnifiedSplit
Large file (1,237 lines). Load full file
Addedsrc/views/daily-brief.tsx+634−0View fileUnifiedSplit
@@ -0,0 +1,634 @@
1import type { FC } from "hono/jsx";
2
3export interface NeedsYouItem {
4 title: string;
5 detail: string;
6 sessionHref?: string;
7 diffHref?: string;
8}
9
10export interface ShippedItem {
11 time: string;
12 title: string;
13 detail: string;
14 meta: string;
15}
16
17export interface HandledItem {
18 what: string;
19 detail: string;
20}
21
22export interface DailyBriefProps {
23 needsYou?: NeedsYouItem[];
24 shipped?: ShippedItem[];
25 handled?: HandledItem[];
26 savedHrs?: number;
27 user?: { name?: string; initials?: string };
28}
29
30const DEFAULT_NEEDS_YOU: NeedsYouItem[] = [
31 {
32 title: "Approve the repair on PR №218 — auto-repair × sandbox unification",
33 detail:
34 "Claude fixed the failing tests inside the PR sandbox overnight. Gates are green; Performance flagged an unbounded retry which Claude then capped. All that's left is your approval — or take the branch over manually if you'd rather work the diff yourself.",
35 sessionHref: "#",
36 diffHref: "#",
37 },
38];
39
40const DEFAULT_SHIPPED: ShippedItem[] = [
41 {
42 time: "02:14",
43 title: "Dependency bumps merged across 3 repos",
44 detail:
45 "drizzle-orm 0.44 → 0.45, hono patch, bun types. Fleet changeset, one review, all gates green.",
46 meta: "gluecron · gluecron-cli · editor-extensions — reverted in one click if needed",
47 },
48 {
49 time: "03:40",
50 title: "Flaky test quarantined, then fixed",
51 detail:
52 "auto-repair bisected sandbox-boot.test.ts to a race on port assignment, patched the fixture, and un-quarantined it after 40 green runs.",
53 meta: "PR №216 · merged by policy · 0 human minutes spent",
54 },
55 {
56 time: "05:52",
57 title: "Security advisory patched before you knew it existed",
58 detail:
59 "CVE in a transitive dep of the registry middleware. Patched, gated, deployed. Exposure window: 19 minutes.",
60 meta: "PR №217 · deploy 82321e8 · advisory GHSA-4q7x",
61 },
62];
63
64const DEFAULT_HANDLED: HandledItem[] = [
65 {
66 what: "4 PRs continuously rebased",
67 detail: "no merge conflicts reached any developer this week",
68 },
69 {
70 what: "EU semantic index backfill",
71 detail: "completed 04:10, helvetia-fintech ticket auto-resolved",
72 },
73 {
74 what: "2 stale branches archived",
75 detail: "per repo policy, both had merged upstream",
76 },
77];
78
79export const DailyBrief: FC<DailyBriefProps> = (props) => {
80 const needsYou = props.needsYou ?? DEFAULT_NEEDS_YOU;
81 const shipped = props.shipped ?? DEFAULT_SHIPPED;
82 const handled = props.handled ?? DEFAULT_HANDLED;
83 const savedHrs = props.savedHrs ?? 3.1;
84 const user = props.user ?? {};
85 const initials = user.initials ?? "C";
86
87 const today = new Date();
88 const dateLabel = today.toLocaleDateString("en-US", {
89 weekday: "long",
90 month: "long",
91 day: "numeric",
92 });
93
94 return (
95 <>
96 <style dangerouslySetInnerHTML={{ __html: css }} />
97 <div class="db-root">
98 <header class="db-header">
99 <a href="/" class="db-logo">
100 <span class="db-logo-mark" />
101 gluecron
102 </a>
103 <span class="db-header-sep">/</span>
104 <span class="db-header-crumb">Brief</span>
105 <div class="db-header-right">
106 <span class="db-header-date">{dateLabel}</span>
107 <span class="db-avatar">{initials}</span>
108 </div>
109 </header>
110
111 <main class="db-main">
112 <div class="db-eyebrow">Your morning brief · 7:00 am</div>
113 <h1 class="db-headline">
114 While you were away, the platform shipped three things and held one
115 back for you.
116 </h1>
117 <p class="db-subtitle">
118 Two-minute read. One decision needed. Everything below is linked,
119 audited, and reversible.
120 </p>
121
122 {/* Needs You section */}
123 <section class="db-section db-needs">
124 <h2 class="db-section-label db-label-amber">
125 <span class="db-pulse-dot db-pulse-amber" />
126 Needs you · {needsYou.length}
127 </h2>
128 {needsYou.map((item) => (
129 <div class="db-action-card">
130 <div class="db-action-title">{item.title}</div>
131 <p class="db-action-detail">{item.detail}</p>
132 <div class="db-action-buttons">
133 <a
134 href={item.sessionHref ?? "#"}
135 class="db-btn db-btn-primary"
136 >
137 Open session
138 </a>
139 <a href={item.diffHref ?? "#"} class="db-btn db-btn-secondary">
140 Review diff manually
141 </a>
142 </div>
143 </div>
144 ))}
145 </section>
146
147 {/* Shipped overnight section */}
148 <section class="db-section db-shipped">
149 <h2 class="db-section-label db-label-green">
150 Shipped overnight · {shipped.length}
151 </h2>
152 <div class="db-timeline">
153 {shipped.map((item) => (
154 <div class="db-timeline-row">
155 <span class="db-timeline-time">{item.time}</span>
156 <div class="db-timeline-body">
157 <div class="db-timeline-title">{item.title}</div>
158 <div class="db-timeline-detail">{item.detail}</div>
159 <div class="db-timeline-meta">{item.meta}</div>
160 </div>
161 </div>
162 ))}
163 </div>
164 </section>
165
166 {/* Handled quietly section */}
167 <section class="db-section db-handled">
168 <h2 class="db-section-label db-label-muted">Handled quietly</h2>
169 <div class="db-handled-list">
170 {handled.map((item) => (
171 <div class="db-handled-row">
172 <span class="db-handled-dot" />
173 <span class="db-handled-text">
174 <strong class="db-handled-what">{item.what}</strong>
175 {" — "}
176 {item.detail}
177 </span>
178 </div>
179 ))}
180 </div>
181 </section>
182
183 {/* Footer */}
184 <div class="db-footer">
185 <p class="db-footer-note">
186 Your team's standup, replaced. Every item links to its full audit
187 trail.
188 </p>
189 <span class="db-footer-saved">
190 saved you ≈ {savedHrs.toFixed(1)} hrs overnight
191 </span>
192 </div>
193 </main>
194 </div>
195 <script
196 dangerouslySetInnerHTML={{
197 __html: clientJs,
198 }}
199 />
200 </>
201 );
202};
203
204export default DailyBrief;
205
206const css = `
207@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
208
209*, *::before, *::after {
210 box-sizing: border-box;
211 margin: 0;
212 padding: 0;
213}
214
215@keyframes gcPulse {
216 0%, 100% { opacity: 1; }
217 50% { opacity: 0.35; }
218}
219
220.db-root {
221 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
222 font-size: 14px;
223 line-height: 1.55;
224 letter-spacing: -0.008em;
225 color: #16181d;
226 background: #fcfcfd;
227 min-height: 100vh;
228 display: flex;
229 flex-direction: column;
230 -webkit-font-smoothing: antialiased;
231 text-rendering: optimizeLegibility;
232}
233
234/* ── Header ── */
235.db-header {
236 height: 56px;
237 border-bottom: 1px solid rgba(22, 24, 29, 0.07);
238 background: #fcfcfd;
239 display: flex;
240 align-items: center;
241 padding: 0 28px;
242 gap: 20px;
243 position: sticky;
244 top: 0;
245 z-index: 50;
246}
247
248.db-logo {
249 display: inline-flex;
250 align-items: center;
251 gap: 9px;
252 font-family: 'Inter Tight', sans-serif;
253 font-weight: 600;
254 font-size: 15px;
255 letter-spacing: -0.02em;
256 color: #16181d;
257 text-decoration: none;
258}
259
260.db-logo-mark {
261 width: 16px;
262 height: 16px;
263 border-radius: 5px;
264 background: #4353c9;
265 display: inline-block;
266 flex-shrink: 0;
267}
268
269.db-header-sep {
270 color: #c4c6cf;
271}
272
273.db-header-crumb {
274 font-size: 13px;
275 color: #16181d;
276 font-weight: 500;
277}
278
279.db-header-right {
280 margin-left: auto;
281 display: flex;
282 align-items: center;
283 gap: 14px;
284}
285
286.db-header-date {
287 font-size: 12.5px;
288 color: #6b7080;
289}
290
291.db-avatar {
292 width: 28px;
293 height: 28px;
294 border-radius: 50%;
295 background: #16181d;
296 color: #ffffff;
297 font-size: 11px;
298 font-weight: 600;
299 display: inline-flex;
300 align-items: center;
301 justify-content: center;
302 font-family: 'Inter', sans-serif;
303 flex-shrink: 0;
304}
305
306/* ── Main layout ── */
307.db-main {
308 flex: 1;
309 max-width: 760px;
310 width: 100%;
311 margin: 0 auto;
312 padding: 56px 32px 88px;
313}
314
315/* ── Eyebrow + headline ── */
316.db-eyebrow {
317 font-family: 'JetBrains Mono', monospace;
318 font-size: 11px;
319 letter-spacing: 0.12em;
320 text-transform: uppercase;
321 color: #8a8d99;
322 margin-bottom: 12px;
323}
324
325.db-headline {
326 font-family: 'Inter Tight', sans-serif;
327 font-size: 32px;
328 font-weight: 600;
329 letter-spacing: -0.028em;
330 line-height: 1.12;
331 color: #111318;
332 margin: 0 0 12px;
333}
334
335.db-subtitle {
336 font-size: 15px;
337 color: #6b7080;
338 margin: 0 0 44px;
339 line-height: 1.6;
340 max-width: 58ch;
341}
342
343/* ── Sections ── */
344.db-section {
345 margin-bottom: 48px;
346}
347
348.db-section-label {
349 font-family: 'JetBrains Mono', monospace;
350 font-size: 11px;
351 letter-spacing: 0.12em;
352 text-transform: uppercase;
353 font-weight: 500;
354 margin: 0 0 14px;
355 display: flex;
356 align-items: center;
357 gap: 8px;
358}
359
360.db-label-amber {
361 color: #b45309;
362}
363
364.db-label-green {
365 color: #1e7f5c;
366}
367
368.db-label-muted {
369 color: #8a8d99;
370}
371
372/* ── Pulse dot ── */
373.db-pulse-dot {
374 width: 7px;
375 height: 7px;
376 border-radius: 50%;
377 flex-shrink: 0;
378}
379
380.db-pulse-amber {
381 background: #b45309;
382 animation: gcPulse 1.4s ease-in-out infinite;
383}
384
385/* ── Action card (Needs You) ── */
386.db-action-card {
387 border: 1px solid rgba(22, 24, 29, 0.10);
388 border-radius: 14px;
389 background: #ffffff;
390 padding: 24px 26px;
391}
392
393.db-action-title {
394 font-size: 15px;
395 font-weight: 600;
396 color: #16181d;
397 margin-bottom: 6px;
398 line-height: 1.4;
399}
400
401.db-action-detail {
402 font-size: 13.5px;
403 color: #6b7080;
404 margin: 0 0 16px;
405 line-height: 1.6;
406}
407
408.db-action-buttons {
409 display: flex;
410 gap: 10px;
411 flex-wrap: wrap;
412}
413
414/* ── Buttons ── */
415.db-btn {
416 padding: 8px 16px;
417 border-radius: 8px;
418 font-size: 13px;
419 font-weight: 500;
420 text-decoration: none;
421 white-space: nowrap;
422 display: inline-block;
423 line-height: 1.5;
424 transition: border-color 0.12s ease, background 0.12s ease;
425 font-family: 'Inter', sans-serif;
426}
427
428.db-btn-primary {
429 background: #16181d;
430 color: #ffffff;
431 border: 1px solid transparent;
432}
433
434.db-btn-primary:hover {
435 background: #2a2d36;
436}
437
438.db-btn-secondary {
439 background: #ffffff;
440 color: #16181d;
441 border: 1px solid rgba(22, 24, 29, 0.12);
442}
443
444.db-btn-secondary:hover {
445 border-color: rgba(22, 24, 29, 0.22);
446}
447
448/* ── Timeline (Shipped) ── */
449.db-timeline {
450 display: flex;
451 flex-direction: column;
452}
453
454.db-timeline-row {
455 display: grid;
456 grid-template-columns: 64px minmax(0, 1fr);
457 gap: 18px;
458 padding: 16px 4px;
459 border-bottom: 1px solid rgba(22, 24, 29, 0.06);
460}
461
462.db-timeline-row:last-child {
463 border-bottom: none;
464}
465
466.db-timeline-time {
467 font-family: 'JetBrains Mono', monospace;
468 font-size: 12px;
469 color: #8a8d99;
470 padding-top: 2px;
471 flex-shrink: 0;
472}
473
474.db-timeline-body {
475 min-width: 0;
476}
477
478.db-timeline-title {
479 font-size: 14px;
480 font-weight: 500;
481 color: #16181d;
482 line-height: 1.45;
483}
484
485.db-timeline-detail {
486 font-size: 13px;
487 color: #6b7080;
488 margin-top: 2px;
489 line-height: 1.55;
490}
491
492.db-timeline-meta {
493 font-family: 'JetBrains Mono', monospace;
494 font-size: 11.5px;
495 color: #8a8d99;
496 margin-top: 6px;
497}
498
499/* ── Handled quietly ── */
500.db-handled-list {
501 display: flex;
502 flex-direction: column;
503 gap: 10px;
504}
505
506.db-handled-row {
507 display: flex;
508 gap: 12px;
509 align-items: baseline;
510 font-size: 13.5px;
511 color: #6b7080;
512}
513
514.db-handled-dot {
515 width: 6px;
516 height: 6px;
517 border-radius: 50%;
518 background: #c4c6cf;
519 flex-shrink: 0;
520 position: relative;
521 top: -2px;
522}
523
524.db-handled-text {
525 line-height: 1.55;
526}
527
528.db-handled-what {
529 color: #16181d;
530 font-weight: 500;
531}
532
533/* ── Footer ── */
534.db-footer {
535 border-top: 1px solid rgba(22, 24, 29, 0.07);
536 padding-top: 20px;
537 display: flex;
538 justify-content: space-between;
539 align-items: baseline;
540 gap: 16px;
541 flex-wrap: wrap;
542}
543
544.db-footer-note {
545 font-size: 12.5px;
546 color: #8a8d99;
547 margin: 0;
548 line-height: 1.55;
549}
550
551.db-footer-saved {
552 font-family: 'JetBrains Mono', monospace;
553 font-size: 11.5px;
554 color: #8a8d99;
555 white-space: nowrap;
556}
557
558/* ── Responsive ── */
559@media (max-width: 600px) {
560 .db-main {
561 padding: 40px 20px 64px;
562 }
563
564 .db-headline {
565 font-size: 26px;
566 }
567
568 .db-timeline-row {
569 grid-template-columns: 48px minmax(0, 1fr);
570 gap: 12px;
571 }
572
573 .db-footer {
574 flex-direction: column;
575 gap: 8px;
576 }
577
578 .db-header {
579 padding: 0 16px;
580 }
581}
582`;
583
584const clientJs = `
585(function () {
586 'use strict';
587
588 // Hover state for secondary button — pure CSS handles most, but ensure
589 // any programmatic focus rings stay subtle.
590 var secondaryBtns = document.querySelectorAll('.db-btn-secondary');
591 secondaryBtns.forEach(function (btn) {
592 btn.addEventListener('mouseenter', function () {
593 btn.style.borderColor = 'rgba(22,24,29,0.22)';
594 });
595 btn.addEventListener('mouseleave', function () {
596 btn.style.borderColor = 'rgba(22,24,29,0.12)';
597 });
598 });
599
600 // Animate timeline rows in on load
601 var rows = document.querySelectorAll('.db-timeline-row');
602 rows.forEach(function (row, i) {
603 row.style.opacity = '0';
604 row.style.transform = 'translateY(8px)';
605 row.style.transition = 'opacity 0.28s ease, transform 0.28s ease';
606 setTimeout(function () {
607 row.style.opacity = '1';
608 row.style.transform = 'translateY(0)';
609 }, 120 + i * 80);
610 });
611
612 // Animate handled rows
613 var handledRows = document.querySelectorAll('.db-handled-row');
614 handledRows.forEach(function (row, i) {
615 row.style.opacity = '0';
616 row.style.transition = 'opacity 0.24s ease';
617 setTimeout(function () {
618 row.style.opacity = '1';
619 }, 400 + i * 60);
620 });
621
622 // Action card entrance
623 var actionCard = document.querySelector('.db-action-card');
624 if (actionCard) {
625 actionCard.style.opacity = '0';
626 actionCard.style.transform = 'translateY(6px)';
627 actionCard.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
628 setTimeout(function () {
629 actionCard.style.opacity = '1';
630 actionCard.style.transform = 'translateY(0)';
631 }, 60);
632 }
633})();
634`;
Addedsrc/views/distribution.tsx+756−0View fileUnifiedSplit
@@ -0,0 +1,756 @@
1import type { FC } from "hono/jsx";
2
3export interface DistributionViewProps {
4 user?: any;
5}
6
7const agents = [
8 { name: "migration-pilot — schema change agent", initials: "MP", bg: "#41707e", installs: "3.1k installs" },
9 { name: "a11y-guard — accessibility gate", initials: "AG", bg: "#4353c9", installs: "2.4k installs" },
10 { name: "perf-sentry — bundle & latency budget", initials: "PS", bg: "#b45309", installs: "1.8k installs" },
11 { name: "license-warden — dependency compliance", initials: "LW", bg: "#16181d", installs: "1.2k installs" },
12];
13
14export const DistributionView: FC<DistributionViewProps> = (props) => {
15 const { user } = props;
16
17 return (
18 <>
19 <style dangerouslySetInnerHTML={{ __html: css }} />
20 <div class="di-root">
21
22 <header class="di-header">
23 <a href="/" class="di-logo">
24 <span class="di-logo-mark" />
25 gluecron
26 </a>
27 <span class="di-header-sep">/</span>
28 <span class="di-header-crumb">Everywhere you work</span>
29 <div class="di-header-right">
30 {user ? (
31 <a href="/dashboard" class="di-btn-primary">Dashboard</a>
32 ) : (
33 <a href="/login" class="di-btn-primary">Sign in</a>
34 )}
35 </div>
36 </header>
37
38 <main class="di-main">
39
40 <div class="di-hero">
41 <div class="di-eyebrow">Distribution · five channels, one platform</div>
42 <h1 class="di-hero-title">Gluecron lives where you already work.</h1>
43 <p class="di-hero-desc">Editor, terminal, README, chat, marketplace — the web app is just one door of five.</p>
44 </div>
45
46 <div class="di-cards">
47
48 {/* Card 1 — IDE */}
49 <div class="di-card">
50 <div class="di-card-copy">
51 <div class="di-eyebrow">01 · The IDE invasion</div>
52 <h2 class="di-card-title">A full Gluecron client in your editor sidebar.</h2>
53 <p class="di-card-desc">PR live sessions, AI commit messages, issue tracking, agent supervision — VS Code and JetBrains extensions with zero context switching. The browser becomes optional.</p>
54 <a href="/ide-multiplayer" class="di-card-link">See a live editor session →</a>
55 </div>
56 <div class="di-preview di-preview-light di-preview-ide">
57 <div class="di-code-comment">// VS Code · Gluecron sidebar</div>
58 <div class="di-ide-row">
59 <span class="di-dot di-dot-amber di-dot-pulse" />
60 <span class="di-ide-text">PR №218 — repair in progress</span>
61 </div>
62 <div class="di-ide-row">
63 <span class="di-dot di-dot-green" />
64 <span class="di-ide-text">3 issues assigned to you</span>
65 </div>
66 <div class="di-ide-row">
67 <span class="di-dot di-dot-indigo" />
68 <span class="di-ide-text">✎ commit message drafted — accept?</span>
69 </div>
70 </div>
71 </div>
72
73 {/* Card 2 — CLI */}
74 <div class="di-card">
75 <div class="di-card-copy">
76 <div class="di-eyebrow">02 · Command-line dominance</div>
77 <h2 class="di-card-title">The whole platform from your shell.</h2>
78 <p class="di-card-desc">One single-file binary. Clone, watch gates stream live, trigger deploys, supervise agents — Gluecron feels like part of the operating system, not a distant web app.</p>
79 <a href="/repos" class="di-card-link">Get the CLI →</a>
80 </div>
81 <div class="di-preview di-preview-dark">
82 <div class="di-term-line"><span class="di-term-prompt">$</span> gluecron watch</div>
83 <div class="di-term-line di-term-dim">⏧ feat/auth-header · pushed 4s ago</div>
84 <div class="di-term-line">
85 <span class="di-term-green">✓</span> gate: security ·{" "}
86 <span class="di-term-green">✓</span> gate: types
87 </div>
88 <div class="di-term-line">
89 <span class="di-term-amber">●</span> gate: tests — running in sandbox…
90 </div>
91 <div class="di-term-line">
92 <span class="di-term-green">✓</span> sonnet-5 review — approved
93 </div>
94 </div>
95 </div>
96
97 {/* Card 3 — Badges */}
98 <div class="di-card">
99 <div class="di-card-copy">
100 <div class="di-eyebrow">03 · Public proof & viral loops</div>
101 <h2 class="di-card-title">Every public README becomes a billboard.</h2>
102 <p class="di-card-desc">Live health scores, green rates, and self-repair stats embed as dynamic badges. Every repo that shows one is a working demo of the platform — proof, not marketing.</p>
103 <a href="/trust" class="di-card-link">See the live numbers behind them →</a>
104 </div>
105 <div class="di-preview di-preview-light di-preview-badges">
106 <div class="di-badges-filename">README.md</div>
107 <div class="di-badge-strip">
108 <span class="di-badge">
109 <span class="di-badge-key">health</span>
110 <span class="di-badge-val di-badge-green">A · 91</span>
111 </span>
112 <span class="di-badge">
113 <span class="di-badge-key">gates</span>
114 <span class="di-badge-val di-badge-green">green 30d</span>
115 </span>
116 <span class="di-badge">
117 <span class="di-badge-key">self-repair</span>
118 <span class="di-badge-val di-badge-indigo">93.5%</span>
119 </span>
120 <span class="di-badge">
121 <span class="di-badge-key">reviewed by</span>
122 <span class="di-badge-val di-badge-dark">sonnet 5</span>
123 </span>
124 </div>
125 <div class="di-badges-meta">live · updates on every push · one line of markdown</div>
126 </div>
127 </div>
128
129 {/* Card 4 — ChatOps */}
130 <div class="di-card">
131 <div class="di-card-copy">
132 <div class="di-eyebrow">04 · ChatOps</div>
133 <h2 class="di-card-title">Inside the team's nervous system.</h2>
134 <p class="di-card-desc">Slack and Discord integrations push the moments that matter — a repaired build, an agent PR awaiting approval — and take action right from the thread. Approve without leaving chat.</p>
135 <a href="/daily-brief" class="di-card-link">See the daily brief it feeds →</a>
136 </div>
137 <div class="di-preview di-preview-light di-preview-chat">
138 <div class="di-chat-msg">
139 <span class="di-chat-avatar">GC</span>
140 <div class="di-chat-body">
141 <div class="di-chat-header">
142 <strong>gluecron</strong>
143 <span class="di-chat-meta">APP · #eng-core</span>
144 </div>
145 <div class="di-chat-text">
146 ✅ Repair converged on <strong>gluecron №218</strong> — gates green, waiting on one approval.
147 </div>
148 <div class="di-chat-actions">
149 <span class="di-chat-btn-primary">Approve & merge</span>
150 <span class="di-chat-btn-secondary">Open diff</span>
151 </div>
152 </div>
153 </div>
154 </div>
155 </div>
156
157 {/* Card 5 — Marketplace */}
158 <div class="di-card">
159 <div class="di-card-copy">
160 <div class="di-eyebrow">05 · The agent marketplace</div>
161 <h2 class="di-card-title">An ecosystem that builds itself.</h2>
162 <p class="di-card-desc">Scoped bot identities and developer apps let anyone ship specialized agents on the platform — every install runs under the same gates, audit trail, and reversibility guarantee as ours.</p>
163 <a href="/trust" class="di-card-link">The trust rules every agent obeys →</a>
164 </div>
165 <div class="di-preview di-preview-light di-preview-marketplace">
166 {agents.map((a) => (
167 <div class="di-agent-row" key={a.name}>
168 <span class="di-agent-avatar" style={`background:${a.bg}`}>{a.initials}</span>
169 <span class="di-agent-name">{a.name}</span>
170 <span class="di-agent-installs">{a.installs}</span>
171 </div>
172 ))}
173 </div>
174 </div>
175
176 </div>
177 </main>
178 </div>
179 <script dangerouslySetInnerHTML={{ __html: js }} />
180 </>
181 );
182};
183
184export default DistributionView;
185
186const js = `
187(function() {
188 // Animate CLI terminal lines sequentially on scroll into view
189 var termLines = document.querySelectorAll('.di-preview-dark .di-term-line');
190 if (termLines.length) {
191 termLines.forEach(function(el, i) {
192 el.style.opacity = '0';
193 el.style.transform = 'translateX(-6px)';
194 el.style.transition = 'opacity 0.28s ease, transform 0.28s ease';
195 });
196 var termAnimated = false;
197 function animateTerminal() {
198 if (termAnimated) return;
199 termAnimated = true;
200 termLines.forEach(function(el, i) {
201 setTimeout(function() {
202 el.style.opacity = '1';
203 el.style.transform = 'translateX(0)';
204 }, i * 140);
205 });
206 }
207 var observer = new IntersectionObserver(function(entries) {
208 entries.forEach(function(entry) {
209 if (entry.isIntersecting) animateTerminal();
210 });
211 }, { threshold: 0.3 });
212 var termCard = document.querySelector('.di-preview-dark');
213 if (termCard) observer.observe(termCard);
214 }
215
216 // Badge hover effect — slight lift
217 var badges = document.querySelectorAll('.di-badge');
218 badges.forEach(function(b) {
219 b.addEventListener('mouseenter', function() {
220 b.style.transform = 'translateY(-1px)';
221 b.style.transition = 'transform 0.15s ease';
222 });
223 b.addEventListener('mouseleave', function() {
224 b.style.transform = '';
225 });
226 });
227
228 // Chat action buttons — mock interactivity
229 var approveBtn = document.querySelector('.di-chat-btn-primary');
230 if (approveBtn) {
231 approveBtn.addEventListener('click', function() {
232 var original = approveBtn.textContent;
233 approveBtn.textContent = 'Merged!';
234 approveBtn.style.background = '#1e7f5c';
235 setTimeout(function() {
236 approveBtn.textContent = original;
237 approveBtn.style.background = '';
238 }, 2000);
239 });
240 }
241
242 // Agent marketplace rows — stagger fade in on scroll
243 var agentRows = document.querySelectorAll('.di-agent-row');
244 if (agentRows.length) {
245 agentRows.forEach(function(el) {
246 el.style.opacity = '0';
247 el.style.transform = 'translateY(6px)';
248 el.style.transition = 'opacity 0.26s ease, transform 0.26s ease';
249 });
250 var agentObserver = new IntersectionObserver(function(entries) {
251 entries.forEach(function(entry) {
252 if (entry.isIntersecting) {
253 agentRows.forEach(function(el, i) {
254 setTimeout(function() {
255 el.style.opacity = '1';
256 el.style.transform = 'translateY(0)';
257 }, i * 80);
258 });
259 agentObserver.disconnect();
260 }
261 });
262 }, { threshold: 0.2 });
263 var mp = document.querySelector('.di-preview-marketplace');
264 if (mp) agentObserver.observe(mp);
265 }
266})();
267`;
268
269const css = `
270@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
271
272@keyframes gcPulse {
273 0%, 100% { opacity: 1; }
274 50% { opacity: 0.35; }
275}
276
277*, *::before, *::after {
278 box-sizing: border-box;
279}
280
281.di-root {
282 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
283 font-size: 14px;
284 line-height: 1.55;
285 letter-spacing: -0.008em;
286 color: #16181d;
287 background: #fcfcfd;
288 min-height: 100vh;
289 display: flex;
290 flex-direction: column;
291 -webkit-font-smoothing: antialiased;
292 text-rendering: optimizeLegibility;
293}
294
295/* ── Header ── */
296.di-header {
297 height: 56px;
298 border-bottom: 1px solid rgba(22,24,29,0.07);
299 background: #fcfcfd;
300 display: flex;
301 align-items: center;
302 padding: 0 28px;
303 gap: 20px;
304 position: sticky;
305 top: 0;
306 z-index: 50;
307}
308
309.di-logo {
310 display: inline-flex;
311 align-items: center;
312 gap: 9px;
313 font-family: 'Inter Tight', sans-serif;
314 font-weight: 600;
315 font-size: 15px;
316 letter-spacing: -0.02em;
317 color: #16181d;
318 text-decoration: none;
319}
320
321.di-logo-mark {
322 width: 16px;
323 height: 16px;
324 border-radius: 5px;
325 background: #4353c9;
326 display: inline-block;
327 flex-shrink: 0;
328}
329
330.di-header-sep {
331 color: #c4c6cf;
332 user-select: none;
333}
334
335.di-header-crumb {
336 font-size: 13px;
337 color: #16181d;
338 font-weight: 500;
339}
340
341.di-header-right {
342 margin-left: auto;
343}
344
345.di-btn-primary {
346 padding: 8px 15px;
347 border-radius: 8px;
348 font-size: 13px;
349 font-weight: 600;
350 background: #16181d;
351 color: #fff;
352 text-decoration: none;
353 white-space: nowrap;
354 display: inline-block;
355 letter-spacing: -0.008em;
356}
357
358.di-btn-primary:hover {
359 background: #2a2d36;
360}
361
362/* ── Main ── */
363.di-main {
364 flex: 1;
365 max-width: 1180px;
366 width: 100%;
367 margin: 0 auto;
368 padding: 52px 32px 88px;
369}
370
371/* ── Hero ── */
372.di-hero {
373 max-width: 660px;
374 margin-bottom: 44px;
375}
376
377.di-eyebrow {
378 font-family: 'JetBrains Mono', monospace;
379 font-size: 11px;
380 letter-spacing: 0.12em;
381 text-transform: uppercase;
382 color: #8a8d99;
383 margin-bottom: 10px;
384}
385
386.di-hero-title {
387 font-family: 'Inter Tight', sans-serif;
388 font-size: 30px;
389 font-weight: 600;
390 letter-spacing: -0.026em;
391 line-height: 1.12;
392 margin: 0 0 10px;
393 color: #111318;
394}
395
396.di-hero-desc {
397 font-size: 14.5px;
398 color: #6b7080;
399 margin: 0;
400 line-height: 1.6;
401}
402
403/* ── Cards list ── */
404.di-cards {
405 display: flex;
406 flex-direction: column;
407 gap: 18px;
408}
409
410/* ── Individual card ── */
411.di-card {
412 display: grid;
413 grid-template-columns: minmax(0,1fr) minmax(0,1.1fr);
414 gap: 44px;
415 align-items: center;
416 border: 1px solid rgba(22,24,29,0.08);
417 border-radius: 18px;
418 background: #ffffff;
419 padding: 38px 42px;
420 transition: border-color 0.18s ease;
421}
422
423.di-card:hover {
424 border-color: rgba(22,24,29,0.14);
425}
426
427/* ── Card copy ── */
428.di-card-copy .di-eyebrow {
429 margin-bottom: 10px;
430}
431
432.di-card-title {
433 font-family: 'Inter Tight', sans-serif;
434 font-size: 22px;
435 font-weight: 600;
436 letter-spacing: -0.02em;
437 margin: 0 0 10px;
438 color: #111318;
439 line-height: 1.2;
440}
441
442.di-card-desc {
443 font-size: 13.5px;
444 color: #6b7080;
445 margin: 0 0 14px;
446 line-height: 1.65;
447}
448
449.di-card-link {
450 font-size: 13px;
451 font-weight: 600;
452 color: #4353c9;
453 text-decoration: none;
454}
455
456.di-card-link:hover {
457 text-decoration: underline;
458}
459
460/* ── Preview panels (shared) ── */
461.di-preview {
462 border-radius: 12px;
463 padding: 18px 20px;
464}
465
466.di-preview-light {
467 border: 1px solid rgba(22,24,29,0.07);
468 background: #fcfcfd;
469}
470
471/* ── IDE preview ── */
472.di-preview-ide {
473 font-family: 'JetBrains Mono', monospace;
474 font-size: 12px;
475 line-height: 1.9;
476}
477
478.di-code-comment {
479 color: #8a8d99;
480}
481
482.di-ide-row {
483 display: flex;
484 align-items: center;
485 gap: 9px;
486}
487
488.di-ide-text {
489 color: #16181d;
490}
491
492.di-dot {
493 width: 6px;
494 height: 6px;
495 border-radius: 50%;
496 flex-shrink: 0;
497 display: inline-block;
498}
499
500.di-dot-amber { background: #b45309; }
501.di-dot-green { background: #1e7f5c; }
502.di-dot-indigo { background: #4353c9; }
503.di-dot-pulse { animation: gcPulse 1.4s ease-in-out infinite; }
504
505/* ── CLI / Terminal preview ── */
506.di-preview-dark {
507 border: 1px solid rgba(22,24,29,0.9);
508 border-radius: 12px;
509 background: #0f1116;
510 padding: 18px 20px;
511 font-family: 'JetBrains Mono', monospace;
512 font-size: 12px;
513 line-height: 1.9;
514 color: #d6d8e0;
515}
516
517.di-term-line {
518 display: block;
519}
520
521.di-term-dim {
522 color: #8a8d99;
523}
524
525.di-term-prompt { color: #5f8fa0; }
526.di-term-green { color: #2fbf83; }
527.di-term-amber { color: #e0a35a; }
528
529/* ── Badges preview ── */
530.di-preview-badges {}
531
532.di-badges-filename {
533 font-family: 'JetBrains Mono', monospace;
534 font-size: 11px;
535 color: #8a8d99;
536 margin-bottom: 12px;
537}
538
539.di-badge-strip {
540 display: flex;
541 gap: 8px;
542 flex-wrap: wrap;
543}
544
545.di-badge {
546 display: inline-flex;
547 font-size: 11px;
548 font-weight: 600;
549 border-radius: 5px;
550 overflow: hidden;
551 border: 1px solid rgba(22,24,29,0.10);
552 cursor: default;
553 user-select: none;
554}
555
556.di-badge-key {
557 background: #3a3d45;
558 color: #fff;
559 padding: 4px 8px;
560}
561
562.di-badge-val {
563 padding: 4px 8px;
564 color: #fff;
565}
566
567.di-badge-green { background: #1e7f5c; }
568.di-badge-indigo { background: #4353c9; }
569.di-badge-dark { background: #16181d; }
570
571.di-badges-meta {
572 font-family: 'JetBrains Mono', monospace;
573 font-size: 10.5px;
574 color: #8a8d99;
575 margin-top: 12px;
576}
577
578/* ── ChatOps preview ── */
579.di-preview-chat {
580 display: flex;
581 flex-direction: column;
582 gap: 10px;
583}
584
585.di-chat-msg {
586 display: flex;
587 gap: 10px;
588 align-items: flex-start;
589}
590
591.di-chat-avatar {
592 width: 26px;
593 height: 26px;
594 border-radius: 7px;
595 background: #4353c9;
596 color: #fff;
597 font-size: 9px;
598 font-weight: 700;
599 display: inline-flex;
600 align-items: center;
601 justify-content: center;
602 flex-shrink: 0;
603 font-family: 'Inter', sans-serif;
604}
605
606.di-chat-body {
607 flex: 1;
608}
609
610.di-chat-header {
611 font-size: 12.5px;
612 color: #16181d;
613}
614
615.di-chat-meta {
616 color: #8a8d99;
617 font-size: 11px;
618 margin-left: 4px;
619}
620
621.di-chat-text {
622 font-size: 12.5px;
623 color: #3a3d45;
624 margin-top: 2px;
625 line-height: 1.55;
626}
627
628.di-chat-actions {
629 display: flex;
630 gap: 6px;
631 margin-top: 8px;
632}
633
634.di-chat-btn-primary {
635 font-size: 11px;
636 font-weight: 600;
637 padding: 4px 12px;
638 border-radius: 6px;
639 background: #16181d;
640 color: #fff;
641 cursor: pointer;
642 user-select: none;
643 transition: background 0.18s ease;
644 display: inline-block;
645}
646
647.di-chat-btn-primary:hover {
648 background: #2a2d36;
649}
650
651.di-chat-btn-secondary {
652 font-size: 11px;
653 font-weight: 600;
654 padding: 4px 12px;
655 border-radius: 6px;
656 border: 1px solid rgba(22,24,29,0.14);
657 color: #16181d;
658 cursor: pointer;
659 user-select: none;
660 background: transparent;
661 display: inline-block;
662 transition: border-color 0.18s ease;
663}
664
665.di-chat-btn-secondary:hover {
666 border-color: rgba(22,24,29,0.28);
667}
668
669/* ── Marketplace preview ── */
670.di-preview-marketplace {
671 display: flex;
672 flex-direction: column;
673 gap: 8px;
674}
675
676.di-agent-row {
677 display: flex;
678 align-items: center;
679 gap: 10px;
680 padding: 8px 10px;
681 border: 1px solid rgba(22,24,29,0.06);
682 border-radius: 9px;
683 background: #ffffff;
684 transition: border-color 0.15s ease, box-shadow 0.15s ease;
685}
686
687.di-agent-row:hover {
688 border-color: rgba(22,24,29,0.13);
689 box-shadow: 0 1px 4px rgba(22,24,29,0.06);
690}
691
692.di-agent-avatar {
693 width: 24px;
694 height: 24px;
695 border-radius: 7px;
696 color: #fff;
697 font-size: 9px;
698 font-weight: 700;
699 display: inline-flex;
700 align-items: center;
701 justify-content: center;
702 flex-shrink: 0;
703 font-family: 'Inter', sans-serif;
704}
705
706.di-agent-name {
707 font-size: 12.5px;
708 font-weight: 500;
709 color: #16181d;
710 flex: 1;
711 min-width: 0;
712 overflow: hidden;
713 text-overflow: ellipsis;
714 white-space: nowrap;
715}
716
717.di-agent-installs {
718 font-size: 11px;
719 color: #8a8d99;
720 font-family: 'JetBrains Mono', monospace;
721 flex-shrink: 0;
722 margin-left: auto;
723}
724
725/* ── Responsive ── */
726@media (max-width: 860px) {
727 .di-card {
728 grid-template-columns: 1fr;
729 gap: 24px;
730 padding: 28px 24px;
731 }
732
733 .di-main {
734 padding: 36px 20px 64px;
735 }
736
737 .di-hero-title {
738 font-size: 24px;
739 }
740}
741
742@media (max-width: 560px) {
743 .di-header {
744 padding: 0 16px;
745 gap: 12px;
746 }
747
748 .di-header-crumb {
749 display: none;
750 }
751
752 .di-badge-strip {
753 gap: 6px;
754 }
755}
756`;
Addedsrc/views/fleet-refactor.tsx+848−0View fileUnifiedSplit
@@ -0,0 +1,848 @@
1import type { FC } from "hono/jsx";
2
3export interface FleetItem {
4 repo: string;
5 pr: string;
6 added: number;
7 removed: number;
8 gates: string;
9 sandboxUrl: string;
10 status: string;
11}
12
13export interface FleetRefactorProps {
14 fleet?: FleetItem[];
15 user?: any;
16}
17
18const DEFAULT_FLEET: FleetItem[] = [
19 {
20 repo: "gluecron",
21 pr: "PR №220 · feat/auth-header",
22 added: 34,
23 removed: 30,
24 gates: "4/4 green",
25 sandboxUrl: "pr-220.preview",
26 status: "Ready",
27 },
28 {
29 repo: "gluecron-cli",
30 pr: "PR №221 · feat/auth-header",
31 added: 12,
32 removed: 12,
33 gates: "3/3 green",
34 sandboxUrl: "pr-221.preview",
35 status: "Ready",
36 },
37 {
38 repo: "editor-extensions",
39 pr: "PR №222 · feat/auth-header",
40 added: 18,
41 removed: 16,
42 gates: "4/5 running",
43 sandboxUrl: "pr-222.preview",
44 status: "Gating",
45 },
46 {
47 repo: "gatetest",
48 pr: "PR №223 · feat/auth-header",
49 added: 6,
50 removed: 6,
51 gates: "rebasing",
52 sandboxUrl: "pr-223.preview",
53 status: "Rebasing",
54 },
55];
56
57export const FleetRefactor: FC<FleetRefactorProps> = (props) => {
58 const fleet = props.fleet ?? DEFAULT_FLEET;
59 const user = props.user;
60 const avatarLabel = user?.username?.[0]?.toUpperCase() ?? "C";
61
62 return (
63 <>
64 <style dangerouslySetInnerHTML={{ __html: css }} />
65 <div class="fr-root">
66 <header class="fr-header">
67 <a href="/" class="fr-logo">
68 <span class="fr-logo-mark" />
69 gluecron
70 </a>
71 <span class="fr-header-sep">/</span>
72 <span class="fr-header-breadcrumb">Refactors</span>
73 <div class="fr-header-right">
74 <span class="fr-avatar">{avatarLabel}</span>
75 </div>
76 </header>
77
78 <main class="fr-main">
79 <div class="fr-hero">
80 <div class="fr-hero-text">
81 <div class="fr-eyebrow">Fleet refactor №7</div>
82 <h1 class="fr-title">Rename API auth header across every repo</h1>
83 <p class="fr-subtitle">
84 One spec, four repos, one reviewed changeset. Each repo gets its own PR and sandbox; they merge together or not at all.
85 </p>
86 </div>
87 <div class="fr-hero-actions">
88 <a href="#" class="fr-btn-secondary">Review combined diff</a>
89 <button
90 type="button"
91 id="fr-merge-btn"
92 class="fr-btn-primary fr-btn-disabled"
93 data-ready="false"
94 data-merged="false"
95 >
96 Waiting on gates…
97 </button>
98 </div>
99 </div>
100
101 <div class="fr-board">
102 <div class="fr-board-header">
103 <span>Repository</span>
104 <span>Changes</span>
105 <span>Gates</span>
106 <span>Sandbox</span>
107 <span>Status</span>
108 </div>
109
110 {fleet.map((f, i) => (
111 <div class="fr-board-row" data-row-index={String(i)}>
112 <span class="fr-repo-cell">
113 <span class="fr-repo-name">{f.repo}</span>
114 <span class="fr-pr-label">{f.pr}</span>
115 </span>
116 <span class="fr-diff-cell">
117 <span class="fr-diff-add">+{f.added}</span>{" "}
118 <span class="fr-diff-del">−{f.removed}</span>
119 </span>
120 <span class="fr-gate-cell" data-gate-index={String(i)}>
121 <span class="fr-gate-dot" data-dot-index={String(i)} />
122 <span class="fr-gate-text" data-gate-text={String(i)}>{f.gates}</span>
123 </span>
124 <a
125 href="#"
126 class="fr-sandbox-link"
127 >
128 {f.sandboxUrl}
129 </a>
130 <span
131 class="fr-status-chip"
132 data-status-index={String(i)}
133 >
134 {f.status}
135 </span>
136 </div>
137 ))}
138
139 <div class="fr-atomic-note">
140 <span class="fr-atomic-dot" id="fr-atomic-dot" />
141 <span class="fr-atomic-text" id="fr-atomic-text">
142 Atomic changeset: all four merge together, or none do. Waiting for 2 repos to finish gating.
143 </span>
144 </div>
145 </div>
146
147 <div class="fr-lower">
148 <section class="fr-diff-section">
149 <h2 class="fr-section-heading">The change — same pattern, every repo</h2>
150 <div class="fr-diff-card">
151 <div class="fr-diff-card-header">
152 <span>gluecron-cli · src/http.ts</span>
153 <span>
154 <span class="fr-diff-add">+2</span>{" "}
155 <span class="fr-diff-del">−2</span>
156 </span>
157 </div>
158 <div class="fr-diff-body">
159 <div class="fr-diff-line fr-diff-ctx">{" const headers = {"}</div>
160 <div class="fr-diff-line fr-diff-rem">{'− "X-Gluecron-Token": token,'}</div>
161 <div class="fr-diff-line fr-diff-ins">{'+ "Authorization": `Bearer $' + '{token}`,'}</div>
162 <div class="fr-diff-line fr-diff-ctx">{' "Content-Type": "application/json",'}</div>
163 <div class="fr-diff-line fr-diff-ctx">{" };"}</div>
164 </div>
165 </div>
166 <p class="fr-diff-note">
167 41 call sites matched across the fleet. The server accepts both headers for 30 days (compatibility shim in step 1), so repos can merge in any order without breaking each other — but the changeset still merges atomically by default.
168 </p>
169 </section>
170
171 <aside class="fr-aside">
172 <div class="fr-aside-block">
173 <h3 class="fr-aside-eyebrow">Conflicts</h3>
174 <div class="fr-aside-card">
175 <p class="fr-aside-body">
176 <strong class="fr-aside-strong">Zero, by design.</strong>{" "}
177 All four branches are continuously rebased in their sandboxes. A conflicting push to gluecron/main at 11:32 was absorbed and re-tested automatically — you never saw it.
178 </p>
179 </div>
180 </div>
181
182 <div class="fr-aside-block">
183 <h3 class="fr-aside-eyebrow">How this reverses</h3>
184 <div class="fr-aside-card">
185 <p class="fr-aside-body">
186 One click reverts all four merges together. The 30-day compatibility shim means a revert is safe at any point in the window.
187 </p>
188 </div>
189 </div>
190
191 <div class="fr-aside-footer">
192 <p class="fr-aside-footer-text">
193 Prefer to do one repo by hand? Detach it from the changeset and its PR becomes a normal manual PR — the rest still merge atomically.
194 </p>
195 </div>
196 </aside>
197 </div>
198 </main>
199 </div>
200
201 <script dangerouslySetInnerHTML={{
202 __html: `
203(function() {
204 var step = 0;
205 var merged = false;
206
207 var GREEN = "#1e7f5c";
208 var AMBER = "#b45309";
209 var BLUE = "#4353c9";
210 var PULSE = "gcPulse 1.4s ease-in-out infinite";
211
212 function getRowState(rowIndex, step, merged) {
213 var green = GREEN, amber = AMBER, blue = BLUE;
214 var pulse = PULSE;
215 if (rowIndex === 0) {
216 return {
217 gates: "4/4 green",
218 gateDotColor: green,
219 gateAnim: "none",
220 gateTextColor: green,
221 status: merged ? "Merged" : "Ready",
222 statusColor: merged ? blue : green,
223 };
224 }
225 if (rowIndex === 1) {
226 return {
227 gates: "3/3 green",
228 gateDotColor: green,
229 gateAnim: "none",
230 gateTextColor: green,
231 status: merged ? "Merged" : "Ready",
232 statusColor: merged ? blue : green,
233 };
234 }
235 if (rowIndex === 2) {
236 var ready2 = step >= 1;
237 return {
238 gates: ready2 ? "5/5 green" : "4/5 running",
239 gateDotColor: ready2 ? green : amber,
240 gateAnim: ready2 ? "none" : pulse,
241 gateTextColor: ready2 ? green : amber,
242 status: merged ? "Merged" : ready2 ? "Ready" : "Gating",
243 statusColor: merged ? blue : ready2 ? green : amber,
244 };
245 }
246 if (rowIndex === 3) {
247 var ready3 = step >= 2;
248 return {
249 gates: ready3 ? "3/3 green" : "rebasing",
250 gateDotColor: ready3 ? green : amber,
251 gateAnim: ready3 ? "none" : pulse,
252 gateTextColor: ready3 ? green : amber,
253 status: merged ? "Merged" : ready3 ? "Ready" : "Rebasing",
254 statusColor: merged ? blue : ready3 ? green : amber,
255 };
256 }
257 return {};
258 }
259
260 function isAllReady(step) {
261 return step >= 2;
262 }
263
264 function render() {
265 var ready = isAllReady(step);
266
267 // Update each row
268 for (var i = 0; i < 4; i++) {
269 var state = getRowState(i, step, merged);
270
271 var dotEl = document.querySelector('[data-dot-index="' + i + '"]');
272 var gateTextEl = document.querySelector('[data-gate-text="' + i + '"]');
273 var statusEl = document.querySelector('[data-status-index="' + i + '"]');
274
275 if (dotEl) {
276 dotEl.style.background = state.gateDotColor;
277 dotEl.style.animation = state.gateAnim;
278 }
279 if (gateTextEl) {
280 gateTextEl.textContent = state.gates;
281 gateTextEl.style.color = state.gateTextColor;
282 }
283 if (statusEl) {
284 statusEl.textContent = state.status;
285 statusEl.style.color = state.statusColor;
286 }
287 }
288
289 // Atomic note
290 var atomicDot = document.getElementById('fr-atomic-dot');
291 var atomicText = document.getElementById('fr-atomic-text');
292 if (atomicDot) {
293 atomicDot.style.background = merged ? BLUE : ready ? GREEN : AMBER;
294 }
295 if (atomicText) {
296 if (merged) {
297 atomicText.textContent = "All four merged atomically at 12:41 — one revert restores the whole fleet.";
298 } else if (ready) {
299 atomicText.textContent = "Atomic changeset: all four merge together, or none do. Everything is green — one approval merges the fleet.";
300 } else {
301 atomicText.textContent = "Atomic changeset: all four merge together, or none do. Waiting for 2 repos to finish gating.";
302 }
303 }
304
305 // Merge button
306 var btn = document.getElementById('fr-merge-btn');
307 if (btn) {
308 if (merged) {
309 btn.textContent = "Fleet merged ✓";
310 btn.style.background = GREEN;
311 btn.style.cursor = "default";
312 btn.classList.remove('fr-btn-disabled');
313 btn.classList.add('fr-btn-merged');
314 } else if (ready) {
315 btn.textContent = "Approve & merge fleet";
316 btn.style.background = "#16181d";
317 btn.style.cursor = "pointer";
318 btn.classList.remove('fr-btn-disabled');
319 } else {
320 btn.textContent = "Waiting on gates…";
321 btn.style.background = "#9a9da8";
322 btn.style.cursor = "default";
323 btn.classList.add('fr-btn-disabled');
324 }
325 }
326 }
327
328 // Timer: advance step every 3.5s up to step 2
329 var timer = setInterval(function() {
330 if (step < 2) {
331 step++;
332 render();
333 } else {
334 clearInterval(timer);
335 }
336 }, 3500);
337
338 // Merge button click
339 document.addEventListener('DOMContentLoaded', function() {
340 render();
341 var btn = document.getElementById('fr-merge-btn');
342 if (btn) {
343 btn.addEventListener('click', function() {
344 if (isAllReady(step) && !merged) {
345 merged = true;
346 render();
347 }
348 });
349 }
350 });
351
352 // Run immediately if DOM already ready
353 if (document.readyState !== 'loading') {
354 render();
355 var btn = document.getElementById('fr-merge-btn');
356 if (btn) {
357 btn.addEventListener('click', function() {
358 if (isAllReady(step) && !merged) {
359 merged = true;
360 render();
361 }
362 });
363 }
364 }
365})();
366 `
367 }} />
368 </>
369 );
370};
371
372export default FleetRefactor;
373
374const css = `
375@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
376
377@keyframes gcPulse {
378 0%, 100% { opacity: 1; }
379 50% { opacity: 0.35; }
380}
381
382*, *::before, *::after {
383 box-sizing: border-box;
384}
385
386.fr-root {
387 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
388 font-size: 14px;
389 line-height: 1.55;
390 letter-spacing: -0.008em;
391 color: #16181d;
392 background: #fcfcfd;
393 min-height: 100vh;
394 display: flex;
395 flex-direction: column;
396 -webkit-font-smoothing: antialiased;
397 text-rendering: optimizeLegibility;
398}
399
400/* Header */
401.fr-header {
402 height: 56px;
403 border-bottom: 1px solid rgba(22,24,29,0.07);
404 background: #fcfcfd;
405 display: flex;
406 align-items: center;
407 padding: 0 28px;
408 gap: 20px;
409 position: sticky;
410 top: 0;
411 z-index: 50;
412}
413
414.fr-logo {
415 display: inline-flex;
416 align-items: center;
417 gap: 9px;
418 font-family: 'Inter Tight', sans-serif;
419 font-weight: 600;
420 font-size: 15px;
421 letter-spacing: -0.02em;
422 color: #16181d;
423 text-decoration: none;
424}
425
426.fr-logo-mark {
427 width: 16px;
428 height: 16px;
429 border-radius: 5px;
430 background: #4353c9;
431 display: inline-block;
432 flex-shrink: 0;
433}
434
435.fr-header-sep {
436 color: #c4c6cf;
437}
438
439.fr-header-breadcrumb {
440 font-size: 13px;
441 color: #16181d;
442 font-weight: 500;
443}
444
445.fr-header-right {
446 margin-left: auto;
447 display: flex;
448 align-items: center;
449 gap: 14px;
450}
451
452.fr-avatar {
453 width: 28px;
454 height: 28px;
455 border-radius: 50%;
456 background: #16181d;
457 color: #fff;
458 font-size: 11px;
459 font-weight: 600;
460 display: inline-flex;
461 align-items: center;
462 justify-content: center;
463 flex-shrink: 0;
464}
465
466/* Main */
467.fr-main {
468 flex: 1;
469 max-width: 1180px;
470 width: 100%;
471 margin: 0 auto;
472 padding: 44px 32px 80px;
473}
474
475/* Hero */
476.fr-hero {
477 display: flex;
478 align-items: flex-end;
479 justify-content: space-between;
480 gap: 24px;
481 flex-wrap: wrap;
482 margin-bottom: 36px;
483}
484
485.fr-hero-text {
486 max-width: 640px;
487}
488
489.fr-eyebrow {
490 font-family: 'JetBrains Mono', monospace;
491 font-size: 11px;
492 letter-spacing: 0.12em;
493 text-transform: uppercase;
494 color: #8a8d99;
495 margin-bottom: 10px;
496}
497
498.fr-title {
499 font-family: 'Inter Tight', sans-serif;
500 font-size: 28px;
501 font-weight: 600;
502 letter-spacing: -0.025em;
503 line-height: 1.15;
504 margin: 0 0 8px;
505 color: #111318;
506}
507
508.fr-subtitle {
509 font-size: 14px;
510 color: #6b7080;
511 margin: 0;
512 line-height: 1.6;
513}
514
515.fr-hero-actions {
516 display: flex;
517 gap: 10px;
518 align-items: center;
519 flex-shrink: 0;
520}
521
522.fr-btn-secondary {
523 padding: 8px 16px;
524 border-radius: 8px;
525 font-size: 13px;
526 font-weight: 500;
527 border: 1px solid rgba(22,24,29,0.12);
528 background: #ffffff;
529 color: #16181d;
530 text-decoration: none;
531 display: inline-block;
532 white-space: nowrap;
533 cursor: pointer;
534 font-family: inherit;
535 transition: border-color 0.15s;
536}
537
538.fr-btn-secondary:hover {
539 border-color: rgba(22,24,29,0.24);
540}
541
542.fr-btn-primary {
543 padding: 8px 16px;
544 border-radius: 8px;
545 font-size: 13px;
546 font-weight: 600;
547 border: 0;
548 background: #16181d;
549 color: #fff;
550 cursor: pointer;
551 font-family: inherit;
552 white-space: nowrap;
553 transition: background 0.2s, cursor 0.1s;
554}
555
556.fr-btn-disabled {
557 background: #9a9da8 !important;
558 cursor: default !important;
559}
560
561.fr-btn-merged {
562 background: #1e7f5c !important;
563 cursor: default !important;
564}
565
566/* Fleet board */
567.fr-board {
568 border: 1px solid rgba(22,24,29,0.07);
569 border-radius: 14px;
570 background: #ffffff;
571 overflow: hidden;
572 margin-bottom: 36px;
573}
574
575.fr-board-header {
576 display: grid;
577 grid-template-columns: minmax(150px,1.4fr) minmax(90px,0.9fr) minmax(120px,1.2fr) minmax(90px,0.9fr) minmax(120px,1fr);
578 gap: 12px;
579 padding: 10px 22px;
580 border-bottom: 1px solid rgba(22,24,29,0.07);
581 font-family: 'JetBrains Mono', monospace;
582 font-size: 10.5px;
583 letter-spacing: 0.1em;
584 text-transform: uppercase;
585 color: #8a8d99;
586}
587
588.fr-board-row {
589 display: grid;
590 grid-template-columns: minmax(150px,1.4fr) minmax(90px,0.9fr) minmax(120px,1.2fr) minmax(90px,0.9fr) minmax(120px,1fr);
591 gap: 12px;
592 padding: 15px 22px;
593 border-bottom: 1px solid rgba(22,24,29,0.05);
594 align-items: center;
595}
596
597.fr-board-row:last-of-type {
598 border-bottom: 1px solid rgba(22,24,29,0.05);
599}
600
601.fr-repo-cell {
602 min-width: 0;
603}
604
605.fr-repo-name {
606 display: block;
607 font-size: 13.5px;
608 font-weight: 500;
609 color: #16181d;
610}
611
612.fr-pr-label {
613 display: block;
614 font-family: 'JetBrains Mono', monospace;
615 font-size: 11px;
616 color: #8a8d99;
617 margin-top: 1px;
618}
619
620.fr-diff-cell {
621 font-family: 'JetBrains Mono', monospace;
622 font-size: 12px;
623 font-variant-numeric: tabular-nums;
624}
625
626.fr-diff-add {
627 color: #1e7f5c;
628}
629
630.fr-diff-del {
631 color: #b42318;
632}
633
634.fr-gate-cell {
635 display: inline-flex;
636 align-items: center;
637 gap: 7px;
638 font-size: 12.5px;
639}
640
641.fr-gate-dot {
642 width: 7px;
643 height: 7px;
644 border-radius: 50%;
645 background: #1e7f5c;
646 flex-shrink: 0;
647 display: inline-block;
648}
649
650.fr-gate-text {
651 color: #1e7f5c;
652}
653
654.fr-sandbox-link {
655 font-family: 'JetBrains Mono', monospace;
656 font-size: 11.5px;
657 color: #4353c9;
658 text-decoration: none;
659}
660
661.fr-sandbox-link:hover {
662 text-decoration: underline;
663}
664
665.fr-status-chip {
666 font-size: 12.5px;
667 font-weight: 500;
668 color: #1e7f5c;
669}
670
671.fr-atomic-note {
672 padding: 13px 22px;
673 display: flex;
674 align-items: center;
675 gap: 10px;
676 background: #fcfcfd;
677}
678
679.fr-atomic-dot {
680 width: 7px;
681 height: 7px;
682 border-radius: 50%;
683 background: #b45309;
684 flex-shrink: 0;
685 display: inline-block;
686 transition: background 0.3s;
687}
688
689.fr-atomic-text {
690 font-size: 12.5px;
691 color: #6b7080;
692}
693
694/* Lower two-column */
695.fr-lower {
696 display: grid;
697 grid-template-columns: minmax(0,1.5fr) minmax(300px,1fr);
698 gap: 44px;
699 align-items: start;
700}
701
702/* Diff section */
703.fr-diff-section {
704 /* left column */
705}
706
707.fr-section-heading {
708 font-family: 'Inter Tight', sans-serif;
709 font-size: 15px;
710 font-weight: 600;
711 margin: 0 0 16px;
712 color: #16181d;
713 letter-spacing: -0.015em;
714}
715
716.fr-diff-card {
717 border: 1px solid rgba(22,24,29,0.07);
718 border-radius: 12px;
719 overflow: hidden;
720 background: #ffffff;
721}
722
723.fr-diff-card-header {
724 padding: 9px 18px;
725 border-bottom: 1px solid rgba(22,24,29,0.07);
726 font-family: 'JetBrains Mono', monospace;
727 font-size: 11.5px;
728 color: #6b7080;
729 display: flex;
730 justify-content: space-between;
731 align-items: center;
732}
733
734.fr-diff-body {
735 font-family: 'JetBrains Mono', monospace;
736 font-size: 12px;
737 line-height: 1.75;
738 padding: 8px 0;
739}
740
741.fr-diff-line {
742 padding: 0 18px;
743 white-space: pre;
744}
745
746.fr-diff-ctx {
747 color: #8a8d99;
748}
749
750.fr-diff-rem {
751 background: rgba(180,35,24,0.05);
752 color: #b42318;
753}
754
755.fr-diff-ins {
756 background: rgba(30,127,92,0.06);
757 color: #1e7f5c;
758}
759
760.fr-diff-note {
761 font-size: 12.5px;
762 color: #8a8d99;
763 margin: 12px 0 0;
764 line-height: 1.6;
765}
766
767/* Aside */
768.fr-aside {
769 display: flex;
770 flex-direction: column;
771 gap: 32px;
772 position: sticky;
773 top: 92px;
774}
775
776.fr-aside-block {
777 /* individual block */
778}
779
780.fr-aside-eyebrow {
781 font-family: 'JetBrains Mono', monospace;
782 font-size: 11px;
783 letter-spacing: 0.12em;
784 text-transform: uppercase;
785 color: #8a8d99;
786 font-weight: 500;
787 margin: 0 0 14px;
788}
789
790.fr-aside-card {
791 border: 1px solid rgba(22,24,29,0.07);
792 border-radius: 12px;
793 background: #ffffff;
794 padding: 20px;
795}
796
797.fr-aside-body {
798 font-size: 13px;
799 color: #6b7080;
800 margin: 0;
801 line-height: 1.65;
802}
803
804.fr-aside-strong {
805 color: #16181d;
806 font-weight: 500;
807}
808
809.fr-aside-footer {
810 border-top: 1px solid rgba(22,24,29,0.07);
811 padding-top: 20px;
812}
813
814.fr-aside-footer-text {
815 font-size: 12.5px;
816 color: #8a8d99;
817 margin: 0;
818 line-height: 1.6;
819}
820
821@media (max-width: 900px) {
822 .fr-lower {
823 grid-template-columns: 1fr;
824 }
825 .fr-aside {
826 position: static;
827 }
828}
829
830@media (max-width: 700px) {
831 .fr-main {
832 padding: 28px 16px 60px;
833 }
834 .fr-board-header,
835 .fr-board-row {
836 grid-template-columns: 1fr 1fr;
837 gap: 8px;
838 }
839 .fr-board-header span:nth-child(n+3),
840 .fr-board-row > *:nth-child(n+3) {
841 display: none;
842 }
843 .fr-hero {
844 flex-direction: column;
845 align-items: flex-start;
846 }
847}
848`;
Addedsrc/views/ide-multiplayer.tsx+773−0View fileUnifiedSplit
@@ -0,0 +1,773 @@
1import type { FC } from "hono/jsx";
2
3export interface IdeMultiplayerProps {
4 user?: any;
5 sessionId?: string;
6}
7
8const baseLines = [
9 { n: 148, text: " const session = await sandbox.attach(pr.id);", color: "#16181d", bg: "transparent", agentCursor: false },
10 { n: 149, text: "", color: "#16181d", bg: "transparent", agentCursor: false },
11 { n: 150, text: " // Bounded repair: never hold a sandbox forever.", color: "#8a8d99", bg: "transparent", agentCursor: false },
12 { n: 151, text: " for (let i = 0; i < MAX_REPAIR_ATTEMPTS; i++) {", color: "#2fbf83", bg: "rgba(30,127,92,0.05)", agentCursor: false },
13 { n: 152, text: " const patch = await attemptPatch(gate, session);", color: "#2fbf83", bg: "rgba(30,127,92,0.05)", agentCursor: false },
14 { n: 153, text: " if (await gates.rerun(session, patch)) break;", color: "#2fbf83", bg: "rgba(30,127,92,0.05)", agentCursor: false },
15 { n: 154, text: " }", color: "#2fbf83", bg: "rgba(30,127,92,0.05)", agentCursor: true },
16 { n: 155, text: "", color: "#16181d", bg: "transparent", agentCursor: false },
17 { n: 156, text: ' telemetry.record("repair.attempt", {', color: "#16181d", bg: "transparent", agentCursor: false },
18 { n: 157, text: " gate: gate.name,", color: "#16181d", bg: "transparent", agentCursor: false },
19 { n: 158, text: " attempts: i,", color: "#16181d", bg: "transparent", agentCursor: false },
20 { n: 159, text: " outcome: session.lastRun,", color: "#16181d", bg: "transparent", agentCursor: false },
21 { n: 160, text: " });", color: "#16181d", bg: "transparent", agentCursor: false },
22];
23
24const chatMessages = [
25 { who: "CC", avatarBg: "#4353c9", text: "The loop needs an upper bound — a red gate could hold the sandbox forever." },
26 { who: "CL", avatarBg: "#1e7f5c", text: "Agreed. Capping at MAX_REPAIR_ATTEMPTS and threading the session through each attempt — typing it now, lines 151–154." },
27 { who: "CL", avatarBg: "#1e7f5c", text: "Done. Accept the suggestion and I'll re-run gate: tests in the sandbox." },
28];
29
30export const IdeMultiplayer: FC<IdeMultiplayerProps> = (props) => {
31 const { user, sessionId = "pr-218" } = props;
32 const initials = user?.username
33 ? user.username.slice(0, 2).toUpperCase()
34 : "CC";
35
36 return (
37 <>
38 <style dangerouslySetInnerHTML={{ __html: css }} />
39 <div class="im-root">
40 {/* Top nav */}
41 <header class="im-header">
42 <a href="/" class="im-logo">
43 <span class="im-logo-mark" />
44 gluecron
45 </a>
46 <span class="im-header-sep">/</span>
47 <span class="im-header-sub">IDE extension · VS Code</span>
48 <div class="im-header-right">
49 <span class="im-header-note">Side-by-side, in your editor — the browser is optional</span>
50 </div>
51 </header>
52
53 <main class="im-main">
54 {/* Page heading */}
55 <div class="im-intro">
56 <div class="im-eyebrow">Multiplayer session · {sessionId} sandbox</div>
57 <h1 class="im-h1">You and the agent, in the same file.</h1>
58 <p class="im-lead">The extension joins the PR sandbox as a live session. You see the agent's cursor, it sees your edits, and either of you can take the keyboard — no context switch, no browser.</p>
59 </div>
60
61 {/* Editor mock */}
62 <div class="im-window" id="im-window">
63 {/* Title bar */}
64 <div class="im-titlebar">
65 <span class="im-dot im-dot-r" />
66 <span class="im-dot im-dot-y" />
67 <span class="im-dot im-dot-g" />
68 <span class="im-titlebar-filename">auto-repair.ts — gluecron · sandbox {sessionId}</span>
69 <span class="im-titlebar-chips">
70 <span class="im-chip im-chip-you">
71 <span class="im-chip-avatar im-chip-avatar-you">{initials}</span>
72 you
73 </span>
74 <span class="im-chip im-chip-claude">
75 <span class="im-chip-avatar im-chip-avatar-claude">CL</span>
76 claude
77 </span>
78 </span>
79 </div>
80
81 {/* Body: code pane + session rail */}
82 <div class="im-body">
83 {/* Code pane */}
84 <div class="im-code-pane" id="im-code-pane">
85 {baseLines.map((line) => (
86 <div
87 class="im-code-line"
88 style={`background:${line.bg}`}
89 data-agent-line={line.agentCursor ? "true" : "false"}
90 >
91 <span class="im-line-num">{line.n}</span>
92 <span
93 class="im-line-text"
94 style={`color:${line.color}`}
95 data-is-green={line.color === "#2fbf83" ? "true" : "false"}
96 >
97 {line.text}
98 {line.agentCursor && (
99 <>
100 <span class="im-caret" />
101 <span class="im-caret-label">← claude</span>
102 </>
103 )}
104 </span>
105 </div>
106 ))}
107 </div>
108
109 {/* Session rail */}
110 <div class="im-rail">
111 {/* Status section */}
112 <div class="im-rail-status">
113 <div class="im-rail-label">Session</div>
114 <div class="im-status-rows">
115 <div class="im-status-row">
116 <span class="im-dot-status im-dot-green im-dot-pulse" />
117 <span class="im-status-text" id="im-agent-status">claude finished — 4 lines suggested</span>
118 </div>
119 <div class="im-status-row">
120 <span class="im-dot-status im-dot-green" />
121 <span class="im-status-secondary">sandbox {sessionId} · terminal shared</span>
122 </div>
123 <div class="im-status-row">
124 <span class="im-dot-status im-gate-dot" id="im-gate-dot" />
125 <span class="im-status-secondary" id="im-gate-status">gates ready to re-run on accept</span>
126 </div>
127 </div>
128 </div>
129
130 {/* Chat feed */}
131 <div class="im-chat-feed" id="im-chat-feed">
132 <div class="im-rail-label">Chat</div>
133 {chatMessages.map((msg) => (
134 <div class="im-chat-msg">
135 <span
136 class="im-chat-avatar"
137 style={`background:${msg.avatarBg}`}
138 >
139 {msg.who}
140 </span>
141 <p class="im-chat-text">{msg.text}</p>
142 </div>
143 ))}
144 </div>
145
146 {/* Message input */}
147 <div class="im-chat-input-row">
148 <input
149 class="im-chat-input"
150 placeholder="Message claude…"
151 id="im-msg-input"
152 type="text"
153 />
154 <button class="im-chat-send" type="button" id="im-msg-send">Send</button>
155 </div>
156 </div>
157 </div>
158
159 {/* Status bar */}
160 <div class="im-statusbar">
161 <span>⎇ feat/repair-sandbox-loop</span>
162 <span>gluecron: connected</span>
163 <span class="im-statusbar-right">
164 agent edits: suggested → you accept ·{" "}
165 <button
166 class="im-mode-toggle"
167 type="button"
168 id="im-mode-toggle"
169 >
170 switch to observe-only
171 </button>
172 </span>
173 </div>
174 </div>
175
176 {/* Manual note row */}
177 <div class="im-footer-note">
178 <p class="im-footer-text">
179 Don't want the agent typing? Set it to{" "}
180 <strong class="im-footer-strong">observe-only</strong> and it becomes a reviewer in your gutter — you keep the keyboard, it keeps the receipts. Works identically in JetBrains.
181 </p>
182 <a href="#" class="im-footer-link">Open this session on the web →</a>
183 </div>
184 </main>
185 </div>
186
187 <script dangerouslySetInnerHTML={{ __html: js }} />
188 </>
189 );
190};
191
192export default IdeMultiplayer;
193
194const js = `
195(function() {
196 var step = 3;
197 var agentMode = "suggest";
198
199 var agentStatusEl = document.getElementById("im-agent-status");
200 var gateDotEl = document.getElementById("im-gate-dot");
201 var gateStatusEl = document.getElementById("im-gate-status");
202 var modeToggleEl = document.getElementById("im-mode-toggle");
203 var msgInput = document.getElementById("im-msg-input");
204 var msgSend = document.getElementById("im-msg-send");
205 var chatFeed = document.getElementById("im-chat-feed");
206
207 function applyStep(s) {
208 if (!agentStatusEl) return;
209 if (s < 3) {
210 agentStatusEl.textContent = "claude is typing in auto-repair.ts";
211 } else {
212 agentStatusEl.textContent = "claude finished — 4 lines suggested";
213 }
214 if (gateDotEl) {
215 if (s >= 3) {
216 gateDotEl.style.background = "#b45309";
217 gateDotEl.classList.add("im-dot-pulse");
218 } else {
219 gateDotEl.style.background = "#c4c6cf";
220 gateDotEl.classList.remove("im-dot-pulse");
221 }
222 }
223 if (gateStatusEl) {
224 gateStatusEl.textContent = s >= 3 ? "gates ready to re-run on accept" : "gates idle";
225 }
226
227 // show/hide green lines based on step
228 var codePane = document.getElementById("im-code-pane");
229 if (codePane) {
230 var greenLines = codePane.querySelectorAll("[data-is-green='true']");
231 greenLines.forEach(function(el, i) {
232 var lineDiv = el.parentElement;
233 var shown = s === 0 ? 1 : s === 1 ? 3 : 4;
234 if (i < shown) {
235 lineDiv.style.display = "";
236 } else {
237 lineDiv.style.display = "none";
238 }
239 // update cursor visibility
240 var agentLine = lineDiv.getAttribute("data-agent-line");
241 if (agentLine === "true") {
242 var caret = lineDiv.querySelector(".im-caret");
243 var caretLabel = lineDiv.querySelector(".im-caret-label");
244 if (s < 3) {
245 if (i === shown - 1) {
246 if (caret) caret.style.display = "inline-block";
247 if (caretLabel) caretLabel.style.display = "inline-block";
248 } else {
249 if (caret) caret.style.display = "none";
250 if (caretLabel) caretLabel.style.display = "none";
251 }
252 } else {
253 if (caret) caret.style.display = "none";
254 if (caretLabel) caretLabel.style.display = "none";
255 }
256 }
257 });
258 }
259 }
260
261 applyStep(step);
262
263 if (modeToggleEl) {
264 modeToggleEl.addEventListener("click", function() {
265 agentMode = agentMode === "suggest" ? "observe" : "suggest";
266 modeToggleEl.textContent = agentMode === "suggest" ? "switch to observe-only" : "switch to suggest";
267 });
268 }
269
270 if (msgSend && msgInput && chatFeed) {
271 function sendMsg() {
272 var val = msgInput.value.trim();
273 if (!val) return;
274 var msg = document.createElement("div");
275 msg.className = "im-chat-msg";
276 msg.innerHTML = '<span class="im-chat-avatar" style="background:#4353c9">CC</span><p class="im-chat-text">' + val.replace(/</g,"<").replace(/>/g,">") + '</p>';
277 chatFeed.appendChild(msg);
278 chatFeed.scrollTop = chatFeed.scrollHeight;
279 msgInput.value = "";
280 }
281 msgSend.addEventListener("click", sendMsg);
282 msgInput.addEventListener("keydown", function(e) {
283 if (e.key === "Enter") sendMsg();
284 });
285 }
286})();
287`;
288
289const css = `
290@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
291
292@keyframes imPulse {
293 0%, 100% { opacity: 1; }
294 50% { opacity: 0.35; }
295}
296@keyframes imCaret {
297 0%, 49% { opacity: 1; }
298 50%, 100% { opacity: 0; }
299}
300
301*, *::before, *::after { box-sizing: border-box; }
302
303.im-root {
304 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
305 font-size: 14px;
306 line-height: 1.55;
307 letter-spacing: -0.008em;
308 color: #16181d;
309 background: #fcfcfd;
310 min-height: 100vh;
311 display: flex;
312 flex-direction: column;
313 -webkit-font-smoothing: antialiased;
314 text-rendering: optimizeLegibility;
315}
316
317/* Header */
318.im-header {
319 height: 56px;
320 border-bottom: 1px solid rgba(22,24,29,0.07);
321 background: #fcfcfd;
322 display: flex;
323 align-items: center;
324 padding: 0 28px;
325 gap: 20px;
326}
327
328.im-logo {
329 display: inline-flex;
330 align-items: center;
331 gap: 9px;
332 font-family: 'Inter Tight', sans-serif;
333 font-weight: 600;
334 font-size: 15px;
335 letter-spacing: -0.02em;
336 color: #16181d;
337 text-decoration: none;
338}
339
340.im-logo-mark {
341 width: 16px;
342 height: 16px;
343 border-radius: 5px;
344 background: #4353c9;
345 display: inline-block;
346 flex-shrink: 0;
347}
348
349.im-header-sep {
350 color: #c4c6cf;
351}
352
353.im-header-sub {
354 font-size: 13px;
355 color: #16181d;
356 font-weight: 500;
357}
358
359.im-header-right {
360 margin-left: auto;
361 display: flex;
362 align-items: center;
363 gap: 14px;
364}
365
366.im-header-note {
367 font-size: 12.5px;
368 color: #6b7080;
369}
370
371/* Main */
372.im-main {
373 flex: 1;
374 max-width: 1240px;
375 width: 100%;
376 margin: 0 auto;
377 padding: 44px 32px 80px;
378 box-sizing: border-box;
379}
380
381/* Intro */
382.im-intro {
383 max-width: 680px;
384 margin-bottom: 32px;
385}
386
387.im-eyebrow {
388 font-family: 'JetBrains Mono', monospace;
389 font-size: 11px;
390 letter-spacing: 0.12em;
391 text-transform: uppercase;
392 color: #8a8d99;
393 margin-bottom: 10px;
394}
395
396.im-h1 {
397 font-family: 'Inter Tight', sans-serif;
398 font-size: 28px;
399 font-weight: 600;
400 letter-spacing: -0.025em;
401 line-height: 1.15;
402 margin: 0 0 8px;
403 color: #111318;
404}
405
406.im-lead {
407 font-size: 14px;
408 color: #6b7080;
409 margin: 0;
410 line-height: 1.6;
411}
412
413/* Editor window */
414.im-window {
415 border: 1px solid rgba(22,24,29,0.10);
416 border-radius: 14px;
417 background: #ffffff;
418 overflow: hidden;
419 box-shadow: 0 1px 2px rgba(22,24,29,0.03), 0 16px 44px -24px rgba(22,24,29,0.16);
420}
421
422/* Title bar */
423.im-titlebar {
424 display: flex;
425 align-items: center;
426 gap: 8px;
427 padding: 10px 16px;
428 border-bottom: 1px solid rgba(22,24,29,0.07);
429 background: #fcfcfd;
430}
431
432.im-dot {
433 width: 10px;
434 height: 10px;
435 border-radius: 50%;
436 flex-shrink: 0;
437}
438.im-dot-r { background: #ff5f57; }
439.im-dot-y { background: #febc2e; }
440.im-dot-g { background: #28c840; }
441
442.im-titlebar-filename {
443 margin-left: 10px;
444 font-family: 'JetBrains Mono', monospace;
445 font-size: 12px;
446 color: #6b7080;
447}
448
449.im-titlebar-chips {
450 margin-left: auto;
451 display: inline-flex;
452 gap: 6px;
453}
454
455.im-chip {
456 display: inline-flex;
457 align-items: center;
458 gap: 5px;
459 padding: 2px 10px 2px 4px;
460 border-radius: 9999px;
461 font-size: 11px;
462 font-weight: 600;
463}
464
465.im-chip-you {
466 background: rgba(67,83,201,0.07);
467 border: 1px solid rgba(67,83,201,0.20);
468 color: #4353c9;
469}
470
471.im-chip-claude {
472 background: rgba(30,127,92,0.07);
473 border: 1px solid rgba(30,127,92,0.22);
474 color: #1e7f5c;
475}
476
477.im-chip-avatar {
478 width: 16px;
479 height: 16px;
480 border-radius: 50%;
481 display: inline-flex;
482 align-items: center;
483 justify-content: center;
484 font-size: 8px;
485 font-weight: 700;
486 color: #fff;
487 flex-shrink: 0;
488}
489
490.im-chip-avatar-you { background: #4353c9; }
491.im-chip-avatar-claude { background: #1e7f5c; }
492
493/* Body grid */
494.im-body {
495 display: grid;
496 grid-template-columns: minmax(0, 1.7fr) minmax(280px, 1fr);
497}
498
499/* Code pane */
500.im-code-pane {
501 border-right: 1px solid rgba(22,24,29,0.07);
502 background: #0f1116;
503 font-family: 'JetBrains Mono', monospace;
504 font-size: 12.5px;
505 line-height: 1.85;
506 padding: 14px 0;
507 overflow-x: auto;
508}
509
510.im-code-line {
511 display: grid;
512 grid-template-columns: 44px minmax(0, 1fr);
513}
514
515.im-line-num {
516 text-align: right;
517 padding-right: 14px;
518 color: #c4c6cf;
519 user-select: none;
520 opacity: 0.4;
521}
522
523.im-line-text {
524 white-space: pre;
525 position: relative;
526 padding-right: 16px;
527}
528
529.im-caret {
530 display: inline-block;
531 width: 2px;
532 height: 15px;
533 background: #1e7f5c;
534 vertical-align: text-bottom;
535 animation: imCaret 1s step-end infinite;
536}
537
538.im-caret-label {
539 display: inline-block;
540 font-family: 'Inter', sans-serif;
541 font-size: 9px;
542 font-weight: 600;
543 color: #fff;
544 background: #1e7f5c;
545 border-radius: 3px;
546 padding: 0 5px;
547 position: relative;
548 top: -9px;
549 left: 4px;
550}
551
552/* Session rail */
553.im-rail {
554 display: flex;
555 flex-direction: column;
556 background: #fcfcfd;
557 min-height: 340px;
558}
559
560.im-rail-status {
561 padding: 14px 18px;
562 border-bottom: 1px solid rgba(22,24,29,0.06);
563}
564
565.im-rail-label {
566 font-family: 'JetBrains Mono', monospace;
567 font-size: 10.5px;
568 letter-spacing: 0.1em;
569 text-transform: uppercase;
570 color: #8a8d99;
571 font-weight: 500;
572 margin-bottom: 10px;
573}
574
575.im-status-rows {
576 display: flex;
577 flex-direction: column;
578 gap: 8px;
579 font-size: 12.5px;
580}
581
582.im-status-row {
583 display: flex;
584 align-items: center;
585 gap: 8px;
586}
587
588.im-dot-status {
589 width: 7px;
590 height: 7px;
591 border-radius: 50%;
592 flex-shrink: 0;
593}
594
595.im-dot-green { background: #1e7f5c; }
596.im-gate-dot { background: #b45309; }
597
598.im-dot-pulse {
599 animation: imPulse 1.4s ease-in-out infinite;
600}
601
602.im-status-text {
603 color: #16181d;
604}
605
606.im-status-secondary {
607 color: #6b7080;
608}
609
610/* Chat feed */
611.im-chat-feed {
612 flex: 1;
613 padding: 14px 18px;
614 display: flex;
615 flex-direction: column;
616 gap: 12px;
617 overflow-y: auto;
618 max-height: 260px;
619}
620
621.im-chat-msg {
622 display: flex;
623 gap: 9px;
624 align-items: flex-start;
625}
626
627.im-chat-avatar {
628 width: 20px;
629 height: 20px;
630 border-radius: 50%;
631 color: #fff;
632 display: inline-flex;
633 align-items: center;
634 justify-content: center;
635 font-size: 8px;
636 font-weight: 700;
637 flex-shrink: 0;
638 margin-top: 1px;
639}
640
641.im-chat-text {
642 font-size: 12.5px;
643 color: #16181d;
644 margin: 0;
645 line-height: 1.55;
646}
647
648/* Chat input */
649.im-chat-input-row {
650 padding: 12px 14px;
651 border-top: 1px solid rgba(22,24,29,0.06);
652 display: flex;
653 gap: 8px;
654}
655
656.im-chat-input {
657 flex: 1;
658 border: 1px solid rgba(22,24,29,0.10);
659 border-radius: 8px;
660 padding: 7px 12px;
661 outline: none;
662 font-family: inherit;
663 font-size: 12.5px;
664 color: #16181d;
665 background: #ffffff;
666 box-sizing: border-box;
667 min-width: 0;
668}
669
670.im-chat-input:focus {
671 border-color: rgba(67,83,201,0.35);
672 box-shadow: 0 0 0 2px rgba(67,83,201,0.08);
673}
674
675.im-chat-send {
676 padding: 7px 13px;
677 border-radius: 8px;
678 font-size: 12px;
679 font-weight: 500;
680 border: 0;
681 background: #16181d;
682 color: #fff;
683 cursor: pointer;
684 font-family: inherit;
685 white-space: nowrap;
686 transition: opacity 0.15s;
687}
688
689.im-chat-send:hover {
690 opacity: 0.85;
691}
692
693/* Status bar */
694.im-statusbar {
695 display: flex;
696 align-items: center;
697 gap: 16px;
698 padding: 7px 16px;
699 border-top: 1px solid rgba(22,24,29,0.07);
700 background: #fcfcfd;
701 font-family: 'JetBrains Mono', monospace;
702 font-size: 11px;
703 color: #8a8d99;
704}
705
706.im-statusbar-right {
707 margin-left: auto;
708}
709
710.im-mode-toggle {
711 border: 0;
712 background: none;
713 color: #4353c9;
714 cursor: pointer;
715 font-family: 'JetBrains Mono', monospace;
716 font-size: 11px;
717 padding: 0;
718 text-decoration: underline;
719 text-decoration-color: transparent;
720 transition: text-decoration-color 0.15s;
721}
722
723.im-mode-toggle:hover {
724 text-decoration-color: #4353c9;
725}
726
727/* Footer note */
728.im-footer-note {
729 display: flex;
730 justify-content: space-between;
731 gap: 24px;
732 flex-wrap: wrap;
733 margin-top: 24px;
734 align-items: flex-start;
735}
736
737.im-footer-text {
738 font-size: 12.5px;
739 color: #8a8d99;
740 margin: 0;
741 max-width: 60ch;
742 line-height: 1.6;
743}
744
745.im-footer-strong {
746 color: #16181d;
747 font-weight: 500;
748}
749
750.im-footer-link {
751 font-size: 13px;
752 color: #4353c9;
753 text-decoration: none;
754 white-space: nowrap;
755}
756
757.im-footer-link:hover {
758 text-decoration: underline;
759}
760
761@media (max-width: 768px) {
762 .im-main {
763 padding: 28px 18px 60px;
764 }
765 .im-body {
766 grid-template-columns: 1fr;
767 }
768 .im-rail {
769 border-top: 1px solid rgba(22,24,29,0.07);
770 border-right: none;
771 }
772}
773`;
Addedsrc/views/landing-v2.tsx+1447−0View fileUnifiedSplit
Large file (1,447 lines). Load full file
Addedsrc/views/onboarding-v2.tsx+750−0View fileUnifiedSplit
@@ -0,0 +1,750 @@
1import type { FC } from "hono/jsx";
2
3export interface OnboardingV2Props {
4 situation?: string;
5 user?: { username: string };
6}
7
8export const OnboardingV2: FC<OnboardingV2Props> = (props) => {
9 const initSit = props.situation ?? "broken-codebase";
10
11 return (
12 <>
13 <style dangerouslySetInnerHTML={{ __html: css }} />
14 <div class="ob-root" {...{ "data-init-situation": initSit }}>
15 <header class="ob-header">
16 <a href="/" class="ob-logo">
17 <span class="ob-logo-mark"></span>gluecron
18 </a>
19 <span class="ob-sep">/</span>
20 <span class="ob-page-label">Welcome</span>
21 <span class="ob-tagline">First five minutes — the wizard adapts to who's arriving</span>
22 </header>
23
24 <main class="ob-main">
25 <div class="ob-intro">
26 <div class="ob-eyebrow">Adaptive onboarding · one flow, four shapes</div>
27 <h1 class="ob-h1">Onboarding that reads the situation.</h1>
28 <p class="ob-lead">
29 The same signup senses who's arriving — a human with a broken repo, an agent teammate,
30 a locked-down enterprise, or someone just browsing — and reshapes itself. Pick a situation
31 to see the wizard adapt.
32 </p>
33 </div>
34
35 <div class="ob-pills" id="ob-pills"></div>
36
37 <div class="ob-grid">
38 <section class="ob-wizard">
39 <div class="ob-wiz-head" id="ob-wiz-head"></div>
40 <div class="ob-wiz-steps" id="ob-wiz-steps"></div>
41 <div class="ob-wiz-footer" id="ob-wiz-footer"></div>
42 </section>
43
44 <aside class="ob-rail">
45 <div>
46 <h3 class="ob-rail-hed">What the system sensed</h3>
47 <div class="ob-sensed-card" id="ob-sensed-card"></div>
48 </div>
49 <div>
50 <h3 class="ob-rail-hed">The three invariants</h3>
51 <div class="ob-invs">
52 <div class="ob-inv">
53 <div class="ob-inv-title">Identity coherence</div>
54 <p class="ob-inv-body">
55 Playground → verified profile keeps your full sandbox history. Nothing is lost on upgrade.
56 </p>
57 </div>
58 <div class="ob-inv">
59 <div class="ob-inv-title">Telemetry handshake</div>
60 <p class="ob-inv-body">
61 Setup parameters log to flywheel telemetry from the first second — onboarding health is
62 measured, not assumed.
63 </p>
64 </div>
65 <div class="ob-inv">
66 <div class="ob-inv-title">Local sync rails</div>
67 <p class="ob-inv-body">
68 The setup wizard connects your machine straight to the sovereign array — no external edge
69 network in the path.
70 </p>
71 </div>
72 </div>
73 </div>
74 </aside>
75 </div>
76 </main>
77 </div>
78 <script dangerouslySetInnerHTML={{ __html: js }} />
79 </>
80 );
81};
82
83export default OnboardingV2;
84
85/* ─── styles ─────────────────────────────────────────────────────────────── */
86
87const css = `
88@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
89
90@keyframes gcPulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
91
92*, *::before, *::after { box-sizing: border-box; }
93
94.ob-root {
95 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
96 font-size: 14px;
97 line-height: 1.55;
98 letter-spacing: -0.008em;
99 color: #16181d;
100 background: #fcfcfd;
101 min-height: 100vh;
102 display: flex;
103 flex-direction: column;
104 -webkit-font-smoothing: antialiased;
105 text-rendering: optimizeLegibility;
106}
107
108/* ── header ─────────────────────────────────────────────────────────────── */
109.ob-header {
110 height: 56px;
111 border-bottom: 1px solid rgba(22,24,29,0.07);
112 background: #fcfcfd;
113 display: flex;
114 align-items: center;
115 padding: 0 28px;
116 gap: 20px;
117 position: sticky;
118 top: 0;
119 z-index: 50;
120}
121.ob-logo {
122 display: inline-flex;
123 align-items: center;
124 gap: 9px;
125 font-family: 'Inter Tight', sans-serif;
126 font-weight: 600;
127 font-size: 15px;
128 letter-spacing: -0.02em;
129 color: #16181d;
130 text-decoration: none;
131}
132.ob-logo-mark {
133 width: 16px;
134 height: 16px;
135 border-radius: 5px;
136 background: #4353c9;
137 display: inline-block;
138 flex-shrink: 0;
139}
140.ob-sep { color: #c4c6cf; }
141.ob-page-label { font-size: 13px; color: #16181d; font-weight: 500; }
142.ob-tagline { margin-left: auto; font-size: 12.5px; color: #6b7080; }
143
144/* ── main layout ─────────────────────────────────────────────────────────── */
145.ob-main {
146 flex: 1;
147 max-width: 1180px;
148 width: 100%;
149 margin: 0 auto;
150 padding: 44px 32px 88px;
151}
152
153/* ── intro ───────────────────────────────────────────────────────────────── */
154.ob-intro { max-width: 680px; margin-bottom: 32px; }
155.ob-eyebrow {
156 font-family: 'JetBrains Mono', monospace;
157 font-size: 11px;
158 letter-spacing: 0.12em;
159 text-transform: uppercase;
160 color: #8a8d99;
161 margin-bottom: 10px;
162}
163.ob-h1 {
164 font-family: 'Inter Tight', sans-serif;
165 font-size: 28px;
166 font-weight: 600;
167 letter-spacing: -0.025em;
168 line-height: 1.15;
169 margin: 0 0 8px;
170 color: #111318;
171}
172.ob-lead { font-size: 14px; color: #6b7080; margin: 0; line-height: 1.6; }
173
174/* ── situation pills ─────────────────────────────────────────────────────── */
175.ob-pills { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 28px; }
176.ob-pill {
177 padding: 8px 16px;
178 border-radius: 9999px;
179 font-size: 13px;
180 font-weight: 500;
181 border: 1px solid rgba(22,24,29,0.12);
182 background: #ffffff;
183 color: #6b7080;
184 cursor: pointer;
185 font-family: inherit;
186 white-space: nowrap;
187 transition: border-color 0.12s;
188}
189.ob-pill:hover { border-color: rgba(22,24,29,0.30); }
190.ob-pill--active {
191 background: #16181d;
192 color: #ffffff;
193 border-color: #16181d;
194 font-weight: 600;
195}
196
197/* ── two-col grid ────────────────────────────────────────────────────────── */
198.ob-grid {
199 display: grid;
200 grid-template-columns: minmax(0, 1.5fr) minmax(300px, 1fr);
201 gap: 44px;
202 align-items: start;
203}
204
205/* ── wizard card ─────────────────────────────────────────────────────────── */
206.ob-wizard {
207 border: 1px solid rgba(22,24,29,0.09);
208 border-radius: 16px;
209 background: #ffffff;
210 overflow: hidden;
211}
212
213.ob-wiz-head {
214 padding: 20px 26px;
215 border-bottom: 1px solid rgba(22,24,29,0.06);
216 background: #fcfcfd;
217}
218.ob-wiz-head-row { display: flex; align-items: center; gap: 10px; }
219.ob-wiz-dot {
220 width: 8px;
221 height: 8px;
222 border-radius: 50%;
223 flex-shrink: 0;
224 animation: gcPulse 1.4s ease-in-out infinite;
225}
226.ob-wiz-title { font-size: 14.5px; font-weight: 600; color: #16181d; }
227.ob-wiz-body { font-size: 13px; color: #6b7080; margin: 6px 0 0; line-height: 1.6; }
228
229/* ── step timeline ───────────────────────────────────────────────────────── */
230.ob-wiz-steps { padding: 22px 26px; display: flex; flex-direction: column; gap: 0; }
231.ob-step {
232 display: grid;
233 grid-template-columns: 20px minmax(0, 1fr);
234 gap: 16px;
235 position: relative;
236}
237.ob-step:not(:last-child) { padding-bottom: 24px; }
238.ob-step-track { display: flex; flex-direction: column; align-items: center; }
239.ob-step-dot {
240 width: 8px;
241 height: 8px;
242 border-radius: 50%;
243 margin-top: 6px;
244 flex-shrink: 0;
245}
246.ob-step-line {
247 width: 1px;
248 flex: 1;
249 background: rgba(22,24,29,0.08);
250 margin-top: 6px;
251}
252.ob-step-title { font-size: 13.5px; font-weight: 600; color: #16181d; }
253.ob-step-body {
254 font-size: 13px;
255 color: #6b7080;
256 margin: 3px 0 0;
257 line-height: 1.6;
258 max-width: 56ch;
259}
260
261/* ── terminal snippet ────────────────────────────────────────────────────── */
262.ob-term {
263 margin: 12px 0 0;
264 border: 1px solid rgba(22,24,29,0.9);
265 border-radius: 10px;
266 background: #0f1116;
267 overflow: hidden;
268}
269.ob-term-bar {
270 display: flex;
271 align-items: center;
272 justify-content: space-between;
273 padding: 8px 14px;
274 border-bottom: 1px solid rgba(255,255,255,0.07);
275}
276.ob-term-label {
277 font-family: 'JetBrains Mono', monospace;
278 font-size: 10.5px;
279 color: #8a8d99;
280 letter-spacing: 0.08em;
281 text-transform: uppercase;
282}
283.ob-copy-btn {
284 padding: 4px 12px;
285 border-radius: 6px;
286 font-size: 11px;
287 font-weight: 600;
288 border: 1px solid rgba(255,255,255,0.14);
289 background: transparent;
290 color: #d6d8e0;
291 cursor: pointer;
292 font-family: inherit;
293 transition: background 0.1s;
294}
295.ob-copy-btn:hover { background: rgba(255,255,255,0.06); }
296.ob-term-body {
297 padding: 12px 16px;
298 font-family: 'JetBrains Mono', monospace;
299 font-size: 12px;
300 line-height: 1.8;
301 color: #d6d8e0;
302}
303.ob-term-prompt { color: #5f8fa0; }
304.ob-term-comment { color: #8a8d99; }
305
306/* ── connection status row ───────────────────────────────────────────────── */
307.ob-conn {
308 display: flex;
309 align-items: center;
310 gap: 8px;
311 margin-top: 10px;
312 font-size: 12.5px;
313}
314.ob-conn-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
315.ob-conn-text { flex: 1; }
316.ob-conn-btn {
317 margin-left: 8px;
318 padding: 3px 12px;
319 border-radius: 6px;
320 font-size: 11.5px;
321 font-weight: 600;
322 border: 1px solid rgba(22,24,29,0.14);
323 background: #ffffff;
324 color: #16181d;
325 cursor: pointer;
326 font-family: inherit;
327 white-space: nowrap;
328}
329.ob-conn-btn:hover { background: #f5f5f7; }
330
331/* ── wizard footer / CTA ─────────────────────────────────────────────────── */
332.ob-wiz-footer {
333 padding: 16px 26px;
334 border-top: 1px solid rgba(22,24,29,0.06);
335 display: flex;
336 align-items: center;
337 gap: 12px;
338 flex-wrap: wrap;
339}
340.ob-cta-btn {
341 padding: 9px 18px;
342 border-radius: 9px;
343 font-size: 13.5px;
344 font-weight: 600;
345 background: #16181d;
346 color: #fff;
347 text-decoration: none;
348 white-space: nowrap;
349 display: inline-block;
350}
351.ob-cta-btn:hover { opacity: 0.88; }
352.ob-cta-note { font-size: 12px; color: #8a8d99; }
353
354/* ── right rail ──────────────────────────────────────────────────────────── */
355.ob-rail {
356 display: flex;
357 flex-direction: column;
358 gap: 32px;
359 position: sticky;
360 top: 92px;
361}
362.ob-rail-hed {
363 font-family: 'JetBrains Mono', monospace;
364 font-size: 11px;
365 letter-spacing: 0.12em;
366 text-transform: uppercase;
367 color: #8a8d99;
368 font-weight: 500;
369 margin: 0 0 14px;
370}
371
372/* ── sensed card ─────────────────────────────────────────────────────────── */
373.ob-sensed-card {
374 border: 1px solid rgba(22,24,29,0.07);
375 border-radius: 12px;
376 background: #ffffff;
377 padding: 20px;
378 display: flex;
379 flex-direction: column;
380 gap: 11px;
381}
382.ob-sensed-row { display: flex; justify-content: space-between; gap: 12px; font-size: 12.5px; }
383.ob-sensed-k { color: #6b7080; }
384.ob-sensed-v {
385 font-family: 'JetBrains Mono', monospace;
386 font-size: 11.5px;
387 color: #16181d;
388 text-align: right;
389}
390
391/* ── invariant cards ─────────────────────────────────────────────────────── */
392.ob-invs { display: flex; flex-direction: column; gap: 10px; }
393.ob-inv {
394 border: 1px solid rgba(22,24,29,0.07);
395 border-radius: 12px;
396 background: #ffffff;
397 padding: 14px 18px;
398}
399.ob-inv-title { font-size: 13px; font-weight: 600; color: #16181d; margin-bottom: 3px; }
400.ob-inv-body { font-size: 12px; color: #6b7080; margin: 0; line-height: 1.55; }
401
402/* ── responsive ──────────────────────────────────────────────────────────── */
403@media (max-width: 860px) {
404 .ob-grid { grid-template-columns: 1fr; }
405 .ob-rail { position: static; }
406 .ob-tagline { display: none; }
407}
408`;
409
410/* ─── client script ───────────────────────────────────────────────────────── */
411
412const js = `(function () {
413 var GREEN = '#1e7f5c';
414 var AMBER = '#b45309';
415 var INDIGO = '#4353c9';
416 var INK = '#16181d';
417 var GREY = '#c4c6cf';
418 var PULSE = 'gcPulse 1.4s ease-in-out infinite';
419
420 var SITUATIONS = [
421 {
422 id: 'broken-codebase',
423 label: 'Broken codebase import',
424 headDot: AMBER,
425 headTitle: 'Auto-Repair Mode engaged',
426 headBody: 'Your import arrived with 7 failing tests and a missing config. Instead of a red build bar, your first experience is a fix.',
427 steps: [
428 {
429 title: 'Repo imported · full history + branches',
430 body: '142 commits, 3 branches, semantic index building in the background.',
431 dot: GREEN, snippet: false
432 },
433 {
434 title: 'Archaeology scan complete',
435 body: 'Tech-debt hotspots flagged, symbols mapped, architecture topology drawn — your team gets the map on day one.',
436 dot: GREEN, snippet: false
437 },
438 {
439 title: 'Auto-repair drafted your first patch',
440 body: 'The healer reproduced the failures in a sandbox and drafted a fix for the missing config and 5 of 7 tests. Review it like any PR — or fix it manually.',
441 dot: AMBER, snippet: false
442 },
443 {
444 title: 'One-shot local setup',
445 body: 'Configures git, injects host keys, and tests the connection — no manual terminal spelunking.',
446 dot: INK, snippet: true
447 }
448 ],
449 ctaLabel: 'Review your first repair PR',
450 ctaHref: '/',
451 ctaNote: 'Nothing merges without you — the manual diff is one click away.',
452 sensed: [
453 { k: 'Import source', v: 'github mirror' },
454 { k: 'Build status', v: '7 tests failing' },
455 { k: 'Config', v: '.env.example missing' },
456 { k: 'Flow selected', v: 'auto-repair mode' }
457 ]
458 },
459 {
460 id: 'agent-teammate',
461 label: 'Agent teammate',
462 headDot: INDIGO,
463 headTitle: 'Machine user provisioning',
464 headBody: 'A non-human teammate is joining. No email drips, no UI tours — scoped credentials and machine-speed rails, immediately.',
465 steps: [
466 {
467 title: 'Bot identity created · migration-pilot',
468 body: 'Scoped API token minted with exactly the permissions its manifest declares — nothing more.',
469 dot: GREEN, snippet: false
470 },
471 {
472 title: 'Personal semantic space seeded',
473 body: 'The agent gets its own index scope so its queries never leak across tenants.',
474 dot: GREEN, snippet: false
475 },
476 {
477 title: 'Fast-lane execution rights',
478 body: 'Trusted-agent patches skip human-task scheduling; agent-to-agent verification runs at machine speed — still gated, still audited.',
479 dot: INDIGO, snippet: false
480 },
481 {
482 title: 'Reversibility contract signed',
483 body: 'Every action the agent takes obeys the same one-click-undo guarantee as ours. No exceptions for bots.',
484 dot: INK, snippet: false
485 }
486 ],
487 ctaLabel: 'See agents at work',
488 ctaHref: '/',
489 ctaNote: 'Every agent action lands in the same audit trail as human ones.',
490 sensed: [
491 { k: 'Account type', v: 'machine user' },
492 { k: 'Manifest scopes', v: 'repo:write, pr:open' },
493 { k: 'Drip sequence', v: 'skipped' },
494 { k: 'Flow selected', v: 'fast-lane provisioning' }
495 ]
496 },
497 {
498 id: 'air-gapped-enterprise',
499 label: 'Air-gapped enterprise',
500 headDot: INK,
501 headTitle: 'Sovereign tenant setup',
502 headBody: 'This tenant is flagged bare-metal / private-cloud. Public identity options are gone; everything stays inside your network boundary.',
503 steps: [
504 {
505 title: 'Public OAuth removed',
506 body: 'GitHub and Google sign-in don\'t appear. Mandatory SSH key + passkey registration, enforced at the door.',
507 dot: INK, snippet: false
508 },
509 {
510 title: 'Local signing keys seeded',
511 body: 'Commit and artifact signing keys generated on your hardware — verification never phones home.',
512 dot: GREEN, snippet: false
513 },
514 {
515 title: 'OCI registry fenced to your perimeter',
516 body: 'Container builds, package caches, and signed binaries route entirely inside your Caddy boundary. Zero external CDN or proxy.',
517 dot: GREEN, snippet: false
518 },
519 {
520 title: 'Air-gapped sync scheduled',
521 body: 'Repos, semantic indices, and registries replicate hourly to your offline array.',
522 dot: INK, snippet: false
523 }
524 ],
525 ctaLabel: 'Inspect the sovereign stack',
526 ctaHref: '/',
527 ctaNote: 'All 13 production layers, self-owned and self-healing.',
528 sensed: [
529 { k: 'Tenant flag', v: 'air-gapped' },
530 { k: 'Public OAuth', v: 'disabled' },
531 { k: 'Registry scope', v: 'local network only' },
532 { k: 'Flow selected', v: 'sovereign setup' }
533 ]
534 },
535 {
536 id: 'just-browsing',
537 label: 'Just browsing',
538 headDot: GREEN,
539 headTitle: 'Playground account · zero strings',
540 headBody: 'No migration, no commitment. An ephemeral, isolated sandbox with everything switched on — expires on its own unless you keep it.',
541 steps: [
542 {
543 title: 'Instant sandbox account',
544 body: 'No email required. A demo repo with real history, gates, and an agent ready to talk.',
545 dot: GREEN, snippet: false
546 },
547 {
548 title: 'Try the whole loop',
549 body: 'Push a change, watch gates fire at the wire, see Sonnet 5 review it, break something and watch the repair.',
550 dot: GREEN, snippet: false
551 },
552 {
553 title: 'Keep it when you\'re ready',
554 body: 'Claiming your account bridges everything — your sandbox history, repos, and sessions come with you. Nothing resets.',
555 dot: INDIGO, snippet: false
556 }
557 ],
558 ctaLabel: 'Enter the playground',
559 ctaHref: '/',
560 ctaNote: 'Expires in 24h unless claimed — claim keeps all history.',
561 sensed: [
562 { k: 'Signup', v: 'none — anonymous' },
563 { k: 'Environment', v: 'ephemeral sandbox' },
564 { k: 'Expiry', v: '24h · claimable' },
565 { k: 'Flow selected', v: 'playground' }
566 ]
567 }
568 ];
569
570 /* ── state ─────────────────────────────────────────────────────────────── */
571 var state = { situation: 0, copied: false, conn: 'idle' };
572
573 var root = document.querySelector('.ob-root');
574 if (root) {
575 var initSit = root.getAttribute('data-init-situation') || 'broken-codebase';
576 for (var si = 0; si < SITUATIONS.length; si++) {
577 if (SITUATIONS[si].id === initSit) { state.situation = si; break; }
578 }
579 }
580
581 function setState(patch) {
582 for (var k in patch) { if (Object.prototype.hasOwnProperty.call(patch, k)) state[k] = patch[k]; }
583 render();
584 }
585
586 /* ── html escaping ─────────────────────────────────────────────────────── */
587 function esc(str) {
588 return String(str)
589 .replace(/&/g, '&')
590 .replace(/</g, '<')
591 .replace(/>/g, '>')
592 .replace(/"/g, '"');
593 }
594
595 /* ── render pills ──────────────────────────────────────────────────────── */
596 function renderPills() {
597 var el = document.getElementById('ob-pills');
598 if (!el) return;
599 var html = '';
600 for (var i = 0; i < SITUATIONS.length; i++) {
601 var active = i === state.situation;
602 html += '<button type="button" class="ob-pill' + (active ? ' ob-pill--active' : '') +
603 '" data-sit="' + i + '">' + esc(SITUATIONS[i].label) + '</button>';
604 }
605 el.innerHTML = html;
606 var pills = el.querySelectorAll('.ob-pill');
607 for (var j = 0; j < pills.length; j++) {
608 (function (pill) {
609 pill.addEventListener('click', function () {
610 setState({ situation: parseInt(pill.getAttribute('data-sit'), 10), copied: false, conn: 'idle' });
611 });
612 })(pills[j]);
613 }
614 }
615
616 /* ── render wizard header ──────────────────────────────────────────────── */
617 function renderWizHead() {
618 var el = document.getElementById('ob-wiz-head');
619 if (!el) return;
620 var d = SITUATIONS[state.situation];
621 el.innerHTML =
622 '<div class="ob-wiz-head-row">' +
623 '<span class="ob-wiz-dot" style="background:' + d.headDot + '"></span>' +
624 '<span class="ob-wiz-title">' + esc(d.headTitle) + '</span>' +
625 '</div>' +
626 '<p class="ob-wiz-body">' + esc(d.headBody) + '</p>';
627 }
628
629 /* ── render steps ──────────────────────────────────────────────────────── */
630 function renderSteps() {
631 var el = document.getElementById('ob-wiz-steps');
632 if (!el) return;
633 var d = SITUATIONS[state.situation];
634 var html = '';
635
636 for (var i = 0; i < d.steps.length; i++) {
637 var s = d.steps[i];
638 var isLast = i === d.steps.length - 1;
639 var snippetHtml = '';
640
641 if (s.snippet) {
642 var copyLabel = state.copied ? 'Copied ✓' : 'Copy';
643 var connLabel = state.conn === 'ok'
644 ? 'SSH + OAuth verified — direct to sovereign array, no external edge'
645 : state.conn === 'testing'
646 ? 'Testing local → sovereign connection…'
647 : 'Connection not yet tested';
648 var connDot = state.conn === 'ok' ? GREEN : state.conn === 'testing' ? AMBER : GREY;
649 var connColor = state.conn === 'ok' ? GREEN : '#6b7080';
650 var connAnim = state.conn === 'testing' ? PULSE : 'none';
651 var testLabel = state.conn === 'testing' ? 'Testing…'
652 : state.conn === 'ok' ? 'Re-test'
653 : 'Test connection';
654
655 snippetHtml =
656 '<div class="ob-term">' +
657 '<div class="ob-term-bar">' +
658 '<span class="ob-term-label">one-shot local setup</span>' +
659 '<button type="button" class="ob-copy-btn" id="ob-copy-btn">' + esc(copyLabel) + '</button>' +
660 '</div>' +
661 '<div class="ob-term-body">' +
662 '<div><span class="ob-term-prompt">$</span> curl -sSL gluecron.com/setup | bash</div>' +
663 '<div class="ob-term-comment"># configures git · injects host keys · tests the connection</div>' +
664 '</div>' +
665 '</div>' +
666 '<div class="ob-conn" style="color:' + connColor + '">' +
667 '<span class="ob-conn-dot" style="background:' + connDot + ';animation:' + connAnim + '"></span>' +
668 '<span class="ob-conn-text">' + esc(connLabel) + '</span>' +
669 '<button type="button" class="ob-conn-btn" id="ob-conn-btn">' + esc(testLabel) + '</button>' +
670 '</div>';
671 }
672
673 html +=
674 '<div class="ob-step">' +
675 '<div class="ob-step-track">' +
676 '<span class="ob-step-dot" style="background:' + s.dot + '"></span>' +
677 (!isLast ? '<span class="ob-step-line"></span>' : '') +
678 '</div>' +
679 '<div>' +
680 '<div class="ob-step-title">' + esc(s.title) + '</div>' +
681 '<p class="ob-step-body">' + esc(s.body) + '</p>' +
682 snippetHtml +
683 '</div>' +
684 '</div>';
685 }
686
687 el.innerHTML = html;
688
689 /* copy button */
690 var copyBtn = document.getElementById('ob-copy-btn');
691 if (copyBtn) {
692 copyBtn.addEventListener('click', function () {
693 try { navigator.clipboard.writeText('curl -sSL https://gluecron.com/setup | bash'); } catch (e) {}
694 setState({ copied: true });
695 setTimeout(function () { setState({ copied: false }); }, 1200);
696 });
697 }
698
699 /* test-connection button */
700 var connBtn = document.getElementById('ob-conn-btn');
701 if (connBtn) {
702 connBtn.addEventListener('click', function () {
703 if (state.conn === 'testing') return;
704 setState({ conn: 'testing' });
705 setTimeout(function () { setState({ conn: 'ok' }); }, 1600);
706 });
707 }
708 }
709
710 /* ── render footer / CTA ───────────────────────────────────────────────── */
711 function renderFooter() {
712 var el = document.getElementById('ob-wiz-footer');
713 if (!el) return;
714 var d = SITUATIONS[state.situation];
715 el.innerHTML =
716 '<a href="' + esc(d.ctaHref) + '" class="ob-cta-btn">' + esc(d.ctaLabel) + '</a>' +
717 '<span class="ob-cta-note">' + esc(d.ctaNote) + '</span>';
718 }
719
720 /* ── render sensed card ────────────────────────────────────────────────── */
721 function renderSensed() {
722 var el = document.getElementById('ob-sensed-card');
723 if (!el) return;
724 var d = SITUATIONS[state.situation];
725 var html = '';
726 for (var i = 0; i < d.sensed.length; i++) {
727 html +=
728 '<div class="ob-sensed-row">' +
729 '<span class="ob-sensed-k">' + esc(d.sensed[i].k) + '</span>' +
730 '<span class="ob-sensed-v">' + esc(d.sensed[i].v) + '</span>' +
731 '</div>';
732 }
733 el.innerHTML = html;
734 }
735
736 /* ── full render ───────────────────────────────────────────────────────── */
737 function render() {
738 renderPills();
739 renderWizHead();
740 renderSteps();
741 renderFooter();
742 renderSensed();
743 }
744
745 if (document.readyState === 'loading') {
746 document.addEventListener('DOMContentLoaded', render);
747 } else {
748 render();
749 }
750})();`;
Addedsrc/views/org-memory.tsx+590−0View fileUnifiedSplit
@@ -0,0 +1,590 @@
1import type { FC } from "hono/jsx";
2
3export interface OrgMemoryProps {
4 query?: string;
5 answer?: { text: string; sources: number; prs: number };
6 receipts?: Array<{ when: string; title: string; detail: string; ref: string }>;
7 owner?: string;
8 repo?: string;
9 user?: any;
10}
11
12const DEFAULT_RECEIPTS = [
13 {
14 when: "Mar 12",
15 title: "Issue №42 — Gate execution isolation",
16 detail: "Original design discussion; env-leak prototype documented here.",
17 ref: "#42",
18 },
19 {
20 when: "Mar 19",
21 title: "feat: subprocess gate runner",
22 detail: "The founding commit — rationale in the commit body.",
23 ref: "9c41f2a",
24 },
25 {
26 when: "May 30",
27 title: "PR №171 — in-process gates for latency",
28 detail: "Merged, then reverted 36h later. Incident review attached.",
29 ref: "#171",
30 },
31 {
32 when: "Jun 24",
33 title: "PR №203 — sandbox security model",
34 detail: "Codifies the subprocess boundary as a security assumption.",
35 ref: "#203",
36 },
37];
38
39const SUGGESTIONS = [
40 "Why Bun instead of Node?",
41 "What broke in the 0083 migration?",
42 "History of the auto-repair loop",
43];
44
45export const OrgMemory: FC<OrgMemoryProps> = (props) => {
46 const {
47 query = "",
48 owner = "ccanty",
49 repo = "gluecron",
50 user,
51 receipts = DEFAULT_RECEIPTS,
52 } = props;
53
54 const userInitial = user?.username?.[0]?.toUpperCase() ?? "C";
55
56 return (
57 <>
58 <style dangerouslySetInnerHTML={{ __html: css }} />
59 <div class="om-root">
60 <header class="om-header">
61 <a href="/" class="om-logo">
62 <span class="om-logo-mark" />
63 gluecron
64 </a>
65 <span class="om-header-sep">/</span>
66 <span class="om-header-label">Memory</span>
67 <div class="om-header-right">
68 <span class="om-avatar">{userInitial}</span>
69 </div>
70 </header>
71
72 <main class="om-main">
73 <div class="om-hero">
74 <div class="om-hero-eyebrow">
75 Org memory · every commit, PR, issue, and decision since day one
76 </div>
77 <h1 class="om-title">Ask your codebase why.</h1>
78 <div class="om-search-wrap">
79 <input
80 id="om-input"
81 class="om-search-input"
82 type="text"
83 value={query}
84 placeholder="Why does the gate runner use a subprocess instead of a worker thread?"
85 />
86 <button id="om-ask-btn" type="button" class="om-ask-btn">
87 Ask
88 </button>
89 </div>
90 <div class="om-pills">
91 {SUGGESTIONS.map((s) => (
92 <button
93 type="button"
94 class="om-pill"
95 data-suggestion={s}
96 >
97 {s}
98 </button>
99 ))}
100 </div>
101 </div>
102
103 <div class="om-answer-card">
104 <div class="om-answer-head">
105 <div id="om-answer-eyebrow" class="om-section-label">
106 Answer · grounded in 2,841 commits, 218 PRs, 194 issues
107 </div>
108 <p class="om-answer-lead">
109 The gate runner uses a subprocess because gates execute untrusted,
110 customer-defined commands — process isolation was chosen deliberately over speed.
111 </p>
112 <p class="om-answer-body">
113 The decision was made in issue №42 (March), after a prototype
114 worker-thread runner leaked environment variables between concurrent gate runs.
115 A later attempt to move back in-process for latency (PR №171) was reverted
116 within 36 hours after memory isolation failed under load. The subprocess boundary
117 is now relied on by the sandbox security model, so changing it means
118 re-reviewing that model too.
119 </p>
120 </div>
121 <div class="om-receipts-wrap">
122 <div class="om-section-label">Receipts — the actual history</div>
123 <div class="om-receipts-list">
124 {receipts.map((r, i) => (
125 <a href="#" class="om-receipt-row">
126 <span class="om-receipt-when">{r.when}</span>
127 <span class="om-receipt-body">
128 <span class="om-receipt-title">{r.title}</span>
129 <span class="om-receipt-detail">{r.detail}</span>
130 </span>
131 <span class="om-receipt-ref">{r.ref}</span>
132 </a>
133 ))}
134 </div>
135 </div>
136 </div>
137
138 <div class="om-bottom-grid">
139 <div class="om-bottom-card">
140 <div class="om-section-label">Last time someone touched this</div>
141 <p class="om-bottom-text">
142 PR №171 tried to move gates in-process for speed. It shipped, broke memory
143 isolation under load, and was reverted in 36 hours. The revert commit links the
144 incident review — worth reading before trying again.
145 </p>
146 </div>
147 <div class="om-bottom-card">
148 <div class="om-section-label">Who to ask</div>
149 <p class="om-bottom-text">
150 <strong class="om-strong">{owner}nz</strong> wrote 84% of the gate runner and
151 reviewed every change to it. The original design discussion lives in
152 issue №42.
153 </p>
154 </div>
155 </div>
156 </main>
157 </div>
158 <script dangerouslySetInnerHTML={{ __html: js }} />
159 </>
160 );
161};
162
163export default OrgMemory;
164
165const js = `
166(function () {
167 var input = document.getElementById('om-input');
168 var btn = document.getElementById('om-ask-btn');
169 var eyebrow = document.getElementById('om-answer-eyebrow');
170 var asked = false;
171
172 var BASE_EYEBROW = 'Answer · grounded in 2,841 commits, 218 PRs, 194 issues';
173 var ASKED_EYEBROW = 'Answer · re-grounded just now · 2,841 commits, 218 PRs, 194 issues';
174
175 function resetState() {
176 asked = false;
177 if (btn) btn.textContent = 'Ask';
178 if (eyebrow) eyebrow.textContent = BASE_EYEBROW;
179 }
180
181 if (btn) {
182 btn.addEventListener('click', function () {
183 if (input && input.value.trim() === '') return;
184 asked = true;
185 btn.textContent = 'Answered ✓';
186 if (eyebrow) eyebrow.textContent = ASKED_EYEBROW;
187 });
188 }
189
190 if (input) {
191 input.addEventListener('input', function () {
192 if (asked) resetState();
193 });
194 input.addEventListener('keydown', function (e) {
195 if (e.key === 'Enter') {
196 if (btn) btn.click();
197 }
198 });
199 }
200
201 var pills = document.querySelectorAll('.om-pill');
202 pills.forEach(function (pill) {
203 pill.addEventListener('click', function () {
204 var suggestion = pill.getAttribute('data-suggestion');
205 if (input && suggestion) {
206 input.value = suggestion;
207 resetState();
208 input.focus();
209 }
210 });
211 });
212})();
213`;
214
215const css = `
216@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
217
218*, *::before, *::after { box-sizing: border-box; }
219
220.om-root {
221 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
222 font-size: 14px;
223 line-height: 1.55;
224 letter-spacing: -0.008em;
225 color: #16181d;
226 background: #fcfcfd;
227 min-height: 100vh;
228 display: flex;
229 flex-direction: column;
230 -webkit-font-smoothing: antialiased;
231 text-rendering: optimizeLegibility;
232}
233
234/* ── Header ── */
235
236.om-header {
237 height: 56px;
238 border-bottom: 1px solid rgba(22,24,29,0.07);
239 background: #fcfcfd;
240 display: flex;
241 align-items: center;
242 padding: 0 28px;
243 gap: 20px;
244 position: sticky;
245 top: 0;
246 z-index: 50;
247}
248
249.om-logo {
250 display: inline-flex;
251 align-items: center;
252 gap: 9px;
253 font-family: 'Inter Tight', sans-serif;
254 font-weight: 600;
255 font-size: 15px;
256 letter-spacing: -0.02em;
257 color: #16181d;
258 text-decoration: none;
259}
260
261.om-logo-mark {
262 width: 16px;
263 height: 16px;
264 border-radius: 5px;
265 background: #4353c9;
266 display: inline-block;
267 flex-shrink: 0;
268}
269
270.om-header-sep {
271 color: #c4c6cf;
272 font-size: 14px;
273}
274
275.om-header-label {
276 font-size: 13px;
277 color: #16181d;
278 font-weight: 500;
279}
280
281.om-header-right {
282 margin-left: auto;
283 display: flex;
284 align-items: center;
285 gap: 14px;
286}
287
288.om-avatar {
289 width: 28px;
290 height: 28px;
291 border-radius: 50%;
292 background: #16181d;
293 color: #fff;
294 font-size: 11px;
295 font-weight: 600;
296 display: inline-flex;
297 align-items: center;
298 justify-content: center;
299 flex-shrink: 0;
300}
301
302/* ── Main layout ── */
303
304.om-main {
305 flex: 1;
306 max-width: 880px;
307 width: 100%;
308 margin: 0 auto;
309 padding: 56px 32px 88px;
310}
311
312/* ── Hero / search ── */
313
314.om-hero {
315 text-align: center;
316 margin-bottom: 36px;
317}
318
319.om-hero-eyebrow {
320 font-family: 'JetBrains Mono', monospace;
321 font-size: 11px;
322 letter-spacing: 0.12em;
323 text-transform: uppercase;
324 color: #8a8d99;
325 margin-bottom: 12px;
326}
327
328.om-title {
329 font-family: 'Inter Tight', sans-serif;
330 font-size: 30px;
331 font-weight: 600;
332 letter-spacing: -0.026em;
333 line-height: 1.12;
334 color: #111318;
335 margin: 0 0 24px;
336}
337
338.om-search-wrap {
339 display: flex;
340 gap: 12px;
341 align-items: center;
342 border: 1px solid rgba(22,24,29,0.12);
343 border-radius: 12px;
344 background: #ffffff;
345 padding: 8px 8px 8px 20px;
346 max-width: 640px;
347 margin: 0 auto;
348 box-shadow: 0 1px 2px rgba(22,24,29,0.03);
349 transition: border-color 0.15s;
350}
351
352.om-search-wrap:focus-within {
353 border-color: rgba(22,24,29,0.22);
354}
355
356.om-search-input {
357 flex: 1;
358 border: 0;
359 outline: none;
360 font-family: inherit;
361 font-size: 14.5px;
362 letter-spacing: -0.008em;
363 color: #16181d;
364 background: transparent;
365 min-width: 0;
366}
367
368.om-search-input::placeholder {
369 color: #8a8d99;
370}
371
372.om-ask-btn {
373 padding: 9px 18px;
374 border-radius: 9px;
375 font-size: 13px;
376 font-weight: 600;
377 border: 0;
378 background: #16181d;
379 color: #fff;
380 cursor: pointer;
381 font-family: inherit;
382 white-space: nowrap;
383 flex-shrink: 0;
384 transition: opacity 0.15s;
385}
386
387.om-ask-btn:hover {
388 opacity: 0.85;
389}
390
391.om-pills {
392 display: flex;
393 gap: 8px;
394 justify-content: center;
395 flex-wrap: wrap;
396 margin-top: 14px;
397}
398
399.om-pill {
400 padding: 5px 14px;
401 border-radius: 9999px;
402 font-size: 12px;
403 border: 1px solid rgba(22,24,29,0.10);
404 background: #ffffff;
405 color: #6b7080;
406 cursor: pointer;
407 font-family: inherit;
408 white-space: nowrap;
409 transition: border-color 0.15s, color 0.15s;
410}
411
412.om-pill:hover {
413 border-color: rgba(22,24,29,0.24);
414 color: #16181d;
415}
416
417/* ── Shared section label ── */
418
419.om-section-label {
420 font-family: 'JetBrains Mono', monospace;
421 font-size: 11px;
422 letter-spacing: 0.10em;
423 text-transform: uppercase;
424 color: #8a8d99;
425 font-weight: 500;
426 margin-bottom: 10px;
427}
428
429/* ── Answer card ── */
430
431.om-answer-card {
432 border: 1px solid rgba(22,24,29,0.07);
433 border-radius: 14px;
434 background: #ffffff;
435 overflow: hidden;
436 margin-bottom: 32px;
437}
438
439.om-answer-head {
440 padding: 22px 26px;
441 border-bottom: 1px solid rgba(22,24,29,0.06);
442}
443
444.om-answer-lead {
445 font-size: 14.5px;
446 color: #16181d;
447 margin: 0 0 12px;
448 line-height: 1.7;
449}
450
451.om-answer-body {
452 font-size: 14px;
453 color: #6b7080;
454 margin: 0;
455 line-height: 1.7;
456}
457
458/* ── Receipts ── */
459
460.om-receipts-wrap {
461 padding: 18px 26px;
462 display: flex;
463 flex-direction: column;
464 gap: 0;
465}
466
467.om-receipts-list {
468 display: flex;
469 flex-direction: column;
470 margin-top: 4px;
471}
472
473.om-receipt-row {
474 display: grid;
475 grid-template-columns: 80px minmax(0, 1fr) auto;
476 gap: 14px;
477 align-items: baseline;
478 text-decoration: none;
479 padding: 8px 0;
480 border-bottom: 1px solid rgba(22,24,29,0.05);
481 transition: background 0.1s;
482 border-radius: 4px;
483}
484
485.om-receipt-row:last-child {
486 border-bottom: none;
487}
488
489.om-receipt-row:hover {
490 background: rgba(22,24,29,0.015);
491 padding-left: 4px;
492 padding-right: 4px;
493 margin-left: -4px;
494 margin-right: -4px;
495}
496
497.om-receipt-when {
498 font-family: 'JetBrains Mono', monospace;
499 font-size: 11.5px;
500 color: #8a8d99;
501 flex-shrink: 0;
502}
503
504.om-receipt-body {
505 min-width: 0;
506}
507
508.om-receipt-title {
509 display: block;
510 font-size: 13.5px;
511 font-weight: 500;
512 color: #16181d;
513}
514
515.om-receipt-detail {
516 display: block;
517 font-size: 12.5px;
518 color: #6b7080;
519 margin-top: 1px;
520}
521
522.om-receipt-ref {
523 font-family: 'JetBrains Mono', monospace;
524 font-size: 11.5px;
525 color: #4353c9;
526 white-space: nowrap;
527 flex-shrink: 0;
528}
529
530/* ── Bottom two-col grid ── */
531
532.om-bottom-grid {
533 display: grid;
534 grid-template-columns: 1fr 1fr;
535 gap: 20px;
536}
537
538.om-bottom-card {
539 border: 1px solid rgba(22,24,29,0.07);
540 border-radius: 12px;
541 background: #ffffff;
542 padding: 20px 22px;
543}
544
545.om-bottom-text {
546 font-size: 13px;
547 color: #6b7080;
548 margin: 0;
549 line-height: 1.65;
550}
551
552.om-strong {
553 color: #16181d;
554 font-weight: 500;
555}
556
557/* ── Responsive ── */
558
559@media (max-width: 640px) {
560 .om-main {
561 padding: 32px 16px 64px;
562 }
563
564 .om-title {
565 font-size: 24px;
566 }
567
568 .om-search-wrap {
569 padding: 6px 6px 6px 14px;
570 }
571
572 .om-search-input {
573 font-size: 14px;
574 }
575
576 .om-bottom-grid {
577 grid-template-columns: 1fr;
578 gap: 14px;
579 }
580
581 .om-receipt-row {
582 grid-template-columns: 56px minmax(0, 1fr) auto;
583 gap: 8px;
584 }
585
586 .om-header {
587 padding: 0 16px;
588 }
589}
590`;
Addedsrc/views/plan-approval.tsx+773−0View fileUnifiedSplit
@@ -0,0 +1,773 @@
1import type { FC } from "hono/jsx";
2
3export interface PlanStep {
4 n: string;
5 title: string;
6 desc: string;
7}
8
9export interface Plan {
10 id: number | string;
11 spec: string;
12 steps: PlanStep[];
13}
14
15export interface PlanApprovalProps {
16 plan?: Plan;
17 blastRadius?: any;
18 costEstimate?: any;
19 user?: any;
20}
21
22const DEFAULT_STEPS = [
23 {
24 n: "01",
25 title: "Add plan-lapse state to the schema",
26 detail:
27 "New enum value 'lapsed' + lapsedAt timestamp. Additive migration, no backfill needed.",
28 files: "drizzle/0108_plan_lapse.sql · src/db/schema.ts",
29 },
30 {
31 n: "02",
32 title: "Enforce read-only on lapsed orgs",
33 detail:
34 "Git push and web writes rejected with a clear message; clone, fetch, and browsing stay open. Internal orgs short-circuit before any check.",
35 files: "src/middleware/plan-guard.ts · src/git/protocol.ts",
36 },
37 {
38 n: "03",
39 title: "Warning emails at 7 days and 1 day",
40 detail:
41 "Two templated emails via the existing notify pipeline, deduped per org, logged to the audit trail.",
42 files: "src/lib/notify.ts · src/lib/autopilot.ts",
43 },
44 {
45 n: "04",
46 title: "Admin visibility",
47 detail:
48 "Lapsed orgs surface in the customers table with days-remaining, next to the existing exempt/current states.",
49 files: "src/routes/admin.tsx",
50 },
51 {
52 n: "05",
53 title: "Tests",
54 detail:
55 "12 new tests: guard behavior, internal-org exemption, email dedupe, migration idempotence.",
56 files: "src/__tests__/plan-guard.test.ts",
57 },
58];
59
60const DEFAULT_SPEC =
61 "When a customer's plan lapses, keep their repos read-only instead of blocking clone access. Send one warning email at 7 days and one at 1 day. Internal orgs are never affected.";
62
63const STEP_COUNT = DEFAULT_STEPS.length;
64
65const inlineJs = `
66(function () {
67 var manual = {};
68 var approved = false;
69 var editing = false;
70 var STEP_COUNT = ${STEP_COUNT};
71
72 function getManualCount() {
73 var n = 0;
74 for (var k in manual) { if (manual[k]) n++; }
75 return n;
76 }
77
78 function updateUI() {
79 var manualCount = getManualCount();
80 var agentSteps = STEP_COUNT - manualCount;
81
82 for (var i = 0; i < STEP_COUNT; i++) {
83 var btn = document.getElementById('pa-step-btn-' + i);
84 if (!btn) continue;
85 if (manual[i]) {
86 btn.textContent = 'Yours ✓';
87 btn.style.border = '1px solid rgba(67,83,201,0.35)';
88 btn.style.background = 'rgba(67,83,201,0.06)';
89 btn.style.color = '#4353c9';
90 } else {
91 btn.textContent = "I’ll do this";
92 btn.style.border = '1px solid rgba(22,24,29,0.12)';
93 btn.style.background = '#ffffff';
94 btn.style.color = '#6b7080';
95 }
96 }
97
98 var filesTouched = document.getElementById('pa-files-touched');
99 if (filesTouched) filesTouched.textContent = String(9 - manualCount);
100
101 var yourTime = document.getElementById('pa-your-time');
102 if (yourTime) {
103 yourTime.textContent =
104 manualCount > 0
105 ? 'review + ' + manualCount + ' manual step' + (manualCount > 1 ? 's' : '')
106 : '≈ 10 min review';
107 }
108
109 var approveBtn = document.getElementById('pa-approve-btn');
110 if (approveBtn) {
111 if (approved) {
112 approveBtn.textContent = 'Plan approved — agent starting';
113 approveBtn.style.background = '#1e7f5c';
114 } else if (manualCount > 0) {
115 approveBtn.textContent = 'Approve plan (' + agentSteps + ' agent step' + (agentSteps !== 1 ? 's' : '') + ')';
116 approveBtn.style.background = '#16181d';
117 } else {
118 approveBtn.textContent = 'Approve plan';
119 approveBtn.style.background = '#16181d';
120 }
121 }
122 }
123
124 for (var i = 0; i < STEP_COUNT; i++) {
125 (function (idx) {
126 var btn = document.getElementById('pa-step-btn-' + idx);
127 if (btn) {
128 btn.addEventListener('click', function () {
129 manual[idx] = !manual[idx];
130 updateUI();
131 });
132 }
133 })(i);
134 }
135
136 var approveBtn = document.getElementById('pa-approve-btn');
137 if (approveBtn) {
138 approveBtn.addEventListener('click', function () {
139 if (!approved) {
140 approved = true;
141 updateUI();
142 }
143 });
144 }
145
146 var editBtn = document.getElementById('pa-edit-btn');
147 if (editBtn) {
148 editBtn.addEventListener('click', function () {
149 editing = !editing;
150 editBtn.textContent = editing
151 ? "Editing — tap ‘I’ll do this’ on any step"
152 : 'Edit the plan';
153 });
154 }
155})();
156`;
157
158export const PlanApprovalView: FC<PlanApprovalProps> = (props) => {
159 const plan = props.plan;
160 const spec = plan?.spec ?? DEFAULT_SPEC;
161 const planId = plan?.id ?? 31;
162 const steps = DEFAULT_STEPS;
163
164 return (
165 <>
166 <style dangerouslySetInnerHTML={{ __html: css }} />
167 <div class="pa-root">
168 <header class="pa-header">
169 <a href="/" class="pa-logo">
170 <span class="pa-logo-mark"></span>
171 gluecron
172 </a>
173 <nav class="pa-breadcrumb">
174 <span class="pa-breadcrumb-item">ccantynz-alt</span>
175 <span class="pa-breadcrumb-sep">/</span>
176 <span class="pa-breadcrumb-item">gluecron</span>
177 <span class="pa-breadcrumb-sep">/</span>
178 <span class="pa-breadcrumb-item pa-breadcrumb-active">Specs</span>
179 </nav>
180 <div class="pa-header-right">
181 <span class="pa-avatar">C</span>
182 </div>
183 </header>
184
185 <main class="pa-main">
186 <div class="pa-page-header">
187 <div class="pa-eyebrow">Spec №{planId} · Plan review</div>
188 <h1 class="pa-title">Approve the plan, not just the PR.</h1>
189 <p class="pa-subtitle">
190 Before a single line is written, here is exactly what the agent intends to do — the
191 blast radius, the cost, and how to undo it. Approve it, edit it, or take any piece of
192 it manually.
193 </p>
194 </div>
195
196 <div class="pa-grid">
197 {/* Left: the plan */}
198 <section class="pa-left">
199 {/* Spec quote card */}
200 <div class="pa-spec-card">
201 <div class="pa-spec-label">Your spec — as written</div>
202 <p class="pa-spec-quote">"{spec}"</p>
203 </div>
204
205 {/* Plan steps */}
206 <h2 class="pa-steps-heading">The plan · {steps.length} steps</h2>
207 <div class="pa-steps-list">
208 {steps.map((step, i) => (
209 <div class="pa-step-row" id={"pa-step-row-" + i}>
210 <span class="pa-step-num">{step.n}</span>
211 <div class="pa-step-body">
212 <div class="pa-step-title">{step.title}</div>
213 <div class="pa-step-detail">{step.detail}</div>
214 <div class="pa-step-files">{step.files}</div>
215 </div>
216 <button
217 type="button"
218 id={"pa-step-btn-" + i}
219 class="pa-step-btn"
220 >
221 I'll do this
222 </button>
223 </div>
224 ))}
225 </div>
226 <p class="pa-steps-note">
227 Steps marked{" "}
228 <strong class="pa-steps-note-em">I'll do this</strong> are left out of the agent's
229 branch — the PR arrives with those files untouched and a checklist for you.
230 </p>
231 </section>
232
233 {/* Right: aside */}
234 <aside class="pa-aside">
235 {/* Blast radius */}
236 <div class="pa-section">
237 <h3 class="pa-section-label">Blast radius</h3>
238 <div class="pa-card">
239 <div class="pa-card-rows">
240 <div class="pa-card-row">
241 <span class="pa-card-key">Files touched</span>
242 <span class="pa-card-val pa-mono" id="pa-files-touched">
243 9
244 </span>
245 </div>
246 <div class="pa-card-row">
247 <span class="pa-card-key">Repos affected</span>
248 <span class="pa-card-val pa-mono">1 · gluecron</span>
249 </div>
250 <div class="pa-card-row">
251 <span class="pa-card-key">DB migration</span>
252 <span class="pa-card-val pa-mono pa-amber">1 · additive only</span>
253 </div>
254 <div class="pa-card-row">
255 <span class="pa-card-key">Public API change</span>
256 <span class="pa-card-val pa-mono pa-green">none</span>
257 </div>
258 <div class="pa-card-row">
259 <span class="pa-card-key">Tests to write</span>
260 <span class="pa-card-val pa-mono">12</span>
261 </div>
262 </div>
263 <div class="pa-divider"></div>
264 <p class="pa-card-note">
265 <strong class="pa-card-note-em">Downstream:</strong> billing cron, email
266 digests, and the admin customers table read the plan-state column this plan
267 modifies.
268 </p>
269 </div>
270 </div>
271
272 {/* Cost before code */}
273 <div class="pa-section">
274 <h3 class="pa-section-label">Cost before code</h3>
275 <div class="pa-card">
276 <div class="pa-card-rows">
277 <div class="pa-card-row">
278 <span class="pa-card-key">Estimated AI spend</span>
279 <span class="pa-card-val pa-mono">$1.10 – $1.80</span>
280 </div>
281 <div class="pa-card-row">
282 <span class="pa-card-key">Model routing</span>
283 <span class="pa-card-val pa-mono">local first</span>
284 </div>
285 <div class="pa-card-row">
286 <span class="pa-card-key">Sandbox time</span>
287 <span class="pa-card-val pa-mono">≈ 25 min</span>
288 </div>
289 <div class="pa-card-row">
290 <span class="pa-card-key">Your time needed</span>
291 <span class="pa-card-val pa-mono" id="pa-your-time">
292 ≈ 10 min review
293 </span>
294 </div>
295 </div>
296 </div>
297 </div>
298
299 {/* How this reverses */}
300 <div class="pa-section">
301 <h3 class="pa-section-label">How this reverses</h3>
302 <div class="pa-card">
303 <p class="pa-revert-text">
304 Single revert commit restores all code paths. The migration is additive, so
305 rollback needs no data repair. Warning emails already sent are logged but cannot
306 be unsent — flagged as this plan's only irreversible effect.
307 </p>
308 </div>
309 </div>
310
311 {/* Actions */}
312 <div class="pa-actions">
313 <button type="button" id="pa-approve-btn" class="pa-btn-primary">
314 Approve plan
315 </button>
316 <button type="button" id="pa-edit-btn" class="pa-btn-secondary">
317 Edit the plan
318 </button>
319 <p class="pa-disclaimer">Nothing is written until you approve.</p>
320 </div>
321 </aside>
322 </div>
323 </main>
324 </div>
325 <script dangerouslySetInnerHTML={{ __html: inlineJs }} />
326 </>
327 );
328};
329
330export default PlanApprovalView;
331
332const css = `
333@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
334
335*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
336
337.pa-root {
338 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
339 font-size: 14px;
340 line-height: 1.55;
341 letter-spacing: -0.008em;
342 color: #16181d;
343 background: #fcfcfd;
344 min-height: 100vh;
345 display: flex;
346 flex-direction: column;
347 -webkit-font-smoothing: antialiased;
348 text-rendering: optimizeLegibility;
349}
350
351/* ── Header ─────────────────────────────────────────────── */
352
353.pa-header {
354 height: 56px;
355 border-bottom: 1px solid rgba(22,24,29,0.07);
356 background: #fcfcfd;
357 display: flex;
358 align-items: center;
359 padding: 0 28px;
360 gap: 20px;
361 position: sticky;
362 top: 0;
363 z-index: 50;
364}
365
366.pa-logo {
367 display: inline-flex;
368 align-items: center;
369 gap: 9px;
370 font-family: 'Inter Tight', sans-serif;
371 font-weight: 600;
372 font-size: 15px;
373 letter-spacing: -0.02em;
374 color: #16181d;
375 text-decoration: none;
376 flex-shrink: 0;
377}
378
379.pa-logo-mark {
380 width: 16px;
381 height: 16px;
382 border-radius: 5px;
383 background: #4353c9;
384 display: inline-block;
385 flex-shrink: 0;
386}
387
388.pa-breadcrumb {
389 display: flex;
390 align-items: center;
391 gap: 4px;
392 font-size: 13px;
393 color: #6b7080;
394}
395
396.pa-breadcrumb-item {
397 padding: 5px 8px;
398}
399
400.pa-breadcrumb-sep {
401 color: #c4c6cf;
402}
403
404.pa-breadcrumb-active {
405 color: #16181d;
406 font-weight: 500;
407}
408
409.pa-header-right {
410 margin-left: auto;
411 display: flex;
412 align-items: center;
413 gap: 14px;
414}
415
416.pa-avatar {
417 width: 28px;
418 height: 28px;
419 border-radius: 50%;
420 background: #16181d;
421 color: #fff;
422 font-size: 11px;
423 font-weight: 600;
424 display: inline-flex;
425 align-items: center;
426 justify-content: center;
427 flex-shrink: 0;
428}
429
430/* ── Main ───────────────────────────────────────────────── */
431
432.pa-main {
433 flex: 1;
434 max-width: 1180px;
435 width: 100%;
436 margin: 0 auto;
437 padding: 44px 32px 80px;
438}
439
440/* ── Page header ────────────────────────────────────────── */
441
442.pa-page-header {
443 max-width: 720px;
444 margin-bottom: 40px;
445}
446
447.pa-eyebrow {
448 font-family: 'JetBrains Mono', monospace;
449 font-size: 11px;
450 letter-spacing: 0.12em;
451 text-transform: uppercase;
452 color: #8a8d99;
453 margin-bottom: 10px;
454}
455
456.pa-title {
457 font-family: 'Inter Tight', sans-serif;
458 font-size: 28px;
459 font-weight: 600;
460 letter-spacing: -0.025em;
461 line-height: 1.15;
462 color: #111318;
463 margin: 0 0 10px;
464}
465
466.pa-subtitle {
467 font-size: 14px;
468 color: #6b7080;
469 line-height: 1.6;
470}
471
472/* ── Two-column grid ────────────────────────────────────── */
473
474.pa-grid {
475 display: grid;
476 grid-template-columns: minmax(0, 1.5fr) minmax(300px, 1fr);
477 gap: 44px;
478 align-items: start;
479}
480
481/* ── Left column ────────────────────────────────────────── */
482
483.pa-left {}
484
485/* Spec quote card */
486
487.pa-spec-card {
488 border: 1px solid rgba(22,24,29,0.07);
489 border-left: 3px solid #4353c9;
490 border-radius: 12px;
491 background: #ffffff;
492 padding: 20px 24px;
493 margin-bottom: 28px;
494}
495
496.pa-spec-label {
497 font-family: 'JetBrains Mono', monospace;
498 font-size: 11px;
499 letter-spacing: 0.1em;
500 text-transform: uppercase;
501 color: #8a8d99;
502 font-weight: 500;
503 margin-bottom: 10px;
504}
505
506.pa-spec-quote {
507 font-size: 14px;
508 color: #16181d;
509 line-height: 1.65;
510 font-style: italic;
511}
512
513/* Plan steps */
514
515.pa-steps-heading {
516 font-family: 'Inter Tight', sans-serif;
517 font-size: 15px;
518 font-weight: 600;
519 letter-spacing: -0.015em;
520 color: #16181d;
521 margin: 0 0 16px;
522}
523
524.pa-steps-list {
525 display: flex;
526 flex-direction: column;
527 margin-bottom: 8px;
528}
529
530.pa-step-row {
531 display: grid;
532 grid-template-columns: 24px minmax(0, 1fr) auto;
533 gap: 14px;
534 padding: 14px 4px;
535 border-bottom: 1px solid rgba(22,24,29,0.06);
536 align-items: start;
537}
538
539.pa-step-num {
540 font-family: 'JetBrains Mono', monospace;
541 font-size: 12px;
542 color: #8a8d99;
543 padding-top: 2px;
544 line-height: 1.55;
545}
546
547.pa-step-body {
548 min-width: 0;
549}
550
551.pa-step-title {
552 font-size: 14px;
553 font-weight: 500;
554 color: #16181d;
555 line-height: 1.4;
556}
557
558.pa-step-detail {
559 font-size: 12.5px;
560 color: #6b7080;
561 margin-top: 2px;
562 line-height: 1.5;
563}
564
565.pa-step-files {
566 font-family: 'JetBrains Mono', monospace;
567 font-size: 11.5px;
568 color: #8a8d99;
569 margin-top: 6px;
570 line-height: 1.5;
571}
572
573.pa-step-btn {
574 padding: 5px 12px;
575 border-radius: 7px;
576 font-size: 12px;
577 font-weight: 500;
578 border: 1px solid rgba(22,24,29,0.12);
579 background: #ffffff;
580 color: #6b7080;
581 cursor: pointer;
582 font-family: inherit;
583 white-space: nowrap;
584 transition: border-color 0.12s, background 0.12s, color 0.12s;
585 flex-shrink: 0;
586 align-self: start;
587 margin-top: 2px;
588}
589
590.pa-step-btn:hover {
591 border-color: rgba(22,24,29,0.22);
592}
593
594.pa-steps-note {
595 font-size: 12.5px;
596 color: #8a8d99;
597 margin: 10px 0 0;
598 line-height: 1.6;
599}
600
601.pa-steps-note-em {
602 color: #16181d;
603 font-weight: 500;
604}
605
606/* ── Right aside ────────────────────────────────────────── */
607
608.pa-aside {
609 display: flex;
610 flex-direction: column;
611 gap: 32px;
612 position: sticky;
613 top: 92px;
614}
615
616.pa-section {}
617
618.pa-section-label {
619 font-family: 'JetBrains Mono', monospace;
620 font-size: 11px;
621 letter-spacing: 0.12em;
622 text-transform: uppercase;
623 color: #8a8d99;
624 font-weight: 500;
625 margin: 0 0 14px;
626 display: block;
627}
628
629.pa-card {
630 border: 1px solid rgba(22,24,29,0.07);
631 border-radius: 12px;
632 background: #ffffff;
633 padding: 20px;
634}
635
636.pa-card-rows {
637 display: flex;
638 flex-direction: column;
639 gap: 12px;
640}
641
642.pa-card-row {
643 display: flex;
644 justify-content: space-between;
645 align-items: baseline;
646 font-size: 13px;
647 gap: 12px;
648}
649
650.pa-card-key {
651 color: #6b7080;
652 flex-shrink: 0;
653}
654
655.pa-card-val {
656 color: #16181d;
657 text-align: right;
658}
659
660.pa-mono {
661 font-family: 'JetBrains Mono', monospace;
662 font-variant-numeric: tabular-nums;
663}
664
665.pa-amber {
666 color: #b45309;
667}
668
669.pa-green {
670 color: #1e7f5c;
671}
672
673.pa-divider {
674 height: 1px;
675 background: rgba(22,24,29,0.06);
676 margin: 16px 0;
677}
678
679.pa-card-note {
680 font-size: 12.5px;
681 color: #6b7080;
682 line-height: 1.6;
683}
684
685.pa-card-note-em {
686 color: #16181d;
687 font-weight: 500;
688}
689
690.pa-revert-text {
691 font-size: 12.5px;
692 color: #6b7080;
693 line-height: 1.65;
694}
695
696/* ── Actions ────────────────────────────────────────────── */
697
698.pa-actions {
699 display: flex;
700 flex-direction: column;
701 gap: 10px;
702}
703
704.pa-btn-primary {
705 padding: 10px 16px;
706 border-radius: 9px;
707 font-size: 13.5px;
708 font-weight: 500;
709 border: none;
710 background: #16181d;
711 color: #fff;
712 cursor: pointer;
713 font-family: inherit;
714 white-space: nowrap;
715 transition: background 0.18s;
716 text-align: center;
717 display: block;
718 width: 100%;
719}
720
721.pa-btn-primary:hover {
722 background: #2a2d38;
723}
724
725.pa-btn-secondary {
726 padding: 10px 16px;
727 border-radius: 9px;
728 font-size: 13.5px;
729 font-weight: 500;
730 border: 1px solid rgba(22,24,29,0.12);
731 background: #ffffff;
732 color: #16181d;
733 cursor: pointer;
734 font-family: inherit;
735 white-space: nowrap;
736 transition: border-color 0.12s;
737 text-align: center;
738 display: block;
739 width: 100%;
740}
741
742.pa-btn-secondary:hover {
743 border-color: rgba(22,24,29,0.24);
744}
745
746.pa-disclaimer {
747 font-size: 12px;
748 color: #8a8d99;
749 margin: 2px 0 0;
750 text-align: center;
751 line-height: 1.5;
752}
753
754/* ── Responsive ─────────────────────────────────────────── */
755
756@media (max-width: 820px) {
757 .pa-grid {
758 grid-template-columns: 1fr;
759 }
760 .pa-aside {
761 position: static;
762 }
763}
764
765@media (max-width: 600px) {
766 .pa-main {
767 padding: 32px 20px 60px;
768 }
769 .pa-header {
770 padding: 0 20px;
771 }
772}
773`;
Addedsrc/views/pr-redesign.tsx+1511−0View fileUnifiedSplit
Large file (1,511 lines). Load full file
Addedsrc/views/production-layers.tsx+779−0View fileUnifiedSplit
@@ -0,0 +1,779 @@
1import type { FC } from "hono/jsx";
2
3export interface LayerData {
4 name: string;
5 tech: string;
6 state: "healthy" | "watched";
7 detail: string;
8 facts: string[];
9 fix?: string;
10}
11
12export interface ProductionLayersProps {
13 layers?: LayerData[];
14 user?: any;
15}
16
17const DEFAULT_LAYERS: LayerData[] = [
18 {
19 name: "Frontend foundation",
20 tech: "Hono JSX, server-rendered · zero client framework",
21 state: "healthy",
22 detail:
23 "Every page is server-rendered from the same box that serves git. No SPA bundle to break, no hydration drift — first paint is the page.",
24 facts: ["p75 paint 240ms", "0 client deps", "themes: light/dark"],
25 },
26 {
27 name: "Request routing",
28 tech: "Caddy · TLS termination · path-based routing",
29 state: "healthy",
30 detail:
31 "Caddy terminates TLS and routes by path prefix — no gateway DSL to learn. Certificates auto-renew; routing changes take effect on config reload with zero dropped connections.",
32 facts: ["2 edge nodes", "certs auto-renew 71d", "failover < 3s"],
33 },
34 {
35 name: "Auth & identity",
36 tech: "Sessions, PATs, OAuth2, passkeys, OIDC SSO",
37 state: "healthy",
38 detail:
39 "Every credential type funnels through one permission resolver. Org/team roles, branch protection, and CODEOWNERS all evaluate in the same place — one model to audit.",
40 facts: ["2FA + WebAuthn", "SSO: Okta/Azure/Google", "token scopes: 6"],
41 },
42 {
43 name: "Repo storage",
44 tech: "Postgres (Neon) + Drizzle · git objects on disk",
45 state: "healthy",
46 detail:
47 "108 migrations, all additive-first. Git object storage stays plain files on disk — recoverable with nothing but git itself.",
48 facts: ["4ms median query", "0 lock waits 24h", "108 migrations"],
49 },
50 {
51 name: "Git protocol",
52 tech: "Smart HTTP · pack negotiation · push-time hooks",
53 state: "healthy",
54 detail:
55 "Git Smart HTTP served directly from Bun. Pack negotiation handles shallow clones and partial fetches; post-receive hooks trigger gates and webhooks inline on every push.",
56 facts: ["clone p50 1.2s", "push gate: sync", "protocol v2"],
57 },
58 {
59 name: "AI orchestration",
60 tech: "Local LLM router · private cloud burst · MCP server",
61 state: "healthy",
62 detail:
63 "PR sandboxes and repair loops run on our own compute pool. The model router prefers local inference and bursts to private cloud only under load — spend is metered per repo.",
64 facts: ["62% local inference", "24s sandbox boot", "pool: 8 workers"],
65 },
66 {
67 name: "Gate engine",
68 tech: "Push-time gates · 15-pattern secret scan · AI review",
69 state: "watched",
70 detail:
71 "Gates run at the moment of push, not minutes later. 15-pattern secret scan plus AI security review on every push. One policy gap on the new flywheel_telemetry table has a drafted fix awaiting apply.",
72 facts: ["gate p99 38s", "0 secrets leaked", "advisory scan: nightly"],
73 fix: "Apply RLS policy to flywheel_telemetry",
74 },
75 {
76 name: "Workflow runner",
77 tech: ".gluecron/workflows · per-step timeouts · size-capped logs",
78 state: "healthy",
79 detail:
80 "The workflow runner executes .gluecron/workflows with per-step timeouts and size-capped logs. No YAML knowledge required — steps are plain shell commands annotated with timeouts.",
81 facts: ["1,412 merges/mo", "0 YAML required", "log cap: 1 MB/step"],
82 },
83 {
84 name: "Package registry",
85 tech: "npm-compatible · LRU cache · packuments · tarballs",
86 state: "healthy",
87 detail:
88 "npm-compatible registry served from the same Bun process. Packuments and tarballs are LRU-cached in-process; publish gated by the same permission model as git push.",
89 facts: ["hit rate 94%", "publish: gated", "compat: npm/pnpm/yarn"],
90 },
91 {
92 name: "Notification bus",
93 tech: "Webhooks · HMAC-SHA256 · event fan-out · retry queue",
94 state: "healthy",
95 detail:
96 "Outbound webhooks sign every payload with HMAC-SHA256. A retry queue handles transient failures; the delivery log is queryable from the repo settings page.",
97 facts: ["retry: 3× exp backoff", "HMAC-SHA256", "events: 8 types"],
98 },
99 {
100 name: "Search & index",
101 tech: "Full-text · semantic index · code search · regex",
102 state: "healthy",
103 detail:
104 "Full-text search over commit messages, issue bodies, and file contents. Semantic index rebuilt on push; code search supports regex and filename filters with sub-second latency.",
105 facts: ["index lag < 5s", "regex: yes", "semantic: per-repo"],
106 },
107 {
108 name: "Telemetry",
109 tech: "Request-ID tracing · /metrics · audit log",
110 state: "healthy",
111 detail:
112 "Every request carries an ID from edge to database. Errors correlate to audit entries and gate runs automatically — the support console reads the same trail.",
113 facts: ["trace coverage 100%", "retention 90d", "/healthz /readyz"],
114 },
115 {
116 name: "Availability & recovery",
117 tech: "Air-gapped region sync · hourly bare-metal replicas",
118 state: "watched",
119 detail:
120 "Repo state, semantic indices, and the package registry replicate hourly to an offline array (0083 data-region rails). EU shard backfill completed; replica lag briefly above target during it — recovering.",
121 facts: ["RPO 1h · RTO 15m", "replica lag 4m (target 2m)", "last drill: Jun 20 ✓"],
122 fix: "Flush EU shard backlog · estimated 18 min",
123 },
124];
125
126export const ProductionLayers: FC<ProductionLayersProps> = (props) => {
127 const layers = props.layers ?? DEFAULT_LAYERS;
128 const watchedCount = layers.filter((l) => l.state === "watched").length;
129 const healthyCount = layers.filter((l) => l.state === "healthy").length;
130
131 return (
132 <>
133 <style dangerouslySetInnerHTML={{ __html: css }} />
134 <div class="pl-root">
135 <header class="pl-header">
136 <a href="/" class="pl-logo">
137 <span class="pl-logo-mark"></span>
138 gluecron
139 <span class="pl-logo-eyebrow">admin</span>
140 </a>
141 <span class="pl-sep">/</span>
142 <span class="pl-breadcrumb">Production layers</span>
143 <div class="pl-header-right">
144 <span class="pl-summary">
145 <span class="pl-summary-dot"></span>
146 {layers.length} layers · {healthyCount} healthy · {watchedCount} watched
147 </span>
148 <span class="pl-avatar">C</span>
149 </div>
150 </header>
151
152 <main class="pl-main">
153 <div class="pl-intro">
154 <div class="pl-eyebrow">Infrastructure sovereignty · every layer self-owned</div>
155 <h1 class="pl-heading">The whole stack, one board.</h1>
156 <p class="pl-desc">
157 Thirteen production layers, no magic edge proxies, no external runtime dependencies.
158 Each layer is watched by the same repair engine that heals customer pushes — click
159 a layer for its live detail.
160 </p>
161 </div>
162
163 <div class="pl-rows" id="pl-rows">
164 {layers.map((layer, i) => {
165 const num = String(i + 1).padStart(2, "0");
166 const watched = layer.state === "watched";
167 return (
168 <div
169 class={`pl-item${watched ? " pl-item--watched" : ""}`}
170 data-index={String(i)}
171 >
172 <button
173 type="button"
174 class={`pl-row${watched ? " pl-row--watched" : ""}`}
175 data-index={String(i)}
176 aria-expanded="false"
177 aria-controls={`pl-detail-${i}`}
178 >
179 <span class="pl-row-num">{num}</span>
180 <span class="pl-row-name">{layer.name}</span>
181 <span class="pl-row-tech">{layer.tech}</span>
182 <span class={`pl-row-status${watched ? " pl-row-status--watched" : ""}`}>
183 <span class={`pl-dot${watched ? " pl-dot--watched" : ""}`}></span>
184 {watched ? "watched" : "healthy"}
185 </span>
186 <span class="pl-caret" aria-hidden="true">▼</span>
187 </button>
188
189 {watched && (
190 <div class="pl-fix-callout">
191 <span class="pl-fix-label">drafted fix</span>
192 <span class="pl-fix-sep">—</span>
193 <span class="pl-fix-text">{layer.fix}</span>
194 </div>
195 )}
196
197 <div
198 class="pl-detail"
199 id={`pl-detail-${i}`}
200 role="region"
201 hidden={true}
202 >
203 <p class="pl-detail-desc">{layer.detail}</p>
204 <div class="pl-facts">
205 {layer.facts.map((f, fi) => (
206 <span class="pl-fact">{f}</span>
207 ))}
208 </div>
209 {watched && (
210 <div class="pl-apply-wrap">
211 <button
212 type="button"
213 class="pl-apply-btn"
214 data-fix={layer.fix ?? ""}
215 data-layer={layer.name}
216 >
217 Apply fix
218 </button>
219 </div>
220 )}
221 </div>
222 </div>
223 );
224 })}
225 </div>
226
227 <div class="pl-footer">
228 <p class="pl-footer-text">
229 Watched layers have a drafted fix or an active mitigation — nothing here requires
230 a terminal. Air-gapped replicas sync all thirteen layers to bare-metal arrays on the hour.
231 </p>
232 <a href="/admin" class="pl-footer-link">Open diagnostics →</a>
233 </div>
234 </main>
235 </div>
236 <script dangerouslySetInnerHTML={{ __html: js }} />
237 </>
238 );
239};
240
241export default ProductionLayers;
242
243const css = `
244@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
245
246@keyframes gcPulse {
247 0%, 100% { opacity: 1; }
248 50% { opacity: 0.35; }
249}
250
251/* ── Root ── */
252.pl-root {
253 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
254 font-size: 14px;
255 line-height: 1.55;
256 letter-spacing: -0.008em;
257 color: #16181d;
258 background: #fcfcfd;
259 min-height: 100vh;
260 display: flex;
261 flex-direction: column;
262 -webkit-font-smoothing: antialiased;
263 text-rendering: optimizeLegibility;
264}
265
266/* ── Header ── */
267.pl-header {
268 height: 56px;
269 border-bottom: 1px solid rgba(22,24,29,0.07);
270 background: #fcfcfd;
271 display: flex;
272 align-items: center;
273 padding: 0 28px;
274 gap: 20px;
275 position: sticky;
276 top: 0;
277 z-index: 50;
278 box-sizing: border-box;
279}
280
281.pl-logo {
282 display: inline-flex;
283 align-items: center;
284 gap: 9px;
285 font-family: 'Inter Tight', sans-serif;
286 font-weight: 600;
287 font-size: 15px;
288 letter-spacing: -0.02em;
289 color: #16181d;
290 text-decoration: none;
291 flex-shrink: 0;
292}
293
294.pl-logo-mark {
295 width: 16px;
296 height: 16px;
297 border-radius: 5px;
298 background: #4353c9;
299 display: inline-block;
300 flex-shrink: 0;
301}
302
303.pl-logo-eyebrow {
304 font-family: 'JetBrains Mono', monospace;
305 font-size: 10px;
306 letter-spacing: 0.1em;
307 text-transform: uppercase;
308 color: #8a8d99;
309 font-weight: 500;
310 margin-left: 2px;
311}
312
313.pl-sep {
314 color: #c4c6cf;
315 flex-shrink: 0;
316}
317
318.pl-breadcrumb {
319 font-size: 13px;
320 color: #16181d;
321 font-weight: 500;
322}
323
324.pl-header-right {
325 margin-left: auto;
326 display: flex;
327 align-items: center;
328 gap: 14px;
329 flex-shrink: 0;
330}
331
332.pl-summary {
333 display: inline-flex;
334 align-items: center;
335 gap: 7px;
336 font-size: 12.5px;
337 color: #6b7080;
338 white-space: nowrap;
339}
340
341.pl-summary-dot {
342 width: 6px;
343 height: 6px;
344 border-radius: 50%;
345 background: #1e7f5c;
346 flex-shrink: 0;
347}
348
349.pl-avatar {
350 width: 28px;
351 height: 28px;
352 border-radius: 50%;
353 background: #16181d;
354 color: #fff;
355 font-size: 11px;
356 font-weight: 600;
357 display: inline-flex;
358 align-items: center;
359 justify-content: center;
360 flex-shrink: 0;
361}
362
363/* ── Main ── */
364.pl-main {
365 flex: 1;
366 max-width: 1100px;
367 width: 100%;
368 margin: 0 auto;
369 padding: 44px 32px 88px;
370 box-sizing: border-box;
371}
372
373/* ── Intro ── */
374.pl-intro {
375 max-width: 680px;
376 margin-bottom: 36px;
377}
378
379.pl-eyebrow {
380 font-family: 'JetBrains Mono', monospace;
381 font-size: 11px;
382 letter-spacing: 0.12em;
383 text-transform: uppercase;
384 color: #8a8d99;
385 margin-bottom: 10px;
386}
387
388.pl-heading {
389 font-family: 'Inter Tight', sans-serif;
390 font-size: 28px;
391 font-weight: 600;
392 letter-spacing: -0.025em;
393 line-height: 1.15;
394 margin: 0 0 8px;
395 color: #111318;
396}
397
398.pl-desc {
399 font-size: 14px;
400 color: #6b7080;
401 margin: 0;
402 line-height: 1.6;
403}
404
405/* ── Layer rows stack ── */
406.pl-rows {
407 display: flex;
408 flex-direction: column;
409 gap: 8px;
410}
411
412.pl-item {
413 display: flex;
414 flex-direction: column;
415}
416
417/* ── Row button ── */
418.pl-row {
419 display: grid;
420 grid-template-columns: 32px minmax(140px, 1.3fr) minmax(0, 2fr) minmax(110px, 0.9fr) 20px;
421 gap: 16px;
422 align-items: center;
423 width: 100%;
424 text-align: left;
425 padding: 14px 20px;
426 border: 1px solid rgba(22,24,29,0.07);
427 border-radius: 12px;
428 background: #ffffff;
429 cursor: pointer;
430 font-family: inherit;
431 box-sizing: border-box;
432 transition: border-color 0.15s ease;
433 outline: none;
434}
435
436.pl-row:hover {
437 border-color: rgba(22,24,29,0.22);
438}
439
440.pl-row:focus-visible {
441 box-shadow: 0 0 0 3px rgba(67,83,201,0.18);
442}
443
444.pl-row--watched {
445 border-color: rgba(180,83,9,0.28);
446 border-left: 3px solid rgba(180,83,9,0.65);
447 padding-left: 18px;
448 border-radius: 12px 12px 4px 4px;
449}
450
451.pl-row--watched:hover {
452 border-color: rgba(180,83,9,0.45);
453 border-left-color: rgba(180,83,9,0.85);
454}
455
456.pl-row-num {
457 font-family: 'JetBrains Mono', monospace;
458 font-size: 11px;
459 color: #c4c6cf;
460 font-weight: 400;
461 line-height: 1;
462}
463
464.pl-row-name {
465 font-family: 'Inter Tight', sans-serif;
466 font-size: 14px;
467 font-weight: 600;
468 color: #16181d;
469 white-space: nowrap;
470 overflow: hidden;
471 text-overflow: ellipsis;
472 letter-spacing: -0.012em;
473}
474
475.pl-row-tech {
476 font-family: 'JetBrains Mono', monospace;
477 font-size: 11.5px;
478 color: #6b7080;
479 white-space: nowrap;
480 overflow: hidden;
481 text-overflow: ellipsis;
482 font-weight: 400;
483}
484
485.pl-row-status {
486 display: inline-flex;
487 align-items: center;
488 gap: 7px;
489 font-size: 12.5px;
490 color: #1e7f5c;
491 font-weight: 500;
492 white-space: nowrap;
493}
494
495.pl-row-status--watched {
496 color: #b45309;
497}
498
499.pl-dot {
500 width: 7px;
501 height: 7px;
502 border-radius: 50%;
503 background: #1e7f5c;
504 flex-shrink: 0;
505 display: inline-block;
506}
507
508.pl-dot--watched {
509 background: #b45309;
510 animation: gcPulse 1.4s ease-in-out infinite;
511}
512
513.pl-caret {
514 color: #c4c6cf;
515 font-size: 10px;
516 text-align: center;
517 display: block;
518 transition: transform 0.2s ease;
519 line-height: 1;
520}
521
522.pl-row[aria-expanded="true"] .pl-caret {
523 transform: rotate(180deg);
524}
525
526/* ── Fix callout (always visible for watched rows) ── */
527.pl-fix-callout {
528 display: flex;
529 align-items: center;
530 gap: 8px;
531 padding: 8px 20px 8px 22px;
532 background: rgba(180,83,9,0.04);
533 border: 1px solid rgba(180,83,9,0.18);
534 border-top: none;
535 border-radius: 0 0 8px 8px;
536 margin-bottom: 2px;
537}
538
539.pl-fix-label {
540 font-family: 'JetBrains Mono', monospace;
541 font-size: 10px;
542 letter-spacing: 0.1em;
543 text-transform: uppercase;
544 color: #b45309;
545 font-weight: 500;
546 white-space: nowrap;
547 flex-shrink: 0;
548}
549
550.pl-fix-sep {
551 color: rgba(180,83,9,0.35);
552 font-size: 11px;
553 flex-shrink: 0;
554}
555
556.pl-fix-text {
557 font-size: 12.5px;
558 color: #6b7080;
559 line-height: 1.4;
560 overflow: hidden;
561 text-overflow: ellipsis;
562 white-space: nowrap;
563}
564
565/* ── Detail panel ── */
566.pl-detail {
567 margin-top: 6px;
568 padding: 18px 22px 18px 68px;
569 border: 1px solid rgba(22,24,29,0.05);
570 border-radius: 12px;
571 background: rgba(22,24,29,0.015);
572 box-sizing: border-box;
573}
574
575.pl-detail[hidden] {
576 display: none !important;
577}
578
579.pl-detail-desc {
580 font-size: 13px;
581 color: #6b7080;
582 margin: 0 0 12px;
583 line-height: 1.65;
584 max-width: 72ch;
585}
586
587.pl-facts {
588 display: flex;
589 gap: 22px;
590 flex-wrap: wrap;
591 font-family: 'JetBrains Mono', monospace;
592 font-size: 11.5px;
593 color: #8a8d99;
594 font-weight: 400;
595}
596
597.pl-fact {
598 white-space: nowrap;
599}
600
601.pl-apply-wrap {
602 margin-top: 16px;
603}
604
605.pl-apply-btn {
606 background: #16181d;
607 color: #ffffff;
608 border: none;
609 border-radius: 8px;
610 padding: 8px 16px;
611 font-size: 13px;
612 font-weight: 600;
613 font-family: inherit;
614 letter-spacing: -0.008em;
615 cursor: pointer;
616 transition: background 0.15s ease, opacity 0.15s ease;
617 outline: none;
618}
619
620.pl-apply-btn:hover:not(:disabled) {
621 background: #2a2d36;
622}
623
624.pl-apply-btn:focus-visible {
625 box-shadow: 0 0 0 3px rgba(67,83,201,0.18);
626}
627
628.pl-apply-btn:disabled {
629 cursor: default;
630 opacity: 0.75;
631}
632
633/* ── Footer ── */
634.pl-footer {
635 border-top: 1px solid rgba(22,24,29,0.07);
636 margin-top: 36px;
637 padding-top: 20px;
638 display: flex;
639 justify-content: space-between;
640 gap: 20px;
641 flex-wrap: wrap;
642 align-items: flex-start;
643}
644
645.pl-footer-text {
646 font-size: 12.5px;
647 color: #8a8d99;
648 margin: 0;
649 max-width: 62ch;
650 line-height: 1.6;
651}
652
653.pl-footer-link {
654 font-size: 13px;
655 color: #4353c9;
656 text-decoration: none;
657 white-space: nowrap;
658 flex-shrink: 0;
659}
660
661.pl-footer-link:hover {
662 text-decoration: underline;
663}
664
665/* ── Responsive ── */
666@media (max-width: 720px) {
667 .pl-header {
668 padding: 0 16px;
669 gap: 12px;
670 }
671 .pl-summary {
672 display: none;
673 }
674 .pl-main {
675 padding: 28px 16px 60px;
676 }
677 .pl-row {
678 grid-template-columns: 28px minmax(0, 1fr) auto 16px;
679 gap: 10px;
680 padding: 12px 16px;
681 }
682 .pl-row--watched {
683 padding-left: 14px;
684 }
685 .pl-row-tech {
686 display: none;
687 }
688 .pl-detail {
689 padding: 14px 16px 14px 20px;
690 }
691 .pl-fix-callout {
692 padding: 7px 16px;
693 }
694 .pl-facts {
695 gap: 14px;
696 }
697}
698`;
699
700const js = `
701(function () {
702 // Toggle expand / collapse for each layer row
703 document.querySelectorAll('.pl-row').forEach(function (btn) {
704 btn.addEventListener('click', function () {
705 var idx = btn.getAttribute('data-index');
706 var detail = document.getElementById('pl-detail-' + idx);
707 if (!detail) return;
708
709 var isOpen = btn.getAttribute('aria-expanded') === 'true';
710 if (isOpen) {
711 btn.setAttribute('aria-expanded', 'false');
712 detail.hidden = true;
713 } else {
714 btn.setAttribute('aria-expanded', 'true');
715 detail.hidden = false;
716 }
717 });
718 });
719
720 // Apply fix buttons
721 document.querySelectorAll('.pl-apply-btn').forEach(function (applyBtn) {
722 applyBtn.addEventListener('click', function (e) {
723 e.stopPropagation();
724 if (applyBtn.disabled) return;
725
726 applyBtn.disabled = true;
727 applyBtn.textContent = 'Applying…';
728
729 setTimeout(function () {
730 applyBtn.textContent = 'Fix applied ✓';
731 applyBtn.style.background = '#1e7f5c';
732 applyBtn.style.cursor = 'default';
733
734 var item = applyBtn.closest('.pl-item');
735 if (!item) return;
736
737 // Swap dot from watched (amber pulse) to healthy (green)
738 var dot = item.querySelector('.pl-dot--watched');
739 if (dot) {
740 dot.classList.remove('pl-dot--watched');
741 dot.style.background = '#1e7f5c';
742 dot.style.animation = 'none';
743 }
744
745 // Swap status label colour + text
746 var status = item.querySelector('.pl-row-status--watched');
747 if (status) {
748 status.classList.remove('pl-row-status--watched');
749 status.style.color = '#1e7f5c';
750 // Last child is the text node "watched"
751 var nodes = status.childNodes;
752 for (var n = 0; n < nodes.length; n++) {
753 if (nodes[n].nodeType === 3 /* TEXT_NODE */) {
754 nodes[n].textContent = 'healthy';
755 break;
756 }
757 }
758 }
759
760 // Remove amber border from the row button
761 var rowBtn = item.querySelector('.pl-row--watched');
762 if (rowBtn) {
763 rowBtn.classList.remove('pl-row--watched');
764 }
765
766 // Fade and remove the fix callout strip
767 var callout = item.querySelector('.pl-fix-callout');
768 if (callout) {
769 callout.style.transition = 'opacity 0.35s ease';
770 callout.style.opacity = '0';
771 setTimeout(function () {
772 callout.style.display = 'none';
773 }, 350);
774 }
775 }, 1400);
776 });
777 });
778})();
779`;
Addedsrc/views/signin-v2.tsx+544−0View fileUnifiedSplit
@@ -0,0 +1,544 @@
1import type { FC } from "hono/jsx";
2
3export interface SignInV2Props {
4 redirect?: string;
5 error?: string;
6}
7
8export const SignInV2: FC<SignInV2Props> = (props) => {
9 const { redirect = "", error = "" } = props;
10
11 return (
12 <>
13 <style dangerouslySetInnerHTML={{ __html: css }} />
14 <div class="si-root">
15 {/* Left: form panel */}
16 <div class="si-left">
17 <a href="/" class="si-logo">
18 <span class="si-logo-mark"></span>gluecron
19 </a>
20
21 <div class="si-form-wrap">
22 <h1 class="si-heading">Sign in</h1>
23 <p class="si-subheading">Use the identity you already have — no new passwords.</p>
24
25 {error && (
26 <div class="si-error-banner">{error}</div>
27 )}
28
29 {redirect && (
30 <input type="hidden" name="redirect" value={redirect} />
31 )}
32
33 <div class="si-oauth-group">
34 <a href="/auth/github" class="si-btn si-btn-github">
35 <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
36 <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z" />
37 </svg>
38 Continue with GitHub
39 </a>
40
41 <a href="/auth/google" class="si-btn si-btn-secondary">
42 <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
43 <path fill="#4285F4" d="M23.5 12.27c0-.85-.08-1.66-.22-2.45H12v4.63h6.45a5.52 5.52 0 0 1-2.39 3.62v3h3.87c2.26-2.09 3.57-5.16 3.57-8.8z" />
44 <path fill="#34A853" d="M12 24c3.24 0 5.96-1.07 7.94-2.91l-3.87-3c-1.07.72-2.45 1.15-4.07 1.15-3.13 0-5.78-2.11-6.73-4.96H1.29v3.1A12 12 0 0 0 12 24z" />
45 <path fill="#FBBC05" d="M5.27 14.28a7.2 7.2 0 0 1 0-4.56v-3.1H1.29a12 12 0 0 0 0 10.76l3.98-3.1z" />
46 <path fill="#EA4335" d="M12 4.76c1.76 0 3.34.61 4.59 1.8l3.44-3.44C17.95 1.19 15.24 0 12 0A12 12 0 0 0 1.29 6.62l3.98 3.1C6.22 6.87 8.87 4.76 12 4.76z" />
47 </svg>
48 Continue with Google
49 </a>
50
51 <a href="/passkey" class="si-btn si-btn-secondary">
52 <span class="si-passkey-icon" aria-hidden="true">⌘</span>
53 Continue with a passkey
54 </a>
55 </div>
56
57 <div class="si-divider">
58 <span class="si-divider-line"></span>
59 <span class="si-divider-label">OR</span>
60 <span class="si-divider-line"></span>
61 </div>
62
63 <div class="si-magic-row">
64 <input
65 id="si-email-input"
66 type="email"
67 placeholder="work@company.com"
68 class="si-email-input"
69 autocomplete="email"
70 />
71 <button
72 id="si-magic-btn"
73 type="button"
74 class="si-magic-btn"
75 >
76 Email me a link
77 </button>
78 </div>
79 <p class="si-magic-hint">Magic link — no password, expires in 10 minutes.</p>
80
81 <div class="si-aux-links">
82 <a href="/sso" class="si-aux-link si-aux-link-accent">Enterprise SSO (Okta, Azure AD, Google Workspace) →</a>
83 <a href="/keys" class="si-aux-link">Just cloning? SSH keys work without signing in →</a>
84 <a href="/play" class="si-aux-link">Try without an account at /play →</a>
85 </div>
86 </div>
87
88 <p class="si-footer-note">TLS terminated on our own metal · no third-party proxy in the auth loop</p>
89 </div>
90
91 {/* Right: proof panel */}
92 <div class="si-right">
93 <div class="si-grid-overlay"></div>
94 <div class="si-proof-inner">
95 <div class="si-proof-eyebrow">Why teams switch</div>
96 <h2 class="si-proof-heading">Sign in once.<br />Ship without waiting, forever.</h2>
97 <div class="si-props-list">
98 <div class="si-prop-item">
99 <span class="si-green-dot"></span>
100 <p class="si-prop-text">Sonnet 5 reviews every PR the moment it opens — with a manual path that's always one click away.</p>
101 </div>
102 <div class="si-prop-item">
103 <span class="si-green-dot"></span>
104 <p class="si-prop-text">Failures repair themselves in isolated sandboxes; everything is audited and reversible in one click.</p>
105 </div>
106 <div class="si-prop-item">
107 <span class="si-green-dot"></span>
108 <p class="si-prop-text">Continuous two-way GitHub mirror — trial for months, cut over when you're ready, leave any time.</p>
109 </div>
110 </div>
111 <div class="si-proof-footer">quality numbers published live · gluecron.com/trust</div>
112 </div>
113 </div>
114 </div>
115
116 <script dangerouslySetInnerHTML={{ __html: js }} />
117 </>
118 );
119};
120
121export default SignInV2;
122
123const js = `
124(function () {
125 var emailInput = document.getElementById('si-email-input');
126 var magicBtn = document.getElementById('si-magic-btn');
127 if (!emailInput || !magicBtn) return;
128
129 var sent = false;
130
131 function updateBtn() {
132 if (sent) {
133 magicBtn.textContent = '\\u2713 Link sent';
134 magicBtn.classList.add('si-magic-btn--sent');
135 } else {
136 magicBtn.textContent = 'Email me a link';
137 magicBtn.classList.remove('si-magic-btn--sent');
138 }
139 }
140
141 emailInput.addEventListener('input', function () {
142 if (sent) {
143 sent = false;
144 updateBtn();
145 }
146 });
147
148 magicBtn.addEventListener('click', function () {
149 var val = emailInput.value;
150 if (val && val.indexOf('@') !== -1) {
151 sent = true;
152 updateBtn();
153 } else {
154 emailInput.focus();
155 emailInput.classList.add('si-email-input--shake');
156 setTimeout(function () {
157 emailInput.classList.remove('si-email-input--shake');
158 }, 400);
159 }
160 });
161})();
162`;
163
164const css = `
165@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
166
167*, *::before, *::after { box-sizing: border-box; }
168
169.si-root {
170 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
171 font-size: 14px;
172 line-height: 1.55;
173 letter-spacing: -0.008em;
174 color: #16181d;
175 background: #fcfcfd;
176 min-height: 100vh;
177 display: grid;
178 grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
179 -webkit-font-smoothing: antialiased;
180 text-rendering: optimizeLegibility;
181}
182
183/* ── Left panel ── */
184
185.si-left {
186 display: flex;
187 flex-direction: column;
188 padding: 28px 32px;
189 min-height: 100vh;
190}
191
192.si-logo {
193 display: inline-flex;
194 align-items: center;
195 gap: 9px;
196 font-family: 'Inter Tight', sans-serif;
197 font-weight: 600;
198 font-size: 15px;
199 letter-spacing: -0.02em;
200 color: #16181d;
201 text-decoration: none;
202 align-self: flex-start;
203}
204
205.si-logo-mark {
206 width: 16px;
207 height: 16px;
208 border-radius: 5px;
209 background: #4353c9;
210 display: inline-block;
211 flex-shrink: 0;
212}
213
214.si-form-wrap {
215 flex: 1;
216 display: flex;
217 flex-direction: column;
218 justify-content: center;
219 max-width: 380px;
220 width: 100%;
221 margin: 0 auto;
222 padding: 48px 0;
223}
224
225.si-heading {
226 font-family: 'Inter Tight', sans-serif;
227 font-size: 26px;
228 font-weight: 600;
229 letter-spacing: -0.024em;
230 line-height: 1.15;
231 margin: 0 0 6px;
232 color: #111318;
233}
234
235.si-subheading {
236 font-size: 13.5px;
237 color: #6b7080;
238 margin: 0 0 28px;
239}
240
241.si-error-banner {
242 background: rgba(180, 35, 24, 0.08);
243 border: 1px solid rgba(180, 35, 24, 0.18);
244 border-radius: 10px;
245 color: #b42318;
246 font-size: 13.5px;
247 padding: 10px 14px;
248 margin-bottom: 18px;
249}
250
251/* ── OAuth buttons ── */
252
253.si-oauth-group {
254 display: flex;
255 flex-direction: column;
256 gap: 10px;
257}
258
259.si-btn {
260 display: flex;
261 align-items: center;
262 justify-content: center;
263 gap: 10px;
264 padding: 11px 16px;
265 border-radius: 10px;
266 font-size: 14px;
267 font-weight: 600;
268 font-family: 'Inter', sans-serif;
269 text-decoration: none;
270 cursor: pointer;
271 transition: box-shadow 0.15s ease, border-color 0.15s ease;
272 letter-spacing: -0.008em;
273}
274
275.si-btn-github {
276 background: #16181d;
277 color: #fff;
278 border: 1px solid #16181d;
279}
280
281.si-btn-github:hover {
282 box-shadow: 0 8px 22px rgba(22, 24, 29, 0.18);
283}
284
285.si-btn-secondary {
286 background: #ffffff;
287 color: #16181d;
288 border: 1px solid rgba(22, 24, 29, 0.14);
289}
290
291.si-btn-secondary:hover {
292 border-color: rgba(22, 24, 29, 0.30);
293}
294
295.si-passkey-icon {
296 font-size: 15px;
297 line-height: 1;
298}
299
300/* ── OR divider ── */
301
302.si-divider {
303 display: flex;
304 align-items: center;
305 gap: 14px;
306 margin: 22px 0;
307}
308
309.si-divider-line {
310 flex: 1;
311 height: 1px;
312 background: rgba(22, 24, 29, 0.08);
313}
314
315.si-divider-label {
316 font-size: 11.5px;
317 color: #8a8d99;
318 font-family: 'JetBrains Mono', monospace;
319 letter-spacing: 0.08em;
320}
321
322/* ── Magic link row ── */
323
324.si-magic-row {
325 display: flex;
326 gap: 8px;
327}
328
329.si-email-input {
330 flex: 1;
331 border: 1px solid rgba(22, 24, 29, 0.12);
332 border-radius: 10px;
333 padding: 10px 14px;
334 outline: none;
335 font-family: 'Inter', sans-serif;
336 font-size: 14px;
337 letter-spacing: -0.008em;
338 color: #16181d;
339 background: #ffffff;
340 min-width: 0;
341 transition: border-color 0.15s ease, box-shadow 0.15s ease;
342}
343
344.si-email-input:focus {
345 border-color: rgba(22, 24, 29, 0.28);
346 box-shadow: 0 0 0 3px rgba(67, 83, 201, 0.10);
347}
348
349.si-email-input::placeholder {
350 color: #c4c6cf;
351}
352
353@keyframes si-shake {
354 0%, 100% { transform: translateX(0); }
355 20% { transform: translateX(-4px); }
356 40% { transform: translateX(4px); }
357 60% { transform: translateX(-3px); }
358 80% { transform: translateX(3px); }
359}
360
361.si-email-input--shake {
362 animation: si-shake 0.4s ease;
363 border-color: rgba(180, 35, 24, 0.40);
364}
365
366.si-magic-btn {
367 padding: 10px 16px;
368 border-radius: 10px;
369 font-size: 13.5px;
370 font-weight: 600;
371 font-family: 'Inter', sans-serif;
372 letter-spacing: -0.008em;
373 border: 1px solid rgba(22, 24, 29, 0.14);
374 background: #ffffff;
375 color: #16181d;
376 cursor: pointer;
377 white-space: nowrap;
378 transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
379}
380
381.si-magic-btn:hover {
382 border-color: rgba(22, 24, 29, 0.28);
383}
384
385.si-magic-btn--sent {
386 background: #1e7f5c;
387 color: #ffffff;
388 border-color: #1e7f5c;
389}
390
391.si-magic-btn--sent:hover {
392 border-color: #1e7f5c;
393}
394
395.si-magic-hint {
396 font-size: 12px;
397 color: #8a8d99;
398 margin: 10px 0 0;
399}
400
401/* ── Auxiliary links ── */
402
403.si-aux-links {
404 border-top: 1px solid rgba(22, 24, 29, 0.07);
405 margin-top: 28px;
406 padding-top: 20px;
407 display: flex;
408 flex-direction: column;
409 gap: 8px;
410}
411
412.si-aux-link {
413 font-size: 13px;
414 color: #6b7080;
415 text-decoration: none;
416 transition: color 0.12s ease;
417}
418
419.si-aux-link:hover {
420 color: #16181d;
421}
422
423.si-aux-link-accent {
424 color: #4353c9;
425}
426
427.si-aux-link-accent:hover {
428 color: #3544b0;
429 text-decoration: underline;
430}
431
432/* ── Footer note ── */
433
434.si-footer-note {
435 font-size: 11.5px;
436 color: #8a8d99;
437 margin: 0;
438 font-family: 'JetBrains Mono', monospace;
439 letter-spacing: 0.01em;
440}
441
442/* ── Right panel ── */
443
444.si-right {
445 background: radial-gradient(130% 130% at 50% -10%, #1b2030 0%, #12151f 45%, #0b0d13 100%);
446 color: #fff;
447 display: flex;
448 flex-direction: column;
449 justify-content: center;
450 padding: 64px 56px;
451 position: relative;
452 overflow: hidden;
453}
454
455.si-grid-overlay {
456 position: absolute;
457 inset: 0;
458 opacity: 0.35;
459 background-image:
460 linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
461 linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
462 background-size: 56px 56px;
463 -webkit-mask-image: radial-gradient(90% 90% at 50% 0%, #000, transparent 80%);
464 mask-image: radial-gradient(90% 90% at 50% 0%, #000, transparent 80%);
465 pointer-events: none;
466}
467
468.si-proof-inner {
469 position: relative;
470 max-width: 440px;
471}
472
473.si-proof-eyebrow {
474 font-family: 'JetBrains Mono', monospace;
475 font-size: 11px;
476 letter-spacing: 0.12em;
477 text-transform: uppercase;
478 color: #a9b4ee;
479 margin-bottom: 16px;
480}
481
482.si-proof-heading {
483 font-family: 'Inter Tight', sans-serif;
484 font-size: 30px;
485 font-weight: 600;
486 letter-spacing: -0.024em;
487 line-height: 1.15;
488 margin: 0 0 28px;
489 color: #fff;
490}
491
492.si-props-list {
493 display: flex;
494 flex-direction: column;
495 gap: 18px;
496}
497
498.si-prop-item {
499 display: flex;
500 gap: 12px;
501 align-items: flex-start;
502}
503
504.si-green-dot {
505 width: 7px;
506 height: 7px;
507 border-radius: 50%;
508 background: #1e7f5c;
509 flex-shrink: 0;
510 margin-top: 7px;
511}
512
513.si-prop-text {
514 font-size: 14px;
515 color: rgba(255, 255, 255, 0.78);
516 margin: 0;
517 line-height: 1.6;
518}
519
520.si-proof-footer {
521 border-top: 1px solid rgba(255, 255, 255, 0.10);
522 margin-top: 32px;
523 padding-top: 20px;
524 font-family: 'JetBrains Mono', monospace;
525 font-size: 11.5px;
526 color: rgba(255, 255, 255, 0.45);
527}
528
529/* ── Responsive ── */
530
531@media (max-width: 768px) {
532 .si-root {
533 grid-template-columns: 1fr;
534 }
535
536 .si-right {
537 display: none;
538 }
539
540 .si-left {
541 min-height: 100vh;
542 }
543}
544`;
Addedsrc/views/trust-report.tsx+597−0View fileUnifiedSplit
@@ -0,0 +1,597 @@
1import type { FC } from "hono/jsx";
2
3export interface TrustReportProps {
4 stats?: {
5 repairRate: string;
6 revertRate: string;
7 autonomousActions: string;
8 };
9}
10
11const headline = [
12 {
13 value: "1.9%",
14 label: "AI merges reverted within 30 days",
15 trend: "▾ 0.4pt vs May",
16 trendColor: "#1e7f5c",
17 },
18 {
19 value: "2.1",
20 label: "median repair attempts to converge",
21 trend: "— flat vs May",
22 trendColor: "#8a8d99",
23 },
24 {
25 value: "27s",
26 label: "median time to first AI review",
27 trend: "▾ 3s vs May",
28 trendColor: "#1e7f5c",
29 },
30];
31
32const metrics = [
33 {
34 name: "AI-authored merges",
35 value: "1,412",
36 note: "across all customer repos this month",
37 },
38 {
39 name: "Reverted by humans",
40 value: "27",
41 note: "19 preference, 6 behavior, 2 regressions",
42 },
43 {
44 name: "Review false-positive rate",
45 value: "4.7%",
46 note: "AI flags later dismissed by a human reviewer",
47 },
48 {
49 name: "Review false-negative rate",
50 value: "0.8%",
51 note: "human-found defects the AI review missed",
52 },
53 {
54 name: "Repair loops that converged",
55 value: "93.5%",
56 note: "the rest were pinned for human review — never looped forever",
57 },
58 {
59 name: "Gate p99 latency",
60 value: "38s",
61 note: "push-time; measured at the git wire, not the dashboard",
62 },
63 {
64 name: "Irreversible actions taken autonomously",
65 value: "0",
66 note: "by policy — flagged effects always wait for a human",
67 },
68];
69
70const honest = [
71 {
72 what: "Long-lived migrations",
73 detail:
74 "repair convergence drops to 71% when a failing gate involves a schema migration. These are pinned to humans early.",
75 },
76 {
77 what: "Frontend visual regressions",
78 detail:
79 "gates catch logic, not looks. Visual diff coverage ships later this year.",
80 },
81 {
82 what: "Monorepos over 2M LOC",
83 detail:
84 "semantic index freshness lags ~40 min at that scale; cross-repo impact maps can be stale.",
85 },
86];
87
88export const TrustReport: FC<TrustReportProps> = (props) => {
89 const revertRate = props.stats?.revertRate ?? "1.9%";
90 const repairRate = props.stats?.repairRate ?? "2.1";
91 const autonomousActions = props.stats?.autonomousActions ?? "0";
92
93 const displayHeadline = [
94 {
95 value: revertRate,
96 label: headline[0].label,
97 trend: headline[0].trend,
98 trendColor: headline[0].trendColor,
99 },
100 {
101 value: repairRate,
102 label: headline[1].label,
103 trend: headline[1].trend,
104 trendColor: headline[1].trendColor,
105 },
106 {
107 value: headline[2].value,
108 label: headline[2].label,
109 trend: headline[2].trend,
110 trendColor: headline[2].trendColor,
111 },
112 ];
113
114 const displayMetrics = metrics.map((m, i) => {
115 if (i === 6) {
116 return { ...m, value: autonomousActions };
117 }
118 return m;
119 });
120
121 return (
122 <>
123 <style dangerouslySetInnerHTML={{ __html: css }} />
124 <div class="tr-root">
125 <header class="tr-header">
126 <a href="/" class="tr-logo">
127 <span class="tr-logo-mark"></span>
128 gluecron
129 </a>
130 <span class="tr-breadcrumb-sep">/</span>
131 <span class="tr-breadcrumb-label">Trust</span>
132 <div class="tr-header-right">
133 <span class="tr-live-badge">
134 <span class="tr-live-dot"></span>
135 Live · updates hourly
136 </span>
137 </div>
138 </header>
139
140 <main class="tr-main">
141 <div class="tr-hero">
142 <div class="tr-eyebrow">Public trust report · June 2026</div>
143 <h1 class="tr-h1">How good is the AI, really?</h1>
144 <p class="tr-hero-sub">
145 Every number below is computed from production data and published
146 automatically. We show them because you shouldn't have to take
147 autonomous merging on faith.
148 </p>
149 </div>
150
151 <div class="tr-stats-grid">
152 {displayHeadline.map((h) => (
153 <div class="tr-stat-cell">
154 <div class="tr-stat-value">{h.value}</div>
155 <div class="tr-stat-label">{h.label}</div>
156 <div
157 class="tr-stat-trend"
158 style={`color:${h.trendColor}`}
159 >
160 {h.trend}
161 </div>
162 </div>
163 ))}
164 </div>
165 <p class="tr-stats-note">
166 Reverted = any AI-authored merge undone within 30 days, for any
167 reason.
168 </p>
169
170 <h2 class="tr-section-heading">The numbers behind the numbers</h2>
171 <div class="tr-metrics-table">
172 {displayMetrics.map((m, i) => (
173 <div
174 class={
175 i === displayMetrics.length - 1
176 ? "tr-metric-row tr-metric-row--last"
177 : "tr-metric-row"
178 }
179 >
180 <span class="tr-metric-name">{m.name}</span>
181 <span class="tr-metric-value">{m.value}</span>
182 <span class="tr-metric-note">{m.note}</span>
183 </div>
184 ))}
185 </div>
186
187 <div class="tr-guarantee-card">
188 <div class="tr-guarantee-eyebrow">The reversibility guarantee</div>
189 <p class="tr-guarantee-primary">
190 Every autonomous action on Gluecron — merge, repair, deploy — can
191 be undone in one click, and shows you its blast radius before you
192 approve it.
193 </p>
194 <p class="tr-guarantee-secondary">
195 Irreversible side effects (like a sent email) are always flagged in
196 the plan before execution. If we can't reverse it, we won't do it
197 autonomously.
198 </p>
199 </div>
200
201 <h2 class="tr-section-heading">Where we're not good enough yet</h2>
202 <div class="tr-honest-list">
203 {honest.map((h) => (
204 <div class="tr-honest-item">
205 <span class="tr-honest-dot"></span>
206 <div class="tr-honest-body">
207 <span class="tr-honest-what">{h.what}</span>
208 <span class="tr-honest-detail"> — {h.detail}</span>
209 </div>
210 </div>
211 ))}
212 </div>
213
214 <div class="tr-footer">
215 <p class="tr-footer-line">
216 Methodology and raw data:{" "}
217 <a href="/trust/methodology" class="tr-footer-link">
218 gluecron.com/trust/methodology
219 </a>
220 </p>
221 <p class="tr-footer-meta">
222 generated 2026-07-03 06:00 UTC · from production telemetry ·
223 unedited
224 </p>
225 </div>
226 </main>
227 </div>
228 </>
229 );
230};
231
232export default TrustReport;
233
234const css = `
235@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
236
237*,
238*::before,
239*::after {
240 box-sizing: border-box;
241}
242
243.tr-root {
244 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
245 font-size: 14px;
246 line-height: 1.55;
247 letter-spacing: -0.008em;
248 color: #16181d;
249 background: #fcfcfd;
250 min-height: 100vh;
251 display: flex;
252 flex-direction: column;
253 -webkit-font-smoothing: antialiased;
254 text-rendering: optimizeLegibility;
255}
256
257/* Header */
258.tr-header {
259 height: 56px;
260 border-bottom: 1px solid rgba(22, 24, 29, 0.07);
261 background: #fcfcfd;
262 display: flex;
263 align-items: center;
264 padding: 0 28px;
265 gap: 20px;
266 position: sticky;
267 top: 0;
268 z-index: 50;
269}
270
271.tr-logo {
272 display: inline-flex;
273 align-items: center;
274 gap: 9px;
275 font-family: 'Inter Tight', sans-serif;
276 font-weight: 600;
277 font-size: 15px;
278 letter-spacing: -0.02em;
279 color: #16181d;
280 text-decoration: none;
281}
282
283.tr-logo-mark {
284 width: 16px;
285 height: 16px;
286 border-radius: 5px;
287 background: #4353c9;
288 display: inline-block;
289 flex-shrink: 0;
290}
291
292.tr-breadcrumb-sep {
293 color: #c4c6cf;
294}
295
296.tr-breadcrumb-label {
297 font-size: 13px;
298 color: #16181d;
299 font-weight: 500;
300}
301
302.tr-header-right {
303 margin-left: auto;
304 display: flex;
305 align-items: center;
306 gap: 14px;
307}
308
309.tr-live-badge {
310 display: inline-flex;
311 align-items: center;
312 gap: 7px;
313 font-size: 12.5px;
314 color: #6b7080;
315}
316
317.tr-live-dot {
318 width: 6px;
319 height: 6px;
320 border-radius: 50%;
321 background: #1e7f5c;
322 flex-shrink: 0;
323}
324
325/* Main content */
326.tr-main {
327 flex: 1;
328 max-width: 880px;
329 width: 100%;
330 margin: 0 auto;
331 padding: 64px 32px 96px;
332}
333
334/* Hero */
335.tr-hero {
336 text-align: center;
337 margin-bottom: 52px;
338}
339
340.tr-eyebrow {
341 font-family: 'JetBrains Mono', monospace;
342 font-size: 11px;
343 letter-spacing: 0.12em;
344 text-transform: uppercase;
345 color: #8a8d99;
346 margin-bottom: 14px;
347}
348
349.tr-h1 {
350 font-family: 'Inter Tight', sans-serif;
351 font-size: 34px;
352 font-weight: 600;
353 letter-spacing: -0.028em;
354 line-height: 1.1;
355 margin: 0 0 14px;
356 color: #111318;
357}
358
359.tr-hero-sub {
360 font-size: 15px;
361 color: #6b7080;
362 margin: 0 auto;
363 line-height: 1.65;
364 max-width: 54ch;
365}
366
367/* Headline stats grid */
368.tr-stats-grid {
369 display: grid;
370 grid-template-columns: repeat(3, 1fr);
371 gap: 1px;
372 background: rgba(22, 24, 29, 0.07);
373 border: 1px solid rgba(22, 24, 29, 0.07);
374 border-radius: 14px;
375 overflow: hidden;
376 margin-bottom: 16px;
377}
378
379.tr-stat-cell {
380 background: #ffffff;
381 padding: 26px 24px;
382 text-align: center;
383}
384
385.tr-stat-value {
386 font-family: 'Inter Tight', sans-serif;
387 font-size: 34px;
388 font-weight: 600;
389 letter-spacing: -0.025em;
390 color: #16181d;
391 font-variant-numeric: tabular-nums;
392}
393
394.tr-stat-label {
395 font-size: 13px;
396 color: #6b7080;
397 margin-top: 6px;
398}
399
400.tr-stat-trend {
401 font-size: 11.5px;
402 margin-top: 4px;
403 font-family: 'JetBrains Mono', monospace;
404}
405
406.tr-stats-note {
407 font-size: 12px;
408 color: #8a8d99;
409 text-align: center;
410 margin: 0 0 52px;
411}
412
413/* Section heading */
414.tr-section-heading {
415 font-family: 'Inter Tight', sans-serif;
416 font-size: 16px;
417 font-weight: 600;
418 margin: 0 0 16px;
419 color: #16181d;
420 letter-spacing: -0.015em;
421}
422
423/* Metrics table */
424.tr-metrics-table {
425 border: 1px solid rgba(22, 24, 29, 0.07);
426 border-radius: 12px;
427 background: #ffffff;
428 overflow: hidden;
429 margin-bottom: 52px;
430}
431
432.tr-metric-row {
433 display: grid;
434 grid-template-columns: minmax(0, 1.6fr) minmax(90px, 0.6fr) minmax(0, 1.4fr);
435 gap: 16px;
436 padding: 14px 22px;
437 border-bottom: 1px solid rgba(22, 24, 29, 0.05);
438 align-items: baseline;
439}
440
441.tr-metric-row--last {
442 border-bottom: none;
443}
444
445.tr-metric-name {
446 font-size: 13.5px;
447 font-weight: 500;
448 color: #16181d;
449}
450
451.tr-metric-value {
452 font-family: 'JetBrains Mono', monospace;
453 font-size: 13px;
454 color: #16181d;
455 font-variant-numeric: tabular-nums;
456}
457
458.tr-metric-note {
459 font-size: 12.5px;
460 color: #6b7080;
461}
462
463/* Guarantee card */
464.tr-guarantee-card {
465 border: 1px solid rgba(22, 24, 29, 0.07);
466 border-radius: 14px;
467 background: #ffffff;
468 padding: 32px 36px;
469 margin-bottom: 52px;
470}
471
472.tr-guarantee-eyebrow {
473 font-family: 'JetBrains Mono', monospace;
474 font-size: 11px;
475 letter-spacing: 0.12em;
476 text-transform: uppercase;
477 color: #4353c9;
478 font-weight: 500;
479 margin-bottom: 14px;
480}
481
482.tr-guarantee-primary {
483 font-size: 15px;
484 color: #16181d;
485 margin: 0 0 10px;
486 line-height: 1.7;
487 font-weight: 500;
488}
489
490.tr-guarantee-secondary {
491 font-size: 13.5px;
492 color: #6b7080;
493 margin: 0;
494 line-height: 1.7;
495}
496
497/* Honest list */
498.tr-honest-list {
499 display: flex;
500 flex-direction: column;
501 gap: 12px;
502 margin-bottom: 52px;
503}
504
505.tr-honest-item {
506 display: flex;
507 gap: 14px;
508 align-items: baseline;
509 border: 1px solid rgba(22, 24, 29, 0.07);
510 border-radius: 12px;
511 background: #ffffff;
512 padding: 16px 20px;
513}
514
515.tr-honest-dot {
516 width: 7px;
517 height: 7px;
518 border-radius: 50%;
519 background: #b45309;
520 flex-shrink: 0;
521 position: relative;
522 top: 1px;
523 margin-top: 2px;
524}
525
526.tr-honest-body {
527 line-height: 1.6;
528}
529
530.tr-honest-what {
531 font-size: 13.5px;
532 font-weight: 500;
533 color: #16181d;
534}
535
536.tr-honest-detail {
537 font-size: 13px;
538 color: #6b7080;
539}
540
541/* Footer */
542.tr-footer {
543 text-align: center;
544 border-top: 1px solid rgba(22, 24, 29, 0.07);
545 padding-top: 28px;
546}
547
548.tr-footer-line {
549 font-size: 12.5px;
550 color: #8a8d99;
551 margin: 0 0 4px;
552}
553
554.tr-footer-link {
555 color: #4353c9;
556 text-decoration: none;
557}
558
559.tr-footer-link:hover {
560 text-decoration: underline;
561}
562
563.tr-footer-meta {
564 font-family: 'JetBrains Mono', monospace;
565 font-size: 11px;
566 color: #8a8d99;
567 margin: 0;
568}
569
570@media (max-width: 640px) {
571 .tr-stats-grid {
572 grid-template-columns: 1fr;
573 }
574
575 .tr-metric-row {
576 grid-template-columns: 1fr;
577 gap: 4px;
578 }
579
580 .tr-metric-note {
581 color: #8a8d99;
582 font-size: 12px;
583 }
584
585 .tr-guarantee-card {
586 padding: 24px 20px;
587 }
588
589 .tr-main {
590 padding: 40px 20px 64px;
591 }
592
593 .tr-h1 {
594 font-size: 26px;
595 }
596}
597`;
0598