@bongos/core 1.19.114 → 1.19.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,7 +37,6 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
37
37
  Same trust boundary as /create — every button only ASKS (enqueues an
38
38
  intent); the control-plane runner does the real infra work (ADR 0111 §2). */
39
39
  .viewWrap{padding-bottom:40px;}
40
- .viewWrap.narrow{max-width:820px;}
41
40
  .card{margin-bottom:26px;}
42
41
 
43
42
  /* The bongo pair on the New-project view. Component-scoped custom properties,
@@ -51,6 +50,207 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
51
50
  --skin-hi:#fffdf7;--skin-mid:#f4e6cd;--skin-lo:#cdb896;--drum-shadow:var(--ink);}
52
51
  :root[data-mode="dark"] .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;--drum-shadow:#000;}
53
52
  @media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;--drum-shadow:#000;}}
53
+
54
+ /* ── the create wizard (task 1002763, absorbed from /create) ──────────────
55
+ Tokens, the reset, the sky and the shared card/button/rail vocabulary all
56
+ come from cosmos.css; what lands here is the wizard's own structure. No
57
+ literal color anywhere: every surface is a cosmos token or a color-mix
58
+ over one, so both modes and a re-brand carry it for free (C1/C2/C4).
59
+ Naming: the wizard's field wrapper is .wizField — cosmos owns .field (the
60
+ orb map) — and its done checklist is .humanSteps — cosmos owns .steps
61
+ (the four-beat rail). Its inputs are scoped to the view so the glass
62
+ search pills (#mapQ, #mineQ) keep their own look. */
63
+ .panel{display:none;}
64
+ .panel.on{display:block;animation:fadeStep .3s ease both;}
65
+ @keyframes fadeStep{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:none}}
66
+ /* show() moves focus to each panel's h1 for keyboard/SR context — keep the
67
+ focus, lose the ring (a black rectangle around a heading; owner walk) */
68
+ h1[tabindex="-1"]:focus{outline:none;}
69
+ /* the done step needs the full card width — the split collapses and the
70
+ bongo pair steps aside for the checklist */
71
+ .createSplit.wizDone{display:block;}
72
+ .createSplit.wizDone .bongoCell{display:none;}
73
+
74
+ .wizField{display:block;margin-bottom:18px;}
75
+ .wizField>span{display:block;font-size:13.5px;font-weight:700;color:var(--ink-soft);margin-bottom:7px;letter-spacing:0.2px;}
76
+ .wizField small{font-weight:500;color:var(--ink-faint);}
77
+ #view-new input[type=text]{
78
+ width:100%;font-family:var(--font-body);font-size:17px;font-weight:600;color:var(--ink);
79
+ background:var(--bg-card);border:2px solid var(--line);border-radius:14px;padding:13px 16px;outline:none;
80
+ transition:border-color .15s ease, box-shadow .15s ease;
81
+ }
82
+ /* no fade toward the card — a 70% mix washed placeholders to 2.89:1 (task 1002749) */
83
+ #view-new input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
84
+ #view-new input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
85
+ #view-new input[type=text].bad{border-color:var(--err);}
86
+ .hint{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:7px;min-height:18px;}
87
+ .hint.ok{color:var(--ok);}
88
+ .hint.err{color:var(--err);}
89
+ .hint code{font-family:var(--font-mono);font-size:12.5px;background:var(--bg-deep);border-radius:6px;padding:1px 6px;color:var(--accent-deep);}
90
+
91
+ .row2{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
92
+
93
+ /* choice cards (greenfield / adopt) */
94
+ .choices{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px;}
95
+ .choice{
96
+ text-align:left;cursor:pointer;background:var(--bg-card);border:2px solid var(--line);border-radius:18px;
97
+ padding:16px;font-family:var(--font-body);transition:all .15s ease;
98
+ }
99
+ .choice:hover{border-color:var(--rule);transform:translateY(-1px);}
100
+ .choice[aria-pressed=true]{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 5%,var(--bg-card));box-shadow:0 8px 24px -12px color-mix(in srgb,var(--accent) 35%,transparent);}
101
+ .choice svg{display:block;margin-bottom:9px;}
102
+ .choice b{display:block;font-size:15.5px;font-weight:700;margin-bottom:3px;}
103
+ .choice p{font-size:13px;font-weight:500;color:var(--ink-soft);line-height:1.45;}
104
+ .reassure{
105
+ display:none;background:color-mix(in srgb,var(--sea) 8%,var(--bg-card));
106
+ border:1.5px solid color-mix(in srgb,var(--sea) 25%,var(--bg-card));border-radius:14px;
107
+ padding:12px 15px;font-size:13.5px;font-weight:500;color:color-mix(in srgb,var(--sea) 40%,var(--ink));line-height:1.5;margin-bottom:18px;
108
+ }
109
+ .reassure.on{display:block;}
110
+
111
+ /* one-click deep link to GitHub's own "New repository" page with the name
112
+ pre-filled (greenfield only). No permissions — the visitor creates the empty
113
+ repo in GitHub's own UI; we just fill in the blanks. The GitHub brand black
114
+ is intentional, not a token (it must not follow a re-brand). */
115
+ .ghbtn{
116
+ display:inline-flex;align-items:center;gap:9px;text-decoration:none;cursor:pointer;
117
+ background:#24292f;color:#fff;border:none;border-radius:999px;
118
+ font-family:var(--font-body);font-weight:700;font-size:15px;padding:12px 22px;
119
+ box-shadow:0 12px 26px -12px rgba(36,41,47,0.55);
120
+ transition:transform .12s ease, background .12s ease;
121
+ }
122
+ .ghbtn:hover{background:#1b1f24;transform:translateY(-1px);}
123
+ .ghbtn:active{transform:translateY(0) scale(.98);}
124
+ .ghbtn .gh{width:18px;height:18px;fill:currentColor;}
125
+ .humanSteps .ghbtn{font-size:14px;padding:9px 16px;margin-top:10px;}
126
+
127
+ .nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:26px;}
128
+ .btn.big{width:100%;justify-content:center;font-size:18px;padding:17px 30px;}
129
+
130
+ /* review list */
131
+ .review{list-style:none;border:2px solid var(--line);border-radius:18px;overflow:hidden;margin-bottom:6px;}
132
+ .review li{display:flex;justify-content:space-between;gap:16px;padding:13px 17px;font-size:14.5px;}
133
+ .review li+li{border-top:1.5px solid var(--line);}
134
+ .review .k{font-weight:600;color:var(--ink-faint);flex:0 0 auto;}
135
+ .review .v{font-weight:700;text-align:right;word-break:break-word;}
136
+ .review .v code{font-family:var(--font-mono);font-size:13px;background:var(--bg-deep);border-radius:6px;padding:1px 7px;color:var(--accent-deep);}
137
+
138
+ /* ── done / human steps ── */
139
+ .doneHead{display:flex;align-items:center;gap:12px;margin-bottom:8px;}
140
+ .doneHead .tick{
141
+ flex:0 0 44px;width:44px;height:44px;border-radius:50%;background:var(--ok);color:var(--on-ok);
142
+ display:flex;align-items:center;justify-content:center;
143
+ animation:pop .45s cubic-bezier(.34,1.56,.64,1) both;
144
+ }
145
+ @keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
146
+ .humanSteps{list-style:none;counter-reset:hs;margin-top:20px;}
147
+ .humanSteps>li{
148
+ position:relative;background:var(--bg-card);border:2px solid var(--line);border-radius:18px;
149
+ padding:16px 18px 16px 58px;margin-bottom:12px;counter-increment:hs;
150
+ }
151
+ .humanSteps>li::before{
152
+ content:counter(hs);position:absolute;left:16px;top:16px;width:28px;height:28px;border-radius:50%;
153
+ background:var(--bg-deep);color:var(--accent-deep);font-family:var(--font-display);font-weight:700;
154
+ font-size:14px;display:flex;align-items:center;justify-content:center;
155
+ }
156
+ .humanSteps b{font-size:15px;}
157
+ .humanSteps p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.55;margin-top:4px;}
158
+ /* live checklist states (task 1002694): a step the platform has verified done, and a
159
+ step that can't be performed yet (its prerequisite — usually the domain — is missing) */
160
+ .humanSteps>li.sdone{border-color:color-mix(in srgb,var(--ok) 30%,var(--bg-card));background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);}
161
+ .humanSteps>li.sdone::before{content:"✓";background:var(--ok);color:var(--on-ok);}
162
+ .humanSteps>li.slocked{opacity:.6;}
163
+ .humanSteps details{margin-top:10px;}
164
+ .humanSteps summary{cursor:pointer;font-weight:700;font-size:12.5px;color:var(--ink-faint);}
165
+ .humanSteps details p{margin-top:8px;}
166
+ .kv{
167
+ display:flex;align-items:center;gap:8px;margin-top:9px;background:var(--bg);border:1.5px solid var(--line);
168
+ border-radius:11px;padding:8px 8px 8px 12px;font-size:12.5px;
169
+ }
170
+ .kv .kk{font-weight:700;color:var(--ink-faint);flex:0 0 auto;}
171
+ .kv code{font-family:var(--font-mono);font-size:12px;color:var(--accent-deep);word-break:break-all;flex:1;}
172
+ .copy{
173
+ flex:0 0 auto;cursor:pointer;border-radius:8px;background:var(--bg-card);border:1.5px solid var(--line);
174
+ font-family:var(--font-body);font-size:11.5px;font-weight:700;color:var(--ink-soft);padding:5px 10px;
175
+ transition:all .12s ease;
176
+ }
177
+ .copy:hover{border-color:var(--accent);color:var(--accent-deep);}
178
+ .copy.did{background:var(--ok);border-color:var(--ok);color:var(--on-ok);}
179
+ .samePath{font-size:12.5px;font-weight:500;color:var(--ink-faint);margin-top:16px;}
180
+ #liveProgress{margin-top:4px;}
181
+
182
+ /* ── live build progress (task 2083) ── */
183
+ .prog{
184
+ background:linear-gradient(180deg,color-mix(in srgb,var(--sea) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);
185
+ border:2px solid var(--sea-soft);border-radius:18px;padding:18px 20px 16px;margin-top:14px;
186
+ }
187
+ .prog .stages{display:flex;list-style:none;margin-bottom:4px;}
188
+ .prog .stages li{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;position:relative;}
189
+ .prog .stages li:not(:first-child)::before{
190
+ content:"";position:absolute;top:11px;right:50%;width:100%;height:3px;border-radius:2px;
191
+ background:var(--sea-soft);z-index:0;margin-right:12px;
192
+ }
193
+ .prog .stages li.done:not(:first-child)::before{background:var(--sea);}
194
+ .prog .sdot{
195
+ position:relative;z-index:1;width:24px;height:24px;border-radius:50%;
196
+ border:3px solid var(--sea-soft);background:var(--bg-card);display:flex;align-items:center;justify-content:center;
197
+ }
198
+ .prog .stages li.done .sdot{border-color:var(--sea);background:var(--sea);}
199
+ .prog .stages li.done .sdot::after{content:"";width:8px;height:5px;border-left:2.5px solid #fff;border-bottom:2.5px solid #fff;transform:rotate(-45deg) translate(1px,-1px);}
200
+ .prog .stages li.now .sdot{border-color:var(--sea);}
201
+ .prog .stages li.now .sdot::after{
202
+ content:"";width:8px;height:8px;border-radius:50%;background:var(--sea);
203
+ animation:beatPulse 1.2s ease-in-out infinite;
204
+ }
205
+ /* idle labels match cosmos's .strip .slbl — the 55% sea mix washed to 4.15:1 in dark */
206
+ .prog .slbl{font-size:12px;font-weight:700;color:var(--ink-faint);}
207
+ .prog .stages li.now .slbl,.prog .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
208
+ .prog .pnote{text-align:center;font-size:13px;font-weight:600;color:color-mix(in srgb,var(--sea) 45%,var(--ink-soft));min-height:18px;}
209
+ .prog .pnote a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
210
+ .prog .pnote a:hover{text-decoration:underline;}
211
+ .prog.isLive{background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);border-color:color-mix(in srgb,var(--ok) 30%,var(--bg-card));}
212
+ .prog.isLive .stages li.done:not(:first-child)::before{background:var(--ok);}
213
+ .prog.isLive .stages li.done .sdot{border-color:var(--ok);background:var(--ok);}
214
+ .prog.isLive .slbl{color:var(--ok);}
215
+ .prog.isLive .stages li.done .slbl{color:color-mix(in srgb,var(--ok) 75%,var(--ink));}
216
+ .prog.isLive .pnote{color:color-mix(in srgb,var(--ok) 75%,var(--ink));font-size:14px;}
217
+ .prog.isErr{background:var(--err-soft);border-color:color-mix(in srgb,var(--err) 25%,var(--bg-card));}
218
+ .prog.isErr .pnote{color:var(--err);}
219
+ .again{
220
+ display:inline-block;margin-top:14px;font-size:13px;font-weight:700;color:var(--ink-faint);
221
+ background:none;border:none;cursor:pointer;font-family:var(--font-body);text-decoration:underline;
222
+ text-underline-offset:3px;padding:0;
223
+ }
224
+ .again:hover{color:var(--accent-deep);}
225
+
226
+ /* the collapsed address step — a confirmation, not a form (task 1002704) */
227
+ .addrConfirm{border:2px solid color-mix(in srgb,var(--ok) 30%,var(--bg-card));border-radius:18px;padding:18px 20px;background:linear-gradient(180deg,color-mix(in srgb,var(--ok) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);margin-bottom:16px;}
228
+ .addrConfirm.optedOut{border-color:var(--line);background:var(--bg);opacity:.75;}
229
+ .addrBig{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;line-height:1.4;}
230
+ .addrBig code{font-family:var(--font-mono);font-size:15px;background:var(--bg-deep);border-radius:8px;padding:2px 9px;color:var(--accent-deep);word-break:break-all;}
231
+ .addrConfirm>p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.5;margin-top:6px;}
232
+ .addrConfirm details{margin-top:12px;}
233
+ .addrConfirm summary{cursor:pointer;font-size:13px;font-weight:700;color:var(--ink-faint);}
234
+ .addrConfirm details .inner{margin-top:10px;}
235
+ .addrOpt{display:flex;align-items:flex-start;gap:8px;margin-top:12px;font-size:12.5px;font-weight:600;color:var(--warn);line-height:1.5;}
236
+ .addrOpt input{margin-top:2px;}
237
+
238
+ /* GitHub repo picker (task 1002099) */
239
+ #ghConnect .btn{margin-top:4px;}
240
+ .ghlist{margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--line);border-radius:10px;background:var(--bg-deep);}
241
+ .ghlist:empty{display:none;}
242
+ /* rows sit on --bg-deep, so their tints mix over THAT, not the card */
243
+ .ghrow{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:0;border-bottom:1px solid color-mix(in srgb,var(--ink) 8%,transparent);padding:9px 12px;font:inherit;font-size:13.5px;color:var(--ink);cursor:pointer;}
244
+ .ghrow:last-child{border-bottom:0;}
245
+ .ghrow:hover{background:color-mix(in srgb,var(--accent) 6%,var(--bg-deep));}
246
+ .ghrow[aria-selected=true]{background:color-mix(in srgb,var(--accent) 12%,var(--bg-deep));box-shadow:inset 3px 0 0 var(--accent);font-weight:600;}
247
+ /* --ink-soft, not --ink-faint: the badge tint is the darkest ground on the page,
248
+ and faint ink only reaches 3.96:1 on it (task 1002749). */
249
+ .ghrow .priv{margin-left:auto;font-size:11px;font-weight:600;color:var(--ink-soft);background:color-mix(in srgb,var(--ink) 8%,transparent);border-radius:5px;padding:1px 6px;}
250
+
251
+ /* 840px matches cosmos's createSplit collapse, the breakpoint the old /create
252
+ used (860) — the two-up grids and the bongo cell fold together */
253
+ @media (max-width:840px){.row2,.choices{grid-template-columns:1fr;}}
54
254
  </style>
