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

onboarding-v2.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

onboarding-v2.tsxBlame750 lines · 1 contributor
11c3ab6ccanty labs1import 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, '&amp;')
590 .replace(/</g, '&lt;')
591 .replace(/>/g, '&gt;')
592 .replace(/"/g, '&quot;');
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})();`;