@bongos/core 1.19.327 → 1.19.328
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 +19 -14
- package/docs/copy-inventory.md +148 -147
- package/docs/copy-registry.json +238 -219
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +27 -5
- package/public-landing/projects.html +253 -61
- package/src/module-api.js +3 -1
- package/tests/projects_hub_pre_uat.mjs +223 -0
|
@@ -42,6 +42,9 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
42
42
|
intent); the control-plane runner does the real infra work (ADR 0111 §2). */
|
|
43
43
|
.viewWrap{padding-bottom:40px;}
|
|
44
44
|
.card{margin-bottom:26px;}
|
|
45
|
+
/* page titles align with the cards below them — cosmos's .hero is 880px-centred
|
|
46
|
+
for the landing hero, which indented every hub title ~48px past its card */
|
|
47
|
+
#view-mine .hero,#view-manage .hero{max-width:none;}
|
|
45
48
|
|
|
46
49
|
/* ── the manage view (task 1003041): container rhythm only — every component
|
|
47
50
|
it draws (card, chip, addr, empty) is cosmos vocabulary ── */
|
|
@@ -553,11 +556,11 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
553
556
|
<p class="mineEmpty" id="mineEmpty" hidden>Nothing matches that. <button type="button" class="linklike" id="mineClear">Clear the filters</button></p>
|
|
554
557
|
|
|
555
558
|
<section class="empty" id="empty" hidden>
|
|
556
|
-
<p>You don't own any projects yet.
|
|
559
|
+
<p>You don't own any projects yet. Name one, pick a home for its code, and it gets an address of its own in this sky.</p>
|
|
557
560
|
<a class="btn" href="?view=new" data-nav-to="new">Create your first project</a>
|
|
558
561
|
</section>
|
|
559
562
|
<ul class="projects" id="projectsList"></ul>
|
|
560
|
-
<p class="finePrint">Buttons here ask the platform to act
|
|
563
|
+
<p class="finePrint">Buttons here ask the platform to act; each card reports only what was verified, and when. A standup keeps going even with this tab closed.</p>
|
|
561
564
|
</section>
|
|
562
565
|
</main>
|
|
563
566
|
</div>
|
|
@@ -597,8 +600,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
597
600
|
|
|
598
601
|
<section id="mMain" hidden>
|
|
599
602
|
<div class="hero" style="padding-left:0;padding-right:0">
|
|
600
|
-
<h1>Manage</h1>
|
|
601
|
-
<p class="lede">
|
|
603
|
+
<h1 id="mTitle">Manage</h1>
|
|
604
|
+
<p class="lede">Its address, its repository, what the platform last <b>verified</b> about it — and the controls you have today.</p>
|
|
602
605
|
</div>
|
|
603
606
|
|
|
604
607
|
<div class="card">
|
|
@@ -606,16 +609,13 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
606
609
|
<div class="projMeta" id="mMeta" hidden></div>
|
|
607
610
|
<div class="addr" id="mAddr" hidden></div>
|
|
608
611
|
<p class="finePrint" id="mCreated" hidden></p>
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
<
|
|
613
|
-
<div class="
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
<div class="card" id="mJoinability">
|
|
617
|
-
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">How people join</h2>
|
|
618
|
-
<div class="empty"><p>Choosing how new people join — walk right in, apply first, or invite only — isn’t wired up here yet. It lands in one of the next updates.</p></div>
|
|
612
|
+
<!-- the card's own story, on the project's own page (task 1003232): the
|
|
613
|
+
per-flow strip, the honest snag, and the doors — the same
|
|
614
|
+
stripFor/errMarkup/canRetry/canRevive the list card reads -->
|
|
615
|
+
<div id="mStrip"></div>
|
|
616
|
+
<div class="projMsg err" id="mErr" hidden></div>
|
|
617
|
+
<div class="projMsg" id="mMsg"></div>
|
|
618
|
+
<div class="actions" id="mActs" hidden></div>
|
|
619
619
|
</div>
|
|
620
620
|
|
|
621
621
|
<!-- live since task 1003045: reads the repo's real visibility from GitHub
|
|
@@ -636,6 +636,18 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
636
636
|
</details>
|
|
637
637
|
</div>
|
|
638
638
|
|
|
639
|
+
<!-- the two settings the sibling tasks (1003043 / 1003044) fill in — placed
|
|
640
|
+
BELOW the sections that work, so a founder meets controls before promises -->
|
|
641
|
+
<div class="card" id="mVisibility">
|
|
642
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Who can see it</h2>
|
|
643
|
+
<div class="empty"><p>Choosing who can open this project’s pages — everyone, or only people signed in to it — isn’t wired up here yet. For now, every project’s pages are open to everyone.</p></div>
|
|
644
|
+
</div>
|
|
645
|
+
|
|
646
|
+
<div class="card" id="mJoinability">
|
|
647
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">How people join</h2>
|
|
648
|
+
<div class="empty"><p>Choosing how new people join — walk right in, apply first, or invite only — isn’t wired up here yet. For now, people apply to join and you invite them.</p></div>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
639
651
|
<!-- live since task 1003042: the teardown control, for a project in ANY
|
|
640
652
|
state that still holds something. The server used to refuse a
|
|
641
653
|
'requested' or an 'error' project outright — and force-teardown reused
|
|
@@ -684,7 +696,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
684
696
|
<section class="panel" id="panel-gate" aria-labelledby="gate-h">
|
|
685
697
|
<h1 id="gate-h">Create a project</h1>
|
|
686
698
|
<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>
|
|
687
|
-
<!-- The wizard's own sign-in opts into repo scope (read:user +
|
|
699
|
+
<!-- The wizard's own sign-in opts into repo scope (read:user + repo, ADR 0155) up
|
|
688
700
|
front (&repos=1) — anyone here intends to create a project, which needs repo
|
|
689
701
|
access, so we ask once at sign-in instead of a second "Connect" step at Home.
|
|
690
702
|
Hall + CLI sign-in stay identity-only (ADR 0143 least-privilege default). -->
|
|
@@ -692,6 +704,11 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
692
704
|
<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>
|
|
693
705
|
Sign in with GitHub
|
|
694
706
|
</a>
|
|
707
|
+
<!-- Informed consent BEFORE GitHub's own wording (ADR 0155): the scope the
|
|
708
|
+
button asks for reads as "all your repositories" on GitHub's screen, so
|
|
709
|
+
the reason is stated here, on the path every founder actually takes —
|
|
710
|
+
not only on the Home step's re-connect branch (task 1003232). -->
|
|
711
|
+
<p class="finePrint" id="wizScopeNote">Signing in asks GitHub for repository access. GitHub words this as access to <em>all</em> your repositories — that is the only permission it offers that can write to a private repo. We use it to create (or read) the one repo your project lives in and push its starting files. Nothing else is touched.</p>
|
|
695
712
|
<!-- Trust link → the AGPL license text (task 1002705): the old private-repo URL
|
|
696
713
|
404ed, and no public code artifact exists until the ADR 0099 mirror flips
|
|
697
714
|
(~2026-11; npm scopes are private too) — the license is the one live,
|
|
@@ -784,7 +801,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
784
801
|
<div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
|
|
785
802
|
<div id="ghAdopt" class="wizField" hidden>
|
|
786
803
|
<span>Pick one of your repositories</span>
|
|
787
|
-
<input type="text" id="ghSearch" placeholder="Search your repos…" autocomplete="off">
|
|
804
|
+
<input type="text" id="ghSearch" placeholder="Search your repos…" autocomplete="off" aria-label="Search your repositories">
|
|
788
805
|
<div id="ghList" class="ghlist" role="listbox" aria-label="Your repositories"></div>
|
|
789
806
|
<div class="hint" id="ghAdoptHint"> </div>
|
|
790
807
|
<!-- visibility stated on EVERY repo path (task 1002721) — the picker
|
|
@@ -793,7 +810,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
793
810
|
</div>
|
|
794
811
|
<div id="ghFresh" class="wizField" hidden>
|
|
795
812
|
<span>New repository name</span>
|
|
796
|
-
<input type="text" id="ghNewName" placeholder="mercury" autocomplete="off" maxlength="100">
|
|
813
|
+
<input type="text" id="ghNewName" placeholder="mercury" autocomplete="off" maxlength="100" aria-label="New repository name">
|
|
797
814
|
<!-- visibility is stated on EVERY repo path (task 1002721) — this baked
|
|
798
815
|
line is the floor; updateFreshPreview() replaces it with the same
|
|
799
816
|
claim plus the resolved repo URL once a name is typed -->
|
|
@@ -830,7 +847,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
830
847
|
choice; the owner read the step as "weird" and never found it. -->
|
|
831
848
|
<section class="panel" id="panel-address" aria-labelledby="addr-h">
|
|
832
849
|
<h1 id="addr-h">Its address in the sky</h1>
|
|
833
|
-
<p class="lede">The handle names your project on the platform — and its address comes with it.
|
|
850
|
+
<p class="lede">The handle names your project on the platform — and its address comes with it. Once it stands up, a few GitHub steps (the next screen walks you through them) switch sign-in on.</p>
|
|
834
851
|
<form data-next="5">
|
|
835
852
|
<label class="wizField"><span>Project handle</span>
|
|
836
853
|
<input type="text" id="f-slug" placeholder="mercury" autocomplete="off" maxlength="40" spellcheck="false">
|
|
@@ -882,7 +899,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
882
899
|
<div id="liveProgress"></div>
|
|
883
900
|
<h2 style="font-family:var(--font-display);font-size:19px;margin-top:22px;">Your steps — the parts only you can play</h2>
|
|
884
901
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
885
|
-
<p class="samePath">
|
|
902
|
+
<p class="samePath">These are the same steps however a project is created — nothing here is special to this page.</p>
|
|
886
903
|
</section>
|
|
887
904
|
</div>
|
|
888
905
|
|
|
@@ -969,6 +986,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
969
986
|
'use strict';
|
|
970
987
|
|
|
971
988
|
var API = '/api/bongos';
|
|
989
|
+
var BASE_TITLE = document.title;
|
|
972
990
|
var POLL_MS = 5000; /* the create.html watch cadence (in-flight cards) */
|
|
973
991
|
var POLL_SOFT_MS = 10000; /* transient failure back-off */
|
|
974
992
|
var REFRESH_MS = 60000; /* settled-card refresh: recency + heartbeat overlay
|
|
@@ -1037,17 +1055,28 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1037
1055
|
|
|
1038
1056
|
function setView(v, focus) {
|
|
1039
1057
|
if (!VIEWS[v]) v = 'map';
|
|
1058
|
+
var prevView = currentView;
|
|
1040
1059
|
currentView = v;
|
|
1041
1060
|
Object.keys(VIEWS).forEach(function (k) { $('view-' + k).hidden = (k !== v); });
|
|
1042
1061
|
document.querySelectorAll('[data-nav]').forEach(function (a) {
|
|
1043
1062
|
/* Home is a PAGE, not a view — it never takes the current-view pill */
|
|
1044
1063
|
if (a.dataset.nav !== 'home' && a.dataset.nav === v) a.setAttribute('aria-current', 'page');
|
|
1064
|
+
/* the manage view is a page OF your projects — that pill stays lit there */
|
|
1065
|
+
else if (v === 'manage' && a.dataset.nav === 'mine') a.setAttribute('aria-current', 'page');
|
|
1045
1066
|
else a.removeAttribute('aria-current');
|
|
1046
1067
|
});
|
|
1047
1068
|
/* the manage view's URL names its project — the deep-link contract grew
|
|
1048
1069
|
an id in task 1003041, so a manage tab is shareable and reload-safe */
|
|
1049
1070
|
var idPart = (v === 'manage' && manageId) ? '&id=' + encodeURIComponent(manageId) : '';
|
|
1050
|
-
|
|
1071
|
+
/* a DELIBERATE view change is a history entry, so the browser's Back button
|
|
1072
|
+
moves between views instead of leaving the hub; boot and background
|
|
1073
|
+
settles keep rewriting the entry in place (task 1003232) */
|
|
1074
|
+
var deliberate = focus || (v === 'manage' && prevView && prevView !== 'manage');
|
|
1075
|
+
try {
|
|
1076
|
+
if (deliberate && location.search !== '?view=' + v + idPart) history.pushState(null, '', '?view=' + v + idPart);
|
|
1077
|
+
else history.replaceState(null, '', '?view=' + v + idPart);
|
|
1078
|
+
} catch (e) {}
|
|
1079
|
+
if (v !== 'manage') document.title = BASE_TITLE;
|
|
1051
1080
|
/* the shell's sign-in door returns the visitor to the view they left, and
|
|
1052
1081
|
carries the repo scope when that view is the wizard (ADR 0143: the
|
|
1053
1082
|
create path asks once up front instead of a second consent at Home) */
|
|
@@ -1080,6 +1109,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1080
1109
|
document.querySelectorAll('[data-nav-to]').forEach(function (a) {
|
|
1081
1110
|
a.addEventListener('click', function (ev) { ev.preventDefault(); setView(a.dataset.navTo, true); });
|
|
1082
1111
|
});
|
|
1112
|
+
/* Back / Forward land on the view the URL names — viewFromUrl() reads the
|
|
1113
|
+
manage id too, so a manage entry restores its project */
|
|
1114
|
+
window.addEventListener('popstate', function () { setView(viewFromUrl()); });
|
|
1083
1115
|
|
|
1084
1116
|
/* ── the self-typing mark. `bongos` is the CONSTANT — it is the word you
|
|
1085
1117
|
type — and the prefix is what varies, which is exactly what the vanity
|
|
@@ -1568,7 +1600,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1568
1600
|
function goToManage(instId) {
|
|
1569
1601
|
manageId = String(instId || '');
|
|
1570
1602
|
manageInst = null;
|
|
1571
|
-
setView('manage');
|
|
1603
|
+
setView('manage'); /* setView pushes a history entry for a list → manage move, so Back returns to the list */
|
|
1572
1604
|
}
|
|
1573
1605
|
/* Card-shell lookup by instance id. CSS.escape, not esc(): the data-inst
|
|
1574
1606
|
attribute holds the DECODED value, so an HTML-escaped id would never match
|
|
@@ -2293,6 +2325,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2293
2325
|
var avail = !custom && !state.noAddress && assigned ? slugAvailState() : 'unknown';
|
|
2294
2326
|
box.classList.toggle('optedOut', state.noAddress && !custom);
|
|
2295
2327
|
box.classList.toggle('unavailable', avail === 'taken' || avail === 'yours' || avail === 'reserved');
|
|
2328
|
+
/* the format verdict must not cheer a handle the box below is refusing */
|
|
2329
|
+
if (avail === 'taken' || avail === 'yours' || avail === 'reserved') { var hs = $('h-slug'); hs.innerHTML = ' '; hs.className = 'hint'; }
|
|
2296
2330
|
if (custom) {
|
|
2297
2331
|
big.innerHTML = '<code>' + esc(custom) + '</code> — your own domain, wired up for you.';
|
|
2298
2332
|
note.textContent = 'DNS pointing happens after the standup; the card on your projects page walks you through it.';
|
|
@@ -2301,7 +2335,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2301
2335
|
note.textContent = 'Sign-in on your project can’t be set up until it has one — add it any time from your projects page.';
|
|
2302
2336
|
} else if (assigned && avail === 'free') {
|
|
2303
2337
|
big.innerHTML = '<code>' + esc(assigned) + '</code> — yours from the moment it stands up.';
|
|
2304
|
-
note.textContent = '
|
|
2338
|
+
note.textContent = 'Sharing works from the moment it stands up; sign-in switches on after the GitHub steps. You can point a domain you own at it any time.';
|
|
2305
2339
|
} else if (assigned && avail === 'taken') {
|
|
2306
2340
|
big.innerHTML = '<code>' + esc(assigned) + '</code> — already taken.';
|
|
2307
2341
|
note.textContent = 'Another project on the platform holds that handle. Pick a different one.';
|
|
@@ -2389,6 +2423,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2389
2423
|
(a pre-auth probe 401s for strangers and, worse, could seed the
|
|
2390
2424
|
draft's owner with myLogin()'s placeholder) */
|
|
2391
2425
|
if (step === 3) { syncHome(); if (authKnown) ensureRepoAccess(); }
|
|
2426
|
+
/* a resumed draft can land PAST the Home step with the probe never run —
|
|
2427
|
+
Create would then file a repo it promised to make but never did */
|
|
2428
|
+
else if (step > 3 && step < 6 && authKnown && repoAccess === null) ensureRepoAccess();
|
|
2392
2429
|
/* re-check on every entry, not just on typing: a resumed draft arrives here with
|
|
2393
2430
|
a handle nobody has asked the server about, and a handle free ten minutes ago
|
|
2394
2431
|
may not be now (task 1002898 N10) */
|
|
@@ -2671,7 +2708,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2671
2708
|
$('h-slug').className = 'hint err'; $('f-slug').classList.add('bad');
|
|
2672
2709
|
return false;
|
|
2673
2710
|
}
|
|
2674
|
-
if (state.domain && !looksLikeDomain(state.domain))
|
|
2711
|
+
if (state.domain && !looksLikeDomain(state.domain)) {
|
|
2712
|
+
$('addrCustom').open = true;
|
|
2713
|
+
var hd0 = $('h-domain');
|
|
2714
|
+
hd0.textContent = 'That doesn’t look like a domain yet (e.g. mercury.example.com) — fix it, or clear the field to use the address above.';
|
|
2715
|
+
hd0.className = 'hint err';
|
|
2716
|
+
$('f-domain').focus();
|
|
2717
|
+
return false;
|
|
2718
|
+
}
|
|
2675
2719
|
/* A handle the server has already said is not free would only 409 at Create —
|
|
2676
2720
|
stop here instead (task 1002898 N10). ONLY a definite negative blocks:
|
|
2677
2721
|
'unknown' and 'checking' must never gate the step, because the check is
|
|
@@ -2730,7 +2774,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2730
2774
|
['Address', resolvedDomain()
|
|
2731
2775
|
? '<code>' + esc(resolvedDomain()) + '</code>'
|
|
2732
2776
|
: '<em style="font-weight:600;color:var(--warn)">none — sign-in stays locked until it has one</em>'],
|
|
2733
|
-
['Shape', '
|
|
2777
|
+
['Shape', 'Its own server, running the platform’s current release']
|
|
2734
2778
|
];
|
|
2735
2779
|
$('reviewList').innerHTML = rows.map(function (r) {
|
|
2736
2780
|
return '<li><span class="k">' + r[0] + '</span><span class="v">' + r[1] + '</span></li>';
|
|
@@ -2821,7 +2865,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2821
2865
|
})
|
|
2822
2866
|
});
|
|
2823
2867
|
}).then(function (res) {
|
|
2824
|
-
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew'); throw new Error('signing in'); }
|
|
2868
|
+
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1'); throw new Error('signing in'); }
|
|
2825
2869
|
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
2826
2870
|
if (!res.ok) {
|
|
2827
2871
|
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
@@ -2877,7 +2921,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2877
2921
|
var live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
|
|
2878
2922
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
2879
2923
|
var lastStepsHtml = '';
|
|
2880
|
-
var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall'];
|
|
2924
|
+
var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall'];
|
|
2881
2925
|
|
|
2882
2926
|
function fetchPlan() {
|
|
2883
2927
|
var domain = resolvedDomain() || '';
|
|
@@ -2960,8 +3004,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2960
3004
|
(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>' +
|
|
2961
3005
|
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
2962
3006
|
}
|
|
2963
|
-
if (live.manifest
|
|
2964
|
-
|
|
3007
|
+
if (live.manifest === 'code_received') {
|
|
3008
|
+
/* GitHub's half is done — say so, rather than keep telling the founder
|
|
3009
|
+
to approve an app they just approved */
|
|
3010
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub’s part is done. The platform is placing the credentials on your project now — this step ticks itself.</p></li>';
|
|
3011
|
+
}
|
|
3012
|
+
if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
|
|
3013
|
+
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>' +
|
|
3014
|
+
(manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
|
|
2965
3015
|
}
|
|
2966
3016
|
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>' +
|
|
2967
3017
|
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
@@ -3003,11 +3053,20 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3003
3053
|
if (!domain) {
|
|
3004
3054
|
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>';
|
|
3005
3055
|
}
|
|
3056
|
+
/* the door opens only when the project ANSWERS and sign-in is wired: a
|
|
3057
|
+
link to an address that does not resolve yet is the worst first
|
|
3058
|
+
impression a founder can get of their own project (task 1003232) */
|
|
3059
|
+
if (live.instStatus !== 'active' || live.manifest !== 'done') {
|
|
3060
|
+
return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks once your project is answering and sign-in above is wired. Then you’ll open <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>, sign in with GitHub once, and be seated as the owner — the founding Archon.</p></li>';
|
|
3061
|
+
}
|
|
3006
3062
|
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>';
|
|
3007
3063
|
}
|
|
3008
3064
|
|
|
3009
3065
|
function renderStepEnterHall() {
|
|
3010
3066
|
var domain = resolvedDomain();
|
|
3067
|
+
if (domain && (live.instStatus !== 'active' || live.manifest !== 'done')) {
|
|
3068
|
+
return '<li class="slocked" data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Unlocks with the first sign-in — your project’s home will be <code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code>.</p></li>';
|
|
3069
|
+
}
|
|
3011
3070
|
return domain
|
|
3012
3071
|
? '<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>'
|
|
3013
3072
|
: '<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>';
|
|
@@ -3095,6 +3154,20 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3095
3154
|
var manifestPollTimer = null;
|
|
3096
3155
|
var manifestState = null; /* the one-time state token — kept so a parked poll can resume */
|
|
3097
3156
|
var manifestPending = false;
|
|
3157
|
+
var manifestStartedAt = 0; /* when the founder was sent to GitHub */
|
|
3158
|
+
var manifestStale = false; /* ~90s with no approval → offer to start again */
|
|
3159
|
+
/* the manifest leg survives a reload: token + last status ride the CREATED
|
|
3160
|
+
record, so enter() re-seeds the step instead of re-offering "Set up
|
|
3161
|
+
GitHub sign-in" for an app the founder already approved (task 1003232) */
|
|
3162
|
+
function rememberManifest(stateToken, status, appSlug) {
|
|
3163
|
+
try {
|
|
3164
|
+
var rec = createdRecord() || {};
|
|
3165
|
+
if (stateToken !== undefined) rec.manifest_state = stateToken;
|
|
3166
|
+
if (status !== undefined) rec.manifest = status;
|
|
3167
|
+
if (appSlug !== undefined) rec.app_slug = appSlug;
|
|
3168
|
+
sessionStorage.setItem(CREATED_KEY, JSON.stringify(rec));
|
|
3169
|
+
} catch (e) {}
|
|
3170
|
+
}
|
|
3098
3171
|
/* a cleared setTimeout id can't cancel a chain that's mid-fetch — each chain
|
|
3099
3172
|
carries the generation it was born under, and stopping bumps it so an
|
|
3100
3173
|
orphaned chain goes quiet instead of re-arming itself forever */
|
|
@@ -3109,7 +3182,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3109
3182
|
method: 'POST', credentials: 'same-origin',
|
|
3110
3183
|
headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
3111
3184
|
}).then(function (res) {
|
|
3112
|
-
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew'); throw new Error('signing in'); }
|
|
3185
|
+
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1'); throw new Error('signing in'); }
|
|
3113
3186
|
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
3114
3187
|
if (!res.ok) {
|
|
3115
3188
|
var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
|
|
@@ -3125,6 +3198,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3125
3198
|
form.appendChild(inp); document.body.appendChild(form);
|
|
3126
3199
|
form.submit(); form.remove();
|
|
3127
3200
|
live.manifest = 'pending'; live.manifestMsg = '';
|
|
3201
|
+
manifestStartedAt = Date.now(); manifestStale = false;
|
|
3202
|
+
rememberManifest(d.state, 'pending');
|
|
3128
3203
|
renderSteps();
|
|
3129
3204
|
pollManifest(d.state);
|
|
3130
3205
|
}).catch(function (err) {
|
|
@@ -3146,9 +3221,10 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3146
3221
|
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
3147
3222
|
.then(function (d) {
|
|
3148
3223
|
if (myGen !== manifestGen) return;
|
|
3149
|
-
if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; renderSteps(); }
|
|
3224
|
+
if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
|
|
3150
3225
|
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
3151
|
-
if (next !== live.manifest) { live.manifest = next; renderSteps(); }
|
|
3226
|
+
if (next !== live.manifest) { live.manifest = next; rememberManifest(undefined, next); renderSteps(); }
|
|
3227
|
+
else if (next === 'pending' && !manifestStale && manifestStartedAt && (Date.now() - manifestStartedAt) > 90000) { manifestStale = true; renderSteps(); }
|
|
3152
3228
|
if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
|
|
3153
3229
|
else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
|
|
3154
3230
|
})
|
|
@@ -3206,7 +3282,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3206
3282
|
}
|
|
3207
3283
|
}
|
|
3208
3284
|
if (s === 'requested') {
|
|
3209
|
-
el.innerHTML = progressMarkup(
|
|
3285
|
+
el.innerHTML = progressMarkup(0, 0, 'Filed — waiting for the platform to pick it up. This updates the moment it starts.', '', revive ? 'revive' : 'standup');
|
|
3210
3286
|
} else if (s === 'provisioning') {
|
|
3211
3287
|
el.innerHTML = progressMarkup(1, 1,
|
|
3212
3288
|
revive ? 'Bringing it back on the newest platform version — nothing needed from you.'
|
|
@@ -3225,8 +3301,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3225
3301
|
/* domainless instances are never probed — there is no address to
|
|
3226
3302
|
answer at, so a verified answer can never arrive; say what IS true
|
|
3227
3303
|
instead of promising the hall (sign-in stays locked without one) */
|
|
3228
|
-
el.innerHTML = progressMarkup(-1,
|
|
3229
|
-
'The build finished — your project is standing, but it has no address yet, so sign-in can’t be wired up. Attach a domain any time from <a href="?view=mine">your projects page</a>; the steps below that need one unlock then.', '
|
|
3304
|
+
el.innerHTML = progressMarkup(-1, 2,
|
|
3305
|
+
'The build finished — your project is standing, but it has no address yet, so sign-in can’t be wired up. Attach a domain any time from <a href="?view=mine">your projects page</a>; the steps below that need one unlock then.', '', flow);
|
|
3230
3306
|
return true;
|
|
3231
3307
|
}
|
|
3232
3308
|
if (inst.last_verified_up === false) {
|
|
@@ -3257,7 +3333,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3257
3333
|
el.innerHTML = progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from <a href="?view=mine">your projects page</a>, or start another below.', '', 'teardown');
|
|
3258
3334
|
return true;
|
|
3259
3335
|
} else {
|
|
3260
|
-
el.innerHTML = progressMarkup(0,
|
|
3336
|
+
el.innerHTML = progressMarkup(0, 0, 'Filed. Watching for the standup to start…');
|
|
3261
3337
|
}
|
|
3262
3338
|
return false;
|
|
3263
3339
|
}
|
|
@@ -3268,8 +3344,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3268
3344
|
watchPending = false;
|
|
3269
3345
|
var myGen = watchGen;
|
|
3270
3346
|
var ticks = 0;
|
|
3347
|
+
var watchFlow = 'standup'; /* the vocabulary the strip is speaking (F22) */
|
|
3271
3348
|
var el = $('liveProgress');
|
|
3272
|
-
el.innerHTML = progressMarkup(0,
|
|
3349
|
+
el.innerHTML = progressMarkup(0, 0, 'Filed. Watching for the standup to start…');
|
|
3273
3350
|
function tick() {
|
|
3274
3351
|
if (myGen !== watchGen) return;
|
|
3275
3352
|
if (!wizPollActive()) { watchPending = true; stopPolling(); return; }
|
|
@@ -3279,14 +3356,18 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3279
3356
|
the long watch to the mine view, the owner's durable surface (task
|
|
3280
3357
|
1002693). The old "stop quietly" here was a dead-end lie: the hall
|
|
3281
3358
|
card it assumed was removed in task 1002675. */
|
|
3282
|
-
|
|
3283
|
-
|
|
3359
|
+
/* a SNAGGED standup keeps its honest error — "still building" would be
|
|
3360
|
+
a lie exactly when the founder most needs the truth */
|
|
3361
|
+
if (live.instStatus !== 'error') {
|
|
3362
|
+
$('liveProgress').innerHTML = progressMarkup(1, 1,
|
|
3363
|
+
'Still working — this tab will stop watching now. Follow it any time on <a href="?view=mine">your projects page</a>.', '', watchFlow);
|
|
3364
|
+
}
|
|
3284
3365
|
stopPolling();
|
|
3285
3366
|
return;
|
|
3286
3367
|
}
|
|
3287
3368
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
3288
3369
|
.then(function (res) {
|
|
3289
|
-
if (res.status === 401) { throw new Error('gone:Your session expired — <a href="' + API + '/auth/web/start?return=/projects%3Fview%3Dnew">sign in again</a> to keep watching.'); }
|
|
3370
|
+
if (res.status === 401) { throw new Error('gone:Your session expired — <a href="' + API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1">sign in again</a> to keep watching.'); }
|
|
3290
3371
|
if (res.status === 404) { throw new Error('gone:Can’t see this instance any more — it may have been torn down.'); }
|
|
3291
3372
|
if (!res.ok) throw new Error('soft');
|
|
3292
3373
|
return res.json();
|
|
@@ -3295,6 +3376,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3295
3376
|
if (myGen !== watchGen) return;
|
|
3296
3377
|
var inst = data.instance;
|
|
3297
3378
|
if (inst) inst.open_intent = data.open_intent || null; /* F22: the strip reads intents */
|
|
3379
|
+
if (inst) watchFlow = inst.torn_down_at ? 'revive' : 'standup';
|
|
3298
3380
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
3299
3381
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
3300
3382
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
@@ -3307,7 +3389,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3307
3389
|
if (myGen !== watchGen) return;
|
|
3308
3390
|
var m = String(err && err.message || '');
|
|
3309
3391
|
if (m.indexOf('gone:') === 0) {
|
|
3310
|
-
$('liveProgress').innerHTML = progressMarkup(1, 1, m.slice(5), 'isErr');
|
|
3392
|
+
$('liveProgress').innerHTML = progressMarkup(1, 1, m.slice(5), 'isErr', watchFlow);
|
|
3311
3393
|
stopPolling();
|
|
3312
3394
|
return;
|
|
3313
3395
|
}
|
|
@@ -3323,10 +3405,13 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3323
3405
|
var again = document.createElement('button');
|
|
3324
3406
|
again.type = 'button'; again.className = 'again'; again.textContent = 'Start another project';
|
|
3325
3407
|
again.addEventListener('click', function () {
|
|
3408
|
+
/* a standup still being watched is not abandoned by accident */
|
|
3409
|
+
if (instanceId && pollTimer && !confirm((state.name || state.slug) + ' keeps building — you’ll find it on your projects page. Start a new one?')) return;
|
|
3326
3410
|
stopPolling();
|
|
3327
3411
|
stopManifestPoll();
|
|
3328
3412
|
instanceId = null;
|
|
3329
3413
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
3414
|
+
manifestStartedAt = 0; manifestStale = false;
|
|
3330
3415
|
live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
|
|
3331
3416
|
lastStepsHtml = '';
|
|
3332
3417
|
try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
@@ -3363,6 +3448,11 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3363
3448
|
/* restore the opt-out too — without it resolvedDomain() re-derives the
|
|
3364
3449
|
assigned address and the done panel promises one that doesn't exist */
|
|
3365
3450
|
state.noAddress = !!created.noaddr;
|
|
3451
|
+
/* the manifest leg's last known truth, kept across a reload */
|
|
3452
|
+
if (created.manifest) { live.manifest = created.manifest; live.appSlug = created.app_slug || null; }
|
|
3453
|
+
if (created.manifest_state && created.manifest !== 'done' && created.manifest !== 'error' && created.manifest !== 'expired') {
|
|
3454
|
+
manifestState = created.manifest_state; manifestPending = true;
|
|
3455
|
+
}
|
|
3366
3456
|
renderDone();
|
|
3367
3457
|
show(6, !!focus);
|
|
3368
3458
|
startProgress(created.id);
|
|
@@ -3511,13 +3601,21 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3511
3601
|
function jsonOrEmpty(res) { return res.json().catch(function () { return {}; }); }
|
|
3512
3602
|
|
|
3513
3603
|
function signInAgain() {
|
|
3514
|
-
|
|
3604
|
+
/* from the wizard, the re-sign-in keeps the repo scope it already granted */
|
|
3605
|
+
var back = currentView === 'new' ? '/projects%3Fview%3Dnew&repos=1' : '/projects';
|
|
3606
|
+
return 'Your session expired — <a href="' + API + '/auth/web/start?return=' + back + '">sign in again</a>.';
|
|
3515
3607
|
}
|
|
3516
3608
|
|
|
3517
3609
|
/* ── status vocabulary (direction §4): the chip only asserts what the
|
|
3518
3610
|
platform verified, and says when. `last_verified_*` is the heartbeat
|
|
3519
3611
|
overlay — absent (pre-migration or never probed) reads as unverified. ── */
|
|
3520
3612
|
|
|
3613
|
+
/* ONE freshness rule (direction §2 principle 1 / §3): "live" is claimed only
|
|
3614
|
+
from an UP probe within FRESH_MS. Chip, orb and address row all read this,
|
|
3615
|
+
so the page can never say "Answering at …" beside a chip that says "live?". */
|
|
3616
|
+
function verifiedAt(inst) { return inst.last_verified_at ? new Date(inst.last_verified_at).getTime() : 0; }
|
|
3617
|
+
function freshUp(inst) { var at = verifiedAt(inst); return !!(inst.last_verified_up && at && (Date.now() - at) <= FRESH_MS); }
|
|
3618
|
+
|
|
3521
3619
|
function chipMarkup(cls, label, rec) {
|
|
3522
3620
|
return '<span class="chip ' + cls + '"><span class="cdot"></span>' + esc(label) +
|
|
3523
3621
|
(rec ? ' <span class="rec">· ' + esc(rec) + '</span>' : '') + '</span>';
|
|
@@ -3525,9 +3623,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3525
3623
|
function chipFor(inst) {
|
|
3526
3624
|
var s = inst.status;
|
|
3527
3625
|
if (s === 'active') {
|
|
3528
|
-
var at =
|
|
3626
|
+
var at = verifiedAt(inst);
|
|
3529
3627
|
if (inst.last_verified_up === false) return chipMarkup('down', 'unreachable', at ? 'since ' + clockOrDate(at) : '');
|
|
3530
|
-
if (inst
|
|
3628
|
+
if (freshUp(inst)) return chipMarkup('live', 'live', 'checked ' + relTime(at));
|
|
3531
3629
|
return chipMarkup('maybe', 'live?', at ? 'last seen ' + relTime(at) : 'not checked yet');
|
|
3532
3630
|
}
|
|
3533
3631
|
if (s === 'requested') return chipMarkup('busy', 'queued');
|
|
@@ -3585,7 +3683,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3585
3683
|
var cls = 'orb';
|
|
3586
3684
|
if (inst.status === 'torn_down' || inst.status === 'tearing_down') cls += ' ghost';
|
|
3587
3685
|
else if (inst.status === 'error' || (inst.status === 'active' && inst.last_verified_up === false)) cls += ' ashen';
|
|
3588
|
-
else if (inst.status === 'active' && !(inst
|
|
3686
|
+
else if (inst.status === 'active' && !freshUp(inst)) cls += ' dim';
|
|
3589
3687
|
var style = '--acc:' + ORB_ACCENTS[k] + ';--glow:' + ORB_GLOWS[k];
|
|
3590
3688
|
if (inst.status === 'active' && inst.domain) {
|
|
3591
3689
|
return '<a class="' + cls + '" style="' + style + '" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener" title="Open ' + esc(inst.slug) + '" aria-label="Open ' + esc(inst.slug) + '"></a>';
|
|
@@ -3616,8 +3714,10 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3616
3714
|
var s = inst.status;
|
|
3617
3715
|
var revive = !!inst.torn_down_at; /* stood down before this standup → it's a revive */
|
|
3618
3716
|
if (s === 'requested') {
|
|
3619
|
-
|
|
3620
|
-
|
|
3717
|
+
/* "Request filed" is the CURRENT stage — the standing-up dot must not
|
|
3718
|
+
light before the platform has actually picked the request up */
|
|
3719
|
+
return stripMarkup(revive ? 'revive' : 'standup', 0, 0,
|
|
3720
|
+
'Filed — waiting for the platform to pick it up. This card updates the moment it starts.');
|
|
3621
3721
|
}
|
|
3622
3722
|
if (s === 'provisioning') {
|
|
3623
3723
|
return stripMarkup(revive ? 'revive' : 'standup', 1, 1,
|
|
@@ -3684,22 +3784,31 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3684
3784
|
}
|
|
3685
3785
|
|
|
3686
3786
|
function addressRow(inst) {
|
|
3787
|
+
/* state FIRST, then domain: a torn-down or snagged project still holds its
|
|
3788
|
+
domain (ADR 0181 keeps port + address), and the standup promise below
|
|
3789
|
+
was reaching it — "answers once the instance is live" two lines above
|
|
3790
|
+
"nothing more will happen" (task 1003232) */
|
|
3791
|
+
if (inst.status === 'torn_down' || inst.status === 'tearing_down') {
|
|
3792
|
+
if (!inst.domain) return '';
|
|
3793
|
+
return 'Its address, <code>' + esc(inst.domain) + '</code>, stays reserved for this project — it shows an error page until you bring it back.';
|
|
3794
|
+
}
|
|
3687
3795
|
if (inst.domain) {
|
|
3688
3796
|
if (inst.status === 'active') {
|
|
3689
|
-
/* "answering" is a VERIFIED claim (direction §3 / F16): only
|
|
3690
|
-
|
|
3797
|
+
/* "answering" is a VERIFIED claim (direction §3 / F16): only a FRESH up
|
|
3798
|
+
probe earns it — never the runner finishing its script, and never a
|
|
3799
|
+
probe the chip has already aged into "live?" */
|
|
3691
3800
|
var link = '<a href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>';
|
|
3692
|
-
if (inst
|
|
3801
|
+
if (freshUp(inst)) return 'Answering at ' + link;
|
|
3693
3802
|
/* the runner had no DNS hook (F16): the CAUSE beats the bare symptom —
|
|
3694
3803
|
a successful A record flips last_verified_up and retires the note */
|
|
3695
3804
|
if (inst.dns_note) return 'Its address is ' + link + '. ' + esc(inst.dns_note);
|
|
3696
|
-
if (inst.last_verified_up === false) return 'Not answering at ' + link + ' right now.';
|
|
3805
|
+
if (inst.last_verified_up === false) return 'Not answering at ' + link + ' right now. We keep checking every few minutes and update this on its own if it comes back.';
|
|
3806
|
+
if (inst.last_verified_up) return 'Its address is ' + link + ' — last answered ' + esc(relTime(verifiedAt(inst))) + '.';
|
|
3697
3807
|
return 'Its address is ' + link + ' — confirming it answers.';
|
|
3698
3808
|
}
|
|
3809
|
+
if (inst.status === 'error') return 'Address <code>' + esc(inst.domain) + '</code> — held for this project, but nothing is answering there yet.';
|
|
3699
3810
|
return 'Address: <code>' + esc(inst.domain) + '</code> — it answers there once the instance is live.';
|
|
3700
3811
|
}
|
|
3701
|
-
/* a torn-down instance has no address to speak of — the card's own line says so */
|
|
3702
|
-
if (inst.status === 'torn_down' || inst.status === 'tearing_down') return '';
|
|
3703
3812
|
/* the HONEST no-address state: sign-in on the instance needs a real domain */
|
|
3704
3813
|
return '<span class="none">No address yet</span> — sign-in on this project can’t be set up until it has one.';
|
|
3705
3814
|
}
|
|
@@ -3711,22 +3820,25 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3711
3820
|
var iid = esc(String(inst.id));
|
|
3712
3821
|
var li = liFor(inst.id);
|
|
3713
3822
|
if (!li) return;
|
|
3823
|
+
if (cardAsking !== null && cardAsking === String(inst.id)) return; /* keep THIS card's open confirmation */
|
|
3714
3824
|
var gone = inst.status === 'torn_down';
|
|
3715
3825
|
li.className = 'proj' + (gone ? ' gone' : '');
|
|
3716
3826
|
var meta = [];
|
|
3717
3827
|
if (inst.target_ref) meta.push('repo <code>' + esc(inst.target_ref) + '</code>');
|
|
3828
|
+
/* each item is ONE flex item (span-wrapped below), so the separator can
|
|
3829
|
+
never strand at the end of a wrapped line on a phone */
|
|
3718
3830
|
if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
|
|
3719
3831
|
if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
|
|
3720
3832
|
if (inst.status === 'active' && inst.torn_down_at) meta.push('back live since ' + esc(shortDate(inst.provisioned_at || inst.torn_down_at)));
|
|
3721
3833
|
var body =
|
|
3722
3834
|
'<div class="projHead"><span class="projName">' + esc(inst.slug) + '</span>' + chipFor(inst) + '</div>' +
|
|
3723
|
-
(meta.length ? '<div class="projMeta">' + meta.
|
|
3835
|
+
(meta.length ? '<div class="projMeta">' + meta.map(function (m) { return '<span>' + m + '</span>'; }).join('') + '</div>' : '');
|
|
3724
3836
|
var addr = addressRow(inst);
|
|
3725
3837
|
if (addr) body += '<div class="addr">' + addr + '</div>';
|
|
3726
3838
|
if (canAttachDomain(inst)) {
|
|
3727
3839
|
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
3728
3840
|
body += '<div class="attach">' +
|
|
3729
|
-
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253"' +
|
|
3841
|
+
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253" aria-label="A domain you own, for ' + esc(inst.slug) + '"' +
|
|
3730
3842
|
(rekick ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
3731
3843
|
'<button type="button" class="btn small" data-attach="' + iid + '">' +
|
|
3732
3844
|
(rekick ? 'Try the address again' : inst.domain ? 'Use your own domain' : 'Give it an address') + '</button></div>';
|
|
@@ -3752,6 +3864,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3752
3864
|
/* every owned card opens its manage view (task 1003041) — a real link, so
|
|
3753
3865
|
middle/ctrl-click works; the delegated handler upgrades a plain click */
|
|
3754
3866
|
var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
|
|
3867
|
+
/* the success moment needs a door: a live project opens from its own card
|
|
3868
|
+
(the orb and the address link were the only ways in — task 1003232) */
|
|
3869
|
+
if (inst.status === 'active' && inst.domain) acts.unshift('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">Open site →</a>');
|
|
3755
3870
|
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-retry="' + iid + '">Try again</button>');
|
|
3756
3871
|
if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-revive="' + iid + '">Bring it back</button>');
|
|
3757
3872
|
if (canTeardown(inst)) acts.push('<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button>');
|
|
@@ -3943,7 +4058,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3943
4058
|
}
|
|
3944
4059
|
if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
|
|
3945
4060
|
if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
|
|
3946
|
-
$('mMeta').innerHTML = meta.
|
|
4061
|
+
$('mMeta').innerHTML = meta.map(function (m) { return '<span>' + m + '</span>'; }).join('');
|
|
3947
4062
|
$('mMeta').hidden = !meta.length;
|
|
3948
4063
|
var addr = addressRow(inst);
|
|
3949
4064
|
$('mAddr').innerHTML = addr;
|
|
@@ -3955,9 +4070,75 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3955
4070
|
it: the delete section reads only status + open_intent, both already in
|
|
3956
4071
|
hand, and a queued teardown must show up without a reload */
|
|
3957
4072
|
renderDelete(inst);
|
|
4073
|
+
/* the page names its project — in the heading and in the tab */
|
|
4074
|
+
$('mTitle').textContent = inst.slug || 'Manage';
|
|
4075
|
+
document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
|
|
4076
|
+
/* the card's own story, on the project's own page (task 1003232): the
|
|
4077
|
+
per-flow strip, the honest snag, and the doors the list card has */
|
|
4078
|
+
$('mStrip').innerHTML = stripFor(inst);
|
|
4079
|
+
var snag = (inst.status === 'error' || inst.error_note) && !inst.open_intent;
|
|
4080
|
+
$('mErr').innerHTML = snag ? errMarkup(inst) : '';
|
|
4081
|
+
$('mErr').hidden = !snag;
|
|
4082
|
+
var acts = [];
|
|
4083
|
+
if (inst.status === 'active' && inst.domain) acts.push('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">Open site →</a>');
|
|
4084
|
+
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-m-retry>Try again</button>');
|
|
4085
|
+
if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-m-revive>Bring it back</button>');
|
|
4086
|
+
$('mActs').innerHTML = acts.join('');
|
|
4087
|
+
$('mActs').hidden = !acts.length;
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
/* Try again / Bring it back from the project's own page — the same POST the
|
|
4091
|
+
list card sends (the server's PROVISION_ENQUEUE_FROM gate is the authority) */
|
|
4092
|
+
function mReProvision(verb) {
|
|
4093
|
+
var inst = manageInst;
|
|
4094
|
+
if (!inst) return;
|
|
4095
|
+
var box = $('mMsg');
|
|
4096
|
+
box.className = 'projMsg'; box.textContent = verb === 'revive' ? 'Asking the platform to bring it back…' : 'Asking the platform to try again…';
|
|
4097
|
+
$('mActs').querySelectorAll('button').forEach(function (b) { b.disabled = true; });
|
|
4098
|
+
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'standalone' };
|
|
4099
|
+
if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
|
|
4100
|
+
if (inst.target_ref) body.target_ref = inst.target_ref;
|
|
4101
|
+
if (inst.domain) body.domain = inst.domain;
|
|
4102
|
+
fetch(API + '/provisioning/instances', {
|
|
4103
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
4104
|
+
body: JSON.stringify(body)
|
|
4105
|
+
}).then(function (res) {
|
|
4106
|
+
if (res.status === 401) { box.className = 'projMsg err'; box.innerHTML = signInAgain(); return; }
|
|
4107
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
4108
|
+
if (res.ok) {
|
|
4109
|
+
box.className = 'projMsg ok';
|
|
4110
|
+
box.textContent = data.message || (verb === 'revive' ? 'Revive queued — bringing it back.' : 'Queued again — the platform picks it up from here.');
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
var e = apiError(data, res.status);
|
|
4114
|
+
box.className = 'projMsg err';
|
|
4115
|
+
box.textContent = e.message || 'The request could not be queued — try again in a moment.';
|
|
4116
|
+
});
|
|
4117
|
+
}).catch(function () {
|
|
4118
|
+
box.className = 'projMsg err';
|
|
4119
|
+
box.textContent = 'Couldn’t reach the platform — check your connection and try again.';
|
|
4120
|
+
}).then(function () { loadManage(true, true); });
|
|
4121
|
+
}
|
|
4122
|
+
$('mActs').addEventListener('click', function (ev) {
|
|
4123
|
+
var t = ev.target.closest && ev.target.closest('button');
|
|
4124
|
+
if (!t) return;
|
|
4125
|
+
if (t.hasAttribute('data-m-retry')) return mReProvision('retry');
|
|
4126
|
+
if (t.hasAttribute('data-m-revive')) return mReProvision('revive');
|
|
4127
|
+
});
|
|
4128
|
+
|
|
4129
|
+
/* an IN-FLIGHT project is watched here at the card's cadence — its own copy
|
|
4130
|
+
says "this page updates when it starts", so it must. A settled project
|
|
4131
|
+
keeps the C8 rule: entry + tab return, plus the request-free minute tick. */
|
|
4132
|
+
function manageWatchTick() {
|
|
4133
|
+
if (document.hidden || currentView !== 'manage' || !manageInst || !shouldWatch(manageInst)) return;
|
|
4134
|
+
loadManage(true, true);
|
|
3958
4135
|
}
|
|
4136
|
+
setInterval(manageWatchTick, 5000);
|
|
3959
4137
|
|
|
3960
|
-
|
|
4138
|
+
/* keepRepo: the in-flight watch below re-reads the instance every few
|
|
4139
|
+
seconds, and must not re-ask GitHub each time — that read stays on entry
|
|
4140
|
+
and tab return */
|
|
4141
|
+
function loadManage(quiet, keepRepo) {
|
|
3961
4142
|
if (!manageId) return;
|
|
3962
4143
|
$('mAlert').classList.remove('on');
|
|
3963
4144
|
$('mSignin').href = manageSignin();
|
|
@@ -3990,7 +4171,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3990
4171
|
renderManage(inst);
|
|
3991
4172
|
/* the SECOND, GitHub-backed read — fired only here (entry + tab
|
|
3992
4173
|
return), never from renderManage, which the fetch-free tick calls */
|
|
3993
|
-
loadRepoVisibility(inst);
|
|
4174
|
+
if (!keepRepo) loadRepoVisibility(inst);
|
|
3994
4175
|
})
|
|
3995
4176
|
.catch(function (err) {
|
|
3996
4177
|
var m = String(err && err.message);
|
|
@@ -4027,11 +4208,16 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
4027
4208
|
|
|
4028
4209
|
function renderRepoVis(repo) {
|
|
4029
4210
|
if (repo.private) {
|
|
4211
|
+
/* a LIVE project with a private repo is not broken — the deploy key reads
|
|
4212
|
+
it (ADR 0176). The warning stays for a project that actually snagged. */
|
|
4213
|
+
var snagged = !!(manageInst && (manageInst.status === 'error' || manageInst.error_note));
|
|
4030
4214
|
repoVisMsg(
|
|
4031
4215
|
'<div class="addr">This repository is <b>private</b> — only accounts it’s shared with can read its code.</div>' +
|
|
4032
|
-
|
|
4216
|
+
(snagged
|
|
4217
|
+
? '<div class="projMsg err">A private repository deploys through the project’s own read-only deploy key, wired at standup. If the standup couldn’t read it, retry it from the card above — or make the repository public.</div>'
|
|
4218
|
+
: '<p class="finePrint">The platform reads it through this project’s own read-only deploy key, set up at standup. Nothing to do here.</p>') +
|
|
4033
4219
|
(repo.admin
|
|
4034
|
-
? '<div class="actions"><button type="button" class="btn small" data-repo-public>Make it public</button></div>'
|
|
4220
|
+
? '<div class="actions"><button type="button" class="btn small ghost" data-repo-public>Make it public</button></div>'
|
|
4035
4221
|
: '<p class="finePrint">Your GitHub account can’t change this repository’s settings.</p>'));
|
|
4036
4222
|
} else {
|
|
4037
4223
|
repoVisMsg(
|
|
@@ -4234,16 +4420,22 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
4234
4420
|
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
4235
4421
|
SAY exactly what the server said ── */
|
|
4236
4422
|
|
|
4423
|
+
/* the id whose confirmation is open — renderCard skips that card, so the
|
|
4424
|
+
minute re-render (sig() buckets recency) cannot wipe the prompt mid-read
|
|
4425
|
+
(the manage page's mDelAsking rule, task 1003232) */
|
|
4426
|
+
var cardAsking = null;
|
|
4237
4427
|
function askTeardown(id) {
|
|
4238
4428
|
var act = $('act-' + id);
|
|
4239
4429
|
if (!act) return;
|
|
4430
|
+
cardAsking = String(id);
|
|
4240
4431
|
act.innerHTML = '<div class="confirmRow">' +
|
|
4241
4432
|
'<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
|
|
4242
4433
|
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
4243
4434
|
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
|
|
4244
4435
|
}
|
|
4245
|
-
function unAskTeardown(id) { var inst = byId[id]; if (inst) renderCard(inst); }
|
|
4436
|
+
function unAskTeardown(id) { cardAsking = null; var inst = byId[id]; if (inst) renderCard(inst); }
|
|
4246
4437
|
function doTeardown(id) {
|
|
4438
|
+
cardAsking = null;
|
|
4247
4439
|
var act = $('act-' + id);
|
|
4248
4440
|
if (act) act.innerHTML = '<span class="projMsg">Asking…</span>';
|
|
4249
4441
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/teardown', {
|
|
@@ -4299,7 +4491,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
4299
4491
|
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
4300
4492
|
return jsonOrEmpty(res).then(function (data) {
|
|
4301
4493
|
if (res.ok) {
|
|
4302
|
-
msg(id, esc(data.message || (verb === 'revive' ? 'Revive queued — bringing it back.' : 'Queued again —
|
|
4494
|
+
msg(id, esc(data.message || (verb === 'revive' ? 'Revive queued — bringing it back.' : 'Queued again — the platform picks it up from here.')), 'ok');
|
|
4303
4495
|
return;
|
|
4304
4496
|
}
|
|
4305
4497
|
var e = apiError(data, res.status);
|