55
255
  </head>
56
256
  <body>
@@ -117,8 +317,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
117
317
  </header>
118
318
 
119
319
  <noscript>
120
- <div class="viewWrap wrap"><div class="fieldNote">The project map needs JavaScript. You can still
121
- <a href="/create">start a project</a> or read what this is on the <a href="/">home page</a>.</div></div>
320
+ <div class="viewWrap wrap"><div class="fieldNote">The project hub needs JavaScript — the map, your
321
+ projects, and starting a new one all live here. You can read what this is on the <a href="/">home page</a>.</div></div>
122
322
  </noscript>
123
323
 
124
324
  <!-- ══════════════════ VIEW · MAP ══════════════════════════════════════════
@@ -296,7 +496,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
296
496
 
297
497
  <section class="empty" id="empty" hidden>
298
498
  <p>You don't own any projects yet. Creating one takes about five minutes — name it, pick a home for its code, and it gets an address of its own in this sky.</p>
299
- <a class="btn" href="/create">Create your first project</a>
499
+ <a class="btn" href="?view=new" data-nav-to="new">Create your first project</a>
300
500
  </section>
301
501
  <ul class="projects" id="projectsList"></ul>
302
502
  <p class="finePrint">Buttons here ask the platform to act — a runner does the real work, and each card reports only what was verified, with a timestamp. Standups keep running even with this tab closed.</p>
@@ -306,31 +506,194 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
306
506
  </section>
307
507
 
308
508
  <!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
309
- Step 1 of the four-beat rail. Naming it here hands straight into the
310
- wizard with the name already seeded (/create?name=…), which is the entry
311
- point the wizard already reads. -->
509
+ The whole create wizard, absorbed from /create (task 1002763; direction
510
+ §4 — that route now 302s here). Six panels behind the four-beat rail; the
511
+ wizard block in the script below drives them. Same trust boundary as
512
+ ever: every button here only ENQUEUES a provisioning request (ADR 0111).
513
+ The name panel carries a static `on` so a no-script visitor still reads
514
+ step 1 rather than an empty card. -->
312
515
  <section id="view-new" hidden>
313
- <div class="wrap viewWrap narrow">
516
+ <div class="wrap viewWrap">
314
517
  <div class="card">
315
- <ol class="steps">
316
- <li class="now"><span class="snum">1</span><span class="slbl">Name</span></li>
317
- <li><span class="snum">2</span><span class="slbl">Home</span></li>
318
- <li><span class="snum">3</span><span class="slbl">Address</span></li>
319
- <li><span class="snum">4</span><span class="slbl">Create</span></li>
518
+ <ol class="steps" aria-label="Creation steps">
519
+ <li class="now" data-step="1"><span class="snum">1</span><span class="slbl">Name</span></li>
520
+ <li data-step="2"><span class="snum">2</span><span class="slbl">Home</span></li>
521
+ <li data-step="3"><span class="snum">3</span><span class="slbl">Address</span></li>
522
+ <li data-step="4"><span class="snum">4</span><span class="slbl">Create</span></li>
320
523
  </ol>
321
524
 
322
525
  <div class="createSplit">
323
526
  <div>
