@bongos/core 1.21.94 → 1.21.96
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 +45 -35
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +13 -3
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +181 -7
- package/docs/api-reference.md +9 -5
- package/docs/copy-inventory.md +367 -358
- package/docs/copy-registry.json +536 -425
- package/docs/design/project-startup-direction.md +17 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +351 -348
- package/modules/provisioning/github-setup.js +27 -5
- package/modules/provisioning/routes/callback-page.js +20 -10
- package/modules/provisioning/routes/github-setup.js +111 -2
- package/modules/provisioning/routes/provisioning.js +29 -4
- package/modules/public-landing/public/projects.html +367 -24
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/github-repos.js +129 -0
- package/src/bongos/routes/github.js +71 -5
- package/src/module-api.js +1 -1
- package/tests/github_handback_any_device.mjs +340 -0
- package/tests/github_org_account.mjs +378 -0
- package/tests/projects_hub.mjs +6 -2
- package/tests/provisioning_callback_page.mjs +3 -2
- package/tests/provisioning_github_app_routes.mjs +15 -10
- package/tests/wizard_draft_resume.mjs +4 -0
- package/tests/wizard_github_feedback.mjs +2 -1
|
@@ -459,6 +459,13 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
|
|
|
459
459
|
.hint strong{color:var(--ink);font-weight:500;}
|
|
460
460
|
|
|
461
461
|
.row2{display:grid;grid-template-columns:1fr 1fr;gap:0 22px;}
|
|
462
|
+
/* whose GitHub account (task 1004651): the account, the organisation, and a typed name
|
|
463
|
+
with its Check button sitting on the field's baseline */
|
|
464
|
+
.acctPick{max-width:680px;}
|
|
465
|
+
.acctPick .row2{grid-template-columns:1fr;}
|
|
466
|
+
.acctPick .acctTyped{display:flex;align-items:flex-end;gap:14px;}
|
|
467
|
+
.acctPick .acctTyped .wizField{flex:1;min-width:0;}
|
|
468
|
+
.acctPick .acctTyped .btn{flex:none;margin:0 0 2px;}
|
|
462
469
|
|
|
463
470
|
/* the choice (start fresh / adopt): two cells of one band on hairlines, the
|
|
464
471
|
chosen one carrying the dot — the dot is the mark, so the icon leaves */
|
|
@@ -1810,6 +1817,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1810
1817
|
<!-- visibility is stated on EVERY repo path (task 1002721); updateFreshPreview()
|
|
1811
1818
|
replaces this floor with the same claim plus the resolved repo URL -->
|
|
1812
1819
|
<div class="hint" id="ghFreshHint">A new <strong>private</strong> repo. You can make it public later.</div>
|
|
1820
|
+
<!-- whose account the new repo is made on (task 1004651); the
|
|
1821
|
+
organisation names are GitHub's, so they are built as DOM
|
|
1822
|
+
text, never as markup -->
|
|
1823
|
+
<div class="acctPick" id="raPick">
|
|
1824
|
+
<div class="row2">
|
|
1825
|
+
<label class="wizField"><span>Whose account</span>
|
|
1826
|
+
<select id="raSel"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
|
|
1827
|
+
<label class="wizField" id="raOrgField" hidden><span>Organisation</span><select id="raOrg"></select></label>
|
|
1828
|
+
</div>
|
|
1829
|
+
<div class="row2 acctTyped" id="raTyped" hidden>
|
|
1830
|
+
<label class="wizField"><span>Organisation name</span><input type="text" id="raName" placeholder="acme-studio" autocomplete="off" maxlength="39"></label>
|
|
1831
|
+
<button type="button" class="btn small ghost" id="raCheck">Check</button>
|
|
1832
|
+
</div>
|
|
1833
|
+
<p class="hint" id="raNote" aria-live="polite"></p>
|
|
1834
|
+
</div>
|
|
1813
1835
|
</div>
|
|
1814
1836
|
<div class="row2" id="manualHome">
|
|
1815
1837
|
<label class="wizField"><span>GitHub owner <small>(user or org)</small></span>
|
|
@@ -2104,6 +2126,20 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2104
2126
|
<p class="hint" id="doneRender" role="status" hidden></p>
|
|
2105
2127
|
<p class="hint" id="doneLook" role="status" hidden></p>
|
|
2106
2128
|
<div id="doneApp" hidden></div>
|
|
2129
|
+
<!-- whose account GitHub makes the app on (task 1004651, the mockup's
|
|
2130
|
+
account select beside Connect GitHub); shown while Connect is offered -->
|
|
2131
|
+
<div class="acctPick" id="aaPick" hidden>
|
|
2132
|
+
<div class="row2">
|
|
2133
|
+
<label class="wizField"><span>Make the app on</span>
|
|
2134
|
+
<select id="aaSel" aria-label="Which GitHub account"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
|
|
2135
|
+
<label class="wizField" id="aaOrgField" hidden><span>Organisation</span><select id="aaOrg"></select></label>
|
|
2136
|
+
</div>
|
|
2137
|
+
<div class="row2 acctTyped" id="aaTyped" hidden>
|
|
2138
|
+
<label class="wizField"><span>Organisation name</span><input type="text" id="aaName" placeholder="acme-studio" autocomplete="off" maxlength="39"></label>
|
|
2139
|
+
<button type="button" class="btn small ghost" id="aaCheck">Check</button>
|
|
2140
|
+
</div>
|
|
2141
|
+
<p class="hint" id="aaNote" aria-live="polite"></p>
|
|
2142
|
+
</div>
|
|
2107
2143
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
2108
2144
|
<!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
|
|
2109
2145
|
and a live region that is itself replaced never announces -->
|
|
@@ -4583,6 +4619,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4583
4619
|
survives the GitHub round-trip; the server, not this page, names the terms version. */
|
|
4584
4620
|
var DRAFT_KEY = 'cb-create-draft-v10';
|
|
4585
4621
|
var CREATED_KEY = 'cb-created-instance';
|
|
4622
|
+
/* the project "Start another project" set down (task 1004650): the server's resume
|
|
4623
|
+
read does not pull this tab back to it */
|
|
4624
|
+
var DECLINED_KEY = 'cb-resume-declined';
|
|
4586
4625
|
/* `description` + `teamShape` are the optional About step (task 1002334) —
|
|
4587
4626
|
both legitimately stay empty, and empty is what the create request omits. */
|
|
4588
4627
|
/* `modules` is the picker's answer (task 1002339) and is THREE-VALUED like
|
|
@@ -5435,13 +5474,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5435
5474
|
Never before identity settles — myLogin()'s 'you' is display copy, and a
|
|
5436
5475
|
draft that stored it would file target_ref "you/<repo>". */
|
|
5437
5476
|
if (!me) return;
|
|
5438
|
-
|
|
5477
|
+
/* the founder's own account, unless they chose an organisation for it (task 1004651) */
|
|
5478
|
+
state.owner = state.repoOrg || myLogin();
|
|
5439
5479
|
$('ghNewName').value = state.repo || '';
|
|
5440
5480
|
updateFreshPreview();
|
|
5481
|
+
if (repoPick) repoPick.load(state.repoAcct === 'org' ? (state.repoOrg || state.repoOrgTyped || '') : null);
|
|
5441
5482
|
}
|
|
5442
5483
|
function updateFreshPreview() {
|
|
5443
5484
|
var hint = $('ghFreshHint');
|
|
5444
|
-
|
|
5485
|
+
/* repoList is the founder's OWN repos, so a clash is only knowable on their account */
|
|
5486
|
+
var clash = !state.repoOrg && state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
|
|
5445
5487
|
if (state.repo && !validRepoName(state.repo)) {
|
|
5446
5488
|
hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
|
|
5447
5489
|
} else if (clash) {
|
|
@@ -5450,11 +5492,177 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5450
5492
|
/* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
|
|
5451
5493
|
private by default (github-repos.js, ADR 0370); the owner can make one
|
|
5452
5494
|
public later from the project's manage page. */
|
|
5453
|
-
hint.className = 'hint'; hint.innerHTML = 'A new <strong>private</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code>. You can make it public later.';
|
|
5495
|
+
hint.className = 'hint'; hint.innerHTML = 'A new <strong>private</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(state.repoOrg || myLogin()) + '/' + esc(state.repo || 'name') + '</code>. You can make it public later.';
|
|
5454
5496
|
}
|
|
5455
5497
|
}
|
|
5456
5498
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
5457
5499
|
|
|
5500
|
+
/* ── whose GitHub account: mine or an organisation's (task 1004651) ──
|
|
5501
|
+
Two pickers share this: the new repo's owner (part 02's Code row) and the app's
|
|
5502
|
+
(part 03). The founder's organisations come from GET /github/orgs; GitHub may not
|
|
5503
|
+
show this sign-in's memberships (it asks for repo access, not org access), so a
|
|
5504
|
+
typed name with a Check button is always offered beside the list, and a role GitHub
|
|
5505
|
+
did not tell us is said as such. For the app, an organisation the founder is known
|
|
5506
|
+
not to own is listed with the reason and cannot be picked: only owners can make
|
|
5507
|
+
apps there. Every organisation name is DOM text (textContent / option text). */
|
|
5508
|
+
var GH_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
5509
|
+
var OTHER_ORG = '__other'; /* a login can never contain "_" */
|
|
5510
|
+
var orgsRead = null;
|
|
5511
|
+
function readOrgs() {
|
|
5512
|
+
if (!orgsRead) {
|
|
5513
|
+
orgsRead = fetch(API + '/github/orgs', { credentials: 'same-origin' })
|
|
5514
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
5515
|
+
.then(function (d) {
|
|
5516
|
+
var orgs = d && Array.isArray(d.orgs) ? d.orgs.filter(function (o) { return o && GH_LOGIN_RE.test(String(o.login || '')); }) : [];
|
|
5517
|
+
return { orgs: orgs, listed: (d && d.listed) || 'none' };
|
|
5518
|
+
})
|
|
5519
|
+
.catch(function () { orgsRead = null; return { orgs: [], listed: 'none' }; });
|
|
5520
|
+
}
|
|
5521
|
+
return orgsRead;
|
|
5522
|
+
}
|
|
5523
|
+
function acctPicker(p, opts) {
|
|
5524
|
+
var el = { box: $(p + 'Pick'), sel: $(p + 'Sel'), orgField: $(p + 'OrgField'), org: $(p + 'Org'), typed: $(p + 'Typed'), name: $(p + 'Name'), check: $(p + 'Check'), note: $(p + 'Note') };
|
|
5525
|
+
var orgs = [], listed = 'none', loaded = false, typed = null, checking = false;
|
|
5526
|
+
function find(login) {
|
|
5527
|
+
var l = String(login || '').toLowerCase();
|
|
5528
|
+
for (var i = 0; i < orgs.length; i++) if (orgs[i].login.toLowerCase() === l) return orgs[i];
|
|
5529
|
+
return null;
|
|
5530
|
+
}
|
|
5531
|
+
/* for the app only: GitHub will not let a known non-owner make it */
|
|
5532
|
+
function refused(o) { return !!(opts.needsOwner && o && o.can_create_apps === false); }
|
|
5533
|
+
function say(text, cls) { el.note.textContent = text || ''; el.note.className = 'hint' + (cls ? ' ' + cls : ''); }
|
|
5534
|
+
function fill(want) {
|
|
5535
|
+
while (el.org.firstChild) el.org.removeChild(el.org.firstChild);
|
|
5536
|
+
var pick = '';
|
|
5537
|
+
orgs.forEach(function (o) {
|
|
5538
|
+
var opt = document.createElement('option');
|
|
5539
|
+
opt.value = o.login;
|
|
5540
|
+
if (refused(o)) {
|
|
5541
|
+
opt.disabled = true;
|
|
5542
|
+
opt.textContent = o.login + (o.role === 'member' ? ' (you’re a member, not an owner)' : ' (not open to you yet)');
|
|
5543
|
+
} else {
|
|
5544
|
+
opt.textContent = o.login;
|
|
5545
|
+
if (!pick) pick = o.login;
|
|
5546
|
+
}
|
|
5547
|
+
el.org.appendChild(opt);
|
|
5548
|
+
});
|
|
5549
|
+
var other = document.createElement('option');
|
|
5550
|
+
other.value = OTHER_ORG;
|
|
5551
|
+
other.textContent = orgs.length ? 'Another organisation…' : 'Type its name…';
|
|
5552
|
+
el.org.appendChild(other);
|
|
5553
|
+
var w = want ? find(want) : null;
|
|
5554
|
+
if (w && !refused(w)) pick = w.login;
|
|
5555
|
+
else if (want) { pick = OTHER_ORG; el.name.value = want; }
|
|
5556
|
+
el.org.value = pick || OTHER_ORG;
|
|
5557
|
+
}
|
|
5558
|
+
/* the choice: personal, or an organisation and whether it can go ahead */
|
|
5559
|
+
function value() {
|
|
5560
|
+
if (el.sel.value !== 'org') return { account: 'personal', ok: true };
|
|
5561
|
+
var v = el.org.value;
|
|
5562
|
+
if (v && v !== OTHER_ORG) { var o = find(v); return { account: 'org', org: o ? o.login : v, ok: !refused(o), access: o }; }
|
|
5563
|
+
var nm = String(el.name.value || '').trim();
|
|
5564
|
+
if (typed && !typed.missing && typed.login.toLowerCase() === nm.toLowerCase()) return { account: 'org', org: typed.login, ok: !refused(typed), access: typed };
|
|
5565
|
+
return { account: 'org', org: '', ok: false, typedName: nm };
|
|
5566
|
+
}
|
|
5567
|
+
function paint() {
|
|
5568
|
+
var isOrg = el.sel.value === 'org';
|
|
5569
|
+
el.orgField.hidden = !isOrg || !loaded || !orgs.length; /* nothing listed: just the typed name */
|
|
5570
|
+
el.typed.hidden = !(isOrg && loaded && el.org.value === OTHER_ORG);
|
|
5571
|
+
var v = value();
|
|
5572
|
+
if (!isOrg) say('');
|
|
5573
|
+
else if (!loaded) say('Reading your organisations from GitHub…');
|
|
5574
|
+
else if (v.org && v.access && v.access.reason && (opts.needsOwner || v.access.can_create_apps === false)) say(v.access.reason, v.access.can_create_apps === false ? 'err' : '');
|
|
5575
|
+
else if (v.org) say(v.access && v.access.can_create_apps ? 'You own ' + v.org + ' on GitHub.' : '');
|
|
5576
|
+
else if (checking) say('Checking with GitHub…');
|
|
5577
|
+
else if (typed && typed.missing && typed.login.toLowerCase() === String(el.name.value || '').trim().toLowerCase()) say(typed.reason, 'err');
|
|
5578
|
+
else say(listed === 'none' && !orgs.length ? 'GitHub didn’t show us your organisations, so type its name and press Check.' : 'Type the organisation’s name and press Check.');
|
|
5579
|
+
if (opts.onChange) opts.onChange(v);
|
|
5580
|
+
}
|
|
5581
|
+
function check() {
|
|
5582
|
+
var nm = String(el.name.value || '').trim();
|
|
5583
|
+
if (!GH_LOGIN_RE.test(nm)) { typed = { login: nm, missing: true, reason: 'That isn’t a GitHub organisation name: letters, numbers and single dashes, up to 39.' }; paint(); return; }
|
|
5584
|
+
checking = true; typed = null; paint();
|
|
5585
|
+
fetch(API + '/github/orgs/' + encodeURIComponent(nm), { credentials: 'same-origin' })
|
|
5586
|
+
.then(function (res) {
|
|
5587
|
+
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
5588
|
+
if (res.ok && d.org && GH_LOGIN_RE.test(String(d.org.login || ''))) return d.org;
|
|
5589
|
+
var msg = (d.error && d.error.message) || '';
|
|
5590
|
+
if (res.status === 404) return { login: nm, missing: true, reason: msg || ('GitHub didn’t show your account an organisation called ' + nm + '.') };
|
|
5591
|
+
return { login: nm, missing: true, reason: 'We couldn’t check that with GitHub just now. Try again in a moment.' };
|
|
5592
|
+
});
|
|
5593
|
+
})
|
|
5594
|
+
.catch(function () { return { login: nm, missing: true, reason: 'We couldn’t check that with GitHub just now. Try again in a moment.' }; })
|
|
5595
|
+
.then(function (r) {
|
|
5596
|
+
checking = false;
|
|
5597
|
+
/* a name typed since the click is not the one this answer is about */
|
|
5598
|
+
if (String(el.name.value || '').trim().toLowerCase() === nm.toLowerCase()) typed = r;
|
|
5599
|
+
paint();
|
|
5600
|
+
});
|
|
5601
|
+
}
|
|
5602
|
+
/* GitHub is asked for the organisations only once one is wanted */
|
|
5603
|
+
function ensureOrgs(want) {
|
|
5604
|
+
if (el.sel.value !== 'org') return;
|
|
5605
|
+
readOrgs().then(function (d) {
|
|
5606
|
+
var first = !loaded;
|
|
5607
|
+
orgs = d.orgs; listed = d.listed; loaded = true;
|
|
5608
|
+
/* a second look keeps what the founder already picked */
|
|
5609
|
+
var keep = first ? want : (value().org || want);
|
|
5610
|
+
fill(keep);
|
|
5611
|
+
if (keep && el.org.value === OTHER_ORG && GH_LOGIN_RE.test(keep)) check();
|
|
5612
|
+
paint();
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
el.sel.addEventListener('change', function () { paint(); ensureOrgs(''); });
|
|
5616
|
+
el.org.addEventListener('change', paint);
|
|
5617
|
+
el.name.addEventListener('input', paint);
|
|
5618
|
+
el.name.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); check(); } });
|
|
5619
|
+
el.check.addEventListener('click', check);
|
|
5620
|
+
return {
|
|
5621
|
+
value: value,
|
|
5622
|
+
el: el,
|
|
5623
|
+
/* want: null = the founder's own account; a login = that organisation; '' = an
|
|
5624
|
+
organisation not picked yet */
|
|
5625
|
+
load: function (want) {
|
|
5626
|
+
el.sel.value = want != null ? 'org' : 'personal';
|
|
5627
|
+
paint();
|
|
5628
|
+
ensureOrgs(want);
|
|
5629
|
+
},
|
|
5630
|
+
/* the reason the choice cannot go ahead, said where it is made */
|
|
5631
|
+
explain: function () {
|
|
5632
|
+
if (el.org.value === OTHER_ORG && el.name) el.name.focus(); else el.org.focus();
|
|
5633
|
+
if (!el.note.textContent) say('Pick the organisation, or choose your own account.', 'err');
|
|
5634
|
+
}
|
|
5635
|
+
};
|
|
5636
|
+
}
|
|
5637
|
+
var repoPick = acctPicker('ra', {
|
|
5638
|
+
needsOwner: false,
|
|
5639
|
+
onChange: function (v) {
|
|
5640
|
+
if (state.repoAutoCreated) return; /* GitHub already made it: the owner is a fact now */
|
|
5641
|
+
state.repoAcct = v.account;
|
|
5642
|
+
state.repoOrg = v.account === 'org' && v.ok ? v.org : '';
|
|
5643
|
+
state.repoOrgTyped = v.account === 'org' && !v.org ? (v.typedName || '') : '';
|
|
5644
|
+
if (me && repoAccess === true && state.mode !== 'adopt') state.owner = state.repoOrg || myLogin();
|
|
5645
|
+
updateFreshPreview();
|
|
5646
|
+
saveDraft();
|
|
5647
|
+
if (typeof paintSetup === 'function') paintSetup();
|
|
5648
|
+
}
|
|
5649
|
+
});
|
|
5650
|
+
/* part 03's account picker (task 1004651). Its default is the organisation the app
|
|
5651
|
+
last went to, else the one the new repo went to, else the founder's own account —
|
|
5652
|
+
set once per project, so a poll never undoes a choice. */
|
|
5653
|
+
var appPick = acctPicker('aa', { needsOwner: true });
|
|
5654
|
+
var appPickFor = null;
|
|
5655
|
+
function syncAppPick(offered) {
|
|
5656
|
+
var box = $('aaPick');
|
|
5657
|
+
if (!box || !appPick) return;
|
|
5658
|
+
box.hidden = !offered;
|
|
5659
|
+
if (!offered || appPickFor === instanceId) return;
|
|
5660
|
+
appPickFor = instanceId;
|
|
5661
|
+
var rec = createdRecord() || {};
|
|
5662
|
+
var want = appOrgLogin() || (state.repoAutoCreated && state.repoAcct === 'org' && state.repoOrg) || rec.repo_org || null;
|
|
5663
|
+
appPick.load(want && /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(want) ? want : null);
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5458
5666
|
/* The adopt picker behaves like a standard combobox (owner design review, task
|
|
5459
5667
|
1002247): the dropdown is open only while the visitor is choosing. Selecting a
|
|
5460
5668
|
repo fills the search box with its full_name and collapses the list; focusing or
|
|
@@ -6016,8 +6224,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6016
6224
|
if (works.indexOf(a.main_work) === -1) return homeErr('Mark which of them is your main one.');
|
|
6017
6225
|
}
|
|
6018
6226
|
if (repoAccess === true && state.mode !== 'adopt') {
|
|
6019
|
-
/* Fresh with access: just the new repo NAME (owner is the signed-in account
|
|
6227
|
+
/* Fresh with access: just the new repo NAME (owner is the signed-in account, or
|
|
6228
|
+
the organisation the founder picked, task 1004651). */
|
|
6020
6229
|
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
6230
|
+
if (state.repoAcct === 'org' && !state.repoOrg) return homeErr('Pick the organisation for the repo, or put it on your own account.');
|
|
6021
6231
|
} else {
|
|
6022
6232
|
/* Adopt (picker or manual) and the manual fallback: need a concrete owner + repo. */
|
|
6023
6233
|
if (!state.owner || !state.repo) {
|
|
@@ -6427,10 +6637,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6427
6637
|
function ensureFreshRepo() {
|
|
6428
6638
|
if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
|
|
6429
6639
|
if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
|
|
6640
|
+
/* the founder's own account sends exactly what it always did; an organisation
|
|
6641
|
+
rides as `org`, and the server checks the name again (task 1004651) */
|
|
6642
|
+
var repoBody = state.repoAcct === 'org' && state.repoOrg ? { name: state.repo, org: state.repoOrg } : { name: state.repo };
|
|
6430
6643
|
return fetch(API + '/github/repos', {
|
|
6431
6644
|
method: 'POST', credentials: 'same-origin',
|
|
6432
6645
|
headers: { 'Content-Type': 'application/json' },
|
|
6433
|
-
body: JSON.stringify(
|
|
6646
|
+
body: JSON.stringify(repoBody)
|
|
6434
6647
|
}).then(function (res) {
|
|
6435
6648
|
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
6436
6649
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -6446,6 +6659,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6446
6659
|
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
6447
6660
|
e.data = data;
|
|
6448
6661
|
if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
6662
|
+
/* the organisation said no: its reason, where the owner is chosen */
|
|
6663
|
+
if (data.error && /^(org_app_restricted|org_forbidden|org_not_found|bad_org)$/.test(data.error.code || '')) { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
6449
6664
|
throw e;
|
|
6450
6665
|
}
|
|
6451
6666
|
var full = data.repo && data.repo.full_name;
|
|
@@ -6551,6 +6766,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6551
6766
|
id: data.instance && data.instance.id, slug: state.slug, domain: madeDomain,
|
|
6552
6767
|
noaddr: !madeDomain,
|
|
6553
6768
|
mode: state.mode, name: state.name, at: Date.now(),
|
|
6769
|
+
/* the organisation the new repo went to, so part 03 offers it for the app */
|
|
6770
|
+
repo_org: state.repoAutoCreated && state.repoAcct === 'org' && state.repoOrg ? state.repoOrg : undefined,
|
|
6554
6771
|
/* a demo's teammates, offered once the hall is up (task 1004503) */
|
|
6555
6772
|
demo_invites: state.demo && state.demo.invites && state.demo.invites.length ? state.demo.invites.slice() : undefined
|
|
6556
6773
|
}));
|
|
@@ -6605,7 +6822,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6605
6822
|
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
6606
6823
|
'">' + label + '</a>';
|
|
6607
6824
|
}
|
|
6608
|
-
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
|
|
6825
|
+
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null };
|
|
6609
6826
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
6610
6827
|
var lastStepsHtml = '';
|
|
6611
6828
|
var lastProgHtml = ''; /* same change-only contract, for the progress strip */
|
|
@@ -6657,7 +6874,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6657
6874
|
: '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLink + ' 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>';
|
|
6658
6875
|
}
|
|
6659
6876
|
var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
|
|
6660
|
-
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 owner and name already filled in), press <strong>Create repository</strong>, and leave it completely empty. It must land at exactly ' + repoLabel + ' — leave GitHub’s Owner box as prefilled
|
|
6877
|
+
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 owner and name already filled in), press <strong>Create repository</strong>, and leave it completely empty. It must land at exactly ' + repoLabel + ' — leave GitHub’s Owner box as prefilled, because the build watches that exact address; a repo created under a different owner is never found. 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>' +
|
|
6661
6878
|
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
|
|
6662
6879
|
}
|
|
6663
6880
|
|
|
@@ -6710,6 +6927,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6710
6927
|
reaches the markup goes through esc(). */
|
|
6711
6928
|
function ghStep(key) { return live.setup && live.setup[key]; }
|
|
6712
6929
|
function appTitle() { return 'Approve the ' + esc(state.name || state.slug || 'project') + ' app on GitHub'; }
|
|
6930
|
+
/* the organisation that owns the app, when the founder made it on one (task 1004651) */
|
|
6931
|
+
function appOrgLogin() {
|
|
6932
|
+
return typeof live.appOrg === 'string' && /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(live.appOrg) ? live.appOrg : null;
|
|
6933
|
+
}
|
|
6934
|
+
/* the app's own settings on GitHub: under its organisation, or under the founder's
|
|
6935
|
+
account. A raw URL; callers esc() it into an attribute. */
|
|
6936
|
+
function appSettingsUrl() {
|
|
6937
|
+
if (!live.appSlug) return null;
|
|
6938
|
+
var org = appOrgLogin();
|
|
6939
|
+
return 'https://github.com' + (org ? '/organizations/' + encodeURIComponent(org) : '') + '/settings/apps/' + encodeURIComponent(live.appSlug);
|
|
6940
|
+
}
|
|
6713
6941
|
|
|
6714
6942
|
/* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
|
|
6715
6943
|
(gap 3 / idea 1000676); locked until a domain exists (the App needs the real
|
|
@@ -6742,12 +6970,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6742
6970
|
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
|
|
6743
6971
|
else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from ' + projectPageLink('its own page') + ', then come back to this step.';
|
|
6744
6972
|
else if (live.manifestFailure === 'sign_in_unconfirmed') {
|
|
6745
|
-
var appLink = live.appSlug ? '<a href="
|
|
6973
|
+
var appLink = live.appSlug ? '<a href="' + esc(appSettingsUrl()) + '" target="_blank" rel="noopener">your app on GitHub</a>' : 'the app on your GitHub account';
|
|
6746
6974
|
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
|
|
6747
6975
|
progressMarkup(1, 1, 'Stopped: your project didn’t confirm sign-in.', 'isErr', 'signin') +
|
|
6748
6976
|
'<button type="button" class="linklike" data-action="manifest-start">Try again anyway</button></li>';
|
|
6749
6977
|
}
|
|
6750
|
-
else if (live.manifestFailure === 'finish_failed') snag =
|
|
6978
|
+
else if (live.manifestFailure === 'finish_failed') snag = appOrgLogin()
|
|
6979
|
+
? 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on ' + esc(appOrgLogin()) + ' — delete it first (the organisation’s Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.'
|
|
6980
|
+
: 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on your account — delete it first (GitHub → Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.';
|
|
6751
6981
|
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
6752
6982
|
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
|
|
6753
6983
|
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
@@ -6784,7 +7014,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6784
7014
|
if (live.manifest !== 'done') {
|
|
6785
7015
|
return '<li class="slocked soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. It lets builders sign in from a terminal, with the <code style="font-family:var(--font-mono)">bongos setup</code> command they run on their own machine; web sign-in works without it.</p></li>';
|
|
6786
7016
|
}
|
|
6787
|
-
var appUrl = live.appSlug ?
|
|
7017
|
+
var appUrl = live.appSlug ? esc(appSettingsUrl()) : null;
|
|
6788
7018
|
return '<li class="soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. GitHub leaves it off on new apps and lets only you switch it on: ' +
|
|
6789
7019
|
(appUrl
|
|
6790
7020
|
? 'open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
|
|
@@ -6862,6 +7092,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6862
7092
|
var r = STEP_RENDERERS[s.key];
|
|
6863
7093
|
return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
|
|
6864
7094
|
}).join('');
|
|
7095
|
+
/* the account picker stands beside Connect GitHub, so it shows while that is offered */
|
|
7096
|
+
syncAppPick(html.indexOf('data-action="manifest-start"') !== -1);
|
|
6865
7097
|
if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
|
|
6866
7098
|
lastStepsHtml = html;
|
|
6867
7099
|
$('humanSteps').innerHTML = html;
|
|
@@ -6926,18 +7158,36 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6926
7158
|
try { localStorage.setItem(AUTO_OPEN_KEY, box.checked ? '1' : '0'); } catch (e) {}
|
|
6927
7159
|
});
|
|
6928
7160
|
|
|
6929
|
-
/* ── C8: the wizard's polls run only on stage,
|
|
6930
|
-
Off-view
|
|
6931
|
-
|
|
7161
|
+
/* ── C8: the wizard's polls run only while the create view is on stage, like the
|
|
7162
|
+
sky. Off-view a due tick parks itself instead of fetching; enter() resumes it.
|
|
7163
|
+
A HIDDEN tab is different (task 1004650): that is exactly when the founder is on
|
|
7164
|
+
GitHub, in another tab or on their phone, so the polls keep checking there at a
|
|
7165
|
+
slower pace (HIDDEN_POLL_MS), and the moment the tab is seen again a waiting tick
|
|
7166
|
+
runs at once — the laptop's checklist ticks on its own. ── */
|
|
7167
|
+
var HIDDEN_POLL_MS = 20000;
|
|
6932
7168
|
function wizPollActive() {
|
|
6933
|
-
return
|
|
7169
|
+
return currentView === 'new';
|
|
7170
|
+
}
|
|
7171
|
+
/* the next tick's wait: the poll's own pace while the tab is seen, the slow one while hidden */
|
|
7172
|
+
function wizPollDelay(ms) {
|
|
7173
|
+
return document.hidden ? Math.max(ms, HIDDEN_POLL_MS) : ms;
|
|
6934
7174
|
}
|
|
6935
7175
|
function resumePolls() {
|
|
6936
7176
|
if (watchPending && instanceId) { watchPending = false; startProgress(instanceId); }
|
|
6937
7177
|
if (manifestPending && manifestState) { manifestPending = false; pollManifest(manifestState); }
|
|
6938
7178
|
}
|
|
7179
|
+
/* each poll's waiting tick, so a return to the tab can run it now instead of later.
|
|
7180
|
+
Only a WAITING tick is run (its timer is set): one mid-fetch re-arms itself, and
|
|
7181
|
+
running it again would start a second chain. */
|
|
7182
|
+
var progressKick = null, manifestKick = null;
|
|
7183
|
+
function kickPolls() {
|
|
7184
|
+
if (pollTimer && progressKick) { clearTimeout(pollTimer); pollTimer = null; progressKick(); }
|
|
7185
|
+
if (manifestPollTimer && manifestKick) { clearTimeout(manifestPollTimer); manifestPollTimer = null; manifestKick(); }
|
|
7186
|
+
}
|
|
6939
7187
|
document.addEventListener('visibilitychange', function () {
|
|
6940
|
-
if (wizPollActive())
|
|
7188
|
+
if (document.hidden || !wizPollActive()) return;
|
|
7189
|
+
resumePolls();
|
|
7190
|
+
kickPolls();
|
|
6941
7191
|
});
|
|
6942
7192
|
|
|
6943
7193
|
/* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
|
|
@@ -6957,6 +7207,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6957
7207
|
var handbackState = (function () {
|
|
6958
7208
|
try { var m = /[#&]ghstate=([0-9a-f]{16,128})/i.exec(location.hash || ''); return m ? m[1] : null; } catch (e) { return null; }
|
|
6959
7209
|
})();
|
|
7210
|
+
/* …and the PROJECT it is for (task 1004650): the return route resolves the state on
|
|
7211
|
+
the server and lands its owner on ?view=new&id=<project>, so a phone, another
|
|
7212
|
+
window or a fresh tab reaches the right project's part 03 with nothing in this
|
|
7213
|
+
tab's storage. Read once at load, for the same reason as the state above. */
|
|
7214
|
+
var handbackProject = (function () {
|
|
7215
|
+
try {
|
|
7216
|
+
var q = new URLSearchParams(location.search || '');
|
|
7217
|
+
var id = q.get('view') === 'new' ? q.get('id') : null;
|
|
7218
|
+
return id && /^\d{1,12}$/.test(id) ? id : null;
|
|
7219
|
+
} catch (e) { return null; }
|
|
7220
|
+
})();
|
|
6960
7221
|
/* the manifest leg survives a reload: token + last status ride the CREATED
|
|
6961
7222
|
record, so enter() re-seeds the step instead of re-offering "Set up
|
|
6962
7223
|
GitHub sign-in" for an app the founder already approved (task 1003232) */
|
|
@@ -6975,13 +7236,26 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6975
7236
|
var manifestGen = 0;
|
|
6976
7237
|
function stopManifestPoll() { manifestGen += 1; if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
|
|
6977
7238
|
|
|
7239
|
+
/* the organisation the server says this attempt's app is on: change-only */
|
|
7240
|
+
function noteAppOrg(org) {
|
|
7241
|
+
var next = typeof org === 'string' && org ? org : null;
|
|
7242
|
+
if (next === live.appOrg) return false;
|
|
7243
|
+
live.appOrg = next;
|
|
7244
|
+
return true;
|
|
7245
|
+
}
|
|
7246
|
+
|
|
6978
7247
|
function startManifest(btn) {
|
|
6979
7248
|
if (!instanceId) return;
|
|
7249
|
+
/* an organisation that cannot go ahead says why where it is chosen, before any
|
|
7250
|
+
tab opens or any link is minted */
|
|
7251
|
+
var acct = appPick ? appPick.value() : { account: 'personal', ok: true };
|
|
7252
|
+
if (!acct.ok) { appPick.explain(); return; }
|
|
6980
7253
|
btn.disabled = true; btn.textContent = 'Preparing GitHub…';
|
|
6981
7254
|
var tab = window.open('about:blank', 'ghAppSetup');
|
|
6982
7255
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
|
|
6983
7256
|
method: 'POST', credentials: 'same-origin',
|
|
6984
|
-
|
|
7257
|
+
/* the founder's own account posts exactly what it always did */
|
|
7258
|
+
headers: { 'Content-Type': 'application/json' }, body: acct.account === 'org' ? JSON.stringify({ account: 'org', org: acct.org }) : '{}'
|
|
6985
7259
|
}).then(function (res) {
|
|
6986
7260
|
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1'); throw new Error('signing in'); }
|
|
6987
7261
|
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
@@ -7000,6 +7274,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7000
7274
|
form.appendChild(inp); document.body.appendChild(form);
|
|
7001
7275
|
form.submit(); form.remove();
|
|
7002
7276
|
live.manifest = 'pending'; live.manifestMsg = ''; live.manifestFailure = null;
|
|
7277
|
+
noteAppOrg(d.account === 'org' ? d.org_login : null);
|
|
7003
7278
|
manifestStartedAt = Date.now(); manifestStale = false;
|
|
7004
7279
|
rememberManifest(d.state, 'pending');
|
|
7005
7280
|
renderSteps();
|
|
@@ -7045,20 +7320,24 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7045
7320
|
function tick() {
|
|
7046
7321
|
if (myGen !== manifestGen) return;
|
|
7047
7322
|
if (!wizPollActive()) { manifestPending = true; stopManifestPoll(); return; }
|
|
7323
|
+
manifestKick = tick;
|
|
7048
7324
|
fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
|
|
7049
7325
|
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
7050
7326
|
.then(function (d) {
|
|
7051
7327
|
if (myGen !== manifestGen) return;
|
|
7052
7328
|
if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
|
|
7053
|
-
|
|
7329
|
+
/* the server-verified ticks (task 1004649), and whose account the app is on */
|
|
7330
|
+
var setupMoved = seedSetup(d.checklist, d.install_url);
|
|
7331
|
+
if (noteAppOrg(d.app_org)) setupMoved = true;
|
|
7332
|
+
if (setupMoved) renderSteps();
|
|
7054
7333
|
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
7055
7334
|
var failure = d.failure || null;
|
|
7056
7335
|
if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
|
|
7057
7336
|
else noteManifestWait();
|
|
7058
|
-
if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
|
|
7337
|
+
if (!d.terminal) manifestPollTimer = setTimeout(tick, wizPollDelay(4000));
|
|
7059
7338
|
else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
|
|
7060
7339
|
})
|
|
7061
|
-
.catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, 8000); });
|
|
7340
|
+
.catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, wizPollDelay(8000)); });
|
|
7062
7341
|
}
|
|
7063
7342
|
tick();
|
|
7064
7343
|
}
|
|
@@ -7249,6 +7528,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7249
7528
|
function tick() {
|
|
7250
7529
|
if (myGen !== watchGen) return;
|
|
7251
7530
|
if (!wizPollActive()) { watchPending = true; stopPolling(); return; }
|
|
7531
|
+
progressKick = tick;
|
|
7252
7532
|
ticks += 1;
|
|
7253
7533
|
if (ticks > 400) {
|
|
7254
7534
|
/* ~33 min: this tab stops watching, but the standup keeps going — hand
|
|
@@ -7295,7 +7575,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7295
7575
|
else noteManifestWait();
|
|
7296
7576
|
}
|
|
7297
7577
|
/* the server-verified checklist rides the same read (task 1004649) */
|
|
7298
|
-
if (data.github_setup && !manifestPollTimer
|
|
7578
|
+
if (data.github_setup && !manifestPollTimer) {
|
|
7579
|
+
var seedMoved = seedSetup(data.github_setup.checklist, data.github_setup.install_url);
|
|
7580
|
+
if (noteAppOrg(data.github_setup.app_org)) seedMoved = true;
|
|
7581
|
+
if (seedMoved) renderSteps();
|
|
7582
|
+
}
|
|
7299
7583
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
7300
7584
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
7301
7585
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
@@ -7305,7 +7589,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7305
7589
|
var terminal = renderProgress(inst);
|
|
7306
7590
|
/* a GitHub leg still in flight keeps this read alive even once the
|
|
7307
7591
|
project is up — after a reload it is the ONLY thing watching it */
|
|
7308
|
-
if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, 5000);
|
|
7592
|
+
if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, wizPollDelay(5000));
|
|
7309
7593
|
else stopPolling();
|
|
7310
7594
|
})
|
|
7311
7595
|
.catch(function (err) {
|
|
@@ -7318,7 +7602,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7318
7602
|
return;
|
|
7319
7603
|
}
|
|
7320
7604
|
/* transient (network / 5xx): keep the last view, try again a bit later */
|
|
7321
|
-
pollTimer = setTimeout(tick, 10000);
|
|
7605
|
+
pollTimer = setTimeout(tick, wizPollDelay(10000));
|
|
7322
7606
|
});
|
|
7323
7607
|
}
|
|
7324
7608
|
tick();
|
|
@@ -7335,7 +7619,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7335
7619
|
instanceId = null;
|
|
7336
7620
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
7337
7621
|
manifestStartedAt = 0; manifestStale = false;
|
|
7338
|
-
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
|
|
7622
|
+
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null };
|
|
7339
7623
|
lastStepsHtml = '';
|
|
7340
7624
|
lastProgHtml = '';
|
|
7341
7625
|
hopReset();
|
|
@@ -7367,6 +7651,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7367
7651
|
resetForNewProject();
|
|
7368
7652
|
/* the deliberate goodbye: the record goes too, so nothing offers to resume
|
|
7369
7653
|
the project the founder has just said they are finished watching */
|
|
7654
|
+
try {
|
|
7655
|
+
var gone = createdRecord();
|
|
7656
|
+
if (gone && gone.id) sessionStorage.setItem(DECLINED_KEY, String(gone.id));
|
|
7657
|
+
} catch (e) {}
|
|
7658
|
+
serverRecord = null;
|
|
7370
7659
|
try { sessionStorage.removeItem(CREATED_KEY); } catch (e) {}
|
|
7371
7660
|
paintResumeOffer(null);
|
|
7372
7661
|
show(1, true);
|
|
@@ -7810,8 +8099,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7810
8099
|
})();
|
|
7811
8100
|
|
|
7812
8101
|
/* ── the hub's three doors ── */
|
|
8102
|
+
/* the server's resume answer (task 1004650), kept in memory too: a browser whose
|
|
8103
|
+
storage throws still has the project it was handed */
|
|
8104
|
+
var serverRecord = null;
|
|
7813
8105
|
function createdRecord() {
|
|
7814
|
-
try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) { return
|
|
8106
|
+
try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null') || serverRecord; } catch (e) { return serverRecord; }
|
|
7815
8107
|
}
|
|
7816
8108
|
/* The "still watching …" offer above step 1 (task 1003503). Its markup is baked
|
|
7817
8109
|
into the panel, so this only names the project and shows or hides the line —
|
|
@@ -7876,6 +8168,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7876
8168
|
/* restore the opt-out too — without it resolvedDomain() re-derives the
|
|
7877
8169
|
assigned address and the done panel promises one that doesn't exist */
|
|
7878
8170
|
state.noAddress = !!created.noaddr;
|
|
8171
|
+
/* a record the server handed over (another device, a fresh tab) carries the repo
|
|
8172
|
+
the project is on, so the install step names it (task 1004650) */
|
|
8173
|
+
if (created.repo_ref && !state.repo) {
|
|
8174
|
+
var rr = String(created.repo_ref).split('/');
|
|
8175
|
+
if (rr.length === 2) { state.owner = rr[0]; state.repo = rr[1]; }
|
|
8176
|
+
}
|
|
7879
8177
|
/* a hand-back from the callback page names the attempt GitHub just approved */
|
|
7880
8178
|
if (handbackState) {
|
|
7881
8179
|
created.manifest_state = handbackState;
|
|
@@ -7898,9 +8196,54 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7898
8196
|
identity settles it shows the draft step optimistically (the page stays
|
|
7899
8197
|
usable while /me is in flight — the boot's own rule); onAuth() re-enters
|
|
7900
8198
|
once the answer lands. Focus only moves on a deliberate nav (the flag). */
|
|
8199
|
+
/* askServerResume(focus) — which project is this founder connecting, by the SERVER's
|
|
8200
|
+
view, when this tab cannot say (task 1004650)? Asked once per page load. A hand-back
|
|
8201
|
+
names its project (handbackProject, from the return route): it always lands on that
|
|
8202
|
+
project's part 03, and only its owner gets an answer. With no name, the server picks
|
|
8203
|
+
the founder's newest project whose GitHub is not connected yet: an arrival lands on
|
|
8204
|
+
it unless something is being typed, and a deliberate "New project" only offers it on
|
|
8205
|
+
the resume line. Nothing found → the page as it was. */
|
|
8206
|
+
var resumeAsked = false;
|
|
8207
|
+
function recordFromServer(p) {
|
|
8208
|
+
return { id: p.id, slug: p.slug || '', name: p.slug || '', domain: p.domain || '', noaddr: !p.domain,
|
|
8209
|
+
mode: p.onboard_mode === 'adopt' ? 'adopt' : 'greenfield', repo_ref: p.repo || null, at: Date.now(), from_server: true };
|
|
8210
|
+
}
|
|
8211
|
+
function askServerResume(focus) {
|
|
8212
|
+
resumeAsked = true;
|
|
8213
|
+
var named = handbackProject;
|
|
8214
|
+
handbackProject = null;
|
|
8215
|
+
return fetch(API + '/provisioning/github-app/resume' + (named ? '?id=' + encodeURIComponent(named) : ''), { credentials: 'same-origin' })
|
|
8216
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
8217
|
+
.catch(function () { return null; })
|
|
8218
|
+
.then(function (d) {
|
|
8219
|
+
var p = d && d.project;
|
|
8220
|
+
var had = createdRecord();
|
|
8221
|
+
if (!p || !p.id || (!named && had && had.id)) { if (named) enter(focus); return; }
|
|
8222
|
+
var declined = null;
|
|
8223
|
+
try { declined = sessionStorage.getItem(DECLINED_KEY); } catch (e) {}
|
|
8224
|
+
if (!named && declined === String(p.id)) return;
|
|
8225
|
+
var rec = recordFromServer(p);
|
|
8226
|
+
/* the same project this tab already knew keeps what it knew (a demo's teammates) */
|
|
8227
|
+
if (had && String(had.id) === String(p.id)) rec = Object.assign({}, had, rec, { name: had.name || rec.name });
|
|
8228
|
+
else if (had && had.id) resetForNewProject(); /* a hand-back for a different project */
|
|
8229
|
+
serverRecord = rec;
|
|
8230
|
+
try { sessionStorage.setItem(CREATED_KEY, JSON.stringify(rec)); } catch (e) {}
|
|
8231
|
+
if (currentView !== 'new') return;
|
|
8232
|
+
if (named || (!focus && !String(state.name || '').trim())) { resumeIntent = true; enterCreated(rec, false); }
|
|
8233
|
+
else paintResumeOffer(rec);
|
|
8234
|
+
});
|
|
8235
|
+
}
|
|
8236
|
+
|
|
7901
8237
|
function enter(focus) {
|
|
7902
8238
|
if (authKnown && !me) { show(0, !!focus); return; }
|
|
7903
8239
|
var created = authKnown ? createdRecord() : null;
|
|
8240
|
+
/* this tab cannot say which project — ask the server (task 1004650). A named
|
|
8241
|
+
hand-back waits for that answer instead of painting another project first. */
|
|
8242
|
+
if (authKnown && !resumeAsked && (handbackProject || !(created && created.id))) {
|
|
8243
|
+
var waitForIt = !!handbackProject;
|
|
8244
|
+
askServerResume(!!focus);
|
|
8245
|
+
if (waitForIt) return;
|
|
8246
|
+
}
|
|
7904
8247
|
/* A draft on the page (step 1) beside a stored create is a SECOND project being
|
|
7905
8248
|
typed. A reload or a GitHub round-trip (repo access) is that same visit
|
|
7906
8249
|
continuing, so the draft keeps its place and the earlier project stays one
|