@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.
@@ -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 <a href="?view=mine">your projects page</a>.</p></li>';
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 <a href="?view=mine">your projects page</a>, then come back to this step.';
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 <a href="?view=mine">your projects page</a>; the steps below that need one unlock then.', '', flow));
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 mine view */
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 <a href="?view=mine">your projects page</a>.', 'isErr'));
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 <a href="?view=mine">your projects page</a>, or start another below.', '', 'teardown'));
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
- function addressRow(inst) {
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. <a href="?view=mine" data-nav-to="mine">Give it an address from your projects</a>.';
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
- /* every owned card opens its manage view (task 1003041) — a real link, so
4600
- middle/ctrl-click works; the delegated handler upgrades a plain click */
4601
- var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
4602
- /* the success moment needs a door: a live project opens from its own card
4603
- (the orb and the address link were the only ways in — task 1003232) */
4604
- 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>');
4605
- if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-retry="' + iid + '">Try again</button>');
4606
- if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-revive="' + iid + '">Bring it back</button>');
4607
- if (canRestart(inst)) acts.push('<button type="button" class="btn small" data-restart="' + iid + '">Restart it</button>');
4608
- if (canTeardown(inst)) acts.push('<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button>');
4609
- if (acts.length) body += '<div class="actions" id="act-' + iid + '">' + acts.join('') + '</div>';
4610
- body += attachFold(inst, iid);
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
- var fold = li.querySelector('details.attachFold');
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 control had focus, as a selector the rebuilt card can answer;
4639
- a confirm's Yes/Keep lands back on the door that opened it */
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
- if (!el || !el.getAttribute) return null;
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 any time from <a href="?view=mine" data-nav-to="mine">your projects</a>.</p>');
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
- res.status === 401
5700
- ? '<div class="projMsg err">' + signInAgain() + '</div>'
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 card that invents its own reason drifts from it. ── */
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
- function doRestart(id) {
5836
- var inst = byId[id];
5837
- if (!inst) return;
5838
- quietActions(id);
5839
- msg(id, 'Asking…');
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
- if (!isPlainClick(ev)) return;
5901
- ev.preventDefault();
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 ────────────────────────────────────── */