324
- <h1>Start a project</h1>
325
- <p class="lede">Your own project: its own repository, its own home on the web, and a hall of AI builders
326
- who do the building. You set the direction; agents lay down the code.</p>
327
- <form class="attach" id="newForm">
328
- <input type="text" id="newName" placeholder="What should it be called?" maxlength="80" autocomplete="off" aria-label="Project name">
329
- <button class="btn" type="submit">Continue →</button>
330
- </form>
331
- <p class="finePrint">Free while we find our feet. Your project’s repo and server are yours —
332
- <span data-brand-product-inline>Cloud Bongos</span> is <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">open source (AGPL-3.0)</a>
333
- and capture-resistant by design.</p>
527
+ <!-- step 0 · sign-in gate -->
528
+ <section class="panel" id="panel-gate" aria-labelledby="gate-h">
529
+ <h1 id="gate-h">Create a project</h1>
530
+ <p class="lede">Your own <strong data-brand-product-inline>Cloud Bongos</strong> project: its own repository, its own home on the web, and a hall of AI builders who do the building. You set the direction; agents lay down the code. Sign in with GitHub so the project belongs to <em>you</em> from the very first beat.</p>
531
+ <!-- The wizard's own sign-in opts into repo scope (read:user + public_repo) up
532
+ front (&repos=1) — anyone here intends to create a project, which needs repo
533
+ access, so we ask once at sign-in instead of a second "Connect" step at Home.
534
+ Hall + CLI sign-in stay identity-only (ADR 0143 least-privilege default). -->
535
+ <a class="btn big" id="wizSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dnew&amp;repos=1">
536
+ <svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><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"/></svg>
537
+ Sign in with GitHub
538
+ </a>
539
+ <!-- Trust link → the AGPL license text (task 1002705): the old private-repo URL
540
+ 404ed, and no public code artifact exists until the ADR 0099 mirror flips
541
+ (~2026-11; npm scopes are private too) — the license is the one live,
542
+ instance-neutral artifact behind the claim today. -->
543
+ <p class="finePrint">Free while we find our feet. Your project’s repo and server are yours —
544
+ <span data-brand-product-inline>Cloud Bongos</span> is <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">open source (AGPL-3.0)</a>
545
+ and capture-resistant by design.</p>
546
+ <div class="alert" id="wizGateAlert" role="alert"></div>
547
+ </section>
548
+
549
+ <!-- step 1 · name (the hub's original step-1 form, now the wizard's) -->
550
+ <section class="panel on" id="panel-name" aria-labelledby="name-h">
551
+ <h1 id="name-h">Start a project</h1>
552
+ <p class="lede">Your own project: its own repository, its own home on the web, and a hall of AI builders
553
+ who do the building. You set the direction; agents lay down the code.</p>
554
+ <form id="newForm" data-next="2">
555
+ <label class="wizField"><span>Project name</span>
556
+ <input type="text" id="newName" placeholder="What should it be called?" autocomplete="off" maxlength="80" aria-label="Project name">
557
+ <div class="hint" id="h-name">&nbsp;</div>
558
+ </label>
559
+ <div class="nav">
560
+ <span></span>
561
+ <button type="submit" class="btn">Continue →</button>
562
+ </div>
563
+ </form>
564
+ <p class="finePrint">Free while we find our feet. Your project’s repo and server are yours —
565
+ <span data-brand-product-inline>Cloud Bongos</span> is <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">open source (AGPL-3.0)</a>
566
+ and capture-resistant by design.</p>
567
+ </section>
568
+
569
+ <!-- step 2 · home (repo + mode) -->
570
+ <section class="panel" id="panel-home" aria-labelledby="home-h">
571
+ <h1 id="home-h">Where the code lives</h1>
572
+ <p class="lede">Every project is a real GitHub repository that you own. Start from a clean slate, or bring a repo you already have.</p>
573
+ <form data-next="3">
574
+ <div class="choices" role="group" aria-label="Starting point">
575
+ <button type="button" class="choice" id="c-green" aria-pressed="true">
576
+ <!-- token-built, not platform orange — public-landing/ ships wholesale to
577
+ every instance (C5), so a re-branded accent must carry these too -->
578
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" aria-hidden="true"><path d="M15 26V14" stroke="var(--accent)" stroke-width="2.6" stroke-linecap="round"/><path d="M15 15c0-5 3.5-8.5 9-8.5 0 5.5-3.5 8.5-9 8.5Z" fill="color-mix(in srgb,var(--accent) 45%,var(--bg-card))" stroke="var(--accent)" stroke-width="2"/><path d="M15 18c0-3.8-2.6-6.5-7-6.5 0 4.3 2.7 6.5 7 6.5Z" fill="color-mix(in srgb,var(--accent) 22%,var(--bg-card))" stroke="var(--accent)" stroke-width="2"/></svg>
579
+ <b>Start fresh</b>
580
+ <p>A new, empty <strong>public</strong> repo. We scaffold everything: config, first version, kickoff tasks.</p>
581
+ </button>
582
+ <button type="button" class="choice" id="c-adopt" aria-pressed="false">
583
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" aria-hidden="true"><rect x="4" y="9" width="22" height="16" rx="3" fill="color-mix(in srgb,var(--accent) 22%,var(--bg-card))" stroke="var(--accent)" stroke-width="2"/><path d="M4 13h22" stroke="var(--accent)" stroke-width="2"/><path d="M10 9V6.5A1.5 1.5 0 0 1 11.5 5h7A1.5 1.5 0 0 1 20 6.5V9" stroke="var(--accent)" stroke-width="2"/><path d="M12 19h6" stroke="var(--accent-deep)" stroke-width="2.4" stroke-linecap="round"/></svg>
584
+ <b>Adopt a repo I have</b>
585
+ <p>Layer <span data-brand-product-inline>Cloud Bongos</span> onto an existing repository — history, code, and backlog intact. Public or private, we never change it.</p>
586
+ </button>
587
+ </div>
588
+ <div class="reassure" id="adoptNote">Adopting never overwrites your files: you get a conflict pre-flight first, nothing is written until you accept, and your existing backlog (issues, TODOs) is imported as real tasks.</div>
589
+
590
+ <!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
591
+ Progressive enhancement: when the signed-in user grants repo access we let
592
+ them PICK a repo (adopt) or NAME one we auto-create (fresh). Without that
593
+ access — not granted yet, or the server can't store the token — we fall back
594
+ to the manual owner/repo inputs below (#manualHome), which stay authoritative. -->
595
+ <div id="ghConnect" class="reassure">
596
+ <p id="ghConnectMsg">&nbsp;</p>
597
+ <button type="button" class="btn" id="ghConnectBtn">Allow repo access</button>
598
+ <button type="button" class="linklike" id="ghManualToggle">or enter it manually</button>
599
+ </div>
600
+ <div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
601
+ <div id="ghAdopt" class="wizField" hidden>
602
+ <span>Pick one of your repositories</span>
603
+ <input type="text" id="ghSearch" placeholder="Search your repos…" autocomplete="off">
604
+ <div id="ghList" class="ghlist" role="listbox" aria-label="Your repositories"></div>
605
+ <div class="hint" id="ghAdoptHint">&nbsp;</div>
606
+ <!-- visibility stated on EVERY repo path (task 1002721) — the picker
607
+ marks private repos, and this says what adoption does about it -->
608
+ <div class="hint">We never change a repo’s visibility — public stays public, private stays private.</div>
609
+ </div>
610
+ <div id="ghFresh" class="wizField" hidden>
611
+ <span>New repository name</span>
612
+ <input type="text" id="ghNewName" placeholder="mercury" autocomplete="off" maxlength="100">
613
+ <!-- visibility is stated on EVERY repo path (task 1002721) — this baked
614
+ line is the floor; updateFreshPreview() replaces it with the same
615
+ claim plus the resolved repo URL once a name is typed -->
616
+ <div class="hint" id="ghFreshHint">We’ll create an empty <strong>public</strong> repo — projects here are open by design. Want private code? Create the repo yourself on GitHub and “adopt” it instead.</div>
617
+ </div>
618
+
619
+ <div class="row2" id="manualHome">
620
+ <label class="wizField"><span>GitHub owner <small>(user or org)</small></span>
621
+ <input type="text" id="f-owner" placeholder="acme" autocomplete="off" maxlength="60">
622
+ </label>
623
+ <label class="wizField"><span>Repository name</span>
624
+ <input type="text" id="f-repo" placeholder="mercury" autocomplete="off" maxlength="120">
625
+ </label>
626
+ <!-- visibility stated BEFORE GitHub's own radio is the first place it
627
+ appears (task 1002721, owner firsthand in the QA walk) -->
628
+ <div class="hint" style="grid-column:1 / -1;">Creating the repo yourself on GitHub? Public or private is your choice there — both work here, and we never change it.</div>
629
+ </div>
630
+ <div class="hint" id="h-home">&nbsp;</div>
631
+ <div class="nav">
632
+ <button type="button" class="btn ghost" data-back="1">← Back</button>
633
+ <button type="submit" class="btn">Next →</button>
634
+ </div>
635
+ </form>
636
+ </section>
637
+
638
+ <!-- step 3 · address — a CONFIRMATION, not a form (task 1002704: every project
639
+ gets its slug subdomain assigned up front; opt-out lives in the disclosure).
640
+ The domainless default was a proven dead-end (QA walk, task 1002702). -->
641
+ <section class="panel" id="panel-address" aria-labelledby="addr-h">
642
+ <h1 id="addr-h">Its address in the sky</h1>
643
+ <p class="lede">The handle names your project on the platform — and its address comes with it. Sign-in, sharing, everything just works from the moment it stands up.</p>
644
+ <form data-next="4">
645
+ <label class="wizField"><span>Project handle</span>
646
+ <input type="text" id="f-slug" placeholder="mercury" autocomplete="off" maxlength="40" spellcheck="false">
647
+ <div class="hint" id="h-slug">&nbsp;</div>
648
+ </label>
649
+ <div class="addrConfirm" id="addrConfirm">
650
+ <div class="addrBig" id="addrAssigned"></div>
651
+ <p id="addrAssignedNote">Yours from the moment it stands up. You can point a domain you own at it any time.</p>
652
+ <details id="addrCustom">
653
+ <summary>Use a domain I own instead</summary>
654
+ <div class="inner">
655
+ <label class="wizField"><span>Domain you own</span>
656
+ <input type="text" id="f-domain" placeholder="mercury.example.com" autocomplete="off" maxlength="253" spellcheck="false">
657
+ <div class="hint" id="h-domain">&nbsp;</div>
658
+ </label>
659
+ <label class="addrOpt"><input type="checkbox" id="f-noaddr">
660
+ <span>Start without an address — possible, but sign-in on your project can’t be set up until it has one.</span>
661
+ </label>
662
+ </div>
663
+ </details>
664
+ </div>
665
+ <div class="nav">
666
+ <button type="button" class="btn ghost" data-back="2">← Back</button>
667
+ <button type="submit" class="btn">Next →</button>
668
+ </div>
669
+ </form>
670
+ </section>
671
+
672
+ <!-- step 4 · review + create -->
673
+ <section class="panel" id="panel-review" aria-labelledby="rev-h">
674
+ <h1 id="rev-h">Ready to roll</h1>
675
+ <p class="lede">One tap files the request and the platform starts standing your project up — server, database, address. Here's the beat you're about to drop:</p>
676
+ <ul class="review" id="reviewList"></ul>
677
+ <div class="alert" id="createAlert" role="alert"></div>
678
+ <div class="nav" style="flex-direction:column;align-items:stretch;">
679
+ <button type="button" class="btn big" id="doCreate">Create my project</button>
680
+ <button type="button" class="btn ghost" data-back="3" style="align-self:flex-start;margin-top:10px;">← Back</button>
681
+ </div>
682
+ <p class="finePrint">This files the request; the control plane does the real standup and nothing touches servers from this page. You'll get the few steps only you can do right after.</p>
683
+ </section>
684
+
685
+ <!-- done · the human steps -->
686
+ <section class="panel" id="panel-done" aria-labelledby="done-h">
687
+ <div class="doneHead">
688
+ <span class="tick" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 22 22" fill="none"><path d="M4 11.5 9 16.5 18 6" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
689
+ <h1 id="done-h">The drums are rolling</h1>
690
+ </div>
691
+ <p class="lede" id="doneLede"></p>
692
+ <div id="liveProgress"></div>
693
+ <h2 style="font-family:var(--font-display);font-size:19px;margin-top:22px;">Your steps — the parts only you can play</h2>
694
+ <ol class="humanSteps" id="humanSteps"></ol>
695
+ <p class="samePath">This is the same sequence the <code style="font-family:var(--font-mono)">/new-project</code> assistant and <code style="font-family:var(--font-mono)">bongos onboard</code> use — three doors, one path.</p>
696
+ </section>
334
697
  </div>
335
698
 
336
699
  <!-- the bongo pair — one mark on every surface (owner, 2026-08-13).
@@ -481,6 +844,10 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
481
844
  else a.removeAttribute('aria-current');
482
845
  });
483
846
  try { history.replaceState(null, '', '?view=' + v); } catch (e) {}
847
+ /* the shell's sign-in door returns the visitor to the view they left, and
848
+ carries the repo scope when that view is the wizard (ADR 0143: the
849
+ create path asks once up front instead of a second consent at Home) */
850
+ $('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v + (v === 'new' ? '&repos=1' : '');
484
851
  /* the mine view is the only one that polls; it re-reads on entry so a
485
852
  card that changed while you were on the map is not stale */
486
853
  if (v === 'mine' && me) refreshList();
@@ -492,9 +859,11 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
492
859
  if (skyNeedsRebuild) rebuildSkyBodies();
493
860
  startSky();
494
861
  } else { stopSky(); hideHover(); }
495
- /* focus the name field only on a deliberate move into the view — never on
496
- boot, where it would steal focus from whatever the visitor arrived at */
497
- if (v === 'new' && focus) $('newName').focus();
862
+ /* the wizard runs only on stage too (C8): enter() paints the right step
863
+ (focusing only on a deliberate move — never on boot, where it would
864
+ steal focus from whatever the visitor arrived at) and leave() parks
865
+ its watch + manifest polls */
866
+ if (v === 'new') wizard.enter(focus); else wizard.leave();
498
867
  }
499
868
 
500
869
  document.querySelectorAll('[data-nav]').forEach(function (a) {
@@ -1548,14 +1917,984 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
1548
1917
  $('mapStars').appendChild(frag);
1549
1918
  })();
1550
1919
 
