@bongos/core 1.19.113 → 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.
- package/.bongos-core.json +26 -31
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +1 -1
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -1
- package/docs/api/openapi.json +1 -1
- package/modules/provisioning/onboard-plan.js +2 -2
- package/modules/provisioning/routes/provisioning.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +31 -3
- package/public-landing/projects.html +1871 -50
- package/scripts/gds/audit-authorship.sh +0 -4
- package/scripts/gds/onboard.js +1 -1
- package/src/module-api.js +5 -1
- package/src/platform-server.js +13 -5
- package/tests/cosmos_token_contrast.mjs +10 -7
- package/tests/module-scope-map.mjs +1 -1
- package/tests/onboard.mjs +20 -16
- package/tests/platform_boot.mjs +21 -8
- package/tests/projects_hub.mjs +148 -11
- package/tests/public_landing_projects.mjs +15 -10
- package/public-landing/create.html +0 -1505
|
@@ -1,1505 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Create a project — Cloud Bongos</title>
|
|
7
|
-
<meta name="description" content="Stand up your own Cloud Bongos project — its own repo, its own home, built by agents.">
|
|
8
|
-
<!-- Fonts: the production instance also injects these from the branding pack
|
|
9
|
-
(config/branding.json theme.ui.fonts.links); kept here so the page reads
|
|
10
|
-
correctly when viewed standalone. -->
|
|
11
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&display=swap">
|
|
14
|
-
<!-- The R07 redesign (task 1002696): shared token + component layer — the space
|
|
15
|
-
sky both modes ride on, light/dark governing the cards. The server re-injects
|
|
16
|
-
the branding :root AFTER stylesheets, so the pack stays authoritative.
|
|
17
|
-
Design contract: docs/design/projects-surfaces-redesign-direction.md -->
|
|
18
|
-
<link rel="stylesheet" href="/assets/cosmos.css">
|
|
19
|
-
<script>
|
|
20
|
-
/* apply the saved color mode BEFORE first paint so there's no flash;
|
|
21
|
-
no saved choice = follow the OS (prefers-color-scheme) */
|
|
22
|
-
try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')document.documentElement.setAttribute('data-mode',_m);}catch(e){}
|
|
23
|
-
</script>
|
|
24
|
-
<style>
|
|
25
|
-
/* ── /create page-specific layout. Tokens, the reset, the sky, the header voice
|
|
26
|
-
and the shared card/chip/button vocabulary all come from cosmos.css above;
|
|
27
|
-
what stays here is this page's own structure (the wizard) plus token-built
|
|
28
|
-
variants of it.
|
|
29
|
-
DUAL MODE (task 1002696, owner decision 2026-08-12): light = the sunrise
|
|
30
|
-
white/orange page, dark = the gate's deep-space sky with dark-navy cards. The
|
|
31
|
-
card INTERIOR flips too — so no literal color may live in this block; every
|
|
32
|
-
surface here is a cosmos token or a color-mix over one. (A local :root token
|
|
33
|
-
block would land after cosmos's dark tiers and silently defeat them — that is
|
|
34
|
-
why the old one is gone.)
|
|
35
|
-
The /create page is the branded project-creation experience (goal 35, task 2081):
|
|
36
|
-
one page, no framework, the same trust boundary as the hall wizard — it only
|
|
37
|
-
ENQUEUES a provisioning request and then hands the human the steps only they
|
|
38
|
-
can do. ── */
|
|
39
|
-
/* The `hidden` attribute must always win. Several blocks below set an explicit
|
|
40
|
-
`display` via a class (.field→block, .row2→grid); an author rule like that
|
|
41
|
-
silently defeats the user-agent [hidden] rule, so `el.hidden = true` fails to
|
|
42
|
-
hide them. The Home step (task 1002228) toggles el.hidden to switch between the
|
|
43
|
-
adopt-picker / fresh-name / manual sub-panels — without this they ALL render at
|
|
44
|
-
once. Make hidden authoritative for the whole page. */
|
|
45
|
-
[hidden]{display:none !important;}
|
|
46
|
-
|
|
47
|
-
/* exit the wizard — Esc, or this button (task 2098). Nothing is created until the
|
|
48
|
-
final step, so leaving simply drops the in-progress project. */
|
|
49
|
-
.exit-wizard{
|
|
50
|
-
display:inline-flex;align-items:center;gap:7px;cursor:pointer;
|
|
51
|
-
background:var(--glass-bg);border:1.5px solid var(--glass-border);border-radius:999px;
|
|
52
|
-
padding:7px 14px;font-family:var(--font-body);font-weight:600;font-size:13px;color:var(--glass-ink);
|
|
53
|
-
text-shadow:var(--chrome-glow);backdrop-filter:blur(4px);
|
|
54
|
-
transition:background .14s ease,border-color .14s ease,color .14s ease;
|
|
55
|
-
}
|
|
56
|
-
.exit-wizard:hover{border-color:var(--chrome-accent);color:var(--chrome-accent);}
|
|
57
|
-
.exit-wizard kbd{font-family:var(--font-mono);font-size:10px;line-height:1;background:color-mix(in srgb,var(--chrome-ink) 12%,transparent);border-radius:4px;padding:2px 5px;color:var(--chrome-ink-dim);}
|
|
58
|
-
|
|
59
|
-
/* ── layout ── */
|
|
60
|
-
.wrap{
|
|
61
|
-
position:relative;z-index:1;max-width:1080px;margin:0 auto;
|
|
62
|
-
padding:clamp(8px,2vw,28px) clamp(16px,4vw,40px) 80px;
|
|
63
|
-
display:flex;gap:clamp(20px,4vw,48px);align-items:flex-start;
|
|
64
|
-
}
|
|
65
|
-
/* the guide — the Bongo Buddha walks you through it */
|
|
66
|
-
.guide{
|
|
67
|
-
flex:0 0 300px;position:sticky;top:24px;display:flex;flex-direction:column;align-items:center;gap:2px;
|
|
68
|
-
animation:rise .7s ease-out both;
|
|
69
|
-
}
|
|
70
|
-
.guide .mascot{position:relative;width:250px;}
|
|
71
|
-
.guide .mascot::before{
|
|
72
|
-
content:"";position:absolute;inset:-8% -12%;z-index:-1;border-radius:50%;
|
|
73
|
-
background:radial-gradient(ellipse at center, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.65) 45%, rgba(255,255,255,0) 72%);
|
|
74
|
-
}
|
|
75
|
-
.guide .mascot img{display:block;width:100%;height:auto;}
|
|
76
|
-
.bubble{
|
|
77
|
-
position:relative;background:var(--bg-card);border-radius:18px;padding:14px 18px;max-width:280px;
|
|
78
|
-
font-size:15px;font-weight:600;color:var(--ink-soft);text-align:center;line-height:1.45;
|
|
79
|
-
box-shadow:0 10px 30px -12px rgba(5,6,15,0.35);
|
|
80
|
-
}
|
|
81
|
-
.bubble::after{
|
|
82
|
-
content:"";position:absolute;top:-8px;left:50%;transform:translateX(-50%) rotate(45deg);
|
|
83
|
-
width:16px;height:16px;background:var(--bg-card);border-radius:3px;
|
|
84
|
-
}
|
|
85
|
-
.guide .bubble{order:-1;margin-bottom:16px;}
|
|
86
|
-
|
|
87
|
-
/* the wizard card — a cloud you build on */
|
|
88
|
-
.card{
|
|
89
|
-
flex:1;min-width:0;background:var(--card);border-radius:26px;
|
|
90
|
-
box-shadow:var(--shadow-card);
|
|
91
|
-
padding:clamp(22px,3.6vw,40px);
|
|
92
|
-
animation:rise .7s ease-out .08s both;
|
|
93
|
-
}
|
|
94
|
-
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
|
95
|
-
|
|
96
|
-
/* ── the beat line (progress) ── */
|
|
97
|
-
.beats{display:flex;align-items:flex-start;list-style:none;margin-bottom:30px;}
|
|
98
|
-
.beats li{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;position:relative;}
|
|
99
|
-
.beats li:not(:first-child)::before{
|
|
100
|
-
content:"";position:absolute;top:13px;right:50%;width:100%;height:3px;border-radius:2px;
|
|
101
|
-
background:var(--line);z-index:0;margin-right:14px;
|
|
102
|
-
}
|
|
103
|
-
.beats li.done:not(:first-child)::before{background:var(--accent);}
|
|
104
|
-
.beats .dot{
|
|
105
|
-
position:relative;z-index:1;width:28px;height:28px;border-radius:50%;
|
|
106
|
-
border:3px solid var(--line);background:var(--bg-card);display:flex;align-items:center;justify-content:center;
|
|
107
|
-
font-family:var(--font-display);font-weight:700;font-size:13px;color:var(--ink-faint);
|
|
108
|
-
transition:all .25s ease;
|
|
109
|
-
}
|
|
110
|
-
.beats li.now .dot{border-color:var(--accent);color:var(--accent-deep);box-shadow:0 0 0 6px color-mix(in srgb,var(--accent) 14%,transparent);}
|
|
111
|
-
.beats li.done .dot{border-color:var(--accent);background:var(--accent);color:#fff;}
|
|
112
|
-
.beats .lbl{font-size:12px;font-weight:700;color:var(--ink-faint);letter-spacing:0.2px;}
|
|
113
|
-
.beats li.now .lbl{color:var(--accent-deep);}
|
|
114
|
-
.beats li.done .lbl{color:var(--ink-soft);}
|
|
115
|
-
|
|
116
|
-
/* ── panels ── */
|
|
117
|
-
.panel{display:none;}
|
|
118
|
-
.panel.on{display:block;animation:fadeStep .3s ease both;}
|
|
119
|
-
@keyframes fadeStep{from{opacity:0;transform:translateX(10px)}to{opacity:1;transform:none}}
|
|
120
|
-
h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32px);letter-spacing:-0.5px;line-height:1.15;margin-bottom:8px;}
|
|
121
|
-
/* show() moves focus to each panel's h1 for keyboard/SR context — keep the
|
|
122
|
-
focus, lose the ring (a black rectangle around a heading; owner walk) */
|
|
123
|
-
h1[tabindex="-1"]:focus{outline:none;}
|
|
124
|
-
.lede{font-size:15.5px;color:var(--ink-soft);font-weight:500;line-height:1.55;margin-bottom:24px;max-width:56ch;}
|
|
125
|
-
|
|
126
|
-
.field{display:block;margin-bottom:18px;}
|
|
127
|
-
.field>span{display:block;font-size:13.5px;font-weight:700;color:var(--ink-soft);margin-bottom:7px;letter-spacing:0.2px;}
|
|
128
|
-
.field small{font-weight:500;color:var(--ink-faint);}
|
|
129
|
-
input[type=text]{
|
|
130
|
-
width:100%;font-family:var(--font-body);font-size:17px;font-weight:600;color:var(--ink);
|
|
131
|
-
background:var(--bg-card);border:2px solid var(--line);border-radius:14px;padding:13px 16px;outline:none;
|
|
132
|
-
transition:border-color .15s ease, box-shadow .15s ease;
|
|
133
|
-
}
|
|
134
|
-
/* no fade toward the card — a 70% mix washed placeholders to 2.89:1 (task 1002749) */
|
|
135
|
-
input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
|
|
136
|
-
input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
|
|
137
|
-
input[type=text].bad{border-color:var(--err);}
|
|
138
|
-
.hint{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:7px;min-height:18px;}
|
|
139
|
-
.hint.ok{color:var(--ok);}
|
|
140
|
-
.hint.err{color:var(--err);}
|
|
141
|
-
.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);}
|
|
142
|
-
|
|
143
|
-
.row2{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
|
|
144
|
-
|
|
145
|
-
/* choice cards (greenfield / adopt) */
|
|
146
|
-
.choices{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:18px;}
|
|
147
|
-
.choice{
|
|
148
|
-
text-align:left;cursor:pointer;background:var(--bg-card);border:2px solid var(--line);border-radius:18px;
|
|
149
|
-
padding:16px;font-family:var(--font-body);transition:all .15s ease;
|
|
150
|
-
}
|
|
151
|
-
.choice:hover{border-color:var(--rule);transform:translateY(-1px);}
|
|
152
|
-
.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);}
|
|
153
|
-
.choice svg{display:block;margin-bottom:9px;}
|
|
154
|
-
.choice b{display:block;font-size:15.5px;font-weight:700;margin-bottom:3px;}
|
|
155
|
-
.choice p{font-size:13px;font-weight:500;color:var(--ink-soft);line-height:1.45;}
|
|
156
|
-
.reassure{
|
|
157
|
-
display:none;background:color-mix(in srgb,var(--sea) 8%,var(--bg-card));
|
|
158
|
-
border:1.5px solid color-mix(in srgb,var(--sea) 25%,var(--bg-card));border-radius:14px;
|
|
159
|
-
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;
|
|
160
|
-
}
|
|
161
|
-
.reassure.on{display:block;}
|
|
162
|
-
|
|
163
|
-
/* one-click deep link to GitHub's own "New repository" page with the name
|
|
164
|
-
pre-filled (greenfield only). No permissions — the visitor creates the empty
|
|
165
|
-
repo in GitHub's own UI; we just fill in the blanks. */
|
|
166
|
-
.ghbtn{
|
|
167
|
-
display:inline-flex;align-items:center;gap:9px;text-decoration:none;cursor:pointer;
|
|
168
|
-
background:#24292f;color:#fff;border:none;border-radius:999px;
|
|
169
|
-
font-family:var(--font-body);font-weight:700;font-size:15px;padding:12px 22px;
|
|
170
|
-
box-shadow:0 12px 26px -12px rgba(36,41,47,0.55);
|
|
171
|
-
transition:transform .12s ease, background .12s ease;
|
|
172
|
-
}
|
|
173
|
-
.ghbtn:hover{background:#1b1f24;transform:translateY(-1px);}
|
|
174
|
-
.ghbtn:active{transform:translateY(0) scale(.98);}
|
|
175
|
-
.ghbtn .gh{width:18px;height:18px;fill:currentColor;}
|
|
176
|
-
.steps .ghbtn{font-size:14px;padding:9px 16px;margin-top:10px;}
|
|
177
|
-
|
|
178
|
-
/* buttons */
|
|
179
|
-
.nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:26px;}
|
|
180
|
-
.btn{
|
|
181
|
-
font-family:var(--font-body);font-weight:700;font-size:16.5px;cursor:pointer;
|
|
182
|
-
border:none;border-radius:999px;padding:14px 30px;text-decoration:none;
|
|
183
|
-
display:inline-flex;align-items:center;gap:9px;
|
|
184
|
-
background:var(--accent);color:#fff;box-shadow:0 12px 28px -10px var(--accent);
|
|
185
|
-
transition:transform .12s ease, box-shadow .12s ease, background .12s ease;
|
|
186
|
-
}
|
|
187
|
-
.btn:hover{transform:translateY(-1px);box-shadow:0 16px 34px -10px var(--accent);}
|
|
188
|
-
.btn:active{transform:translateY(0) scale(.98);}
|
|
189
|
-
.btn:disabled{background:color-mix(in srgb,var(--ink) 12%,var(--bg-deep));color:color-mix(in srgb,var(--ink) 40%,var(--bg-deep));box-shadow:none;cursor:not-allowed;transform:none;}
|
|
190
|
-
.btn.ghost{background:transparent;color:var(--ink-soft);box-shadow:none;border:2px solid var(--line);padding:12px 22px;}
|
|
191
|
-
.btn.ghost:hover{border-color:var(--rule-soft);color:var(--ink);}
|
|
192
|
-
.btn.big{width:100%;justify-content:center;font-size:18px;padding:17px 30px;}
|
|
193
|
-
.gh{width:20px;height:20px;fill:currentColor;}
|
|
194
|
-
|
|
195
|
-
.alert{
|
|
196
|
-
display:none;background:var(--err-soft);border:1.5px solid color-mix(in srgb,var(--err) 25%,transparent);border-radius:14px;
|
|
197
|
-
padding:12px 15px;font-size:14px;font-weight:600;color:var(--err);line-height:1.5;margin-top:16px;
|
|
198
|
-
}
|
|
199
|
-
.alert.on{display:block;}
|
|
200
|
-
|
|
201
|
-
/* review list */
|
|
202
|
-
.review{list-style:none;border:2px solid var(--line);border-radius:18px;overflow:hidden;margin-bottom:6px;}
|
|
203
|
-
.review li{display:flex;justify-content:space-between;gap:16px;padding:13px 17px;font-size:14.5px;}
|
|
204
|
-
.review li+li{border-top:1.5px solid var(--line);}
|
|
205
|
-
.review .k{font-weight:600;color:var(--ink-faint);flex:0 0 auto;}
|
|
206
|
-
.review .v{font-weight:700;text-align:right;word-break:break-word;}
|
|
207
|
-
.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);}
|
|
208
|
-
.finePrint{font-size:12.5px;font-weight:500;color:var(--ink-faint);line-height:1.5;margin-top:14px;}
|
|
209
|
-
|
|
210
|
-
/* ── done / human steps ── */
|
|
211
|
-
.doneHead{display:flex;align-items:center;gap:12px;margin-bottom:8px;}
|
|
212
|
-
.doneHead .tick{
|
|
213
|
-
flex:0 0 44px;width:44px;height:44px;border-radius:50%;background:var(--ok);color:#fff;
|
|
214
|
-
display:flex;align-items:center;justify-content:center;
|
|
215
|
-
animation:pop .45s cubic-bezier(.34,1.56,.64,1) both;
|
|
216
|
-
}
|
|
217
|
-
@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
|
|
218
|
-
.steps{list-style:none;counter-reset:hs;margin-top:20px;}
|
|
219
|
-
.steps>li{
|
|
220
|
-
position:relative;background:var(--bg-card);border:2px solid var(--line);border-radius:18px;
|
|
221
|
-
padding:16px 18px 16px 58px;margin-bottom:12px;counter-increment:hs;
|
|
222
|
-
}
|
|
223
|
-
.steps>li::before{
|
|
224
|
-
content:counter(hs);position:absolute;left:16px;top:16px;width:28px;height:28px;border-radius:50%;
|
|
225
|
-
background:var(--bg-deep);color:var(--accent-deep);font-family:var(--font-display);font-weight:700;
|
|
226
|
-
font-size:14px;display:flex;align-items:center;justify-content:center;
|
|
227
|
-
}
|
|
228
|
-
.steps b{font-size:15px;}
|
|
229
|
-
.steps p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.55;margin-top:4px;}
|
|
230
|
-
/* live checklist states (task 1002694): a step the platform has verified done, and a
|
|
231
|
-
step that can't be performed yet (its prerequisite — usually the domain — is missing) */
|
|
232
|
-
.steps>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%);}
|
|
233
|
-
.steps>li.sdone::before{content:"✓";background:var(--ok);color:#fff;}
|
|
234
|
-
.steps>li.slocked{opacity:.6;}
|
|
235
|
-
.steps details{margin-top:10px;}
|
|
236
|
-
.steps summary{cursor:pointer;font-weight:700;font-size:12.5px;color:var(--ink-faint);}
|
|
237
|
-
.steps details p{margin-top:8px;}
|
|
238
|
-
.kv{
|
|
239
|
-
display:flex;align-items:center;gap:8px;margin-top:9px;background:var(--bg);border:1.5px solid var(--line);
|
|
240
|
-
border-radius:11px;padding:8px 8px 8px 12px;font-size:12.5px;
|
|
241
|
-
}
|
|
242
|
-
.kv .kk{font-weight:700;color:var(--ink-faint);flex:0 0 auto;}
|
|
243
|
-
.kv code{font-family:var(--font-mono);font-size:12px;color:var(--accent-deep);word-break:break-all;flex:1;}
|
|
244
|
-
.copy{
|
|
245
|
-
flex:0 0 auto;cursor:pointer;border:none;border-radius:8px;background:var(--bg-card);border:1.5px solid var(--line);
|
|
246
|
-
font-family:var(--font-body);font-size:11.5px;font-weight:700;color:var(--ink-soft);padding:5px 10px;
|
|
247
|
-
transition:all .12s ease;
|
|
248
|
-
}
|
|
249
|
-
.copy:hover{border-color:var(--accent);color:var(--accent-deep);}
|
|
250
|
-
.copy.did{background:var(--ok);border-color:var(--ok);color:#fff;}
|
|
251
|
-
.samePath{font-size:12.5px;font-weight:500;color:var(--ink-faint);margin-top:16px;}
|
|
252
|
-
#liveProgress{margin-top:4px;}
|
|
253
|
-
|
|
254
|
-
/* ── live build progress (task 2083) ── */
|
|
255
|
-
.prog{
|
|
256
|
-
background:linear-gradient(180deg,color-mix(in srgb,var(--sea) 8%,var(--bg-card)) 0%,var(--bg-card) 100%);
|
|
257
|
-
border:2px solid var(--sea-soft);border-radius:18px;padding:18px 20px 16px;margin-top:14px;
|
|
258
|
-
}
|
|
259
|
-
.prog .stages{display:flex;list-style:none;margin-bottom:4px;}
|
|
260
|
-
.prog .stages li{flex:1;display:flex;flex-direction:column;align-items:center;gap:7px;position:relative;}
|
|
261
|
-
.prog .stages li:not(:first-child)::before{
|
|
262
|
-
content:"";position:absolute;top:11px;right:50%;width:100%;height:3px;border-radius:2px;
|
|
263
|
-
background:var(--sea-soft);z-index:0;margin-right:12px;
|
|
264
|
-
}
|
|
265
|
-
.prog .stages li.done:not(:first-child)::before{background:var(--sea);}
|
|
266
|
-
.prog .sdot{
|
|
267
|
-
position:relative;z-index:1;width:24px;height:24px;border-radius:50%;
|
|
268
|
-
border:3px solid var(--sea-soft);background:var(--bg-card);display:flex;align-items:center;justify-content:center;
|
|
269
|
-
}
|
|
270
|
-
.prog .stages li.done .sdot{border-color:var(--sea);background:var(--sea);}
|
|
271
|
-
.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);}
|
|
272
|
-
.prog .stages li.now .sdot{border-color:var(--sea);}
|
|
273
|
-
.prog .stages li.now .sdot::after{
|
|
274
|
-
content:"";width:8px;height:8px;border-radius:50%;background:var(--sea);
|
|
275
|
-
animation:beatPulse 1.2s ease-in-out infinite;
|
|
276
|
-
}
|
|
277
|
-
@keyframes beatPulse{0%,100%{transform:scale(.7);opacity:.6}50%{transform:scale(1.15);opacity:1}}
|
|
278
|
-
.prog .slbl{font-size:12px;font-weight:700;color:color-mix(in srgb,var(--sea) 55%,var(--ink-faint));}
|
|
279
|
-
.prog .stages li.now .slbl,.prog .stages li.done .slbl{color:color-mix(in srgb,var(--sea) 40%,var(--ink));}
|
|
280
|
-
.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;}
|
|
281
|
-
.prog .pnote a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
|
|
282
|
-
.prog .pnote a:hover{text-decoration:underline;}
|
|
283
|
-
.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));}
|
|
284
|
-
.prog.isLive .stages li.done:not(:first-child)::before{background:var(--ok);}
|
|
285
|
-
.prog.isLive .stages li.done .sdot{border-color:var(--ok);background:var(--ok);}
|
|
286
|
-
.prog.isLive .slbl{color:var(--ok);}
|
|
287
|
-
.prog.isLive .stages li.done .slbl{color:color-mix(in srgb,var(--ok) 75%,var(--ink));}
|
|
288
|
-
.prog.isLive .pnote{color:color-mix(in srgb,var(--ok) 75%,var(--ink));font-size:14px;}
|
|
289
|
-
.prog.isErr{background:var(--err-soft);border-color:color-mix(in srgb,var(--err) 25%,var(--bg-card));}
|
|
290
|
-
.prog.isErr .pnote{color:var(--err);}
|
|
291
|
-
.again{
|
|
292
|
-
display:inline-block;margin-top:14px;font-size:13px;font-weight:700;color:var(--ink-faint);
|
|
293
|
-
background:none;border:none;cursor:pointer;font-family:var(--font-body);text-decoration:underline;
|
|
294
|
-
text-underline-offset:3px;padding:0;
|
|
295
|
-
}
|
|
296
|
-
.again:hover{color:var(--accent-deep);}
|
|
297
|
-
|
|
298
|
-
/* the collapsed address step — a confirmation, not a form (task 1002704) */
|
|
299
|
-
.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;}
|
|
300
|
-
.addrConfirm.optedOut{border-color:var(--line);background:var(--bg);opacity:.75;}
|
|
301
|
-
.addrBig{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;line-height:1.4;}
|
|
302
|
-
.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;}
|
|
303
|
-
.addrConfirm>p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.5;margin-top:6px;}
|
|
304
|
-
.addrConfirm details{margin-top:12px;}
|
|
305
|
-
.addrConfirm summary{cursor:pointer;font-size:13px;font-weight:700;color:var(--ink-faint);}
|
|
306
|
-
.addrConfirm details .inner{margin-top:10px;}
|
|
307
|
-
.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;}
|
|
308
|
-
.addrOpt input{margin-top:2px;}
|
|
309
|
-
|
|
310
|
-
/* ── responsive ── */
|
|
311
|
-
@media (max-width:860px){
|
|
312
|
-
.wrap{flex-direction:column;gap:8px;}
|
|
313
|
-
.guide{position:static;flex:none;width:100%;flex-direction:row;justify-content:flex-start;gap:14px;padding:0 4px 14px;}
|
|
314
|
-
.guide .mascot{width:92px;flex:0 0 92px;}
|
|
315
|
-
.guide .bubble{order:0;margin-bottom:0;text-align:left;font-size:14px;}
|
|
316
|
-
.bubble::after{top:50%;left:-8px;transform:translateY(-50%) rotate(45deg);}
|
|
317
|
-
.row2,.choices{grid-template-columns:1fr;}
|
|
318
|
-
}
|
|
319
|
-
@media (prefers-reduced-motion:reduce){
|
|
320
|
-
*,*::before,*::after{animation:none !important;transition:none !important;}
|
|
321
|
-
}
|
|
322
|
-
/* GitHub repo picker (task 1002099) */
|
|
323
|
-
.linklike{background:none;border:0;padding:0;margin-left:12px;color:var(--accent-deep);font:inherit;font-size:13px;cursor:pointer;text-decoration:underline;}
|
|
324
|
-
#ghConnect .btn{margin-top:4px;}
|
|
325
|
-
.ghlist{margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--line);border-radius:10px;background:var(--bg-deep);}
|
|
326
|
-
.ghlist:empty{display:none;}
|
|
327
|
-
/* rows sit on --bg-deep, so their tints mix over THAT, not the card */
|
|
328
|
-
.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;}
|
|
329
|
-
.ghrow:last-child{border-bottom:0;}
|
|
330
|
-
.ghrow:hover{background:color-mix(in srgb,var(--accent) 6%,var(--bg-deep));}
|
|
331
|
-
.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;}
|
|
332
|
-
/* --ink-soft, not --ink-faint: the badge tint is the darkest ground on the page,
|
|
333
|
-
and faint ink only reaches 3.96:1 on it (task 1002749). */
|
|
334
|
-
.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;}
|
|
335
|
-
</style>
|
|
336
|
-
</head>
|
|
337
|
-
<body>
|
|
338
|
-
<div class="sky" aria-hidden="true">
|
|
339
|
-
<span class="star" style="top:3%;left:14%;width:2px;height:2px;"></span>
|
|
340
|
-
<span class="star" style="top:6%;left:32%;width:1.5px;height:1.5px;animation-delay:.7s;"></span>
|
|
341
|
-
<span class="star" style="top:4%;left:52%;width:2px;height:2px;animation-delay:1.4s;"></span>
|
|
342
|
-
<span class="star" style="top:9%;left:70%;width:1.5px;height:1.5px;animation-delay:.3s;"></span>
|
|
343
|
-
<span class="star" style="top:5%;left:86%;width:2.5px;height:2.5px;animation-delay:2s;"></span>
|
|
344
|
-
<span class="star" style="top:16%;left:8%;width:1.5px;height:1.5px;animation-delay:1s;"></span>
|
|
345
|
-
<span class="star" style="top:24%;left:44%;width:2px;height:2px;animation-delay:1.8s;"></span>
|
|
346
|
-
<span class="star" style="top:32%;left:80%;width:1.5px;height:1.5px;animation-delay:.5s;"></span>
|
|
347
|
-
<span class="star" style="top:46%;left:18%;width:1.5px;height:1.5px;animation-delay:2.4s;"></span>
|
|
348
|
-
<span class="star" style="top:58%;left:62%;width:2px;height:2px;animation-delay:1.2s;"></span>
|
|
349
|
-
<span class="star" style="top:72%;left:36%;width:1.5px;height:1.5px;animation-delay:.9s;"></span>
|
|
350
|
-
<span class="star" style="top:84%;left:74%;width:1.5px;height:1.5px;animation-delay:1.6s;"></span>
|
|
351
|
-
</div>
|
|
352
|
-
|
|
353
|
-
<header class="top">
|
|
354
|
-
<a class="mark" href="/"><span data-brand-product>cloud bongos</span></a>
|
|
355
|
-
<div class="top-right">
|
|
356
|
-
<button type="button" class="exit-wizard" id="exitWizard" title="Leave the wizard — nothing is created">Exit <kbd>esc</kbd></button>
|
|
357
|
-
<button type="button" class="mode-toggle" id="modeToggle" title="Switch light / dark" aria-label="Switch light or dark mode">☾</button>
|
|
358
|
-
<div class="who" id="who" aria-live="polite"></div>
|
|
359
|
-
</div>
|
|
360
|
-
</header>
|
|
361
|
-
|
|
362
|
-
<div class="wrap">
|
|
363
|
-
<aside class="guide" aria-hidden="false">
|
|
364
|
-
<div class="bubble"><span id="guideLine">Welcome, friend. Sign in and we'll lay down the first beat.</span></div>
|
|
365
|
-
<div class="mascot">
|
|
366
|
-
<img src="/buddha.png" alt="The laughing Cloud Bongos buddha, your guide" onerror="this.parentElement.style.display='none'">
|
|
367
|
-
</div>
|
|
368
|
-
</aside>
|
|
369
|
-
|
|
370
|
-
<main class="card">
|
|
371
|
-
<ol class="beats" id="beats" aria-label="Creation steps">
|
|
372
|
-
<li data-step="1"><span class="dot">1</span><span class="lbl">Name</span></li>
|
|
373
|
-
<li data-step="2"><span class="dot">2</span><span class="lbl">Home</span></li>
|
|
374
|
-
<li data-step="3"><span class="dot">3</span><span class="lbl">Address</span></li>
|
|
375
|
-
<li data-step="4"><span class="dot">4</span><span class="lbl">Create</span></li>
|
|
376
|
-
</ol>
|
|
377
|
-
|
|
378
|
-
<!-- step 0 · sign-in gate -->
|
|
379
|
-
<section class="panel" id="panel-gate" aria-labelledby="gate-h">
|
|
380
|
-
<h1 id="gate-h">Create a project</h1>
|
|
381
|
-
<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>
|
|
382
|
-
<!-- The wizard's own sign-in opts into repo scope (read:user + public_repo) up
|
|
383
|
-
front (&repos=1) — anyone here intends to create a project, which needs repo
|
|
384
|
-
access, so we ask once at sign-in instead of a second "Connect" step at Home.
|
|
385
|
-
Hall + CLI sign-in stay identity-only (ADR 0143 least-privilege default). -->
|
|
386
|
-
<a class="btn big" id="signin" href="/api/bongos/auth/web/start?return=/create&repos=1">
|
|
387
|
-
<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>
|
|
388
|
-
Sign in with GitHub
|
|
389
|
-
</a>
|
|
390
|
-
<!-- Trust link → the AGPL license text (task 1002705): the old private-repo URL
|
|
391
|
-
404ed, and no public code artifact exists until the ADR 0099 mirror flips
|
|
392
|
-
(~2026-11; npm scopes are private too) — the license is the one live,
|
|
393
|
-
instance-neutral artifact behind the claim today. -->
|
|
394
|
-
<p class="finePrint">Free while we find our feet. Your project's repo and server are yours — Cloud Bongos is <a href="https://www.gnu.org/licenses/agpl-3.0.html" style="color:inherit" target="_blank" rel="noopener">open source (AGPL-3.0)</a> and capture-resistant by design.</p>
|
|
395
|
-
<div class="alert" id="gateAlert" role="alert"></div>
|
|
396
|
-
</section>
|
|
397
|
-
|
|
398
|
-
<!-- step 1 · name -->
|
|
399
|
-
<section class="panel" id="panel-name" aria-labelledby="name-h">
|
|
400
|
-
<h1 id="name-h">Name your project</h1>
|
|
401
|
-
<p class="lede">The name leads everything that follows — the world your builders talk about, the title on the hall door. You can rename later; the beat just needs somewhere to start.</p>
|
|
402
|
-
<form data-next="2">
|
|
403
|
-
<label class="field"><span>Project name</span>
|
|
404
|
-
<input type="text" id="f-name" placeholder="Mercury" autocomplete="off" maxlength="80">
|
|
405
|
-
<div class="hint" id="h-name"> </div>
|
|
406
|
-
</label>
|
|
407
|
-
<div class="nav">
|
|
408
|
-
<span></span>
|
|
409
|
-
<button type="submit" class="btn">Next →</button>
|
|
410
|
-
</div>
|
|
411
|
-
</form>
|
|
412
|
-
</section>
|
|
413
|
-
|
|
414
|
-
<!-- step 2 · home (repo + mode) -->
|
|
415
|
-
<section class="panel" id="panel-home" aria-labelledby="home-h">
|
|
416
|
-
<h1 id="home-h">Where the code lives</h1>
|
|
417
|
-
<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>
|
|
418
|
-
<form data-next="3">
|
|
419
|
-
<div class="choices" role="group" aria-label="Starting point">
|
|
420
|
-
<button type="button" class="choice" id="c-green" aria-pressed="true">
|
|
421
|
-
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" aria-hidden="true"><path d="M15 26V14" stroke="#f97316" 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="#fdba74" stroke="#f97316" 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="#fed7aa" stroke="#f97316" stroke-width="2"/></svg>
|
|
422
|
-
<b>Start fresh</b>
|
|
423
|
-
<p>A new, empty <strong>public</strong> repo. We scaffold everything: config, first version, kickoff tasks.</p>
|
|
424
|
-
</button>
|
|
425
|
-
<button type="button" class="choice" id="c-adopt" aria-pressed="false">
|
|
426
|
-
<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="#fed7aa" stroke="#f97316" stroke-width="2"/><path d="M4 13h22" stroke="#f97316" 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="#f97316" stroke-width="2"/><path d="M12 19h6" stroke="#ea580c" stroke-width="2.4" stroke-linecap="round"/></svg>
|
|
427
|
-
<b>Adopt a repo I have</b>
|
|
428
|
-
<p>Layer Cloud Bongos onto an existing repository — history, code, and backlog intact. Public or private, we never change it.</p>
|
|
429
|
-
</button>
|
|
430
|
-
</div>
|
|
431
|
-
<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>
|
|
432
|
-
|
|
433
|
-
<!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
|
|
434
|
-
Progressive enhancement: when the signed-in user grants repo access we let
|
|
435
|
-
them PICK a repo (adopt) or NAME one we auto-create (fresh). Without that
|
|
436
|
-
access — not granted yet, or the server can't store the token — we fall back
|
|
437
|
-
to the manual owner/repo inputs below (#manualHome), which stay authoritative. -->
|
|
438
|
-
<div id="ghConnect" class="reassure">
|
|
439
|
-
<p id="ghConnectMsg"> </p>
|
|
440
|
-
<button type="button" class="btn" id="ghConnectBtn">Allow repo access</button>
|
|
441
|
-
<button type="button" class="linklike" id="ghManualToggle">or enter it manually</button>
|
|
442
|
-
</div>
|
|
443
|
-
<div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
|
|
444
|
-
<div id="ghAdopt" class="field" hidden>
|
|
445
|
-
<span>Pick one of your repositories</span>
|
|
446
|
-
<input type="text" id="ghSearch" placeholder="Search your repos…" autocomplete="off">
|
|
447
|
-
<div id="ghList" class="ghlist" role="listbox" aria-label="Your repositories"></div>
|
|
448
|
-
<div class="hint" id="ghAdoptHint"> </div>
|
|
449
|
-
<!-- visibility stated on EVERY repo path (task 1002721) — the picker
|
|
450
|
-
marks private repos, and this says what adoption does about it -->
|
|
451
|
-
<div class="hint">We never change a repo’s visibility — public stays public, private stays private.</div>
|
|
452
|
-
</div>
|
|
453
|
-
<div id="ghFresh" class="field" hidden>
|
|
454
|
-
<span>New repository name</span>
|
|
455
|
-
<input type="text" id="ghNewName" placeholder="mercury" autocomplete="off" maxlength="100">
|
|
456
|
-
<!-- visibility is stated on EVERY repo path (task 1002721) — this baked
|
|
457
|
-
line is the floor; updateFreshPreview() replaces it with the same
|
|
458
|
-
claim plus the resolved repo URL once a name is typed -->
|
|
459
|
-
<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>
|
|
460
|
-
</div>
|
|
461
|
-
|
|
462
|
-
<div class="row2" id="manualHome">
|
|
463
|
-
<label class="field"><span>GitHub owner <small>(user or org)</small></span>
|
|
464
|
-
<input type="text" id="f-owner" placeholder="acme" autocomplete="off" maxlength="60">
|
|
465
|
-
</label>
|
|
466
|
-
<label class="field"><span>Repository name</span>
|
|
467
|
-
<input type="text" id="f-repo" placeholder="mercury" autocomplete="off" maxlength="120">
|
|
468
|
-
</label>
|
|
469
|
-
<!-- visibility stated BEFORE GitHub's own radio is the first place it
|
|
470
|
-
appears (task 1002721, owner firsthand in the QA walk) -->
|
|
471
|
-
<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>
|
|
472
|
-
</div>
|
|
473
|
-
<div class="hint" id="h-home"> </div>
|
|
474
|
-
<div class="nav">
|
|
475
|
-
<button type="button" class="btn ghost" data-back="1">← Back</button>
|
|
476
|
-
<button type="submit" class="btn">Next →</button>
|
|
477
|
-
</div>
|
|
478
|
-
</form>
|
|
479
|
-
</section>
|
|
480
|
-
|
|
481
|
-
<!-- step 3 · address — a CONFIRMATION, not a form (task 1002704: every project
|
|
482
|
-
gets its slug subdomain assigned up front; opt-out lives in the disclosure).
|
|
483
|
-
The domainless default was a proven dead-end (QA walk, task 1002702). -->
|
|
484
|
-
<section class="panel" id="panel-address" aria-labelledby="addr-h">
|
|
485
|
-
<h1 id="addr-h">Its address in the sky</h1>
|
|
486
|
-
<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>
|
|
487
|
-
<form data-next="4">
|
|
488
|
-
<label class="field"><span>Project handle</span>
|
|
489
|
-
<input type="text" id="f-slug" placeholder="mercury" autocomplete="off" maxlength="40" spellcheck="false">
|
|
490
|
-
<div class="hint" id="h-slug"> </div>
|
|
491
|
-
</label>
|
|
492
|
-
<div class="addrConfirm" id="addrConfirm">
|
|
493
|
-
<div class="addrBig" id="addrAssigned"></div>
|
|
494
|
-
<p id="addrAssignedNote">Yours from the moment it stands up. You can point a domain you own at it any time.</p>
|
|
495
|
-
<details id="addrCustom">
|
|
496
|
-
<summary>Use a domain I own instead</summary>
|
|
497
|
-
<div class="inner">
|
|
498
|
-
<label class="field"><span>Domain you own</span>
|
|
499
|
-
<input type="text" id="f-domain" placeholder="mercury.example.com" autocomplete="off" maxlength="253" spellcheck="false">
|
|
500
|
-
<div class="hint" id="h-domain"> </div>
|
|
501
|
-
</label>
|
|
502
|
-
<label class="addrOpt"><input type="checkbox" id="f-noaddr">
|
|
503
|
-
<span>Start without an address — possible, but sign-in on your project can’t be set up until it has one.</span>
|
|
504
|
-
</label>
|
|
505
|
-
</div>
|
|
506
|
-
</details>
|
|
507
|
-
</div>
|
|
508
|
-
<div class="nav">
|
|
509
|
-
<button type="button" class="btn ghost" data-back="2">← Back</button>
|
|
510
|
-
<button type="submit" class="btn">Next →</button>
|
|
511
|
-
</div>
|
|
512
|
-
</form>
|
|
513
|
-
</section>
|
|
514
|
-
|
|
515
|
-
<!-- step 4 · review + create -->
|
|
516
|
-
<section class="panel" id="panel-review" aria-labelledby="rev-h">
|
|
517
|
-
<h1 id="rev-h">Ready to roll</h1>
|
|
518
|
-
<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>
|
|
519
|
-
<ul class="review" id="reviewList"></ul>
|
|
520
|
-
<div class="alert" id="createAlert" role="alert"></div>
|
|
521
|
-
<div class="nav" style="flex-direction:column;align-items:stretch;">
|
|
522
|
-
<button type="button" class="btn big" id="doCreate">Create my project</button>
|
|
523
|
-
<button type="button" class="btn ghost" data-back="3" style="align-self:flex-start;margin-top:10px;">← Back</button>
|
|
524
|
-
</div>
|
|
525
|
-
<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>
|
|
526
|
-
</section>
|
|
527
|
-
|
|
528
|
-
<!-- done · the human steps -->
|
|
529
|
-
<section class="panel" id="panel-done" aria-labelledby="done-h">
|
|
530
|
-
<div class="doneHead">
|
|
531
|
-
<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="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
|
|
532
|
-
<h1 id="done-h">The drums are rolling</h1>
|
|
533
|
-
</div>
|
|
534
|
-
<p class="lede" id="doneLede"></p>
|
|
535
|
-
<div id="liveProgress"></div>
|
|
536
|
-
<h2 style="font-family:var(--font-display);font-size:19px;margin-top:22px;">Your steps — the parts only you can play</h2>
|
|
537
|
-
<ol class="steps" id="humanSteps"></ol>
|
|
538
|
-
<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>
|
|
539
|
-
</section>
|
|
540
|
-
</main>
|
|
541
|
-
</div>
|
|
542
|
-
|
|
543
|
-
<div class="foot">A non-profit, capture-resistant commons. Built by agents, on Cloud Bongos.</div>
|
|
544
|
-
|
|
545
|
-
<script>
|
|
546
|
-
/* Brand-pack fill (R57): the server injects window.__BRANDING__ ahead of this
|
|
547
|
-
script (src/bongos/serve-internal.js); baked markup above is the standalone
|
|
548
|
-
fallback. */
|
|
549
|
-
(function () {
|
|
550
|
-
'use strict';
|
|
551
|
-
|
|
552
|
-
var API = '/api/bongos';
|
|
553
|
-
var b = (typeof window !== 'undefined' && window.__BRANDING__) || {};
|
|
554
|
-
var productName = (b.identity && b.identity.productName) || 'Cloud Bongos';
|
|
555
|
-
// (task 2094: the wizard no longer links into cloud bongos's own builders hall,
|
|
556
|
-
// so the buildersOrigin it used to render is gone.)
|
|
557
|
-
document.querySelectorAll('[data-brand-product]').forEach(function (n) { n.textContent = productName.toLowerCase(); });
|
|
558
|
-
document.querySelectorAll('[data-brand-product-inline]').forEach(function (n) { n.textContent = productName; });
|
|
559
|
-
|
|
560
|
-
/* ── light/dark: the sky is constant, the mode governs the cards. No saved
|
|
561
|
-
choice = follow the OS; the toggle stores an explicit one. ── */
|
|
562
|
-
function resolvedMode() {
|
|
563
|
-
var m = document.documentElement.getAttribute('data-mode');
|
|
564
|
-
if (m === 'light' || m === 'dark') return m;
|
|
565
|
-
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
566
|
-
}
|
|
567
|
-
function paintModeToggle() { $('modeToggle').textContent = resolvedMode() === 'dark' ? '☀' : '☾'; }
|
|
568
|
-
$('modeToggle').addEventListener('click', function () {
|
|
569
|
-
var next = resolvedMode() === 'dark' ? 'light' : 'dark';
|
|
570
|
-
document.documentElement.setAttribute('data-mode', next);
|
|
571
|
-
try { localStorage.setItem('cb-color-mode', next); } catch (e) {}
|
|
572
|
-
paintModeToggle();
|
|
573
|
-
});
|
|
574
|
-
paintModeToggle();
|
|
575
|
-
|
|
576
|
-
/* ── state ── */
|
|
577
|
-
var DRAFT_KEY = 'cb-create-draft';
|
|
578
|
-
var CREATED_KEY = 'cb-created-instance';
|
|
579
|
-
var state = { step: 0, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
|
|
580
|
-
try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
|
|
581
|
-
var me = null;
|
|
582
|
-
|
|
583
|
-
/* A name handed over from the orbs gate (task 2087): /create?name=… seeds the
|
|
584
|
-
draft BEFORE anything else, so it survives the GitHub sign-in round-trip,
|
|
585
|
-
then the query is cleaned off the URL so a later reload doesn't re-seed
|
|
586
|
-
over the visitor's edits. */
|
|
587
|
-
try {
|
|
588
|
-
var handedName = new URLSearchParams(location.search).get('name');
|
|
589
|
-
if (handedName && handedName.trim()) {
|
|
590
|
-
state.name = handedName.trim().slice(0, 80);
|
|
591
|
-
state.slug = deriveSlug(state.name); state.slugTouched = false; // (hoisted declaration)
|
|
592
|
-
state.repo = deriveRepo(state.name); state.repoTouched = false; // auto-repo from the name
|
|
593
|
-
state.step = state.step >= 1 && state.step <= 4 ? state.step : 1;
|
|
594
|
-
try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
|
|
595
|
-
history.replaceState(null, '', location.pathname);
|
|
596
|
-
}
|
|
597
|
-
} catch (e) {}
|
|
598
|
-
|
|
599
|
-
var guideLines = {
|
|
600
|
-
0: 'Welcome, friend. Sign in and we’ll lay down the first beat.',
|
|
601
|
-
1: 'Every rhythm starts with a name.',
|
|
602
|
-
2: 'Where will the code live?',
|
|
603
|
-
3: 'Every project needs an address in the sky.',
|
|
604
|
-
4: 'Ready? One tap and the drums start.',
|
|
605
|
-
5: 'The beat is rolling. A few steps only you can play.'
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
function $(id) { return document.getElementById(id); }
|
|
609
|
-
function setGuide(step) { $('guideLine').textContent = guideLines[step] || guideLines[1]; }
|
|
610
|
-
|
|
611
|
-
/* Mirror provisioning.isValidSlug (DNS/db-safe): 2–40 of [a-z0-9-], starts+ends
|
|
612
|
-
alnum, no double dash. Client-side pre-check; the server re-validates. */
|
|
613
|
-
function validSlug(s) {
|
|
614
|
-
var v = String(s || '');
|
|
615
|
-
return v.length >= 2 && v.length <= 40 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(v) && v.indexOf('--') === -1;
|
|
616
|
-
}
|
|
617
|
-
function slugify(name, maxLen) {
|
|
618
|
-
return String(name || '').toLowerCase()
|
|
619
|
-
.replace(/['’]/g, '')
|
|
620
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
621
|
-
.replace(/-+/g, '-')
|
|
622
|
-
.replace(/^-|-$/g, '')
|
|
623
|
-
.slice(0, maxLen);
|
|
624
|
-
}
|
|
625
|
-
function deriveSlug(name) { return slugify(name, 40); }
|
|
626
|
-
/* Repo-name default from the project name — the same slug-safe transform (GitHub
|
|
627
|
-
repo names accept [A-Za-z0-9._-], so a lowercase-dashed name is always valid and
|
|
628
|
-
matches the placeholder). Wider cap than the handle; the visitor can still edit. */
|
|
629
|
-
function deriveRepo(name) { return slugify(name, 100); }
|
|
630
|
-
function looksLikeDomain(d) {
|
|
631
|
-
return /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/i.test(String(d || ''));
|
|
632
|
-
}
|
|
633
|
-
/* ── the assigned address (task 1002704): every project gets <slug>.<apex> up
|
|
634
|
-
front, derived from the host this wizard runs on — cloudbongos.com assigns
|
|
635
|
-
slug.cloudbongos.com; a self-hosted apex assigns under its own name. On a
|
|
636
|
-
dotless/local host (localhost, an IP) there is nothing sane to assign, so
|
|
637
|
-
the step degrades to the old optional-domain form. ── */
|
|
638
|
-
function apexBase() {
|
|
639
|
-
var h = String(location.hostname || '').toLowerCase().replace(/^www\./, '');
|
|
640
|
-
if (!h || h.indexOf('.') < 0) return null;
|
|
641
|
-
if (/^(localhost|127\.|\d+\.\d+\.\d+\.\d+$|\[)/.test(h)) return null;
|
|
642
|
-
return h;
|
|
643
|
-
}
|
|
644
|
-
function assignedDomain() {
|
|
645
|
-
var base = apexBase();
|
|
646
|
-
return base && validSlug(state.slug) ? state.slug + '.' + base : null;
|
|
647
|
-
}
|
|
648
|
-
/* What the create actually files: a custom domain wins; opting out files none;
|
|
649
|
-
otherwise the assigned subdomain. Everything downstream (review, submit,
|
|
650
|
-
the done-panel steps) reads THIS, never state.domain directly. */
|
|
651
|
-
function resolvedDomain() {
|
|
652
|
-
if (state.domain) return state.domain;
|
|
653
|
-
if (state.noAddress) return '';
|
|
654
|
-
return assignedDomain() || '';
|
|
655
|
-
}
|
|
656
|
-
function renderAddrPanel() {
|
|
657
|
-
var box = $('addrConfirm');
|
|
658
|
-
if (!box) return;
|
|
659
|
-
var assigned = assignedDomain();
|
|
660
|
-
var custom = state.domain && looksLikeDomain(state.domain) ? state.domain : '';
|
|
661
|
-
box.classList.toggle('optedOut', state.noAddress && !custom);
|
|
662
|
-
var big = $('addrAssigned');
|
|
663
|
-
var note = $('addrAssignedNote');
|
|
664
|
-
if (custom) {
|
|
665
|
-
big.innerHTML = '<code>' + esc(custom) + '</code> — your own domain, wired up for you.';
|
|
666
|
-
note.textContent = 'DNS pointing happens after the standup; the card on your projects page walks you through it.';
|
|
667
|
-
} else if (state.noAddress) {
|
|
668
|
-
big.textContent = 'Starting without an address.';
|
|
669
|
-
note.textContent = 'Sign-in on your project can’t be set up until it has one — add it any time from your projects page.';
|
|
670
|
-
} else if (assigned) {
|
|
671
|
-
big.innerHTML = '<code>' + esc(assigned) + '</code> — yours from the moment it stands up.';
|
|
672
|
-
note.textContent = 'Sign-in, sharing, everything just works. You can point a domain you own at it any time.';
|
|
673
|
-
} else if (!validSlug(state.slug)) {
|
|
674
|
-
big.textContent = 'Pick a handle above and the address comes with it.';
|
|
675
|
-
note.textContent = '';
|
|
676
|
-
} else {
|
|
677
|
-
/* dotless host (local dev): nothing sane to auto-assign — the domain
|
|
678
|
-
field IS the step here, so unfold it (a folded disclosure made the
|
|
679
|
-
opt-out checkbox unreachable-by-default; owner walk, 2026-08-12) */
|
|
680
|
-
big.textContent = 'No address is assigned on this host.';
|
|
681
|
-
note.textContent = 'Add a domain you own below, or start without one.';
|
|
682
|
-
$('addrCustom').open = true;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
/* Pinned GitHub OAuth-app values for a domain (auth.js WEB_REDIRECT_ORIGIN —
|
|
686
|
-
callback is the apex /api/gds/auth/web/callback). Mirrors onboard.oauthAppInstructions. */
|
|
687
|
-
function oauthAppValues(domain) {
|
|
688
|
-
var origin = 'https://' + domain;
|
|
689
|
-
return { homepageUrl: origin, callbackUrl: origin + '/api/gds/auth/web/callback' };
|
|
690
|
-
}
|
|
691
|
-
/* GitHub's own "New repository" page, pre-filled with the owner + name. No auth or
|
|
692
|
-
permissions on our side — the visitor creates the empty repo in GitHub's UI, which
|
|
693
|
-
is exactly what the scaffold then clones. `owner` selects the account/org; omitting
|
|
694
|
-
a blank param just leaves that field empty on GitHub. */
|
|
695
|
-
function githubNewRepoUrl(owner, repo) {
|
|
696
|
-
var u = new URL('https://github.com/new');
|
|
697
|
-
if (repo) u.searchParams.set('name', repo);
|
|
698
|
-
if (owner) u.searchParams.set('owner', owner);
|
|
699
|
-
return u.toString();
|
|
700
|
-
}
|
|
701
|
-
function esc(s) {
|
|
702
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
703
|
-
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
function saveDraft() {
|
|
707
|
-
try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/* ── step navigation ── */
|
|
711
|
-
var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-home', 3: 'panel-address', 4: 'panel-review', 5: 'panel-done' };
|
|
712
|
-
function show(step) {
|
|
713
|
-
state.step = step;
|
|
714
|
-
Object.keys(panels).forEach(function (k) {
|
|
715
|
-
$(panels[k]).classList.toggle('on', Number(k) === step);
|
|
716
|
-
});
|
|
717
|
-
document.querySelectorAll('#beats li').forEach(function (li) {
|
|
718
|
-
var n = Number(li.getAttribute('data-step'));
|
|
719
|
-
li.classList.toggle('now', n === step);
|
|
720
|
-
li.classList.toggle('done', n < step || step === 5);
|
|
721
|
-
});
|
|
722
|
-
setGuide(step);
|
|
723
|
-
saveDraft();
|
|
724
|
-
var h = $(panels[step]).querySelector('h1');
|
|
725
|
-
if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false }); }
|
|
726
|
-
if (step === 2) { syncHome(); ensureRepoAccess(); }
|
|
727
|
-
if (step === 3) renderAddrPanel();
|
|
728
|
-
if (step === 4) renderReview();
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/* ── field wiring ── */
|
|
732
|
-
$('f-name').value = state.name;
|
|
733
|
-
$('f-owner').value = state.owner;
|
|
734
|
-
$('f-repo').value = state.repo;
|
|
735
|
-
$('f-slug').value = state.slug;
|
|
736
|
-
$('f-domain').value = state.domain;
|
|
737
|
-
$('f-noaddr').checked = !!state.noAddress;
|
|
738
|
-
|
|
739
|
-
/* Auto-fill the repo name from the project name until the visitor edits it
|
|
740
|
-
directly (mirrors the handle's auto-derive). The owner is filled once we know
|
|
741
|
-
who's signed in — see the /me boot below. */
|
|
742
|
-
if (!state.repoTouched && state.name && !state.repo) {
|
|
743
|
-
state.repo = deriveRepo(state.name);
|
|
744
|
-
$('f-repo').value = state.repo;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
$('f-name').addEventListener('input', function () {
|
|
748
|
-
state.name = this.value;
|
|
749
|
-
if (!state.slugTouched) {
|
|
750
|
-
state.slug = deriveSlug(state.name);
|
|
751
|
-
$('f-slug').value = state.slug;
|
|
752
|
-
}
|
|
753
|
-
if (!state.repoTouched) {
|
|
754
|
-
state.repo = deriveRepo(state.name);
|
|
755
|
-
$('f-repo').value = state.repo;
|
|
756
|
-
}
|
|
757
|
-
var h = $('h-name');
|
|
758
|
-
if (state.slug) { h.innerHTML = 'its handle on the platform: <code>' + esc(state.slug) + '</code>'; h.className = 'hint'; }
|
|
759
|
-
else { h.innerHTML = ' '; }
|
|
760
|
-
saveDraft();
|
|
761
|
-
});
|
|
762
|
-
if (state.name && state.slug) $('h-name').innerHTML = 'its handle on the platform: <code>' + esc(state.slug) + '</code>';
|
|
763
|
-
|
|
764
|
-
function setMode(mode) {
|
|
765
|
-
state.mode = mode;
|
|
766
|
-
$('c-green').setAttribute('aria-pressed', String(mode === 'greenfield'));
|
|
767
|
-
$('c-adopt').setAttribute('aria-pressed', String(mode === 'adopt'));
|
|
768
|
-
$('adoptNote').classList.toggle('on', mode === 'adopt');
|
|
769
|
-
saveDraft();
|
|
770
|
-
syncHome();
|
|
771
|
-
}
|
|
772
|
-
$('c-green').addEventListener('click', function () { setMode('greenfield'); });
|
|
773
|
-
$('c-adopt').addEventListener('click', function () { setMode('adopt'); });
|
|
774
|
-
setMode(state.mode);
|
|
775
|
-
|
|
776
|
-
/* ── GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099 / ADR 0143.
|
|
777
|
-
Progressive enhancement: probe GET /github/repos once we reach step 2.
|
|
778
|
-
200 → we hold repo access → show the picker (adopt) / name field (fresh);
|
|
779
|
-
403 → offer to grant repo access (a ?repos=1 re-sign-in);
|
|
780
|
-
503 (server can't store the token) or any error → the manual inputs.
|
|
781
|
-
#manualHome (the classic owner/repo fields) is the always-safe fallback. ── */
|
|
782
|
-
var repoAccess = null; // null=unknown/loading · true · 'need-connect' · 'unavailable' · 'manual'
|
|
783
|
-
var repoList = []; // the user's repos (from the probe) — feeds the picker + the fresh-name clash hint
|
|
784
|
-
var ghProbed = false;
|
|
785
|
-
|
|
786
|
-
function isManualHome() { return repoAccess === 'manual' || repoAccess === 'unavailable'; }
|
|
787
|
-
function validRepoName(s) { return /^[A-Za-z0-9._-]{1,100}$/.test(String(s || '')); }
|
|
788
|
-
function myLogin() { return (me && me.github_login) || 'you'; }
|
|
789
|
-
|
|
790
|
-
function connectRepos(e) {
|
|
791
|
-
if (e) e.preventDefault();
|
|
792
|
-
// Same-tab navigation so the sessionStorage draft round-trips back to step 2.
|
|
793
|
-
window.location.assign(API + '/auth/web/start?return=/create&repos=1');
|
|
794
|
-
}
|
|
795
|
-
$('ghConnectBtn').addEventListener('click', connectRepos);
|
|
796
|
-
$('ghManualToggle').addEventListener('click', function () { repoAccess = 'manual'; syncHome(); $('f-owner').focus(); });
|
|
797
|
-
|
|
798
|
-
function syncHome() {
|
|
799
|
-
var need = repoAccess === 'need-connect';
|
|
800
|
-
var ok = repoAccess === true;
|
|
801
|
-
var adopt = state.mode === 'adopt';
|
|
802
|
-
$('ghBusy').hidden = !!repoAccess; // busy only while still probing (null)
|
|
803
|
-
$('ghConnect').classList.toggle('on', need); // .reassure box: shown via .on (not hidden — .reassure sets display:none)
|
|
804
|
-
$('ghAdopt').hidden = !(ok && adopt);
|
|
805
|
-
$('ghFresh').hidden = !(ok && !adopt);
|
|
806
|
-
$('manualHome').hidden = !isManualHome();
|
|
807
|
-
if (need) {
|
|
808
|
-
// Informed consent (ADR 0155): this grants `repo`, which GitHub's own screen
|
|
809
|
-
// words as full control of ALL your repositories — so say plainly why we ask
|
|
810
|
-
// and what we do with it, rather than letting GitHub's wording be the first
|
|
811
|
-
// the user hears of it. Private repos are the reason the scope is this wide.
|
|
812
|
-
$('ghConnectMsg').textContent = adopt
|
|
813
|
-
? '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.'
|
|
814
|
-
: '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.';
|
|
815
|
-
}
|
|
816
|
-
if (ok && adopt) initAdoptPicker();
|
|
817
|
-
if (ok && !adopt) syncFresh();
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function syncFresh() {
|
|
821
|
-
// Fresh + access: the repo is created under the signed-in account. Seed owner for
|
|
822
|
-
// the review/target now; the create response confirms the real full_name.
|
|
823
|
-
state.owner = myLogin();
|
|
824
|
-
$('ghNewName').value = state.repo || '';
|
|
825
|
-
updateFreshPreview();
|
|
826
|
-
}
|
|
827
|
-
function updateFreshPreview() {
|
|
828
|
-
var hint = $('ghFreshHint');
|
|
829
|
-
var clash = state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
|
|
830
|
-
if (state.repo && !validRepoName(state.repo)) {
|
|
831
|
-
hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
|
|
832
|
-
} else if (clash) {
|
|
833
|
-
hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
|
|
834
|
-
} else {
|
|
835
|
-
/* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
|
|
836
|
-
public by design (github-repos.js — open-source, capture-resistant); the
|
|
837
|
-
private path is adopting a private repo the owner makes themselves. */
|
|
838
|
-
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.';
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
842
|
-
|
|
843
|
-
// The adopt picker behaves like a standard combobox (owner design review, task
|
|
844
|
-
// 1002247): the dropdown is open only while the visitor is choosing. Selecting a
|
|
845
|
-
// repo fills the search box with its full_name and collapses the list; focusing or
|
|
846
|
-
// typing reopens it.
|
|
847
|
-
function openRepoList() { $('ghList').hidden = false; renderRepoList(); }
|
|
848
|
-
function closeRepoList() { $('ghList').hidden = true; }
|
|
849
|
-
function renderRepoList() {
|
|
850
|
-
var q = ($('ghSearch').value || '').trim().toLowerCase();
|
|
851
|
-
var sel = state.owner && state.repo ? (state.owner + '/' + state.repo) : '';
|
|
852
|
-
// When the box just shows the current selection verbatim, list everything (so the
|
|
853
|
-
// visitor can switch) instead of filtering down to the one selected repo.
|
|
854
|
-
var effectiveQ = q === sel.toLowerCase() ? '' : q;
|
|
855
|
-
var rows = repoList.filter(function (r) { return !effectiveQ || String(r.full_name).toLowerCase().indexOf(effectiveQ) >= 0; }).slice(0, 100);
|
|
856
|
-
$('ghList').innerHTML = rows.length
|
|
857
|
-
? rows.map(function (r) {
|
|
858
|
-
return '<button type="button" class="ghrow" role="option" data-full="' + esc(r.full_name) + '" aria-selected="' + (r.full_name === sel) + '">' +
|
|
859
|
-
'<span>' + esc(r.full_name) + '</span>' + (r.private ? '<span class="priv">Private</span>' : '') + '</button>';
|
|
860
|
-
}).join('')
|
|
861
|
-
: '<div class="hint" style="padding:10px 12px">No repositories match “' + esc(effectiveQ) + '”.</div>';
|
|
862
|
-
}
|
|
863
|
-
// Fill the search box with the current selection (if any) and collapse the list;
|
|
864
|
-
// with nothing chosen yet, open it so the visitor sees their repos right away.
|
|
865
|
-
function initAdoptPicker() {
|
|
866
|
-
var sel = state.owner && state.repo ? state.owner + '/' + state.repo : '';
|
|
867
|
-
// Only treat it as a confirmed pick if it's actually one of the visitor's repos.
|
|
868
|
-
// owner/repo are auto-derived from the project name, so a bare derived value must
|
|
869
|
-
// NOT masquerade as a chosen repo — that would collapse the list onto a repo that
|
|
870
|
-
// may not even exist.
|
|
871
|
-
var picked = sel && repoList.some(function (r) { return String(r.full_name).toLowerCase() === sel.toLowerCase(); });
|
|
872
|
-
var h = $('ghAdoptHint');
|
|
873
|
-
if (picked) {
|
|
874
|
-
$('ghSearch').value = sel;
|
|
875
|
-
h.className = 'hint ok'; h.innerHTML = 'Adopting <strong>' + esc(sel) + '</strong>.';
|
|
876
|
-
closeRepoList();
|
|
877
|
-
} else {
|
|
878
|
-
$('ghSearch').value = '';
|
|
879
|
-
h.className = 'hint'; h.innerHTML = ' ';
|
|
880
|
-
openRepoList();
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
$('ghSearch').addEventListener('focus', function () { this.select(); openRepoList(); });
|
|
884
|
-
$('ghSearch').addEventListener('input', openRepoList);
|
|
885
|
-
$('ghList').addEventListener('click', function (ev) {
|
|
886
|
-
var row = ev.target.closest('.ghrow'); if (!row) return;
|
|
887
|
-
var full = row.getAttribute('data-full'); var slash = full.indexOf('/');
|
|
888
|
-
state.owner = full.slice(0, slash); state.repo = full.slice(slash + 1);
|
|
889
|
-
state.ownerTouched = true; state.repoTouched = true; // a real pick — don't let name-derive clobber it
|
|
890
|
-
saveDraft();
|
|
891
|
-
$('ghSearch').value = full; // pull the choice into the search box …
|
|
892
|
-
var h = $('ghAdoptHint'); h.className = 'hint ok'; h.innerHTML = 'Adopting <strong>' + esc(full) + '</strong>.';
|
|
893
|
-
closeRepoList(); // … and collapse the dropdown (typical UX)
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
function ensureRepoAccess() {
|
|
897
|
-
if (ghProbed && repoAccess !== null) return; // already known this session
|
|
898
|
-
if (isManualHome()) return; // user chose manual — don't nag
|
|
899
|
-
repoAccess = null; syncHome(); // show "reading your repos…"
|
|
900
|
-
fetch(API + '/github/repos', { credentials: 'same-origin' })
|
|
901
|
-
.then(function (res) {
|
|
902
|
-
ghProbed = true;
|
|
903
|
-
if (res.status === 200) return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
|
|
904
|
-
if (res.status === 403 || res.status === 401) { repoAccess = 'need-connect'; return; }
|
|
905
|
-
repoAccess = 'unavailable'; return; // 503 or anything else → manual fallback
|
|
906
|
-
})
|
|
907
|
-
.catch(function () { ghProbed = true; repoAccess = 'unavailable'; })
|
|
908
|
-
.then(syncHome);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
$('f-owner').addEventListener('input', function () { state.owner = this.value.trim(); state.ownerTouched = true; saveDraft(); });
|
|
912
|
-
$('f-repo').addEventListener('input', function () { state.repo = this.value.trim(); state.repoTouched = true; saveDraft(); });
|
|
913
|
-
|
|
914
|
-
$('f-slug').addEventListener('input', function () {
|
|
915
|
-
state.slugTouched = true;
|
|
916
|
-
state.slug = this.value.trim().toLowerCase();
|
|
917
|
-
this.value = state.slug;
|
|
918
|
-
var h = $('h-slug');
|
|
919
|
-
if (!state.slug) { h.innerHTML = ' '; h.className = 'hint'; this.classList.remove('bad'); }
|
|
920
|
-
else if (validSlug(state.slug)) { h.textContent = 'Nice — that works.'; h.className = 'hint ok'; this.classList.remove('bad'); }
|
|
921
|
-
else { h.textContent = '2–40 lowercase letters, numbers, or dashes; no leading/trailing/double dash.'; h.className = 'hint err'; this.classList.add('bad'); }
|
|
922
|
-
renderAddrPanel();
|
|
923
|
-
saveDraft();
|
|
924
|
-
});
|
|
925
|
-
$('f-domain').addEventListener('input', function () {
|
|
926
|
-
state.domain = this.value.trim().toLowerCase();
|
|
927
|
-
var h = $('h-domain');
|
|
928
|
-
if (!state.domain) { h.innerHTML = ' '; h.className = 'hint'; this.classList.remove('bad'); }
|
|
929
|
-
else if (looksLikeDomain(state.domain)) { h.textContent = 'Looks like a domain.'; h.className = 'hint ok'; this.classList.remove('bad'); }
|
|
930
|
-
else { h.textContent = 'That doesn’t look like a domain yet (e.g. mercury.example.com).'; h.className = 'hint err'; this.classList.add('bad'); }
|
|
931
|
-
renderAddrPanel();
|
|
932
|
-
saveDraft();
|
|
933
|
-
});
|
|
934
|
-
$('f-noaddr').addEventListener('change', function () {
|
|
935
|
-
state.noAddress = !!this.checked;
|
|
936
|
-
renderAddrPanel();
|
|
937
|
-
saveDraft();
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
/* per-panel validation on Next */
|
|
941
|
-
function validateStep(step) {
|
|
942
|
-
if (step === 1) {
|
|
943
|
-
if (!state.name.trim()) { $('h-name').textContent = 'Give it a name to start.'; $('h-name').className = 'hint err'; return false; }
|
|
944
|
-
return true;
|
|
945
|
-
}
|
|
946
|
-
if (step === 2) {
|
|
947
|
-
var homeErr = function (msg) { $('h-home').textContent = msg; $('h-home').className = 'hint err'; return false; };
|
|
948
|
-
if (repoAccess === 'need-connect') return homeErr('Allow GitHub repo access to pick or create your repository — or choose “enter it manually”.');
|
|
949
|
-
if (repoAccess === true && state.mode !== 'adopt') {
|
|
950
|
-
// Fresh with access: just the new repo NAME (owner is the signed-in account).
|
|
951
|
-
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
952
|
-
} else {
|
|
953
|
-
// Adopt (picker or manual) and the manual fallback: need a concrete owner + repo.
|
|
954
|
-
if (!state.owner || !state.repo) {
|
|
955
|
-
return homeErr(state.mode === 'adopt' && repoAccess === true ? 'Pick one of your repositories.' : 'Both owner and repository name are needed.');
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
$('h-home').innerHTML = ' '; $('h-home').className = 'hint';
|
|
959
|
-
return true;
|
|
960
|
-
}
|
|
961
|
-
if (step === 3) {
|
|
962
|
-
if (!validSlug(state.slug)) {
|
|
963
|
-
$('h-slug').textContent = '2–40 lowercase letters, numbers, or dashes; no leading/trailing/double dash.';
|
|
964
|
-
$('h-slug').className = 'hint err'; $('f-slug').classList.add('bad');
|
|
965
|
-
return false;
|
|
966
|
-
}
|
|
967
|
-
if (state.domain && !looksLikeDomain(state.domain)) return false;
|
|
968
|
-
/* no auto-assigned address (dotless host): going domainless must be an
|
|
969
|
-
EXPLICIT choice — a domain, or the opt-out box. Never silent. */
|
|
970
|
-
if (!resolvedDomain() && !state.noAddress) {
|
|
971
|
-
$('addrCustom').open = true;
|
|
972
|
-
var hd = $('h-domain');
|
|
973
|
-
hd.textContent = 'Add a domain you own — or tick “start without an address” to go without one.';
|
|
974
|
-
hd.className = 'hint err';
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
return true;
|
|
978
|
-
}
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
document.querySelectorAll('.panel form').forEach(function (form) {
|
|
983
|
-
form.addEventListener('submit', function (ev) {
|
|
984
|
-
ev.preventDefault();
|
|
985
|
-
var next = Number(form.getAttribute('data-next'));
|
|
986
|
-
if (validateStep(next - 1)) show(next);
|
|
987
|
-
});
|
|
988
|
-
});
|
|
989
|
-
document.querySelectorAll('[data-back]').forEach(function (btn) {
|
|
990
|
-
btn.addEventListener('click', function () { show(Number(btn.getAttribute('data-back'))); });
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
/* ── review ── */
|
|
994
|
-
function renderReview() {
|
|
995
|
-
var rows = [
|
|
996
|
-
['Project', esc(state.name)],
|
|
997
|
-
['Starting from', state.mode === 'adopt' ? 'A repo you already have' : 'A fresh, empty repo'],
|
|
998
|
-
['Repository', '<code>' + esc(state.owner + '/' + state.repo) + '</code>'],
|
|
999
|
-
['Handle', '<code>' + esc(state.slug) + '</code>'],
|
|
1000
|
-
['Address', resolvedDomain()
|
|
1001
|
-
? '<code>' + esc(resolvedDomain()) + '</code>'
|
|
1002
|
-
: '<em style="font-weight:600;color:var(--warn)">none — sign-in stays locked until it has one</em>'],
|
|
1003
|
-
['Shape', 'Standalone — your own repo, pinned platform core']
|
|
1004
|
-
];
|
|
1005
|
-
$('reviewList').innerHTML = rows.map(function (r) {
|
|
1006
|
-
return '<li><span class="k">' + r[0] + '</span><span class="v">' + r[1] + '</span></li>';
|
|
1007
|
-
}).join('');
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
/* ── create ── */
|
|
1011
|
-
function friendlyCreateError(err) {
|
|
1012
|
-
var code = err && err.data && err.data.error && (err.data.error.code || '');
|
|
1013
|
-
if (code === 'slug_taken') return 'That handle is already taken by another project. Pick a different one.';
|
|
1014
|
-
if (code === 'bad_slug') return 'The handle didn’t pass the server’s checks — 2–40 lowercase letters, numbers, or dashes.';
|
|
1015
|
-
return 'Couldn’t file the request: ' + (err && err.message ? err.message : 'something went sideways. Try again.');
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
|
|
1019
|
-
then hand its real full_name to provisioning. No-op for adopt / manual / no-access.
|
|
1020
|
-
Idempotent across a retry — once created we reuse the stashed full_name so a second
|
|
1021
|
-
click doesn't trip GitHub's 422 "name already exists". Errors set err.gotoHome so
|
|
1022
|
-
the caller bounces the user back to the Home step to fix the name / reconnect. */
|
|
1023
|
-
function ensureFreshRepo() {
|
|
1024
|
-
if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
|
|
1025
|
-
if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
|
|
1026
|
-
return fetch(API + '/github/repos', {
|
|
1027
|
-
method: 'POST', credentials: 'same-origin',
|
|
1028
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1029
|
-
body: JSON.stringify({ name: state.repo })
|
|
1030
|
-
}).then(function (res) {
|
|
1031
|
-
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
1032
|
-
if (res.status === 401 || res.status === 403) {
|
|
1033
|
-
repoAccess = 'need-connect';
|
|
1034
|
-
var e0 = new Error('reconnect'); e0.gotoHome = true;
|
|
1035
|
-
e0.homeMsg = 'Your GitHub authorization expired — reconnect on the Home step to create the repo.';
|
|
1036
|
-
throw e0;
|
|
1037
|
-
}
|
|
1038
|
-
if (!res.ok) {
|
|
1039
|
-
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
1040
|
-
e.data = data;
|
|
1041
|
-
if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
1042
|
-
throw e;
|
|
1043
|
-
}
|
|
1044
|
-
var full = data.repo && data.repo.full_name;
|
|
1045
|
-
if (full && full.indexOf('/') > 0) {
|
|
1046
|
-
state.owner = full.slice(0, full.indexOf('/'));
|
|
1047
|
-
state.repo = full.slice(full.indexOf('/') + 1);
|
|
1048
|
-
}
|
|
1049
|
-
state.repoAutoCreated = true; saveDraft();
|
|
1050
|
-
});
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
$('doCreate').addEventListener('click', function () {
|
|
1055
|
-
var btn = this;
|
|
1056
|
-
var alertBox = $('createAlert');
|
|
1057
|
-
alertBox.classList.remove('on');
|
|
1058
|
-
btn.disabled = true; btn.textContent = 'Raising it into the sky…';
|
|
1059
|
-
ensureFreshRepo().then(function () {
|
|
1060
|
-
return fetch(API + '/provisioning/instances', {
|
|
1061
|
-
method: 'POST', credentials: 'same-origin',
|
|
1062
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1063
|
-
body: JSON.stringify({
|
|
1064
|
-
slug: state.slug,
|
|
1065
|
-
hosting_shape: 'standalone',
|
|
1066
|
-
domain: resolvedDomain() || undefined,
|
|
1067
|
-
target_ref: state.owner + '/' + state.repo,
|
|
1068
|
-
/* LOAD-BEARING for the control-plane runner, not just for the checklist we
|
|
1069
|
-
render (task 1002562): 'adopt' makes it clone + layer onto the repo's
|
|
1070
|
-
existing history instead of pushing a fresh tree, which a repo that already
|
|
1071
|
-
has commits rejects as unrelated history. state.mode already tracked
|
|
1072
|
-
greenfield|adopt for the UI — it has to reach the server too. */
|
|
1073
|
-
onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield'
|
|
1074
|
-
})
|
|
1075
|
-
});
|
|
1076
|
-
}).then(function (res) {
|
|
1077
|
-
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/create'); throw new Error('signing in'); }
|
|
1078
|
-
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
1079
|
-
if (!res.ok) {
|
|
1080
|
-
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
1081
|
-
e.data = data; throw e;
|
|
1082
|
-
}
|
|
1083
|
-
return data;
|
|
1084
|
-
});
|
|
1085
|
-
}).then(function (data) {
|
|
1086
|
-
try {
|
|
1087
|
-
sessionStorage.setItem(CREATED_KEY, JSON.stringify({
|
|
1088
|
-
id: data.instance && data.instance.id, slug: state.slug, domain: resolvedDomain(),
|
|
1089
|
-
mode: state.mode, name: state.name, at: Date.now()
|
|
1090
|
-
}));
|
|
1091
|
-
sessionStorage.removeItem(DRAFT_KEY);
|
|
1092
|
-
} catch (e) {}
|
|
1093
|
-
renderDone(data);
|
|
1094
|
-
show(5);
|
|
1095
|
-
startProgress(data.instance && data.instance.id);
|
|
1096
|
-
}).catch(function (err) {
|
|
1097
|
-
if (String(err && err.message) === 'signing in') return;
|
|
1098
|
-
btn.disabled = false; btn.textContent = 'Create my project';
|
|
1099
|
-
if (err && err.gotoHome) {
|
|
1100
|
-
// A repo-creation problem (name taken / reconnect) — bounce back to the Home
|
|
1101
|
-
// step with the reason, so the user fixes it where the repo is chosen.
|
|
1102
|
-
show(2);
|
|
1103
|
-
$('h-home').textContent = err.homeMsg || 'Please revisit the repository step.';
|
|
1104
|
-
$('h-home').className = 'hint err';
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
alertBox.textContent = friendlyCreateError(err);
|
|
1108
|
-
alertBox.classList.add('on');
|
|
1109
|
-
});
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
/* ── done panel: rendered FROM the canonical onboard plan (task 1002694, gap 2 /
|
|
1113
|
-
idea 1000675). GET /provisioning/onboard-plan is the single source the
|
|
1114
|
-
/new-project skill + `bongos onboard` read too, so the checklist can't
|
|
1115
|
-
drift. A per-key renderer adds this page's rich UI; an unknown human step
|
|
1116
|
-
falls back to the plan's own label, so a new canonical step surfaces here
|
|
1117
|
-
by default instead of silently vanishing. ── */
|
|
1118
|
-
function copyBtn(text) {
|
|
1119
|
-
return '<button type="button" class="copy" data-copy="' + esc(text) + '">Copy</button>';
|
|
1120
|
-
}
|
|
1121
|
-
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>';
|
|
1122
|
-
|
|
1123
|
-
var instanceId = null; // set when a create lands / a resumed watch starts
|
|
1124
|
-
var live = { instStatus: null, manifest: null, manifestMsg: '' };
|
|
1125
|
-
var planPromises = {}; // mode|domain → Promise<steps[]>
|
|
1126
|
-
var lastStepsHtml = '';
|
|
1127
|
-
var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall'];
|
|
1128
|
-
|
|
1129
|
-
function fetchPlan() {
|
|
1130
|
-
var domain = resolvedDomain() || '';
|
|
1131
|
-
var key = state.mode + '|' + domain;
|
|
1132
|
-
if (!planPromises[key]) {
|
|
1133
|
-
// shape=standalone: this wizard only creates control-plane standalone
|
|
1134
|
-
// instances, and the plan is shape-filtered server-side (direction §7)
|
|
1135
|
-
planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + '&shape=standalone' + (domain ? '&domain=' + encodeURIComponent(domain) : ''))
|
|
1136
|
-
.then(function (res) { if (!res.ok) throw new Error('plan'); return res.json(); })
|
|
1137
|
-
.then(function (d) { return (d && d.steps) || []; })
|
|
1138
|
-
.catch(function () {
|
|
1139
|
-
delete planPromises[key]; // transient — retried on the next render
|
|
1140
|
-
return FALLBACK_HUMAN_KEYS.map(function (k) { return { key: k, kind: 'human', label: '' }; });
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
return planPromises[key];
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
/* github-repo — adopt / auto-created / manual-create, each aware of whether the
|
|
1147
|
-
scaffold has landed (instance active ⇒ the build already pushed). The wizard
|
|
1148
|
-
files onboard_mode to the server, so for adopt the RUNNER clones + layers
|
|
1149
|
-
(task 1002562) — the owner no longer runs `bongos init --adopt` here. */
|
|
1150
|
-
function renderStepGithubRepo() {
|
|
1151
|
-
var landed = live.instStatus === 'active';
|
|
1152
|
-
var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
|
|
1153
|
-
if (state.mode === 'adopt') {
|
|
1154
|
-
return landed
|
|
1155
|
-
? '<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>'
|
|
1156
|
-
: '<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>';
|
|
1157
|
-
}
|
|
1158
|
-
if (state.repoAutoCreated) {
|
|
1159
|
-
return landed
|
|
1160
|
-
? '<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>'
|
|
1161
|
-
: '<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>';
|
|
1162
|
-
}
|
|
1163
|
-
var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
|
|
1164
|
-
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>' +
|
|
1165
|
-
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
/* The manual OAuth-app path stays as a documented fallback (ADR 0133), but the
|
|
1169
|
-
control-plane placement is named as the operator's, never handed to the owner
|
|
1170
|
-
as a command to run (gap 4). */
|
|
1171
|
-
function manualOauthDetails() {
|
|
1172
|
-
var oa = oauthAppValues(resolvedDomain());
|
|
1173
|
-
return '<details><summary>Prefer to create an OAuth app by hand?</summary>' +
|
|
1174
|
-
'<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>' +
|
|
1175
|
-
'<div class="kv"><span class="kk">Homepage URL</span><code>' + esc(oa.homepageUrl) + '</code>' + copyBtn(oa.homepageUrl) + '</div>' +
|
|
1176
|
-
'<div class="kv"><span class="kk">Callback URL</span><code>' + esc(oa.callbackUrl) + '</code>' + copyBtn(oa.callbackUrl) + '</div>' +
|
|
1177
|
-
'<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>';
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
/* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
|
|
1181
|
-
(gap 3 / idea 1000676); locked until a domain exists (the App needs the real
|
|
1182
|
-
callback host — the server refuses domainless with domain_required). */
|
|
1183
|
-
function renderStepOauthApp() {
|
|
1184
|
-
var domain = resolvedDomain();
|
|
1185
|
-
if (!domain) {
|
|
1186
|
-
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="/projects">your projects page</a>.</p></li>';
|
|
1187
|
-
}
|
|
1188
|
-
/* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
|
|
1189
|
-
manifests whose hook URL isn't publicly reachable, so a locally-hosted
|
|
1190
|
-
platform can't complete the one-click flow — say it BEFORE the bounce. */
|
|
1191
|
-
if (!apexBase()) {
|
|
1192
|
-
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>';
|
|
1193
|
-
}
|
|
1194
|
-
if (live.manifest === 'done') {
|
|
1195
|
-
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>';
|
|
1196
|
-
}
|
|
1197
|
-
if (live.manifest === 'error' || live.manifest === 'expired') {
|
|
1198
|
-
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' +
|
|
1199
|
-
(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>' +
|
|
1200
|
-
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
1201
|
-
}
|
|
1202
|
-
if (live.manifest) { // pending / code_received — GitHub or the runner is mid-flight
|
|
1203
|
-
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>';
|
|
1204
|
-
}
|
|
1205
|
-
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>' +
|
|
1206
|
-
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
function renderStepFirstSignIn() {
|
|
1210
|
-
var domain = resolvedDomain();
|
|
1211
|
-
if (!domain) {
|
|
1212
|
-
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>';
|
|
1213
|
-
}
|
|
1214
|
-
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>';
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function renderStepEnterHall() {
|
|
1218
|
-
var domain = resolvedDomain();
|
|
1219
|
-
return domain
|
|
1220
|
-
? '<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>'
|
|
1221
|
-
: '<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>';
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
var STEP_RENDERERS = {
|
|
1225
|
-
'github-repo': renderStepGithubRepo,
|
|
1226
|
-
'oauth-app': renderStepOauthApp,
|
|
1227
|
-
'first-sign-in': renderStepFirstSignIn,
|
|
1228
|
-
'enter-hall': renderStepEnterHall
|
|
1229
|
-
};
|
|
1230
|
-
|
|
1231
|
-
/* The newborn-instance welcome + account-nag suppression (direction §5.6,
|
|
1232
|
-
idea 1000701) is NOT implemented here by design: it lives on the CHILD
|
|
1233
|
-
instance's own surfaces (its hall greeting + needs roster), which this
|
|
1234
|
-
control-plane wizard cannot reach. Spun out per the direction's §9
|
|
1235
|
-
delivery-split as task 1002739. */
|
|
1236
|
-
|
|
1237
|
-
function renderSteps() {
|
|
1238
|
-
fetchPlan().then(function (steps) {
|
|
1239
|
-
var html = steps.filter(function (s) { return s && s.kind === 'human'; }).map(function (s) {
|
|
1240
|
-
var r = STEP_RENDERERS[s.key];
|
|
1241
|
-
return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
|
|
1242
|
-
}).join('');
|
|
1243
|
-
if (html === lastStepsHtml) return; // change-only re-render — polls never clobber open UI
|
|
1244
|
-
lastStepsHtml = html;
|
|
1245
|
-
$('humanSteps').innerHTML = html;
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function renderDone() {
|
|
1250
|
-
var adopt = state.mode === 'adopt';
|
|
1251
|
-
$('doneLede').innerHTML = 'Provisioning is requested for <strong>' + esc(state.name || state.slug) + '</strong>' +
|
|
1252
|
-
(adopt ? ' — adopting your existing repo' : '') +
|
|
1253
|
-
'. The platform stands up the server, database, and address on its own — watch it right here, or any time on <a href="/projects">your projects page</a>.';
|
|
1254
|
-
renderSteps();
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
/* One delegated listener — renderSteps swaps innerHTML, so handlers live on the list. */
|
|
1258
|
-
$('humanSteps').addEventListener('click', function (ev) {
|
|
1259
|
-
var mBtn = ev.target.closest('[data-action="manifest-start"]');
|
|
1260
|
-
if (mBtn) { startManifest(mBtn); return; }
|
|
1261
|
-
var t = ev.target.closest('.copy');
|
|
1262
|
-
if (!t) return;
|
|
1263
|
-
navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
|
|
1264
|
-
t.classList.add('did'); t.textContent = 'Copied';
|
|
1265
|
-
setTimeout(function () { t.classList.remove('did'); t.textContent = 'Copy'; }, 1600);
|
|
1266
|
-
});
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
/* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
|
|
1270
|
-
form-POST the pre-filled manifest to GitHub in a new tab (opened inside the
|
|
1271
|
-
click gesture so popup blockers allow it; blocked → same-tab, the callback
|
|
1272
|
-
page carries the rest), then poll the public status until the control-plane
|
|
1273
|
-
runner has placed + verified the credentials. ── */
|
|
1274
|
-
var manifestPollTimer = null;
|
|
1275
|
-
function stopManifestPoll() { if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
|
|
1276
|
-
|
|
1277
|
-
function startManifest(btn) {
|
|
1278
|
-
if (!instanceId) return;
|
|
1279
|
-
btn.disabled = true; btn.textContent = 'Preparing GitHub…';
|
|
1280
|
-
var tab = window.open('about:blank', 'ghAppSetup');
|
|
1281
|
-
fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
|
|
1282
|
-
method: 'POST', credentials: 'same-origin',
|
|
1283
|
-
headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
1284
|
-
}).then(function (res) {
|
|
1285
|
-
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/create'); throw new Error('signing in'); }
|
|
1286
|
-
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
1287
|
-
if (!res.ok) {
|
|
1288
|
-
var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
|
|
1289
|
-
throw e;
|
|
1290
|
-
}
|
|
1291
|
-
return d;
|
|
1292
|
-
});
|
|
1293
|
-
}).then(function (d) {
|
|
1294
|
-
var form = document.createElement('form');
|
|
1295
|
-
form.method = 'POST'; form.action = d.post_url; form.target = tab ? 'ghAppSetup' : '_self';
|
|
1296
|
-
var inp = document.createElement('input');
|
|
1297
|
-
inp.type = 'hidden'; inp.name = 'manifest'; inp.value = JSON.stringify(d.manifest);
|
|
1298
|
-
form.appendChild(inp); document.body.appendChild(form);
|
|
1299
|
-
form.submit(); form.remove();
|
|
1300
|
-
live.manifest = 'pending'; live.manifestMsg = '';
|
|
1301
|
-
renderSteps();
|
|
1302
|
-
pollManifest(d.state);
|
|
1303
|
-
}).catch(function (err) {
|
|
1304
|
-
if (String(err && err.message) === 'signing in') return;
|
|
1305
|
-
if (tab) { try { tab.close(); } catch (e) {} }
|
|
1306
|
-
live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
1307
|
-
renderSteps();
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
function pollManifest(stateToken) {
|
|
1312
|
-
stopManifestPoll();
|
|
1313
|
-
function tick() {
|
|
1314
|
-
fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
|
|
1315
|
-
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
1316
|
-
.then(function (d) {
|
|
1317
|
-
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
1318
|
-
if (next !== live.manifest) { live.manifest = next; renderSteps(); }
|
|
1319
|
-
if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
|
|
1320
|
-
})
|
|
1321
|
-
.catch(function () { manifestPollTimer = setTimeout(tick, 8000); });
|
|
1322
|
-
}
|
|
1323
|
-
tick();
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/* ── live build progress (task 2083): poll the instance until it's up.
|
|
1327
|
-
Read-only — GET /provisioning/instances/:id (own-scoped) every 5s. ── */
|
|
1328
|
-
var pollTimer = null;
|
|
1329
|
-
function stopPolling() { if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } }
|
|
1330
|
-
function progressMarkup(stageNow, stagesDone, note, cls) {
|
|
1331
|
-
var names = ['Request filed', 'Standing it up', 'Live'];
|
|
1332
|
-
return '<div class="prog ' + (cls || '') + '" role="status" aria-live="polite">' +
|
|
1333
|
-
'<ul class="stages">' + names.map(function (n, i) {
|
|
1334
|
-
var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
|
|
1335
|
-
return '<li class="' + c + '"><span class="sdot"></span><span class="slbl">' + n + '</span></li>';
|
|
1336
|
-
}).join('') + '</ul>' +
|
|
1337
|
-
'<p class="pnote">' + note + '</p></div>';
|
|
1338
|
-
}
|
|
1339
|
-
/* Renders the current instance state; returns true when the state is terminal
|
|
1340
|
-
(no further polling wanted). */
|
|
1341
|
-
function renderProgress(inst) {
|
|
1342
|
-
var el = $('liveProgress');
|
|
1343
|
-
if (!el) return true;
|
|
1344
|
-
var s = inst && inst.status;
|
|
1345
|
-
if (s === 'requested') {
|
|
1346
|
-
el.innerHTML = progressMarkup(1, 1, 'Waiting for a runner to pick it up. This updates the moment it starts.');
|
|
1347
|
-
} else if (s === 'provisioning') {
|
|
1348
|
-
el.innerHTML = progressMarkup(1, 1, 'Raising the server, database, and address — nothing needed from you.');
|
|
1349
|
-
} else if (s === 'active') {
|
|
1350
|
-
var addr = inst.domain ? 'https://' + inst.domain : null;
|
|
1351
|
-
el.innerHTML = progressMarkup(-1, 3,
|
|
1352
|
-
'Your project is up' + (addr ? ' — <a href="' + esc(addr) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>' : '') +
|
|
1353
|
-
'. Finish the steps below and the hall is yours.', 'isLive');
|
|
1354
|
-
return true;
|
|
1355
|
-
} else if (s === 'error') {
|
|
1356
|
-
/* the honest snag (direction §5 / F19): no raw stderr as the headline, no
|
|
1357
|
-
false "keeps retrying" — retries stop, and the retry door is /projects */
|
|
1358
|
-
el.innerHTML = progressMarkup(1, 1,
|
|
1359
|
-
'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from <a href="/projects">your projects page</a>.', 'isErr');
|
|
1360
|
-
} else if (s === 'tearing_down' || s === 'torn_down') {
|
|
1361
|
-
/* a deliberate end is calm, never styled as a failure (direction §5) */
|
|
1362
|
-
el.innerHTML = progressMarkup(-1, 0, 'This instance was torn down. Bring it back any time from <a href="/projects">your projects page</a>, or start another below.');
|
|
1363
|
-
return true;
|
|
1364
|
-
} else {
|
|
1365
|
-
el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
|
|
1366
|
-
}
|
|
1367
|
-
return false;
|
|
1368
|
-
}
|
|
1369
|
-
function startProgress(id) {
|
|
1370
|
-
if (!id) return;
|
|
1371
|
-
instanceId = id; // the manifest flow mints against this instance
|
|
1372
|
-
stopPolling();
|
|
1373
|
-
var ticks = 0;
|
|
1374
|
-
var el = $('liveProgress');
|
|
1375
|
-
el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
|
|
1376
|
-
function tick() {
|
|
1377
|
-
ticks += 1;
|
|
1378
|
-
if (ticks > 400) {
|
|
1379
|
-
/* ~33 min: this tab stops watching, but the standup keeps going — hand
|
|
1380
|
-
the long watch to /projects, the owner's durable surface (task
|
|
1381
|
-
1002693). The old "stop quietly" here was a dead-end lie: the hall
|
|
1382
|
-
card it assumed was removed in task 1002675. */
|
|
1383
|
-
$('liveProgress').innerHTML = progressMarkup(1, 1,
|
|
1384
|
-
'Still building — this tab will stop watching now. Follow it any time on <a href="/projects">your projects page</a>.');
|
|
1385
|
-
stopPolling();
|
|
1386
|
-
return;
|
|
1387
|
-
}
|
|
1388
|
-
fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
1389
|
-
.then(function (res) {
|
|
1390
|
-
if (res.status === 401) { throw new Error('gone:Your session expired — <a href="' + API + '/auth/web/start?return=/create">sign in again</a> to keep watching.'); }
|
|
1391
|
-
if (res.status === 404) { throw new Error('gone:Can’t see this instance any more — it may have been torn down.'); }
|
|
1392
|
-
if (!res.ok) throw new Error('soft');
|
|
1393
|
-
return res.json();
|
|
1394
|
-
})
|
|
1395
|
-
.then(function (data) {
|
|
1396
|
-
var inst = data.instance;
|
|
1397
|
-
/* the checklist reflects live state (task 1002694): a status transition —
|
|
1398
|
-
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
1399
|
-
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
1400
|
-
if (inst && inst.status !== live.instStatus) { live.instStatus = inst.status; renderSteps(); }
|
|
1401
|
-
var terminal = renderProgress(inst);
|
|
1402
|
-
if (!terminal) pollTimer = setTimeout(tick, 5000);
|
|
1403
|
-
else stopPolling();
|
|
1404
|
-
})
|
|
1405
|
-
.catch(function (err) {
|
|
1406
|
-
var m = String(err && err.message || '');
|
|
1407
|
-
if (m.indexOf('gone:') === 0) {
|
|
1408
|
-
$('liveProgress').innerHTML = progressMarkup(1, 1, m.slice(5), 'isErr');
|
|
1409
|
-
stopPolling();
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
// transient (network / 5xx): keep the last view, try again a bit later
|
|
1413
|
-
pollTimer = setTimeout(tick, 10000);
|
|
1414
|
-
});
|
|
1415
|
-
}
|
|
1416
|
-
tick();
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
/* start another project: clear the stored instance + draft, back to step 1 */
|
|
1420
|
-
function wireAgain() {
|
|
1421
|
-
var again = document.createElement('button');
|
|
1422
|
-
again.type = 'button'; again.className = 'again'; again.textContent = 'Start another project';
|
|
1423
|
-
again.addEventListener('click', function () {
|
|
1424
|
-
stopPolling();
|
|
1425
|
-
stopManifestPoll();
|
|
1426
|
-
instanceId = null;
|
|
1427
|
-
live = { instStatus: null, manifest: null, manifestMsg: '' };
|
|
1428
|
-
lastStepsHtml = '';
|
|
1429
|
-
try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
1430
|
-
state = { step: 1, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
|
|
1431
|
-
$('f-name').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
|
|
1432
|
-
$('f-noaddr').checked = false;
|
|
1433
|
-
// Re-seed the owner from the signed-in account for the next project.
|
|
1434
|
-
if (me && me.github_login) { state.owner = me.github_login; $('f-owner').value = state.owner; }
|
|
1435
|
-
$('h-name').innerHTML = ' ';
|
|
1436
|
-
setMode('greenfield');
|
|
1437
|
-
show(1);
|
|
1438
|
-
});
|
|
1439
|
-
$('panel-done').appendChild(again);
|
|
1440
|
-
}
|
|
1441
|
-
wireAgain();
|
|
1442
|
-
|
|
1443
|
-
/* ── exit the wizard (task 2098) ── Esc, or the header button. Nothing is
|
|
1444
|
-
provisioned until the final Create step, and the named orb is client-only
|
|
1445
|
-
on the gate, so leaving simply means the project is not created. The form
|
|
1446
|
-
draft persists in sessionStorage, so an accidental exit loses no typing. */
|
|
1447
|
-
function exitWizard() { window.location.assign('/gate/'); }
|
|
1448
|
-
$('exitWizard').addEventListener('click', exitWizard);
|
|
1449
|
-
document.addEventListener('keydown', function (e) {
|
|
1450
|
-
if (e.key === 'Escape') { e.preventDefault(); exitWizard(); }
|
|
1451
|
-
});
|
|
1452
|
-
|
|
1453
|
-
/* ── boot: who am I? ── (task 2094: the wizard shows the signed-in identity
|
|
1454
|
-
ONLY — never a link into cloud bongos's own builders hall. A project
|
|
1455
|
-
creator enters a hall from their own project's landing, not from here.) ── */
|
|
1456
|
-
function renderWho() {
|
|
1457
|
-
var w = $('who');
|
|
1458
|
-
if (me) {
|
|
1459
|
-
w.innerHTML = (me.avatar_url ? '<img src="' + esc(me.avatar_url) + '" alt="">' : '') +
|
|
1460
|
-
'<span>' + esc(me.github_login || 'signed in') + '</span>';
|
|
1461
|
-
} else {
|
|
1462
|
-
w.innerHTML = '';
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
fetch(API + '/me', { credentials: 'same-origin' })
|
|
1467
|
-
.then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
|
|
1468
|
-
.then(function (data) {
|
|
1469
|
-
me = data.builder || data.me || data;
|
|
1470
|
-
renderWho();
|
|
1471
|
-
// Default the GitHub owner to the signed-in account until the visitor edits
|
|
1472
|
-
// it — one less field to fill for the common "my own account" case.
|
|
1473
|
-
if (!state.ownerTouched && !state.owner && me && me.github_login) {
|
|
1474
|
-
state.owner = me.github_login;
|
|
1475
|
-
$('f-owner').value = state.owner;
|
|
1476
|
-
saveDraft();
|
|
1477
|
-
}
|
|
1478
|
-
// returning after a create? resume watching that instance instead of a blank wizard
|
|
1479
|
-
var created = null;
|
|
1480
|
-
try { created = JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) {}
|
|
1481
|
-
if (created && created.id) {
|
|
1482
|
-
state.name = created.name || created.slug; state.slug = created.slug || '';
|
|
1483
|
-
state.domain = created.domain || ''; state.mode = created.mode || 'greenfield';
|
|
1484
|
-
renderDone(null);
|
|
1485
|
-
show(5);
|
|
1486
|
-
startProgress(created.id);
|
|
1487
|
-
return;
|
|
1488
|
-
}
|
|
1489
|
-
show(state.step >= 1 && state.step <= 4 ? state.step : 1);
|
|
1490
|
-
})
|
|
1491
|
-
.catch(function (err) {
|
|
1492
|
-
renderWho();
|
|
1493
|
-
if (String(err && err.message) !== '401') {
|
|
1494
|
-
var g = $('gateAlert');
|
|
1495
|
-
if (!navigator.onLine || String(err && err.message).indexOf('Failed') === 0) {
|
|
1496
|
-
g.textContent = 'Can’t reach the platform right now — sign-in will work once you’re online with it.';
|
|
1497
|
-
g.classList.add('on');
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
show(0);
|
|
1501
|
-
});
|
|
1502
|
-
})();
|
|
1503
|
-
</script>
|
|
1504
|
-
</body>
|
|
1505
|
-
</html>
|