@bongos/core 1.19.439 → 1.19.440
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 +18 -18
- package/docs/copy-inventory.md +136 -132
- package/docs/copy-registry.json +207 -170
- package/docs/design/projects-hub-direction-v2.md +20 -0
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +17 -22
- package/public-landing/projects.html +210 -309
- package/public-landing/projects.probes.json +16 -19
- package/public-landing/projects.states.json +5 -4
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +17 -7
- package/tests/projects_hub_pre_uat.mjs +111 -69
|
@@ -695,6 +695,20 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
695
695
|
<div class="actions" id="mActs" hidden></div>
|
|
696
696
|
</div>
|
|
697
697
|
|
|
698
|
+
<!-- its address (task 1003355): the bring-your-own-domain control, moved
|
|
699
|
+
off the list row when a row shrank to view and manage. Its home is
|
|
700
|
+
here, under the ledger's address fact. The section hides itself where
|
|
701
|
+
there is nothing to point an address at (a snagged or torn-down
|
|
702
|
+
project) — the same call the card's fold used to make. -->
|
|
703
|
+
<div class="card mSec" id="mAddress" hidden>
|
|
704
|
+
<h2>Its address</h2>
|
|
705
|
+
<div id="mDom"><p class="finePrint">Reading the project…</p></div>
|
|
706
|
+
<details class="finePrint" id="mDomWhat">
|
|
707
|
+
<summary>Where an address comes from</summary>
|
|
708
|
+
<p>Every project gets one on this platform, at its own handle. You can point a domain you already own at it instead: type the name here, then add a DNS record for it at your registrar. The platform holds the address for this project and wires the secure connection once the name resolves. Changing it later lets the old address go, and nothing on the project restarts for it.</p>
|
|
709
|
+
</details>
|
|
710
|
+
</div>
|
|
711
|
+
|
|
698
712
|
<!-- live since task 1003045: reads the repo's real visibility from GitHub
|
|
699
713
|
with the owner's own repository credential. Flip-to-PUBLIC only — the
|
|
700
714
|
private direction is refused (server + copy) until the private-repo
|
|
@@ -3447,6 +3461,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3447
3461
|
var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
|
|
3448
3462
|
|
|
3449
3463
|
var instanceId = null; /* set when a create lands / a resumed watch starts */
|
|
3464
|
+
/* where a project is ACTED on since task 1003355: its own page. A list row
|
|
3465
|
+
carries only view and manage now, so every pointer that names a control —
|
|
3466
|
+
attach an address, retry a snagged standup, bring a torn-down project
|
|
3467
|
+
back — must land there, not on the list, which has no such button. */
|
|
3468
|
+
function projectPageLink(label) {
|
|
3469
|
+
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
3470
|
+
'">' + label + '</a>';
|
|
3471
|
+
}
|
|
3450
3472
|
var live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
3451
3473
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
3452
3474
|
var lastStepsHtml = '';
|
|
@@ -3518,7 +3540,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3518
3540
|
function renderStepOauthApp() {
|
|
3519
3541
|
var domain = resolvedDomain();
|
|
3520
3542
|
if (!domain) {
|
|
3521
|
-
return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from
|
|
3543
|
+
return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from ' + projectPageLink('this project’s own page') + '.</p></li>';
|
|
3522
3544
|
}
|
|
3523
3545
|
/* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
|
|
3524
3546
|
manifests whose hook URL isn't publicly reachable, so a locally-hosted
|
|
@@ -3535,7 +3557,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3535
3557
|
address yet — says where to go instead of what the server said. */
|
|
3536
3558
|
var snag, raw = '';
|
|
3537
3559
|
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
|
|
3538
|
-
else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from
|
|
3560
|
+
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.';
|
|
3539
3561
|
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
3540
3562
|
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
|
|
3541
3563
|
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
@@ -3866,7 +3888,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3866
3888
|
answer at, so a verified answer can never arrive; say what IS true
|
|
3867
3889
|
instead of promising the hall (sign-in stays locked without one) */
|
|
3868
3890
|
setProgress(progressMarkup(-1, 2,
|
|
3869
|
-
'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
|
|
3891
|
+
'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 ' +
|
|
3892
|
+
projectPageLink('its own page') + '; the steps below that need one unlock then.', '', flow));
|
|
3870
3893
|
return true;
|
|
3871
3894
|
}
|
|
3872
3895
|
if (inst.last_verified_up === false) {
|
|
@@ -3885,16 +3908,19 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3885
3908
|
return false;
|
|
3886
3909
|
} else if (s === 'error') {
|
|
3887
3910
|
/* the honest snag (direction §5 / F19): no raw stderr as the headline, no
|
|
3888
|
-
false "keeps retrying" — retries stop, and the retry door is the
|
|
3911
|
+
false "keeps retrying" — retries stop, and the retry door is the project's
|
|
3912
|
+
own page (task 1003355 moved it off the row) */
|
|
3889
3913
|
setProgress(progressMarkup(1, 1,
|
|
3890
|
-
'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from
|
|
3914
|
+
'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from ' +
|
|
3915
|
+
projectPageLink('its own page') + '.', 'isErr'));
|
|
3891
3916
|
} else if (s === 'tearing_down') {
|
|
3892
3917
|
/* a deliberate end is calm, never styled as a failure (direction §5) —
|
|
3893
3918
|
and it speaks the teardown vocabulary, not the standup's (F22) */
|
|
3894
3919
|
setProgress(progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; ' +
|
|
3895
3920
|
(inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'), '', 'teardown'));
|
|
3896
3921
|
} else if (s === 'torn_down') {
|
|
3897
|
-
setProgress(progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from
|
|
3922
|
+
setProgress(progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from ' +
|
|
3923
|
+
projectPageLink('its own page') + ', or start another below.', '', 'teardown'));
|
|
3898
3924
|
return true;
|
|
3899
3925
|
} else {
|
|
3900
3926
|
setProgress(progressMarkup(0, 0, 'Filed. Watching for the standup to start…'));
|
|
@@ -4234,16 +4260,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4234
4260
|
return chipMarkup('', inst.status);
|
|
4235
4261
|
}
|
|
4236
4262
|
|
|
4237
|
-
/* client mirror of provisioning.TEARDOWN_FROM — every state EXCEPT torn_down
|
|
4238
|
-
(nothing left to release), minus tearing_down (already going) and minus a
|
|
4239
|
-
teardown that is already queued. It used to list only provisioning+active,
|
|
4240
|
-
which is how a snagged project ended up with no way out of the owner's list
|
|
4241
|
-
at all: its only button was "Try again" (walk finding N5, task 1003042). */
|
|
4242
|
-
function canTeardown(inst) {
|
|
4243
|
-
if (inst.open_intent && inst.open_intent.action === 'teardown') return false;
|
|
4244
|
-
if (inst.status === 'tearing_down' || inst.status === 'torn_down') return false;
|
|
4245
|
-
return true;
|
|
4246
|
-
}
|
|
4247
4263
|
/* client mirror of provisioning.DOMAIN_ATTACH_FROM */
|
|
4248
4264
|
/* the platform-assigned <slug>.<apex> for this instance, from the server-stamped
|
|
4249
4265
|
apex only — no host guessing here: the upgrade door below must never show for
|
|
@@ -4471,7 +4487,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4471
4487
|
: 'The server goes away. Your repository is untouched. The database is kept, so bringing it back later restores its memory. Its web address stays reserved for this project and shows an error page until you bring it back. Nothing here erases a project.';
|
|
4472
4488
|
}
|
|
4473
4489
|
|
|
4474
|
-
|
|
4490
|
+
/* onManage: the same facts, but the pointers name the controls THIS surface
|
|
4491
|
+
has. Since task 1003355 the list row acts on nothing, so a row's sentence
|
|
4492
|
+
sends the owner to the project's own page and the manage page's says
|
|
4493
|
+
"below" — the ADR 0199 §8 rule that a refusal must name a reachable door. */
|
|
4494
|
+
function addressRow(inst, onManage) {
|
|
4475
4495
|
/* state FIRST, then domain: a torn-down or snagged project still holds its
|
|
4476
4496
|
domain (ADR 0181 keeps port + address), and the standup promise below
|
|
4477
4497
|
was reaching it — "answers once the instance is live" two lines above
|
|
@@ -4499,7 +4519,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4499
4519
|
available. Where there is no button the sentence stops at the full stop. */
|
|
4500
4520
|
if (inst.last_verified_up === false) {
|
|
4501
4521
|
return 'Not answering at ' + link + ' right now. We keep checking every few minutes and update this on its own if it comes back' +
|
|
4502
|
-
(canRestart(inst) ? ' — or restart it yourself below.' : '.');
|
|
4522
|
+
(canRestart(inst) ? (onManage ? ' — or restart it yourself below.' : ' — or restart it from its own page.') : '.');
|
|
4503
4523
|
}
|
|
4504
4524
|
if (inst.last_verified_up) return 'Its address is ' + link + ' — last answered ' + esc(relTime(verifiedAt(inst))) + '.';
|
|
4505
4525
|
return 'Its address is ' + link + ' — confirming it answers.';
|
|
@@ -4508,50 +4528,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4508
4528
|
return 'Address: <code>' + esc(inst.domain) + '</code> — it answers there once the instance is live.';
|
|
4509
4529
|
}
|
|
4510
4530
|
/* the HONEST no-address state: sign-in on the instance needs a real domain */
|
|
4511
|
-
return '<span class="none">No address yet</span> — sign-in on this project can’t be set up until it has one.
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
/* ── the bring-your-own-domain fold (owner decision 2, 2026-08-24). A live
|
|
4515
|
-
card's loudest control used to be a full-width domain field + filled
|
|
4516
|
-
button — for a domain the founder can't buy here — while the success
|
|
4517
|
-
moment had only "Open site →". The field now sits behind the disclosure
|
|
4518
|
-
the wizard's address step uses (direction §6): closed, after the card's
|
|
4519
|
-
doors, and only where something is or will be answering — live and
|
|
4520
|
-
queued. A snagged or torn-down card has nothing to point a domain at, so
|
|
4521
|
-
it carries no fold (canAttachDomain stays the server's mirror; this is
|
|
4522
|
-
the card's own call). The re-kick door (F19) keeps its shape — open and
|
|
4523
|
-
pre-filled, one button: Try the address again. ── */
|
|
4524
|
-
function attachFold(inst, iid) {
|
|
4525
|
-
if (!canAttachDomain(inst)) return '';
|
|
4526
|
-
if (inst.status !== 'active' && inst.status !== 'requested') return '';
|
|
4527
|
-
/* the strip already says "Wiring the address" — a second attach would only
|
|
4528
|
-
be refused (intent_conflict), so the fold waits for the run to land */
|
|
4529
|
-
if (inst.open_intent && inst.open_intent.action === 'domain-attach') return '';
|
|
4530
|
-
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
4531
|
-
/* the CORRECTION door (task 1003237): the project already answers on an
|
|
4532
|
-
address the founder brought, and it can be changed. The field carries the
|
|
4533
|
-
current one so a typo is fixed by editing it, not retyped from scratch —
|
|
4534
|
-
the re-kick door's shape, for the same reason. The platform-assigned
|
|
4535
|
-
<slug>.<apex> is NOT a correction: moving off it is the upgrade the fold
|
|
4536
|
-
already offered, and the field stays empty for the domain being brought. */
|
|
4537
|
-
var byo = inst.domain && inst.domain !== assignedDefaultFor(inst);
|
|
4538
|
-
var repoint = !rekick && byo;
|
|
4539
|
-
var prefill = rekick || repoint;
|
|
4540
|
-
var label = rekick ? 'Try the address again'
|
|
4541
|
-
: repoint ? 'Use a different domain'
|
|
4542
|
-
: inst.domain ? 'Use a domain I own' : 'Give it an address';
|
|
4543
|
-
return '<details class="attachFold"' + (rekick ? ' open' : '') + '>' +
|
|
4544
|
-
'<summary>' + label + '</summary>' +
|
|
4545
|
-
'<div class="attach">' +
|
|
4546
|
-
'<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) + '"' +
|
|
4547
|
-
(prefill ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
4548
|
-
'<button type="button" class="btn small" data-attach="' + iid + '">' +
|
|
4549
|
-
(rekick ? 'Try the address again' : repoint ? 'Change the address' : 'Use this domain') + '</button></div>' +
|
|
4550
|
-
/* true in every state the fold renders in: a REQUESTED project never
|
|
4551
|
-
answered anywhere yet, so "stops answering" would be a claim about a
|
|
4552
|
-
thing that never happened (the F16 rule) */
|
|
4553
|
-
(repoint ? '<p class="attachNote">Changing it lets <code>' + esc(inst.domain) + '</code> go — this project won’t use that address any more.</p>' : '') +
|
|
4554
|
-
'</details>';
|
|
4531
|
+
return '<span class="none">No address yet</span> — sign-in on this project can’t be set up until it has one. ' +
|
|
4532
|
+
(onManage ? 'Give it an address below.'
|
|
4533
|
+
: '<a href="?view=manage&id=' + esc(String(inst.id)) + '" data-manage="' + esc(String(inst.id)) + '">Give it an address on its own page</a>.');
|
|
4555
4534
|
}
|
|
4556
4535
|
|
|
4557
4536
|
function renderCard(inst) {
|
|
@@ -4561,7 +4540,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4561
4540
|
var iid = esc(String(inst.id));
|
|
4562
4541
|
var li = liFor(inst.id);
|
|
4563
4542
|
if (!li) return;
|
|
4564
|
-
if (cardAsking !== null && cardAsking === String(inst.id)) return; /* keep THIS card's open confirmation */
|
|
4565
4543
|
var gone = inst.status === 'torn_down';
|
|
4566
4544
|
li.className = 'proj' + (gone ? ' gone' : '');
|
|
4567
4545
|
var meta = [];
|
|
@@ -4596,73 +4574,38 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4596
4574
|
/* a live region: an action's result is announced where the pointer is,
|
|
4597
4575
|
not only painted (task 1003244) */
|
|
4598
4576
|
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '" role="status" aria-live="polite">' + (lm ? lm.text : '') + '</div>';
|
|
4599
|
-
/*
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
if (
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
/* sig() buckets recency per minute, so a live card re-renders on the
|
|
4612
|
-
heartbeat — that must not snap an open fold shut, eat a half-typed
|
|
4613
|
-
domain or drop the caret. defaultValue tells a founder's typing from
|
|
4614
|
-
the re-kick pre-fill. */
|
|
4615
|
-
var oldFold = li.querySelector('details.attachFold');
|
|
4616
|
-
var oldInput = oldFold ? oldFold.querySelector('input') : null;
|
|
4617
|
-
var keepOpen = !!(oldFold && oldFold.open);
|
|
4618
|
-
var typed = oldInput && oldInput.value !== oldInput.defaultValue ? oldInput.value : '';
|
|
4619
|
-
var hadFocus = !!oldInput && document.activeElement === oldInput;
|
|
4620
|
-
/* focus anywhere else on the card (a door, a confirm, the fold's summary)
|
|
4621
|
-
comes back to the same control — or the nearest one — after the swap;
|
|
4622
|
-
quietActions() leaves a note when a disabled button already let go */
|
|
4577
|
+
/* TWO DOORS, never more (owner, 2026-08-29 — task 1003355): View opens the
|
|
4578
|
+
project itself, Manage opens its own page. Restart, try again, bring it
|
|
4579
|
+
back, the ender and the address all live there now, so a row SAYS what is
|
|
4580
|
+
true and opens a door — it no longer acts on the project. Both are real
|
|
4581
|
+
links, so middle/ctrl-click works; the delegated handler upgrades a plain
|
|
4582
|
+
click on Manage (task 1003041). */
|
|
4583
|
+
var acts = [];
|
|
4584
|
+
if (inst.status === 'active' && inst.domain) acts.push('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">View</a>');
|
|
4585
|
+
acts.push('<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>');
|
|
4586
|
+
body += '<div class="actions">' + acts.join('') + '</div>';
|
|
4587
|
+
/* sig() buckets recency per minute, so a live row re-renders on the
|
|
4588
|
+
heartbeat — a door that held focus must get it back after the swap */
|
|
4623
4589
|
var wasInside = li.contains(document.activeElement) && document.activeElement !== li;
|
|
4624
4590
|
var prefer = wasInside ? focusKeyOf(document.activeElement) : null;
|
|
4625
|
-
var focusBack = wasInside || li.hasAttribute('data-focus-back');
|
|
4626
|
-
li.removeAttribute('data-focus-back');
|
|
4627
4591
|
li.innerHTML = orbFor(inst) + '<div class="projBody">' + body + '</div>';
|
|
4628
|
-
|
|
4629
|
-
var input = fold ? fold.querySelector('input') : null;
|
|
4630
|
-
if (fold && keepOpen) fold.open = true;
|
|
4631
|
-
if (input && typed) input.value = typed;
|
|
4632
|
-
if (input && hadFocus) input.focus();
|
|
4633
|
-
else if (focusBack) focusCardControl(inst.id, prefer);
|
|
4592
|
+
if (wasInside) focusCardControl(inst.id, prefer);
|
|
4634
4593
|
}
|
|
4635
4594
|
|
|
4636
4595
|
/* ── keeping focus on the card through a re-render (task 1003244) ── */
|
|
4637
4596
|
|
|
4638
|
-
/* which
|
|
4639
|
-
|
|
4597
|
+
/* which door had focus, as a selector the rebuilt row can answer. Manage is
|
|
4598
|
+
the one that carries a marker; View falls through to the .actions fallback,
|
|
4599
|
+
which is the same element whenever both doors render. */
|
|
4640
4600
|
function focusKeyOf(el) {
|
|
4641
|
-
|
|
4642
|
-
if (el.tagName === 'SUMMARY') return 'details.attachFold summary';
|
|
4643
|
-
if (el.hasAttribute('data-teardown-yes') || el.hasAttribute('data-teardown-no')) return '[data-teardown]';
|
|
4644
|
-
var keys = ['data-manage', 'data-attach', 'data-retry', 'data-revive', 'data-restart', 'data-teardown'];
|
|
4645
|
-
for (var i = 0; i < keys.length; i++) if (el.hasAttribute(keys[i])) return '[' + keys[i] + ']';
|
|
4646
|
-
return null;
|
|
4601
|
+
return el && el.getAttribute && el.hasAttribute('data-manage') ? '[data-manage]' : null;
|
|
4647
4602
|
}
|
|
4648
4603
|
function focusCardControl(id, prefer) {
|
|
4649
4604
|
var li = liFor(id);
|
|
4650
4605
|
if (!li) return;
|
|
4651
|
-
var t = (prefer && li.querySelector(prefer)) ||
|
|
4652
|
-
li.querySelector('.actions a, .actions button') ||
|
|
4653
|
-
li.querySelector('details.attachFold summary');
|
|
4606
|
+
var t = (prefer && li.querySelector(prefer)) || li.querySelector('.actions a');
|
|
4654
4607
|
if (t) t.focus();
|
|
4655
4608
|
}
|
|
4656
|
-
/* an action in flight: the row stays put and its buttons go quiet, so
|
|
4657
|
-
nothing under the pointer is deleted mid-click; a button that held
|
|
4658
|
-
focus lets go when disabled, so the card remembers to take it back */
|
|
4659
|
-
function quietActions(id) {
|
|
4660
|
-
var act = $('act-' + id);
|
|
4661
|
-
if (!act) return;
|
|
4662
|
-
var li = liFor(id);
|
|
4663
|
-
if (li && act.contains(document.activeElement)) li.setAttribute('data-focus-back', '');
|
|
4664
|
-
act.querySelectorAll('button').forEach(function (b) { b.disabled = true; });
|
|
4665
|
-
}
|
|
4666
4609
|
|
|
4667
4610
|
function render(instances) {
|
|
4668
4611
|
$('loading').hidden = true;
|
|
@@ -4756,17 +4699,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4756
4699
|
}
|
|
4757
4700
|
timers[id] = setTimeout(tick, POLL_MS);
|
|
4758
4701
|
}
|
|
4759
|
-
function watchOnce(id) {
|
|
4760
|
-
fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
|
|
4761
|
-
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
4762
|
-
.then(function (data) {
|
|
4763
|
-
var inst = data.instance || {};
|
|
4764
|
-
inst.open_intent = data.open_intent || null;
|
|
4765
|
-
takeUpdate(id, inst);
|
|
4766
|
-
if (shouldWatch(inst)) watch(id);
|
|
4767
|
-
})
|
|
4768
|
-
.catch(function () { if (byId[id] && shouldWatch(byId[id])) watch(id); });
|
|
4769
|
-
}
|
|
4770
4702
|
|
|
4771
4703
|
/* ── settled-card refresh: the heartbeat overlay must reach the page.
|
|
4772
4704
|
A card at 'active' has no fast poll; once a minute (and on tab return)
|
|
@@ -4867,7 +4799,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4867
4799
|
if (server) rows.push(['server', server]);
|
|
4868
4800
|
$('mMeta').innerHTML = rows.map(ledgerRow).join('');
|
|
4869
4801
|
$('mMeta').hidden = !rows.length;
|
|
4870
|
-
var addr = addressRow(inst);
|
|
4802
|
+
var addr = addressRow(inst, true);
|
|
4871
4803
|
$('mAddr').innerHTML = addr ? ledgerRow(['address', addr]) : '';
|
|
4872
4804
|
$('mAddr').hidden = !addr;
|
|
4873
4805
|
/* a fact with no answer hides — never a dash (the card rule) */
|
|
@@ -4877,6 +4809,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4877
4809
|
it: the delete section reads only status + open_intent, both already in
|
|
4878
4810
|
hand, and a queued teardown must show up without a reload */
|
|
4879
4811
|
renderDelete(inst);
|
|
4812
|
+
/* its address (task 1003355) is fetch-free too — the control it carries
|
|
4813
|
+
reads status + domain + open_intent, all already in hand */
|
|
4814
|
+
renderAddress(inst);
|
|
4880
4815
|
/* same contract: the visibility section's in-flight line reads open_intent
|
|
4881
4816
|
only, so a queued settings push shows up on the tick without a reload */
|
|
4882
4817
|
renderVisState(inst);
|
|
@@ -4962,6 +4897,121 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4962
4897
|
if (t.hasAttribute('data-m-restart')) return mRestart();
|
|
4963
4898
|
});
|
|
4964
4899
|
|
|
4900
|
+
/* ── its address (task 1003355): the bring-your-own-domain control, moved off
|
|
4901
|
+
the list row when a row shrank to view and manage. Every server truth the
|
|
4902
|
+
card's fold carried comes with it: canAttachDomain mirrors
|
|
4903
|
+
DOMAIN_ATTACH_FROM, a snagged or torn-down project has nothing to point an
|
|
4904
|
+
address at, no second control renders while one is wiring, and the re-kick
|
|
4905
|
+
door (F19) and the correction door (task 1003237) keep their pre-filled
|
|
4906
|
+
shapes. Fetch-free, like every other section here, so the minute tick may
|
|
4907
|
+
repaint it. ── */
|
|
4908
|
+
|
|
4909
|
+
var mDomFor = null; /* the project the section is drawn for */
|
|
4910
|
+
var mDomNote = null; /* its last receipt — a repaint must not swallow it */
|
|
4911
|
+
|
|
4912
|
+
function domNote(html, cls) {
|
|
4913
|
+
mDomNote = html ? { html: html, cls: cls || '' } : null;
|
|
4914
|
+
var box = $('mDomMsg');
|
|
4915
|
+
if (!box) return;
|
|
4916
|
+
box.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
4917
|
+
box.innerHTML = html || '';
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
function renderAddress(inst) {
|
|
4921
|
+
if (mDomFor !== inst.id) { mDomFor = inst.id; mDomNote = null; }
|
|
4922
|
+
/* the server's mirror first, then the card's own call: a project that never
|
|
4923
|
+
stood up and one that is gone have nothing to point an address at */
|
|
4924
|
+
var offer = canAttachDomain(inst) && (inst.status === 'active' || inst.status === 'requested');
|
|
4925
|
+
$('mAddress').hidden = !offer;
|
|
4926
|
+
if (!offer) return;
|
|
4927
|
+
/* the strip already says "Wiring the address" — a second attach would only
|
|
4928
|
+
be refused (intent_conflict), so the control waits for the run to land */
|
|
4929
|
+
if (inst.open_intent && inst.open_intent.action === 'domain-attach') {
|
|
4930
|
+
$('mDom').innerHTML = '<div class="addr">Wiring the address' +
|
|
4931
|
+
(inst.domain ? ' for <b>' + esc(inst.domain) + '</b>' : '') + ' right now.</div>' +
|
|
4932
|
+
'<p class="finePrint">This page updates when it lands.</p>';
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
4936
|
+
/* the platform-assigned <slug>.<apex> is NOT a correction: moving off it is
|
|
4937
|
+
the upgrade this control already offered, and the field stays empty for
|
|
4938
|
+
the domain being brought */
|
|
4939
|
+
var byo = inst.domain && inst.domain !== assignedDefaultFor(inst);
|
|
4940
|
+
var repoint = !rekick && byo;
|
|
4941
|
+
var prefill = rekick || repoint;
|
|
4942
|
+
var lead = rekick ? 'The wiring for this address stopped. Send it again and the platform picks it up.'
|
|
4943
|
+
: repoint ? 'It answers on an address you brought. Point it at a different one by editing the name.'
|
|
4944
|
+
: inst.domain ? 'It has the address the platform assigned it. Point a domain you own at it instead.'
|
|
4945
|
+
: 'It has no address yet. Point a domain you own at it.';
|
|
4946
|
+
/* the minute tick must not eat a half-typed domain or drop the caret;
|
|
4947
|
+
defaultValue tells the founder's typing from the pre-fill (the card rule) */
|
|
4948
|
+
var old = $('mDom').querySelector('input[type=text]');
|
|
4949
|
+
var typed = old && old.value !== old.defaultValue ? old.value : '';
|
|
4950
|
+
var hadFocus = !!old && document.activeElement === old;
|
|
4951
|
+
$('mDom').innerHTML = '<p class="plain">' + lead + '</p>' +
|
|
4952
|
+
'<div class="attach">' +
|
|
4953
|
+
'<input type="text" id="mDomName" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253" aria-label="A domain you own, for ' + esc(inst.slug) + '"' +
|
|
4954
|
+
(prefill ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
4955
|
+
'<button type="button" class="btn small" data-m-attach>' +
|
|
4956
|
+
(rekick ? 'Try the address again' : repoint ? 'Change the address' : 'Use this domain') + '</button></div>' +
|
|
4957
|
+
/* true in every state this renders in: a REQUESTED project never answered
|
|
4958
|
+
anywhere yet, so "stops answering" would be a claim about a thing that
|
|
4959
|
+
never happened (the F16 rule) */
|
|
4960
|
+
(repoint ? '<p class="attachNote">Changing it lets <code>' + esc(inst.domain) + '</code> go — this project won’t use that address any more.</p>' : '') +
|
|
4961
|
+
'<div class="projMsg' + (mDomNote && mDomNote.cls ? ' ' + mDomNote.cls : '') + '" id="mDomMsg" role="status" aria-live="polite">' +
|
|
4962
|
+
(mDomNote ? mDomNote.html : '') + '</div>';
|
|
4963
|
+
var field = $('mDomName');
|
|
4964
|
+
if (field && typed) field.value = typed;
|
|
4965
|
+
if (field && hadFocus) field.focus();
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
/* every refusal as the server worded it (the task 1002692 rule); the codes the
|
|
4969
|
+
page can say more precisely get their own sentence */
|
|
4970
|
+
function attachFailure(data, status) {
|
|
4971
|
+
var e = apiError(data, status);
|
|
4972
|
+
if (e.code === 'bad_domain') return 'That doesn’t look like a usable address — ' + esc(e.details.reason || 'use a real hostname like myproject.example.com') + '.';
|
|
4973
|
+
if (e.code === 'domain_taken') return esc(e.message || 'That address is already in use by another project.');
|
|
4974
|
+
if (e.code === 'domain_not_attachable') return esc(e.message || 'An address can be attached once the instance settles.');
|
|
4975
|
+
if (e.code === 'intent_conflict') {
|
|
4976
|
+
/* the address may still have been RECORDED (details.recorded) — say so */
|
|
4977
|
+
var t = esc(e.message || 'Another run holds the queue right now — re-send once it settles.');
|
|
4978
|
+
return e.details.recorded ? t + ' (The address itself is saved.)' : t;
|
|
4979
|
+
}
|
|
4980
|
+
return esc(e.message || 'The address could not be recorded — try again in a moment.');
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
function mAttach() {
|
|
4984
|
+
var inst = manageInst;
|
|
4985
|
+
if (!inst) return;
|
|
4986
|
+
var field = $('mDomName');
|
|
4987
|
+
var domain = field ? String(field.value || '').trim() : '';
|
|
4988
|
+
if (!domain) { domNote('Type the domain you own first — e.g. <code>myproject.example.com</code>.', 'err'); return; }
|
|
4989
|
+
var btn = $('mDom').querySelector('[data-m-attach]');
|
|
4990
|
+
if (btn) btn.disabled = true;
|
|
4991
|
+
domNote('Recording the address…');
|
|
4992
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(inst.id) + '/domain', {
|
|
4993
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
4994
|
+
body: JSON.stringify({ domain: domain })
|
|
4995
|
+
}).then(function (res) {
|
|
4996
|
+
if (res.status === 401) { domNote(signInAgain(), 'err'); return null; }
|
|
4997
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
4998
|
+
if (res.ok) { domNote(esc(data.message || 'Address recorded.'), 'ok'); return; }
|
|
4999
|
+
domNote(attachFailure(data, res.status), 'err');
|
|
5000
|
+
});
|
|
5001
|
+
}).catch(function () {
|
|
5002
|
+
domNote('Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
5003
|
+
}).then(function () {
|
|
5004
|
+
var b = $('mDom').querySelector('[data-m-attach]');
|
|
5005
|
+
if (b) b.disabled = false;
|
|
5006
|
+
loadManage(true, true); /* the address this page renders just changed */
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
$('mDom').addEventListener('click', function (ev) {
|
|
5011
|
+
var t = ev.target.closest && ev.target.closest('button');
|
|
5012
|
+
if (t && t.hasAttribute('data-m-attach')) mAttach();
|
|
5013
|
+
});
|
|
5014
|
+
|
|
4965
5015
|
/* an IN-FLIGHT project is watched here at the card's cadence — its own copy
|
|
4966
5016
|
says "this page updates when it starts", so it must. A settled project
|
|
4967
5017
|
keeps the C8 rule: entry + tab return, plus the request-free minute tick. */
|
|
@@ -5631,9 +5681,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5631
5681
|
function renderDelete(inst) {
|
|
5632
5682
|
if (mDelAsking && mDelAsking === inst.slug) return; /* keep THIS project's open confirmation */
|
|
5633
5683
|
if (inst.status === 'torn_down') {
|
|
5684
|
+
/* the revive door is on THIS page since task 1003355 — a pointer at the
|
|
5685
|
+
list would now send the owner to a row that only opens doors */
|
|
5634
5686
|
delMsg('<div class="addr">This project is already torn down — its server is stopped.</div>' +
|
|
5635
|
-
'<p class="finePrint">Its repository, its database and its web address are all still here.
|
|
5636
|
-
'Bring it back
|
|
5687
|
+
'<p class="finePrint">Its repository, its database and its web address are all still here.' +
|
|
5688
|
+
(canRevive(inst) ? ' Bring it back with the button at the top of this page.' : '') + '</p>');
|
|
5637
5689
|
return;
|
|
5638
5690
|
}
|
|
5639
5691
|
if (inst.status === 'tearing_down') {
|
|
@@ -5675,6 +5727,22 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5675
5727
|
return !!(field && manageInst && field.value.trim() === manageInst.slug);
|
|
5676
5728
|
}
|
|
5677
5729
|
|
|
5730
|
+
/* every refusal as the server worded it, with the two codes the page can say
|
|
5731
|
+
more precisely spelled out. This vocabulary rode the list row's sender until
|
|
5732
|
+
task 1003355 made the ender the only teardown door — losing it would have
|
|
5733
|
+
turned two named refusals back into the generic "could not be queued" the
|
|
5734
|
+
tasks 1002691/1002692 walk was about. */
|
|
5735
|
+
function teardownFailure(data, status) {
|
|
5736
|
+
var e = apiError(data, status);
|
|
5737
|
+
if (e.code === 'intent_conflict') {
|
|
5738
|
+
return esc(e.message || ('A ' + (e.details.action || 'previous') + ' run is still ' + (e.details.state || 'open') + ' — retry in a moment.'));
|
|
5739
|
+
}
|
|
5740
|
+
if (e.code === 'not_teardownable') {
|
|
5741
|
+
return esc(e.message || ('Nothing to tear down right now — the instance is ' + (e.details.status || 'settled') + '.'));
|
|
5742
|
+
}
|
|
5743
|
+
return esc(e.message || 'The teardown could not be queued — try again in a moment.');
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5678
5746
|
function doDelete() {
|
|
5679
5747
|
var inst = manageInst;
|
|
5680
5748
|
/* the typed name is this page's own gate, never the server's — so a click
|
|
@@ -5696,10 +5764,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5696
5764
|
/* the still-true state first, then exactly what the server said */
|
|
5697
5765
|
renderDelete(inst);
|
|
5698
5766
|
$('mDel').insertAdjacentHTML('beforeend',
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
: '<div class="projMsg err">' + esc(apiError(data, res.status).message ||
|
|
5702
|
-
'The teardown could not be queued — try again in a moment.') + '</div>');
|
|
5767
|
+
'<div class="projMsg err">' +
|
|
5768
|
+
(res.status === 401 ? signInAgain() : teardownFailure(data, res.status)) + '</div>');
|
|
5703
5769
|
});
|
|
5704
5770
|
}).catch(function () {
|
|
5705
5771
|
renderDelete(inst);
|
|
@@ -5722,109 +5788,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5722
5788
|
if (t.hasAttribute('data-del-no')) { mDelAsking = null; if (manageInst) renderDelete(manageInst); }
|
|
5723
5789
|
});
|
|
5724
5790
|
|
|
5725
|
-
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
5726
|
-
SAY exactly what the server said ── */
|
|
5727
|
-
|
|
5728
|
-
/* the id whose confirmation is open — renderCard skips that card, so the
|
|
5729
|
-
minute re-render (sig() buckets recency) cannot wipe the prompt mid-read
|
|
5730
|
-
(the manage page's mDelAsking rule, task 1003232) */
|
|
5731
|
-
var cardAsking = null;
|
|
5732
|
-
function askTeardown(id) {
|
|
5733
|
-
var act = $('act-' + id);
|
|
5734
|
-
if (!act) return;
|
|
5735
|
-
cardAsking = String(id);
|
|
5736
|
-
act.innerHTML = '<div class="confirmRow" tabindex="-1">' +
|
|
5737
|
-
'<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
|
|
5738
|
-
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
5739
|
-
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
|
|
5740
|
-
/* the pressed button is gone — focus lands on the question itself, so
|
|
5741
|
-
the truth is read before Tab reaches Yes (task 1003244) */
|
|
5742
|
-
var row = act.firstChild;
|
|
5743
|
-
if (row && row.focus) row.focus();
|
|
5744
|
-
}
|
|
5745
|
-
function unAskTeardown(id) { cardAsking = null; var inst = byId[id]; if (inst) renderCard(inst); }
|
|
5746
|
-
function doTeardown(id) {
|
|
5747
|
-
cardAsking = null;
|
|
5748
|
-
quietActions(id);
|
|
5749
|
-
msg(id, 'Asking…');
|
|
5750
|
-
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/teardown', {
|
|
5751
|
-
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
5752
|
-
}).then(function (res) {
|
|
5753
|
-
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
5754
|
-
return jsonOrEmpty(res).then(function (data) {
|
|
5755
|
-
if (res.ok) {
|
|
5756
|
-
/* truthful outcomes (task 1002691): queued:true = really queued;
|
|
5757
|
-
queued:false = one was ALREADY queued; superseded = a stale
|
|
5758
|
-
provision intent was cleared to make room */
|
|
5759
|
-
var note = esc(data.message || 'Teardown requested.');
|
|
5760
|
-
if (data.superseded) note += ' (A stale standup request was cleared to make room.)';
|
|
5761
|
-
msg(id, note, 'ok');
|
|
5762
|
-
return;
|
|
5763
|
-
}
|
|
5764
|
-
var e = apiError(data, res.status);
|
|
5765
|
-
if (e.code === 'intent_conflict') {
|
|
5766
|
-
msg(id, esc(e.message || ('A ' + (e.details.action || 'previous') + ' run is still ' + (e.details.state || 'open') + ' — retry in a moment.')), 'err');
|
|
5767
|
-
} else if (e.code === 'not_teardownable') {
|
|
5768
|
-
msg(id, esc(e.message || ('Nothing to tear down right now — the instance is ' + (e.details.status || 'settled') + '.')), 'err');
|
|
5769
|
-
} else {
|
|
5770
|
-
msg(id, esc(e.message || 'The teardown could not be queued — try again in a moment.'), 'err');
|
|
5771
|
-
}
|
|
5772
|
-
});
|
|
5773
|
-
}).catch(function () {
|
|
5774
|
-
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
5775
|
-
}).then(function () {
|
|
5776
|
-
/* wake the quieted row even when the state didn't change (renderCard
|
|
5777
|
-
also hands focus back), then pick up whatever DID change */
|
|
5778
|
-
if (byId[id]) renderCard(byId[id]);
|
|
5779
|
-
watchOnce(id);
|
|
5780
|
-
});
|
|
5781
|
-
}
|
|
5782
|
-
|
|
5783
|
-
/* ── re-enqueue: Try again (snagged) and Bring it back (torn down) both
|
|
5784
|
-
re-POST the slug — the server's PROVISION_ENQUEUE_FROM gate decides.
|
|
5785
|
-
Same truth-telling as every other action: say what the server said. ── */
|
|
5786
|
-
|
|
5787
|
-
function reProvision(id, verb) {
|
|
5788
|
-
var inst = byId[id];
|
|
5789
|
-
if (!inst) return;
|
|
5790
|
-
quietActions(id);
|
|
5791
|
-
msg(id, 'Asking…');
|
|
5792
|
-
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'standalone' };
|
|
5793
|
-
if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
|
|
5794
|
-
if (inst.target_ref) body.target_ref = inst.target_ref;
|
|
5795
|
-
if (inst.domain) body.domain = inst.domain;
|
|
5796
|
-
fetch(API + '/provisioning/instances', {
|
|
5797
|
-
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
5798
|
-
body: JSON.stringify(body)
|
|
5799
|
-
}).then(function (res) {
|
|
5800
|
-
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
5801
|
-
return jsonOrEmpty(res).then(function (data) {
|
|
5802
|
-
if (res.ok) {
|
|
5803
|
-
msg(id, esc(data.message || (verb === 'revive' ? 'Revive queued — bringing it back.' : 'Queued again — the platform picks it up from here.')), 'ok');
|
|
5804
|
-
return;
|
|
5805
|
-
}
|
|
5806
|
-
var e = apiError(data, res.status);
|
|
5807
|
-
if (e.code === 'intent_conflict') {
|
|
5808
|
-
msg(id, esc(e.message || 'A previous run is still open — retry once it settles.'), 'err');
|
|
5809
|
-
} else if (e.code === 'rate_limited') {
|
|
5810
|
-
msg(id, esc(e.message || 'Too many requests right now — give it a minute and try again.'), 'err');
|
|
5811
|
-
} else {
|
|
5812
|
-
msg(id, esc(e.message || 'The request could not be queued — try again in a moment.'), 'err');
|
|
5813
|
-
}
|
|
5814
|
-
});
|
|
5815
|
-
}).catch(function () {
|
|
5816
|
-
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
5817
|
-
}).then(function () {
|
|
5818
|
-
if (byId[id]) renderCard(byId[id]);
|
|
5819
|
-
watchOnce(id);
|
|
5820
|
-
});
|
|
5821
|
-
}
|
|
5822
|
-
|
|
5823
5791
|
/* ── restart an unreachable project (task 1003238). Its own route, because
|
|
5824
5792
|
PROVISION_ENQUEUE_FROM excludes 'active' — re-POSTing the slug the way
|
|
5825
5793
|
Try again / Bring it back do would be refused on every press. Every
|
|
5826
5794
|
refusal is shown as the server worded it: the server owns the gate, and
|
|
5827
|
-
a
|
|
5795
|
+
a page that invents its own reason drifts from it. mRestart() is the one
|
|
5796
|
+
sender since task 1003355 moved the door off the list row. ── */
|
|
5828
5797
|
|
|
5829
5798
|
function restartFailure(data, status) {
|
|
5830
5799
|
var e = apiError(data, status);
|
|
@@ -5832,84 +5801,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5832
5801
|
return e.message || 'The restart could not be queued — try again in a moment.';
|
|
5833
5802
|
}
|
|
5834
5803
|
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/restart', {
|
|
5841
|
-
method: 'POST', credentials: 'same-origin'
|
|
5842
|
-
}).then(function (res) {
|
|
5843
|
-
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
5844
|
-
return jsonOrEmpty(res).then(function (data) {
|
|
5845
|
-
if (res.ok) { msg(id, esc(data.message || 'Restart queued.'), 'ok'); return; }
|
|
5846
|
-
msg(id, esc(restartFailure(data, res.status)), 'err');
|
|
5847
|
-
});
|
|
5848
|
-
}).catch(function () {
|
|
5849
|
-
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
5850
|
-
}).then(function () {
|
|
5851
|
-
if (byId[id]) renderCard(byId[id]);
|
|
5852
|
-
watchOnce(id);
|
|
5853
|
-
});
|
|
5854
|
-
}
|
|
5855
|
-
|
|
5856
|
-
/* ── domain attach: same truth-telling for every refusal (task 1002692) ── */
|
|
5857
|
-
|
|
5858
|
-
function doAttach(id) {
|
|
5859
|
-
var input = $('dom-' + id);
|
|
5860
|
-
var domain = input ? String(input.value || '').trim() : '';
|
|
5861
|
-
if (!domain) { msg(id, 'Type the domain you own first — e.g. <code>myproject.example.com</code>.', 'err'); return; }
|
|
5862
|
-
msg(id, 'Recording the address…');
|
|
5863
|
-
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/domain', {
|
|
5864
|
-
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
5865
|
-
body: JSON.stringify({ domain: domain })
|
|
5866
|
-
}).then(function (res) {
|
|
5867
|
-
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
5868
|
-
return jsonOrEmpty(res).then(function (data) {
|
|
5869
|
-
if (res.ok) {
|
|
5870
|
-
msg(id, esc(data.message || 'Address recorded.'), 'ok');
|
|
5871
|
-
return;
|
|
5872
|
-
}
|
|
5873
|
-
var e = apiError(data, res.status);
|
|
5874
|
-
if (e.code === 'bad_domain') {
|
|
5875
|
-
msg(id, 'That doesn’t look like a usable address — ' + esc(e.details.reason || 'use a real hostname like myproject.example.com') + '.', 'err');
|
|
5876
|
-
} else if (e.code === 'domain_taken') {
|
|
5877
|
-
msg(id, esc(e.message || 'That address is already in use by another project.'), 'err');
|
|
5878
|
-
} else if (e.code === 'domain_not_attachable') {
|
|
5879
|
-
msg(id, esc(e.message || 'An address can be attached once the instance settles.'), 'err');
|
|
5880
|
-
} else if (e.code === 'intent_conflict') {
|
|
5881
|
-
/* the address may still have been RECORDED (details.recorded) — say so */
|
|
5882
|
-
var t = esc(e.message || 'Another run holds the queue right now — re-send once it settles.');
|
|
5883
|
-
if (e.details.recorded) t += ' (The address itself is saved.)';
|
|
5884
|
-
msg(id, t, 'err');
|
|
5885
|
-
} else {
|
|
5886
|
-
msg(id, esc(e.message || 'The address could not be recorded — try again in a moment.'), 'err');
|
|
5887
|
-
}
|
|
5888
|
-
});
|
|
5889
|
-
}).catch(function () {
|
|
5890
|
-
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
5891
|
-
}).then(function () { watchOnce(id); });
|
|
5892
|
-
}
|
|
5893
|
-
|
|
5894
|
-
/* one delegated listener for all card actions */
|
|
5804
|
+
/* the list's one delegated handler. A row holds no sender since task 1003355
|
|
5805
|
+
— only its two doors — so the Manage link is all there is to upgrade: it is
|
|
5806
|
+
a real anchor, a modified click keeps its browser meaning and a plain one
|
|
5807
|
+
stays in-page (the orb-click contract). The address line's "give it an
|
|
5808
|
+
address" link carries the same marker, so it lands on the same page. */
|
|
5895
5809
|
$('projectsList').addEventListener('click', function (ev) {
|
|
5896
|
-
/* the Manage link is a real anchor — a modified click keeps its browser
|
|
5897
|
-
meaning, a plain one stays in-page (the orb-click contract) */
|
|
5898
5810
|
var m = ev.target.closest && ev.target.closest('a[data-manage]');
|
|
5899
|
-
if (m)
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
return goToManage(m.getAttribute('data-manage'));
|
|
5903
|
-
}
|
|
5904
|
-
var t = ev.target.closest('button');
|
|
5905
|
-
if (!t) return;
|
|
5906
|
-
if (t.hasAttribute('data-teardown')) return askTeardown(t.getAttribute('data-teardown'));
|
|
5907
|
-
if (t.hasAttribute('data-teardown-yes')) return doTeardown(t.getAttribute('data-teardown-yes'));
|
|
5908
|
-
if (t.hasAttribute('data-teardown-no')) return unAskTeardown(t.getAttribute('data-teardown-no'));
|
|
5909
|
-
if (t.hasAttribute('data-attach')) return doAttach(t.getAttribute('data-attach'));
|
|
5910
|
-
if (t.hasAttribute('data-retry')) return reProvision(t.getAttribute('data-retry'), 'retry');
|
|
5911
|
-
if (t.hasAttribute('data-revive')) return reProvision(t.getAttribute('data-revive'), 'revive');
|
|
5912
|
-
if (t.hasAttribute('data-restart')) return doRestart(t.getAttribute('data-restart'));
|
|
5811
|
+
if (!m || !isPlainClick(ev)) return;
|
|
5812
|
+
ev.preventDefault();
|
|
5813
|
+
goToManage(m.getAttribute('data-manage'));
|
|
5913
5814
|
});
|
|
5914
5815
|
|
|
5915
5816
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|