1551
- /* ══ NEW PROJECT — step 1 of the four-beat rail. Naming it here hands into
1552
- the wizard with the name already seeded; /create?name= is the entry point
1553
- the wizard already reads, so nothing about the flow changes. ══ */
1554
- $('newForm').addEventListener('submit', function (ev) {
1555
- ev.preventDefault();
1556
- var name = String($('newName').value || '').trim();
1557
- location.assign('/create' + (name ? '?name=' + encodeURIComponent(name) : ''));
1558
- });
1920
+ /* ══ THE CREATE WIZARD (task 1002763) — absorbed whole from /create, which
1921
+ now 302s here. An inner scope so its ~40 helpers stay out of the hub's
1922
+ namespace; it shares the hub's $, esc, API and `me`, so there is one
1923
+ GET /me, one mode toggle and one identity chip on the page. The hub talks
1924
+ to it through exactly three doors: enter(focus) / leave() from setView —
1925
+ the same start/stop contract the sky obeys (C8) — and onAuth() from the
1926
+ boot's identity settle. Same trust boundary as ever: every call here only
1927
+ ENQUEUES; the control-plane runner does the real infra work (ADR 0111). ══ */
1928
+ var wizard = (function () {
1929
+ var root = $('view-new');
1930
+
1931
+ /* ── state ── */
1932
+ var DRAFT_KEY = 'cb-create-draft';
1933
+ var CREATED_KEY = 'cb-created-instance';
1934
+ var state = { step: 0, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
1935
+ try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
1936
+ var authKnown = false; /* flips true when the boot's /me settles, either way */
1937
+
1938
+ /* A name handed over by URL (the /create?name= redirect still carries it,
1939
+ task 2087→1002763): seed the draft BEFORE boot's setView rewrites the
1940
+ URL to its bare '?view=' form, so the seed survives the GitHub sign-in
1941
+ round-trip and a later reload can't re-seed over the visitor's edits. */
1942
+ try {
1943
+ var handedName = new URLSearchParams(location.search).get('name');
1944
+ if (handedName && handedName.trim()) {
1945
+ state.name = handedName.trim().slice(0, 80);
1946
+ state.slug = deriveSlug(state.name); state.slugTouched = false;
1947
+ state.repo = deriveRepo(state.name); state.repoTouched = false;
1948
+ state.step = state.step >= 1 && state.step <= 4 ? state.step : 1;
1949
+ try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
1950
+ }
1951
+ } catch (e) {}
1952
+
1953
+ /* Mirror provisioning.isValidSlug (DNS/db-safe): 2–40 of [a-z0-9-], starts+ends
1954
+ alnum, no double dash. Client-side pre-check; the server re-validates. */
1955
+ function validSlug(s) {
1956
+ var v = String(s || '');
1957
+ return v.length >= 2 && v.length <= 40 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(v) && v.indexOf('--') === -1;
1958
+ }
1959
+ function slugify(name, maxLen) {
1960
+ return String(name || '').toLowerCase()
1961
+ .replace(/['’]/g, '')
1962
+ .replace(/[^a-z0-9]+/g, '-')
1963
+ .replace(/-+/g, '-')
1964
+ .replace(/^-|-$/g, '')
1965
+ .slice(0, maxLen);
1966
+ }
1967
+ function deriveSlug(name) { return slugify(name, 40); }
1968
+ /* Repo-name default from the project name — the same slug-safe transform (GitHub
1969
+ repo names accept [A-Za-z0-9._-], so a lowercase-dashed name is always valid and
1970
+ matches the placeholder). Wider cap than the handle; the visitor can still edit. */
1971
+ function deriveRepo(name) { return slugify(name, 100); }
1972
+ function looksLikeDomain(d) {
1973
+ return /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/i.test(String(d || ''));
1974
+ }
1975
+ /* ── the assigned address (task 1002704): every project gets <slug>.<apex> up
1976
+ front, derived from the host this wizard runs on — the platform apex assigns
1977
+ slug.<apex>; a self-hosted apex assigns under its own name. On a
1978
+ dotless/local host (localhost, an IP) there is nothing sane to assign, so
1979
+ the step degrades to the old optional-domain form. ── */
1980
+ function apexBase() {
1981
+ var h = String(location.hostname || '').toLowerCase().replace(/^www\./, '');
1982
+ if (!h || h.indexOf('.') < 0) return null;
1983
+ if (/^(localhost|127\.|\d+\.\d+\.\d+\.\d+$|\[)/.test(h)) return null;
1984
+ return h;
1985
+ }
1986
+ function assignedDomain() {
1987
+ var base = apexBase();
1988
+ return base && validSlug(state.slug) ? state.slug + '.' + base : null;
1989
+ }
1990
+ /* What the create actually files: a custom domain wins; opting out files none;
1991
+ otherwise the assigned subdomain. Everything downstream (review, submit,
1992
+ the done-panel steps) reads THIS, never state.domain directly. */
1993
+ function resolvedDomain() {
1994
+ if (state.domain) return state.domain;
1995
+ if (state.noAddress) return '';
1996
+ return assignedDomain() || '';
1997
+ }
1998
+ function renderAddrPanel() {
1999
+ var box = $('addrConfirm');
2000
+ if (!box) return;
2001
+ var assigned = assignedDomain();
2002
+ var custom = state.domain && looksLikeDomain(state.domain) ? state.domain : '';
2003
+ box.classList.toggle('optedOut', state.noAddress && !custom);
2004
+ var big = $('addrAssigned');
2005
+ var note = $('addrAssignedNote');
2006
+ if (custom) {
2007
+ big.innerHTML = '<code>' + esc(custom) + '</code> — your own domain, wired up for you.';
2008
+ note.textContent = 'DNS pointing happens after the standup; the card on your projects page walks you through it.';
2009
+ } else if (state.noAddress) {
2010
+ big.textContent = 'Starting without an address.';
2011
+ note.textContent = 'Sign-in on your project can’t be set up until it has one — add it any time from your projects page.';
2012
+ } else if (assigned) {
2013
+ big.innerHTML = '<code>' + esc(assigned) + '</code> — yours from the moment it stands up.';
2014
+ note.textContent = 'Sign-in, sharing, everything just works. You can point a domain you own at it any time.';
2015
+ } else if (!validSlug(state.slug)) {
2016
+ big.textContent = 'Pick a handle above and the address comes with it.';
2017
+ note.textContent = '';
2018
+ } else {
2019
+ /* dotless host (local dev): nothing sane to auto-assign — the domain
2020
+ field IS the step here, so unfold it (a folded disclosure made the
2021
+ opt-out checkbox unreachable-by-default; owner walk, 2026-08-12) */
2022
+ big.textContent = 'No address is assigned on this host.';
2023
+ note.textContent = 'Add a domain you own below, or start without one.';
2024
+ $('addrCustom').open = true;
2025
+ }
2026
+ }
2027
+ /* Pinned GitHub OAuth-app values for a domain (auth.js WEB_REDIRECT_ORIGIN —
2028
+ callback is the apex /api/gds/auth/web/callback). Mirrors onboard.oauthAppInstructions. */
2029
+ function oauthAppValues(domain) {
2030
+ var origin = 'https://' + domain;
2031
+ return { homepageUrl: origin, callbackUrl: origin + '/api/gds/auth/web/callback' };
2032
+ }
2033
+ /* GitHub's own "New repository" page, pre-filled with the owner + name. No auth or
2034
+ permissions on our side — the visitor creates the empty repo in GitHub's UI, which
2035
+ is exactly what the scaffold then clones. `owner` selects the account/org; omitting
2036
+ a blank param just leaves that field empty on GitHub. */
2037
+ function githubNewRepoUrl(owner, repo) {
2038
+ var u = new URL('https://github.com/new');
2039
+ if (repo) u.searchParams.set('name', repo);
2040
+ if (owner) u.searchParams.set('owner', owner);
2041
+ return u.toString();
2042
+ }
2043
+ function saveDraft() {
2044
+ try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
2045
+ }
2046
+
2047
+ /* ── step navigation ── */
2048
+ var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-home', 3: 'panel-address', 4: 'panel-review', 5: 'panel-done' };
2049
+ function show(step, moveFocus) {
2050
+ state.step = step;
2051
+ Object.keys(panels).forEach(function (k) {
2052
+ $(panels[k]).classList.toggle('on', Number(k) === step);
2053
+ });
2054
+ root.querySelectorAll('.steps li').forEach(function (li) {
2055
+ var n = Number(li.getAttribute('data-step'));
2056
+ li.classList.toggle('now', n === step);
2057
+ li.classList.toggle('done', n < step || step === 5);
2058
+ });
2059
+ saveDraft();
2060
+ /* the done step takes the whole card — the bongo pair steps aside */
2061
+ root.querySelector('.createSplit').classList.toggle('wizDone', step === 5);
2062
+ /* focus moves ONLY on a deliberate step (the hub rule setView already
2063
+ applies to this view: never on boot, never on a background settle) */
2064
+ if (moveFocus) {
2065
+ var h = $(panels[step]).querySelector('h1');
2066
+ if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false }); }
2067
+ }
2068
+ /* the repo probe is authenticated — never fire it before /me settles
2069
+ (a pre-auth probe 401s for strangers and, worse, could seed the
2070
+ draft's owner with myLogin()'s placeholder) */
2071
+ if (step === 2) { syncHome(); if (authKnown) ensureRepoAccess(); }
2072
+ if (step === 3) renderAddrPanel();
2073
+ if (step === 4) renderReview();
2074
+ }
2075
+
2076
+ /* ── field wiring ── */
2077
+ $('newName').value = state.name;
2078
+ $('f-owner').value = state.owner;
2079
+ $('f-repo').value = state.repo;
2080
+ $('f-slug').value = state.slug;
2081
+ $('f-domain').value = state.domain;
2082
+ $('f-noaddr').checked = !!state.noAddress;
2083
+
2084
+ /* Auto-fill the repo name from the project name until the visitor edits it
2085
+ directly (mirrors the handle's auto-derive). The owner is filled once we know
2086
+ who's signed in — see onAuth below. */
2087
+ if (!state.repoTouched && state.name && !state.repo) {
2088
+ state.repo = deriveRepo(state.name);
2089
+ $('f-repo').value = state.repo;
2090
+ }
2091
+
2092
+ $('newName').addEventListener('input', function () {
2093
+ state.name = this.value;
2094
+ if (!state.slugTouched) {
2095
+ state.slug = deriveSlug(state.name);
2096
+ $('f-slug').value = state.slug;
2097
+ }
2098
+ if (!state.repoTouched) {
2099
+ state.repo = deriveRepo(state.name);
2100
+ $('f-repo').value = state.repo;
2101
+ }
2102
+ var h = $('h-name');
2103
+ if (state.slug) { h.innerHTML = 'its handle on the platform: <code>' + esc(state.slug) + '</code>'; h.className = 'hint'; }
2104
+ else { h.innerHTML = '&nbsp;'; }
2105
+ saveDraft();
2106
+ });
2107
+ if (state.name && state.slug) $('h-name').innerHTML = 'its handle on the platform: <code>' + esc(state.slug) + '</code>';
2108
+
2109
+ function setMode(mode) {
2110
+ state.mode = mode;
2111
+ $('c-green').setAttribute('aria-pressed', String(mode === 'greenfield'));
2112
+ $('c-adopt').setAttribute('aria-pressed', String(mode === 'adopt'));
2113
+ $('adoptNote').classList.toggle('on', mode === 'adopt');
2114
+ saveDraft();
2115
+ syncHome();
2116
+ }
2117
+ $('c-green').addEventListener('click', function () { setMode('greenfield'); });
2118
+ $('c-adopt').addEventListener('click', function () { setMode('adopt'); });
2119
+ setMode(state.mode);
2120
+
2121
+ /* ── GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099 / ADR 0143.
2122
+ Progressive enhancement: probe GET /github/repos once we reach step 2.
2123
+ 200 → we hold repo access → show the picker (adopt) / name field (fresh);
2124
+ 403 → offer to grant repo access (a ?repos=1 re-sign-in);
2125
+ 503 (server can't store the token) or any error → the manual inputs.
2126
+ #manualHome (the classic owner/repo fields) is the always-safe fallback. ── */
2127
+ var repoAccess = null; /* null=unknown/loading · true · 'need-connect' · 'unavailable' · 'manual' */
2128
+ var repoList = []; /* the user's repos (from the probe) — feeds the picker + the fresh-name clash hint */
2129
+ var ghProbed = false;
2130
+
2131
+ function isManualHome() { return repoAccess === 'manual' || repoAccess === 'unavailable'; }
2132
+ function validRepoName(s) { return /^[A-Za-z0-9._-]{1,100}$/.test(String(s || '')); }
2133
+ function myLogin() { return (me && me.github_login) || 'you'; }
2134
+
2135
+ function connectRepos(e) {
2136
+ if (e) e.preventDefault();
2137
+ /* Same-tab navigation so the sessionStorage draft round-trips back to step 2. */
2138
+ window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1');
2139
+ }
2140
+ $('ghConnectBtn').addEventListener('click', connectRepos);
2141
+ $('ghManualToggle').addEventListener('click', function () { repoAccess = 'manual'; syncHome(); $('f-owner').focus(); });
2142
+
2143
+ function syncHome() {
2144
+ var need = repoAccess === 'need-connect';
2145
+ var ok = repoAccess === true;
2146
+ var adopt = state.mode === 'adopt';
2147
+ $('ghBusy').hidden = !!repoAccess; /* busy only while still probing (null) */
2148
+ $('ghConnect').classList.toggle('on', need); /* .reassure box: shown via .on (not hidden — .reassure sets display:none) */
2149
+ $('ghAdopt').hidden = !(ok && adopt);
2150
+ $('ghFresh').hidden = !(ok && !adopt);
2151
+ $('manualHome').hidden = !isManualHome();
2152
+ if (need) {
2153
+ /* Informed consent (ADR 0155): this grants `repo`, which GitHub's own screen
2154
+ words as full control of ALL your repositories — so say plainly why we ask
2155
+ and what we do with it, rather than letting GitHub's wording be the first
2156
+ the user hears of it. Private repos are the reason the scope is this wide. */
2157
+ $('ghConnectMsg').textContent = adopt
2158
+ ? 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — that is the only scope it offers that can write to a private repo). We use it to read the repo you pick and push the setup commit to it. Nothing else is touched.'
2159
+ : 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — the narrower option cannot write to private repos). We use it to create your new empty repo and push the setup commit to it. Nothing else is touched.';
2160
+ }
2161
+ if (ok && adopt) initAdoptPicker();
2162
+ if (ok && !adopt) syncFresh();
2163
+ }
2164
+
2165
+ function syncFresh() {
2166
+ /* Fresh + access: the repo is created under the signed-in account. Seed owner for
2167
+ the review/target now; the create response confirms the real full_name.
2168
+ Never before identity settles — myLogin()'s 'you' is display copy, and a
2169
+ draft that stored it would file target_ref "you/<repo>". */
2170
+ if (!me) return;
2171
+ state.owner = myLogin();
2172
+ $('ghNewName').value = state.repo || '';
2173
+ updateFreshPreview();
2174
+ }
2175
+ function updateFreshPreview() {
2176
+ var hint = $('ghFreshHint');
2177
+ var clash = state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
2178
+ if (state.repo && !validRepoName(state.repo)) {
2179
+ hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
2180
+ } else if (clash) {
2181
+ hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
2182
+ } else {
2183
+ /* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
2184
+ public by design (github-repos.js — open-source, capture-resistant); the
2185
+ private path is adopting a private repo the owner makes themselves. */
2186
+ hint.className = 'hint'; hint.innerHTML = 'We’ll create an empty <strong>public</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code> — projects here are open by design. Want private code? Create the repo yourself on GitHub and “adopt” it instead.';
2187
+ }
2188
+ }
2189
+ $('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
2190
+
2191
+ /* The adopt picker behaves like a standard combobox (owner design review, task
2192
+ 1002247): the dropdown is open only while the visitor is choosing. Selecting a
2193
+ repo fills the search box with its full_name and collapses the list; focusing or
2194
+ typing reopens it. */
2195
+ function openRepoList() { $('ghList').hidden = false; renderRepoList(); }
2196
+ function closeRepoList() { $('ghList').hidden = true; }
2197
+ function renderRepoList() {
2198
+ var q = ($('ghSearch').value || '').trim().toLowerCase();
2199
+ var sel = state.owner && state.repo ? (state.owner + '/' + state.repo) : '';
2200
+ /* When the box just shows the current selection verbatim, list everything (so the
2201
+ visitor can switch) instead of filtering down to the one selected repo. */
2202
+ var effectiveQ = q === sel.toLowerCase() ? '' : q;
2203
+ var rows = repoList.filter(function (r) { return !effectiveQ || String(r.full_name).toLowerCase().indexOf(effectiveQ) >= 0; }).slice(0, 100);
2204
+ $('ghList').innerHTML = rows.length
2205
+ ? rows.map(function (r) {
2206
+ return '<button type="button" class="ghrow" role="option" data-full="' + esc(r.full_name) + '" aria-selected="' + (r.full_name === sel) + '">' +
2207
+ '<span>' + esc(r.full_name) + '</span>' + (r.private ? '<span class="priv">Private</span>' : '') + '</button>';
2208
+ }).join('')
2209
+ : '<div class="hint" style="padding:10px 12px">No repositories match “' + esc(effectiveQ) + '”.</div>';
2210
+ }
2211
+ /* Fill the search box with the current selection (if any) and collapse the list;
2212
+ with nothing chosen yet, open it so the visitor sees their repos right away. */
2213
+ function initAdoptPicker() {
2214
+ var sel = state.owner && state.repo ? state.owner + '/' + state.repo : '';
2215
+ /* Only treat it as a confirmed pick if it's actually one of the visitor's repos.
2216
+ owner/repo are auto-derived from the project name, so a bare derived value must
2217
+ NOT masquerade as a chosen repo — that would collapse the list onto a repo that
2218
+ may not even exist. */
2219
+ var picked = sel && repoList.some(function (r) { return String(r.full_name).toLowerCase() === sel.toLowerCase(); });
2220
+ var h = $('ghAdoptHint');
2221
+ if (picked) {
2222
+ $('ghSearch').value = sel;
2223
+ h.className = 'hint ok'; h.innerHTML = 'Adopting <strong>' + esc(sel) + '</strong>.';
2224
+ closeRepoList();
2225
+ } else {
2226
+ $('ghSearch').value = '';
2227
+ h.className = 'hint'; h.innerHTML = '&nbsp;';
2228
+ openRepoList();
2229
+ }
2230
+ }
2231
+ $('ghSearch').addEventListener('focus', function () { this.select(); openRepoList(); });
2232
+ $('ghSearch').addEventListener('input', openRepoList);
2233
+ $('ghList').addEventListener('click', function (ev) {
2234
+ var row = ev.target.closest('.ghrow'); if (!row) return;
2235
+ var full = row.getAttribute('data-full'); var slash = full.indexOf('/');
2236
+ state.owner = full.slice(0, slash); state.repo = full.slice(slash + 1);
2237
+ state.ownerTouched = true; state.repoTouched = true; /* a real pick — don't let name-derive clobber it */
2238
+ saveDraft();
2239
+ $('ghSearch').value = full; /* pull the choice into the search box … */
2240
+ var h = $('ghAdoptHint'); h.className = 'hint ok'; h.innerHTML = 'Adopting <strong>' + esc(full) + '</strong>.';
2241
+ closeRepoList(); /* … and collapse the dropdown (typical UX) */
2242
+ });
2243
+
2244
+ function ensureRepoAccess() {
2245
+ if (ghProbed && repoAccess !== null) return; /* already known this session */
2246
+ if (isManualHome()) return; /* user chose manual — don't nag */
2247
+ repoAccess = null; syncHome(); /* show "reading your repos…" */
2248
+ fetch(API + '/github/repos', { credentials: 'same-origin' })
2249
+ .then(function (res) {
2250
+ ghProbed = true;
2251
+ if (res.status === 200) return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
2252
+ if (res.status === 403 || res.status === 401) { repoAccess = 'need-connect'; return; }
2253
+ repoAccess = 'unavailable'; return; /* 503 or anything else → manual fallback */
2254
+ })
2255
+ .catch(function () { ghProbed = true; repoAccess = 'unavailable'; })
2256
+ .then(syncHome);
2257
+ }
2258
+
2259
+ $('f-owner').addEventListener('input', function () { state.owner = this.value.trim(); state.ownerTouched = true; saveDraft(); });
2260
+ $('f-repo').addEventListener('input', function () { state.repo = this.value.trim(); state.repoTouched = true; saveDraft(); });
2261
+
2262
+ $('f-slug').addEventListener('input', function () {
2263
+ state.slugTouched = true;
2264
+ state.slug = this.value.trim().toLowerCase();
2265
+ this.value = state.slug;
2266
+ var h = $('h-slug');
2267
+ if (!state.slug) { h.innerHTML = '&nbsp;'; h.className = 'hint'; this.classList.remove('bad'); }
2268
+ else if (validSlug(state.slug)) { h.textContent = 'Nice — that works.'; h.className = 'hint ok'; this.classList.remove('bad'); }
2269
+ else { h.textContent = '2–40 lowercase letters, numbers, or dashes; no leading/trailing/double dash.'; h.className = 'hint err'; this.classList.add('bad'); }
2270
+ renderAddrPanel();
2271
+ saveDraft();
2272
+ });
2273
+ $('f-domain').addEventListener('input', function () {
2274
+ state.domain = this.value.trim().toLowerCase();
2275
+ var h = $('h-domain');
2276
+ if (!state.domain) { h.innerHTML = '&nbsp;'; h.className = 'hint'; this.classList.remove('bad'); }
2277
+ else if (looksLikeDomain(state.domain)) { h.textContent = 'Looks like a domain.'; h.className = 'hint ok'; this.classList.remove('bad'); }
2278
+ else { h.textContent = 'That doesn’t look like a domain yet (e.g. mercury.example.com).'; h.className = 'hint err'; this.classList.add('bad'); }
2279
+ renderAddrPanel();
2280
+ saveDraft();
2281
+ });
2282
+ $('f-noaddr').addEventListener('change', function () {
2283
+ state.noAddress = !!this.checked;
2284
+ renderAddrPanel();
2285
+ saveDraft();
2286
+ });
2287
+
2288
+ /* per-panel validation on Next */
2289
+ function validateStep(step) {
2290
+ if (step === 1) {
2291
+ if (!state.name.trim()) { $('h-name').textContent = 'Give it a name to start.'; $('h-name').className = 'hint err'; return false; }
2292
+ return true;
2293
+ }
2294
+ if (step === 2) {
2295
+ var homeErr = function (msg) { $('h-home').textContent = msg; $('h-home').className = 'hint err'; return false; };
2296
+ if (repoAccess === 'need-connect') return homeErr('Allow GitHub repo access to pick or create your repository — or choose “enter it manually”.');
2297
+ if (repoAccess === true && state.mode !== 'adopt') {
2298
+ /* Fresh with access: just the new repo NAME (owner is the signed-in account). */
2299
+ if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
2300
+ } else {
2301
+ /* Adopt (picker or manual) and the manual fallback: need a concrete owner + repo. */
2302
+ if (!state.owner || !state.repo) {
2303
+ return homeErr(state.mode === 'adopt' && repoAccess === true ? 'Pick one of your repositories.' : 'Both owner and repository name are needed.');
2304
+ }
2305
+ }
2306
+ $('h-home').innerHTML = '&nbsp;'; $('h-home').className = 'hint';
2307
+ return true;
2308
+ }
2309
+ if (step === 3) {
2310
+ if (!validSlug(state.slug)) {
2311
+ $('h-slug').textContent = '2–40 lowercase letters, numbers, or dashes; no leading/trailing/double dash.';
2312
+ $('h-slug').className = 'hint err'; $('f-slug').classList.add('bad');
2313
+ return false;
2314
+ }
2315
+ if (state.domain && !looksLikeDomain(state.domain)) return false;
2316
+ /* no auto-assigned address (dotless host): going domainless must be an
2317
+ EXPLICIT choice — a domain, or the opt-out box. Never silent. */
2318
+ if (!resolvedDomain() && !state.noAddress) {
2319
+ $('addrCustom').open = true;
2320
+ var hd = $('h-domain');
2321
+ hd.textContent = 'Add a domain you own — or tick “start without an address” to go without one.';
2322
+ hd.className = 'hint err';
2323
+ return false;
2324
+ }
2325
+ return true;
2326
+ }
2327
+ return true;
2328
+ }
2329
+
2330
+ root.querySelectorAll('.panel form').forEach(function (form) {
2331
+ form.addEventListener('submit', function (ev) {
2332
+ ev.preventDefault();
2333
+ var next = Number(form.getAttribute('data-next'));
2334
+ if (validateStep(next - 1)) show(next, true);
2335
+ });
2336
+ });
2337
+ root.querySelectorAll('[data-back]').forEach(function (btn) {
2338
+ btn.addEventListener('click', function () { show(Number(btn.getAttribute('data-back')), true); });
2339
+ });
2340
+
2341
+ /* ── review ── */
2342
+ function renderReview() {
2343
+ /* every road back to this step gets a live button — the create path
2344
+ disables it and only the failure branch used to restore it, which
2345
+ left "Start another project" walking into a dead Create */
2346
+ var cb = $('doCreate');
2347
+ cb.disabled = false; cb.textContent = 'Create my project';
2348
+ var rows = [
2349
+ ['Project', esc(state.name)],
2350
+ ['Starting from', state.mode === 'adopt' ? 'A repo you already have' : 'A fresh, empty repo'],
2351
+ ['Repository', '<code>' + esc(state.owner + '/' + state.repo) + '</code>'],
2352
+ ['Handle', '<code>' + esc(state.slug) + '</code>'],
2353
+ ['Address', resolvedDomain()
2354
+ ? '<code>' + esc(resolvedDomain()) + '</code>'
2355
+ : '<em style="font-weight:600;color:var(--warn)">none — sign-in stays locked until it has one</em>'],
2356
+ ['Shape', 'Standalone — your own repo, pinned platform core']
2357
+ ];
2358
+ $('reviewList').innerHTML = rows.map(function (r) {
2359
+ return '<li><span class="k">' + r[0] + '</span><span class="v">' + r[1] + '</span></li>';
2360
+ }).join('');
2361
+ }
2362
+
2363
+ /* ── create ── */
2364
+ function friendlyCreateError(err) {
2365
+ var code = err && err.data && err.data.error && (err.data.error.code || '');
2366
+ if (code === 'slug_taken') return 'That handle is already taken by another project. Pick a different one.';
2367
+ if (code === 'bad_slug') return 'The handle didn’t pass the server’s checks — 2–40 lowercase letters, numbers, or dashes.';
2368
+ return 'Couldn’t file the request: ' + (err && err.message ? err.message : 'something went sideways. Try again.');
2369
+ }
2370
+
2371
+ /* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
2372
+ then hand its real full_name to provisioning. No-op for adopt / manual / no-access.
2373
+ Idempotent across a retry — once created we reuse the stashed full_name so a second
2374
+ click doesn't trip GitHub's 422 "name already exists". Errors set err.gotoHome so
2375
+ the caller bounces the user back to the Home step to fix the name / reconnect. */
2376
+ function ensureFreshRepo() {
2377
+ if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
2378
+ if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
2379
+ return fetch(API + '/github/repos', {
2380
+ method: 'POST', credentials: 'same-origin',
2381
+ headers: { 'Content-Type': 'application/json' },
2382
+ body: JSON.stringify({ name: state.repo })
2383
+ }).then(function (res) {
2384
+ return res.json().catch(function () { return {}; }).then(function (data) {
2385
+ if (res.status === 401 || res.status === 403) {
2386
+ repoAccess = 'need-connect';
2387
+ var e0 = new Error('reconnect'); e0.gotoHome = true;
2388
+ e0.homeMsg = 'Your GitHub authorization expired — reconnect on the Home step to create the repo.';
2389
+ throw e0;
2390
+ }
2391
+ if (!res.ok) {
2392
+ var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
2393
+ e.data = data;
2394
+ if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
2395
+ throw e;
2396
+ }
2397
+ var full = data.repo && data.repo.full_name;
2398
+ if (full && full.indexOf('/') > 0) {
2399
+ state.owner = full.slice(0, full.indexOf('/'));
2400
+ state.repo = full.slice(full.indexOf('/') + 1);
2401
+ }
2402
+ state.repoAutoCreated = true; saveDraft();
2403
+ });
2404
+ });
2405
+ }
2406
+
2407
+ $('doCreate').addEventListener('click', function () {
2408
+ var btn = this;
2409
+ var alertBox = $('createAlert');
2410
+ alertBox.classList.remove('on');
2411
+ btn.disabled = true; btn.textContent = 'Raising it into the sky…';
2412
+ ensureFreshRepo().then(function () {
2413
+ return fetch(API + '/provisioning/instances', {
2414
+ method: 'POST', credentials: 'same-origin',
2415
+ headers: { 'Content-Type': 'application/json' },
2416
+ body: JSON.stringify({
2417
+ slug: state.slug,
2418
+ hosting_shape: 'standalone',
2419
+ domain: resolvedDomain() || undefined,
2420
+ target_ref: state.owner + '/' + state.repo,
2421
+ /* LOAD-BEARING for the control-plane runner, not just for the checklist we
2422
+ render (task 1002562): 'adopt' makes it clone + layer onto the repo's
2423
+ existing history instead of pushing a fresh tree, which a repo that already
2424
+ has commits rejects as unrelated history. state.mode already tracked
2425
+ greenfield|adopt for the UI — it has to reach the server too. */
2426
+ onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield'
2427
+ })
2428
+ });
2429
+ }).then(function (res) {
2430
+ if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew'); throw new Error('signing in'); }
2431
+ return res.json().catch(function () { return {}; }).then(function (data) {
2432
+ if (!res.ok) {
2433
+ var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
2434
+ e.data = data; throw e;
2435
+ }
2436
+ return data;
2437
+ });
2438
+ }).then(function (data) {
2439
+ try {
2440
+ sessionStorage.setItem(CREATED_KEY, JSON.stringify({
2441
+ id: data.instance && data.instance.id, slug: state.slug, domain: resolvedDomain(),
2442
+ mode: state.mode, name: state.name, at: Date.now()
2443
+ }));
2444
+ sessionStorage.removeItem(DRAFT_KEY);
2445
+ } catch (e) {}
2446
+ renderDone();
2447
+ show(5, true);
2448
+ startProgress(data.instance && data.instance.id);
2449
+ }).catch(function (err) {
2450
+ if (String(err && err.message) === 'signing in') return;
2451
+ btn.disabled = false; btn.textContent = 'Create my project';
2452
+ if (err && err.gotoHome) {
2453
+ /* A repo-creation problem (name taken / reconnect) — bounce back to the Home
2454
+ step with the reason, so the user fixes it where the repo is chosen. */
2455
+ show(2, true);
2456
+ $('h-home').textContent = err.homeMsg || 'Please revisit the repository step.';
2457
+ $('h-home').className = 'hint err';
2458
+ return;
2459
+ }
2460
+ alertBox.textContent = friendlyCreateError(err);
2461
+ alertBox.classList.add('on');
2462
+ });
2463
+ });
2464
+
2465
+ /* ── done panel: rendered FROM the canonical onboard plan (task 1002694, gap 2 /
2466
+ idea 1000675). GET /provisioning/onboard-plan is the single source the
2467
+ /new-project skill + `bongos onboard` read too, so the checklist can't
2468
+ drift. A per-key renderer adds this page's rich UI; an unknown human step
2469
+ falls back to the plan's own label, so a new canonical step surfaces here
2470
+ by default instead of silently vanishing. ── */
2471
+ function copyBtn(text) {
2472
+ return '<button type="button" class="copy" data-copy="' + esc(text) + '">Copy</button>';
2473
+ }
2474
+ var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><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"/></svg>';
2475
+
2476
+ var instanceId = null; /* set when a create lands / a resumed watch starts */
2477
+ var live = { instStatus: null, manifest: null, manifestMsg: '' };
2478
+ var planPromises = {}; /* mode|domain → Promise<steps[]> */
2479
+ var lastStepsHtml = '';
2480
+ var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall'];
2481
+
2482
+ function fetchPlan() {
2483
+ var domain = resolvedDomain() || '';
2484
+ var key = state.mode + '|' + domain;
2485
+ if (!planPromises[key]) {
2486
+ /* shape=standalone: this wizard only creates control-plane standalone
2487
+ instances, and the plan is shape-filtered server-side (direction §7) */
2488
+ planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + '&shape=standalone' + (domain ? '&domain=' + encodeURIComponent(domain) : ''))
2489
+ .then(function (res) { if (!res.ok) throw new Error('plan'); return res.json(); })
2490
+ .then(function (d) { return (d && d.steps) || []; })
2491
+ .catch(function () {
2492
+ delete planPromises[key]; /* transient — retried on the next render */
2493
+ return FALLBACK_HUMAN_KEYS.map(function (k) { return { key: k, kind: 'human', label: '' }; });
2494
+ });
2495
+ }
2496
+ return planPromises[key];
2497
+ }
2498
+
2499
+ /* github-repo — adopt / auto-created / manual-create, each aware of whether the
2500
+ scaffold has landed (instance active ⇒ the build already pushed). The wizard
2501
+ files onboard_mode to the server, so for adopt the RUNNER clones + layers
2502
+ (task 1002562) — the owner no longer runs `bongos init --adopt` here. */
2503
+ function renderStepGithubRepo() {
2504
+ var landed = live.instStatus === 'active';
2505
+ var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
2506
+ if (state.mode === 'adopt') {
2507
+ return landed
2508
+ ? '<li class="sdone" data-onboard-step="github-repo"><b>Your repo carries the platform</b><p>The build layered the platform onto ' + repoLabel + ' — existing files, history, and backlog kept. Pull to see it.</p></li>'
2509
+ : '<li data-onboard-step="github-repo"><b>Your existing repo</b><p>The build clones ' + repoLabel + ' and layers the platform onto its existing history — nothing is created or emptied, and your backlog is imported. Nothing for you to do here.</p></li>';
2510
+ }
2511
+ if (state.repoAutoCreated) {
2512
+ return landed
2513
+ ? '<li class="sdone" data-onboard-step="github-repo"><b>Repo ready — scaffold landed</b><p>We created ' + repoLabel + ' (public, by design) and the build pushed your project’s starting files to it.</p></li>'
2514
+ : '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLabel + ' on GitHub for you — public (projects here are open by design), empty, and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>';
2515
+ }
2516
+ var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
2517
+ return '<li' + (landed ? ' class="sdone"' : '') + ' data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. GitHub will ask public or private — your choice; both work here, and we never change it. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
2518
+ '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
2519
+ }
2520
+
2521
+ /* The manual OAuth-app path stays as a documented fallback (ADR 0133), but the
2522
+ control-plane placement is named as the operator's, never handed to the owner
2523
+ as a command to run (gap 4). */
2524
+ function manualOauthDetails() {
2525
+ var oa = oauthAppValues(resolvedDomain());
2526
+ return '<details><summary>Prefer to create an OAuth app by hand?</summary>' +
2527
+ '<p>GitHub → <em>your account’s</em> Settings → Developer settings → OAuth Apps → New OAuth App. (Developer settings live under your account, not the repo.)</p>' +
2528
+ '<div class="kv"><span class="kk">Homepage URL</span><code>' + esc(oa.homepageUrl) + '</code>' + copyBtn(oa.homepageUrl) + '</div>' +
2529
+ '<div class="kv"><span class="kk">Callback URL</span><code>' + esc(oa.callbackUrl) + '</code>' + copyBtn(oa.callbackUrl) + '</div>' +
2530
+ '<p>The generated credentials are placed on the platform’s control plane by an operator — that part isn’t yours to run (the one-click path above does it all automatically). It’s done when <code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '/healthz</code> reports <code style="font-family:var(--font-mono)">auth_configured: true</code>.</p></details>';
2531
+ }
2532
+
2533
+ /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
2534
+ (gap 3 / idea 1000676); locked until a domain exists (the App needs the real
2535
+ callback host — the server refuses domainless with domain_required). */
2536
+ function renderStepOauthApp() {
2537
+ var domain = resolvedDomain();
2538
+ if (!domain) {
2539
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from <a href="?view=mine">your projects page</a>.</p></li>';
2540
+ }
2541
+ /* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
2542
+ manifests whose hook URL isn't publicly reachable, so a locally-hosted
2543
+ platform can't complete the one-click flow — say it BEFORE the bounce. */
2544
+ if (!apexBase()) {
2545
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
2546
+ }
2547
+ if (live.manifest === 'done') {
2548
+ return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.</p></li>';
2549
+ }
2550
+ if (live.manifest === 'error' || live.manifest === 'expired') {
2551
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' +
2552
+ (live.manifest === 'expired' ? 'That setup link expired (they last 30 minutes) — mint a fresh one.' : 'GitHub setup hit a snag' + (live.manifestMsg ? ': ' + esc(live.manifestMsg) : '') + '. Try again.') + '</p>' +
2553
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
2554
+ }
2555
+ if (live.manifest) { /* pending / code_received — GitHub or the runner is mid-flight */
2556
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p></li>';
2557
+ }
2558
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
2559
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
2560
+ }
2561
+
2562
+ function renderStepFirstSignIn() {
2563
+ var domain = resolvedDomain();
2564
+ if (!domain) {
2565
+ return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks with sign-in above — you’ll open your project’s address, sign in with GitHub once, and be seated as the owner (the founding Archon).</p></li>';
2566
+ }
2567
+ return '<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <a href="https://' + esc(domain) + '" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '</code></a> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>';
2568
+ }
2569
+
2570
+ function renderStepEnterHall() {
2571
+ var domain = resolvedDomain();
2572
+ return domain
2573
+ ? '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Your project’s home — open <a href="https://' + esc(domain) + '/builders" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code></a> and start building.</p></li>'
2574
+ : '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>';
2575
+ }
2576
+
2577
+ var STEP_RENDERERS = {
2578
+ 'github-repo': renderStepGithubRepo,
2579
+ 'oauth-app': renderStepOauthApp,
2580
+ 'first-sign-in': renderStepFirstSignIn,
2581
+ 'enter-hall': renderStepEnterHall
2582
+ };
2583
+
2584
+ /* The newborn-instance welcome + account-nag suppression (direction §5.6,
2585
+ idea 1000701) is NOT implemented here by design: it lives on the CHILD
2586
+ instance's own surfaces (its hall greeting + needs roster), which this
2587
+ control-plane wizard cannot reach. Spun out per the direction's §9
2588
+ delivery-split as task 1002739. */
2589
+
2590
+ function renderSteps() {
2591
+ fetchPlan().then(function (steps) {
2592
+ var html = steps.filter(function (s) { return s && s.kind === 'human'; }).map(function (s) {
2593
+ var r = STEP_RENDERERS[s.key];
2594
+ return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
2595
+ }).join('');
2596
+ if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
2597
+ lastStepsHtml = html;
2598
+ $('humanSteps').innerHTML = html;
2599
+ });
2600
+ }
2601
+
2602
+ function renderDone() {
2603
+ var adopt = state.mode === 'adopt';
2604
+ $('doneLede').innerHTML = 'Provisioning is requested for <strong>' + esc(state.name || state.slug) + '</strong>' +
2605
+ (adopt ? ' — adopting your existing repo' : '') +
2606
+ '. The platform stands up the server, database, and address on its own — watch it right here, or any time on <a href="?view=mine">your projects page</a>.';
2607
+ renderSteps();
2608
+ }
2609
+
2610
+ /* The wizard's own copy links between views ("your projects page", the
2611
+ give-up hand-off) — delegated so innerHTML swaps keep them working, and
2612
+ routed through setView so a click can't tear down the page (and with it
2613
+ the very watch the copy promises to keep). Plain-click only: modified
2614
+ clicks stay with the browser, matching the hub's isPlainClick rule. */
2615
+ root.addEventListener('click', function (ev) {
2616
+ if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
2617
+ var a = ev.target.closest && ev.target.closest('a[href^="?view="]');
2618
+ if (!a || !root.contains(a)) return;
2619
+ ev.preventDefault();
2620
+ setView((a.getAttribute('href').match(/view=(\w+)/) || [])[1], true);
2621
+ });
2622
+
2623
+ /* One delegated listener — renderSteps swaps innerHTML, so handlers live on the list. */
2624
+ $('humanSteps').addEventListener('click', function (ev) {
2625
+ var mBtn = ev.target.closest('[data-action="manifest-start"]');
2626
+ if (mBtn) { startManifest(mBtn); return; }
2627
+ var t = ev.target.closest('.copy');
2628
+ if (!t) return;
2629
+ navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
2630
+ t.classList.add('did'); t.textContent = 'Copied';
2631
+ setTimeout(function () { t.classList.remove('did'); t.textContent = 'Copy'; }, 1600);
2632
+ });
2633
+ });
2634
+
2635
+ /* ── C8: the wizard's polls run only on stage, exactly like the sky.
2636
+ Off-view or hidden-tab, a due tick parks itself instead of fetching;
2637
+ enter() and the visibility flip resume whatever was parked. ── */
2638
+ function wizPollActive() {
2639
+ return !document.hidden && currentView === 'new';
2640
+ }
2641
+ function resumePolls() {
2642
+ if (watchPending && instanceId) { watchPending = false; startProgress(instanceId); }
2643
+ if (manifestPending && manifestState) { manifestPending = false; pollManifest(manifestState); }
2644
+ }
2645
+ document.addEventListener('visibilitychange', function () {
2646
+ if (wizPollActive()) resumePolls();
2647
+ });
2648
+
2649
+ /* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
2650
+ form-POST the pre-filled manifest to GitHub in a new tab (opened inside the
2651
+ click gesture so popup blockers allow it; blocked → same-tab, the callback
2652
+ page carries the rest), then poll the public status until the control-plane
2653
+ runner has placed + verified the credentials. ── */
2654
+ var manifestPollTimer = null;
2655
+ var manifestState = null; /* the one-time state token — kept so a parked poll can resume */
2656
+ var manifestPending = false;
2657
+ /* a cleared setTimeout id can't cancel a chain that's mid-fetch — each chain
2658
+ carries the generation it was born under, and stopping bumps it so an
2659
+ orphaned chain goes quiet instead of re-arming itself forever */
2660
+ var manifestGen = 0;
2661
+ function stopManifestPoll() { manifestGen += 1; if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
2662
+
2663
+ function startManifest(btn) {
2664
+ if (!instanceId) return;
2665
+ btn.disabled = true; btn.textContent = 'Preparing GitHub…';
2666
+ var tab = window.open('about:blank', 'ghAppSetup');
2667
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
2668
+ method: 'POST', credentials: 'same-origin',
2669
+ headers: { 'Content-Type': 'application/json' }, body: '{}'
2670
+ }).then(function (res) {
2671
+ if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew'); throw new Error('signing in'); }
2672
+ return res.json().catch(function () { return {}; }).then(function (d) {
2673
+ if (!res.ok) {
2674
+ var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
2675
+ throw e;
2676
+ }
2677
+ return d;
2678
+ });
2679
+ }).then(function (d) {
2680
+ var form = document.createElement('form');
2681
+ form.method = 'POST'; form.action = d.post_url; form.target = tab ? 'ghAppSetup' : '_self';
2682
+ var inp = document.createElement('input');
2683
+ inp.type = 'hidden'; inp.name = 'manifest'; inp.value = JSON.stringify(d.manifest);
2684
+ form.appendChild(inp); document.body.appendChild(form);
2685
+ form.submit(); form.remove();
2686
+ live.manifest = 'pending'; live.manifestMsg = '';
2687
+ renderSteps();
2688
+ pollManifest(d.state);
2689
+ }).catch(function (err) {
2690
+ if (String(err && err.message) === 'signing in') return;
2691
+ if (tab) { try { tab.close(); } catch (e) {} }
2692
+ live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
2693
+ renderSteps();
2694
+ });
2695
+ }
2696
+
2697
+ function pollManifest(stateToken) {
2698
+ stopManifestPoll();
2699
+ manifestState = stateToken;
2700
+ var myGen = manifestGen;
2701
+ function tick() {
2702
+ if (myGen !== manifestGen) return;
2703
+ if (!wizPollActive()) { manifestPending = true; stopManifestPoll(); return; }
2704
+ fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
2705
+ .then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
2706
+ .then(function (d) {
2707
+ if (myGen !== manifestGen) return;
2708
+ var next = d.ready ? 'done' : (d.status || 'pending');
2709
+ if (next !== live.manifest) { live.manifest = next; renderSteps(); }
2710
+ if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
2711
+ else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
2712
+ })
2713
+ .catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, 8000); });
2714
+ }
2715
+ tick();
2716
+ }
2717
+
2718
+ /* ── live build progress (task 2083): poll the instance until it's up.
2719
+ Read-only — GET /provisioning/instances/:id (own-scoped) every 5s. ── */
2720
+ var pollTimer = null;
2721
+ var watchPending = false; /* a parked progress watch, resumed on re-entry */
2722
+ var watchGen = 0; /* same orphaning contract as manifestGen above */
2723
+ function stopPolling() { watchGen += 1; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } }
2724
+ function progressMarkup(stageNow, stagesDone, note, cls) {
2725
+ var names = ['Request filed', 'Standing it up', 'Live'];
2726
+ return '<div class="prog ' + (cls || '') + '" role="status" aria-live="polite">' +
2727
+ '<ul class="stages">' + names.map(function (n, i) {
2728
+ var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
2729
+ return '<li class="' + c + '"><span class="sdot"></span><span class="slbl">' + n + '</span></li>';
2730
+ }).join('') + '</ul>' +
2731
+ '<p class="pnote">' + note + '</p></div>';
2732
+ }
2733
+ /* Renders the current instance state; returns true when the state is terminal
2734
+ (no further polling wanted). */
2735
+ function renderProgress(inst) {
2736
+ var el = $('liveProgress');
2737
+ if (!el) return true;
2738
+ var s = inst && inst.status;
2739
+ if (s === 'requested') {
2740
+ el.innerHTML = progressMarkup(1, 1, 'Waiting for a runner to pick it up. This updates the moment it starts.');
2741
+ } else if (s === 'provisioning') {
2742
+ el.innerHTML = progressMarkup(1, 1, 'Raising the server, database, and address — nothing needed from you.');
2743
+ } else if (s === 'active') {
2744
+ var addr = inst.domain ? 'https://' + inst.domain : null;
2745
+ el.innerHTML = progressMarkup(-1, 3,
2746
+ 'Your project is up' + (addr ? ' — <a href="' + esc(addr) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>' : '') +
2747
+ '. Finish the steps below and the hall is yours.', 'isLive');
2748
+ return true;
2749
+ } else if (s === 'error') {
2750
+ /* the honest snag (direction §5 / F19): no raw stderr as the headline, no
2751
+ false "keeps retrying" — retries stop, and the retry door is the mine view */
2752
+ el.innerHTML = progressMarkup(1, 1,
2753
+ 'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from <a href="?view=mine">your projects page</a>.', 'isErr');
2754
+ } else if (s === 'tearing_down' || s === 'torn_down') {
2755
+ /* a deliberate end is calm, never styled as a failure (direction §5) */
2756
+ el.innerHTML = progressMarkup(-1, 0, 'This instance was torn down. Bring it back any time from <a href="?view=mine">your projects page</a>, or start another below.');
2757
+ return true;
2758
+ } else {
2759
+ el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
2760
+ }
2761
+ return false;
2762
+ }
2763
+ function startProgress(id) {
2764
+ if (!id) return;
2765
+ instanceId = id; /* the manifest flow mints against this instance */
2766
+ stopPolling();
2767
+ watchPending = false;
2768
+ var myGen = watchGen;
2769
+ var ticks = 0;
2770
+ var el = $('liveProgress');
2771
+ el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
2772
+ function tick() {
2773
+ if (myGen !== watchGen) return;
2774
+ if (!wizPollActive()) { watchPending = true; stopPolling(); return; }
2775
+ ticks += 1;
2776
+ if (ticks > 400) {
2777
+ /* ~33 min: this tab stops watching, but the standup keeps going — hand
2778
+ the long watch to the mine view, the owner's durable surface (task
2779
+ 1002693). The old "stop quietly" here was a dead-end lie: the hall
2780
+ card it assumed was removed in task 1002675. */
2781
+ $('liveProgress').innerHTML = progressMarkup(1, 1,
2782
+ 'Still building — this tab will stop watching now. Follow it any time on <a href="?view=mine">your projects page</a>.');
2783
+ stopPolling();
2784
+ return;
2785
+ }
2786
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
2787
+ .then(function (res) {
2788
+ if (res.status === 401) { throw new Error('gone:Your session expired — <a href="' + API + '/auth/web/start?return=/projects%3Fview%3Dnew">sign in again</a> to keep watching.'); }
2789
+ if (res.status === 404) { throw new Error('gone:Can’t see this instance any more — it may have been torn down.'); }
2790
+ if (!res.ok) throw new Error('soft');
2791
+ return res.json();
2792
+ })
2793
+ .then(function (data) {
2794
+ if (myGen !== watchGen) return;
2795
+ var inst = data.instance;
2796
+ /* the checklist reflects live state (task 1002694): a status transition —
2797
+ e.g. the scaffold landing — re-renders the steps (change-only, so the
2798
+ 5s tick never clobbers an open dropdown or mid-flight button) */
2799
+ if (inst && inst.status !== live.instStatus) { live.instStatus = inst.status; renderSteps(); }
2800
+ var terminal = renderProgress(inst);
2801
+ if (!terminal) pollTimer = setTimeout(tick, 5000);
2802
+ else stopPolling();
2803
+ })
2804
+ .catch(function (err) {
2805
+ if (myGen !== watchGen) return;
2806
+ var m = String(err && err.message || '');
2807
+ if (m.indexOf('gone:') === 0) {
2808
+ $('liveProgress').innerHTML = progressMarkup(1, 1, m.slice(5), 'isErr');
2809
+ stopPolling();
2810
+ return;
2811
+ }
2812
+ /* transient (network / 5xx): keep the last view, try again a bit later */
2813
+ pollTimer = setTimeout(tick, 10000);
2814
+ });
2815
+ }
2816
+ tick();
2817
+ }
2818
+
2819
+ /* start another project: clear the stored instance + draft, back to step 1 */
2820
+ function wireAgain() {
2821
+ var again = document.createElement('button');
2822
+ again.type = 'button'; again.className = 'again'; again.textContent = 'Start another project';
2823
+ again.addEventListener('click', function () {
2824
+ stopPolling();
2825
+ stopManifestPoll();
2826
+ instanceId = null;
2827
+ watchPending = false; manifestPending = false; manifestState = null;
2828
+ live = { instStatus: null, manifest: null, manifestMsg: '' };
2829
+ lastStepsHtml = '';
2830
+ try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
2831
+ state = { step: 1, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
2832
+ $('newName').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
2833
+ $('f-noaddr').checked = false;
2834
+ /* Re-seed the owner from the signed-in account for the next project. */
2835
+ if (me && me.github_login) { state.owner = me.github_login; $('f-owner').value = state.owner; }
2836
+ $('h-name').innerHTML = '&nbsp;';
2837
+ setMode('greenfield');
2838
+ show(1, true);
2839
+ });
2840
+ $('panel-done').appendChild(again);
2841
+ }
2842
+ wireAgain();
2843
+
2844
+ /* ── the hub's three doors ── */
2845
+ function createdRecord() {
2846
+ try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) { return null; }
2847
+ }
2848
+ /* enter(focus) — paint the right step for whoever just arrived. Before the
2849
+ identity settles it shows the draft step optimistically (the page stays
2850
+ usable while /me is in flight — the boot's own rule); onAuth() re-enters
2851
+ once the answer lands. Focus only moves on a deliberate nav (the flag). */
2852
+ function enter(focus) {
2853
+ if (authKnown && !me) { show(0, !!focus); return; }
2854
+ var created = authKnown ? createdRecord() : null;
2855
+ if (created && created.id) {
2856
+ /* returning after a create — resume watching that instance instead of a
2857
+ blank wizard */
2858
+ state.name = created.name || created.slug; state.slug = created.slug || '';
2859
+ state.domain = created.domain || ''; state.mode = created.mode || 'greenfield';
2860
+ renderDone();
2861
+ show(5, !!focus);
2862
+ startProgress(created.id);
2863
+ resumePolls();
2864
+ return;
2865
+ }
2866
+ /* before identity settles, never render past the name step: deeper steps
2867
+ carry authenticated side effects (the step-2 repo probe), and onAuth()
2868
+ re-enters to the real draft step the moment /me answers */
2869
+ var step = authKnown && state.step >= 1 && state.step <= 4 ? state.step : 1;
2870
+ if (step === 1) {
2871
+ show(1, false);
2872
+ if (focus) $('newName').focus();
2873
+ } else {
2874
+ show(step, !!focus);
2875
+ }
2876
+ }
2877
+ /* leave() — park the polls (C8). The due tick would park itself anyway;
2878
+ this just stops the timers immediately so nothing fires off-stage. */
2879
+ function leave() {
2880
+ if (pollTimer) { stopPolling(); watchPending = true; }
2881
+ if (manifestPollTimer) { stopManifestPoll(); manifestPending = true; }
2882
+ }
2883
+ /* onAuth() — the boot's /me settled (either way). Seed the owner field for
2884
+ the common "my own account" case, then settle the view if the visitor is
2885
+ already standing on it. */
2886
+ function onAuth() {
2887
+ authKnown = true;
2888
+ if (me && !state.ownerTouched && !state.owner && me.github_login) {
2889
+ state.owner = me.github_login;
2890
+ $('f-owner').value = state.owner;
2891
+ saveDraft();
2892
+ }
2893
+ if (currentView === 'new') enter(false);
2894
+ }
2895
+
2896
+ return { enter: enter, leave: leave, onAuth: onAuth };
2897
+ })();
1559
2898
 
1560
2899
  /* ══ YOUR PROJECTS — search + the five state pills. The filter GROUPS the
1561
2900
  chip outcomes; it never relabels them, so "Live" holding both `live` and
@@ -1847,7 +3186,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
1847
3186
  body += stripFor(inst);
1848
3187
  if (gone) {
1849
3188
  body += '<div class="projMsg">Torn down' + (inst.torn_down_at ? ' ' + esc(shortDate(inst.torn_down_at)) : '') +
1850
- '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="/create">start another</a>.</div>';
3189
+ '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>';
1851
3190
  }
1852
3191
  var lm = lastMsg[inst.id];
1853
3192
  body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
@@ -2206,6 +3545,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
2206
3545
  me = data.builder || data.me || data;
2207
3546
  renderWho();
2208
3547
  paintShellIdentity();
3548
+ wizard.onAuth();
2209
3549
  $('gate').hidden = true;
2210
3550
  $('mine').hidden = false;
2211
3551
  load();
@@ -2217,12 +3557,16 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
2217
3557
  paintShellIdentity();
2218
3558
  $('gate').hidden = false;
2219
3559
  if (String(err && err.message) !== '401') {
2220
- var g = $('gateAlert');
2221
3560
  if (!navigator.onLine || String(err && err.message).indexOf('Failed') === 0) {
2222
- g.textContent = 'Can’t reach the platform right now — sign-in will work once you’re online with it.';
2223
- g.classList.add('on');
3561
+ /* both sign-in gates (mine + wizard) carry the same honest note */
3562
+ ['gateAlert', 'wizGateAlert'].forEach(function (id) {
3563
+ var g = $(id);
3564
+ g.textContent = 'Can’t reach the platform right now — sign-in will work once you’re online with it.';
3565
+ g.classList.add('on');
3566
+ });
2224
3567
  }
2225
3568
  }
3569
+ wizard.onAuth();
2226
3570
  })
2227
3571
  /* the catalog is PUBLIC, so it loads either way — the map is the view a
2228
3572
  signed-out stranger lands on and it must not depend on identity */