@bongos/core 1.19.439 → 1.19.441
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 -19
- package/docs/copy-inventory.md +175 -168
- package/docs/copy-registry.json +284 -220
- package/docs/design/mocks/hub/mine.html +89 -1
- package/docs/design/projects-hub-direction-v2.md +56 -0
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +98 -87
- package/public-landing/projects.html +310 -377
- package/public-landing/projects.probes.json +26 -21
- package/public-landing/projects.states.json +7 -5
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +104 -69
- package/tests/projects_hub_pre_uat.mjs +122 -78
|
@@ -492,37 +492,32 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
492
492
|
<h2 class="tblTitle">every project</h2>
|
|
493
493
|
<span class="mastObj" aria-hidden="true"></span>
|
|
494
494
|
</div>
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
</tr>
|
|
522
|
-
<!--apex-only:end-->
|
|
523
|
-
</tbody>
|
|
524
|
-
</table>
|
|
525
|
-
<p class="finePrint" style="text-align:center">Sort by Project or Builders.</p>
|
|
495
|
+
<!-- The sort left the column head with the header row (task 1003356): a
|
|
496
|
+
ledger row has no columns to head, so the two orders are hairline
|
|
497
|
+
pills in the mine view's own grammar — real buttons, aria-pressed, the
|
|
498
|
+
live order stated under the list where a screen reader hears it. -->
|
|
499
|
+
<div class="tblSort" role="group" aria-label="Sort the projects">
|
|
500
|
+
<span class="sortLbl">sort</span>
|
|
501
|
+
<button type="button" class="sortPill" data-sort="name" aria-pressed="true">name <span class="dir">a to z</span></button>
|
|
502
|
+
<button type="button" class="sortPill" data-sort="builders" aria-pressed="false">builders</button>
|
|
503
|
+
</div>
|
|
504
|
+
<ul class="tbl" id="mapRows">
|
|
505
|
+
<!-- The platform's own row. Static and apex-only for exactly the reason
|
|
506
|
+
the orb above is (stripApexOnly() is a string transform that cannot
|
|
507
|
+
reach anything a script renders later), and preserved across
|
|
508
|
+
re-renders by the same data-generated contract renderOrbs uses. -->
|
|
509
|
+
<!--apex-only:start-->
|
|
510
|
+
<li class="tblRow" id="selfRow">
|
|
511
|
+
<span class="worldOrb" data-shape="pair" data-style="chrome" data-template="pair-chrome" style="--acc:var(--accent);--d:64px" aria-hidden="true"><span class="disc"></span></span>
|
|
512
|
+
<div class="tWho"><span class="ptitle" data-brand-product-inline>Cloud Bongos</span><span id="selfPill"></span><span class="chip calm"><span class="cdot"></span>not checked</span></div>
|
|
513
|
+
<div class="facts"><span class="fact" id="selfBuilders">builders not known</span><span class="fact" id="selfShipped" hidden></span></div>
|
|
514
|
+
<p class="ptag">this platform</p>
|
|
515
|
+
<div class="runs"><span class="rLbl">runs</span><span id="selfTags">—</span></div>
|
|
516
|
+
<div class="rowDoors"><a class="rowGo" href="/">view</a><a class="rowHall" id="selfHall" href="{{buildersOrigin}}" aria-label="Builders' hall" title="Builders' hall" hidden><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg></a></div>
|
|
517
|
+
</li>
|
|
518
|
+
<!--apex-only:end-->
|
|
519
|
+
</ul>
|
|
520
|
+
<p class="finePrint" id="mapSorted" role="status" aria-live="polite">Sorted by name, A to Z.</p>
|
|
526
521
|
</div>
|
|
527
522
|
|
|
528
523
|
<!-- The project card. An OVERLAY inside the map, not a fourth view: opening
|
|
@@ -695,6 +690,20 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
695
690
|
<div class="actions" id="mActs" hidden></div>
|
|
696
691
|
</div>
|
|
697
692
|
|
|
693
|
+
<!-- its address (task 1003355): the bring-your-own-domain control, moved
|
|
694
|
+
off the list row when a row shrank to view and manage. Its home is
|
|
695
|
+
here, under the ledger's address fact. The section hides itself where
|
|
696
|
+
there is nothing to point an address at (a snagged or torn-down
|
|
697
|
+
project) — the same call the card's fold used to make. -->
|
|
698
|
+
<div class="card mSec" id="mAddress" hidden>
|
|
699
|
+
<h2>Its address</h2>
|
|
700
|
+
<div id="mDom"><p class="finePrint">Reading the project…</p></div>
|
|
701
|
+
<details class="finePrint" id="mDomWhat">
|
|
702
|
+
<summary>Where an address comes from</summary>
|
|
703
|
+
<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>
|
|
704
|
+
</details>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
698
707
|
<!-- live since task 1003045: reads the repo's real visibility from GitHub
|
|
699
708
|
with the owner's own repository credential. Flip-to-PUBLIC only — the
|
|
700
709
|
private direction is refused (server + copy) until the private-repo
|
|
@@ -1457,7 +1466,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1457
1466
|
}
|
|
1458
1467
|
function typeHtml(t) {
|
|
1459
1468
|
var l = typeLabel(t);
|
|
1460
|
-
|
|
1469
|
+
/* a ledger row's fact is a self-describing line, so "not declared" is said
|
|
1470
|
+
in words where the table's Type cell used to print a dash (task 1003356) */
|
|
1471
|
+
return l ? '<span class="typeTag">' + l + '</span>' : 'type not declared';
|
|
1461
1472
|
}
|
|
1462
1473
|
function matchesMapQuery(p, q) {
|
|
1463
1474
|
if (!q) return true;
|
|
@@ -1531,44 +1542,63 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1531
1542
|
});
|
|
1532
1543
|
}
|
|
1533
1544
|
|
|
1545
|
+
/* ── the row's facts, the ledger grammar (task 1003356) ──
|
|
1546
|
+
Each fact is one self-describing label-face line, the shape the Your-projects
|
|
1547
|
+
row uses, so the two views read as one ledger. The distinction the columns
|
|
1548
|
+
drew survives the move: "we could not ask" (null) and "we asked, the answer
|
|
1549
|
+
is none" (0) are two different sentences, not a dash and a zero. A figure
|
|
1550
|
+
that was never a column — shipped in 30 days, the Listed date — takes no
|
|
1551
|
+
space when it has no answer, the poster card's own rule. */
|
|
1552
|
+
function factLines(p) {
|
|
1553
|
+
var out = ['<span class="fact">' + typeHtml(p.type) + '</span>'];
|
|
1554
|
+
out.push('<span class="fact">' + (p.builders == null ? 'builders not known'
|
|
1555
|
+
: esc(String(p.builders)) + (p.builders === 1 ? ' builder' : ' builders')) + '</span>');
|
|
1556
|
+
var shipped = shippedFigure(p);
|
|
1557
|
+
if (shipped) out.push('<span class="fact">' + esc(shipped) + ' shipped in 30 days</span>');
|
|
1558
|
+
if (p.created_at) out.push('<span class="fact">listed ' + esc(listedDate(p.created_at)) + '</span>');
|
|
1559
|
+
return out.join('');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1534
1562
|
function renderRows(list) {
|
|
1535
1563
|
var body = $('mapRows');
|
|
1536
1564
|
/* replace only the generated rows — the platform's own row is static
|
|
1537
1565
|
apex-only markup and must survive every re-render, for the same reason
|
|
1538
1566
|
#selfOrb does in the sky. It stays pinned first: it is "this platform",
|
|
1539
1567
|
not a peer in the catalog's sort order. */
|
|
1540
|
-
body.querySelectorAll('
|
|
1568
|
+
body.querySelectorAll('li[data-generated]').forEach(function (n) { n.remove(); });
|
|
1541
1569
|
body.insertAdjacentHTML('beforeend', list.map(function (p) {
|
|
1542
1570
|
var rel = relOf(p);
|
|
1543
|
-
var pill = rel === 'owner' ? '
|
|
1544
|
-
: rel === 'builder' ? '
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1571
|
+
var pill = rel === 'owner' ? '<span class="pill">yours</span>'
|
|
1572
|
+
: rel === 'builder' ? '<span class="pill">you build here</span>' : '';
|
|
1573
|
+
/* the project's own planet, the object the sky and the Your-projects row
|
|
1574
|
+
already draw — the owner's chosen template and colour first (task
|
|
1575
|
+
1003347), the slug walk otherwise. Decorative here: the name beside it
|
|
1576
|
+
is the row's accessible door. */
|
|
1577
|
+
var planet = '<span class="worldOrb"' + planetAttrs({ tpl: templateOf(p) }) +
|
|
1578
|
+
' style="--acc:' + accentOf(p) + ';--d:64px" aria-hidden="true"><span class="disc"></span></span>';
|
|
1579
|
+
return '<li class="tblRow" data-generated ' + (p._inst ? 'data-manage="' + esc(String(p._inst.id)) + '" ' : '') +
|
|
1580
|
+
'data-origin="' + esc(String(p.origin || '')) + '">' + planet +
|
|
1581
|
+
'<div class="tWho">' + (p._inst
|
|
1550
1582
|
? '<a class="ptitle" data-manage="' + esc(String(p._inst.id)) + '" href="?view=manage&id=' + esc(String(p._inst.id)) + '">' + esc(p.name) + '</a>'
|
|
1551
|
-
: '<span class="ptitle">' + esc(p.name) + '</span>') + pill +
|
|
1552
|
-
'<span class="ptag">' + esc(p.tagline || '') + '</span></span></span></td>' +
|
|
1553
|
-
/* Builders and Runs come from the project's OWN public API, enriched
|
|
1554
|
-
server-side (task 1002771). null is unknown — the project was
|
|
1555
|
-
unreachable — and an em dash says exactly that. 0 is a real answer and
|
|
1556
|
-
renders as 0, because "nobody has joined yet" is a different fact from
|
|
1557
|
-
"we could not ask". */
|
|
1558
|
-
'<td class="colType">' + typeHtml(p.type) + '</td>' +
|
|
1559
|
-
'<td class="right num">' + (p.builders == null ? '—' : esc(String(p.builders))) + '</td>' +
|
|
1560
|
-
'<td class="colTags">' + tagsHtml(p.tags) + '</td>' +
|
|
1583
|
+
: '<span class="ptitle">' + esc(p.name) + '</span>') + pill +
|
|
1561
1584
|
/* liveness on a CATALOG row is NOT probed by this surface — a chip
|
|
1562
1585
|
saying "live" there would claim more than anything verified (C6).
|
|
1563
1586
|
An OWNED row is different: the own-scoped feed answers for it, so
|
|
1564
1587
|
it carries chipFor()'s truth, the same chip its orb and its manage
|
|
1565
1588
|
card show (task 1002744). */
|
|
1566
|
-
|
|
1567
|
-
: '<span class="chip calm"><span class="cdot"></span
|
|
1568
|
-
'<
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1589
|
+
(p._inst ? chipFor(p._inst)
|
|
1590
|
+
: '<span class="chip calm"><span class="cdot"></span>not checked</span>') + '</div>' +
|
|
1591
|
+
'<div class="facts">' + factLines(p) + '</div>' +
|
|
1592
|
+
'<p class="ptag">' + esc(p.tagline || '') + '</p>' +
|
|
1593
|
+
/* Runs stays the module list here, where the poster card's Runs is the
|
|
1594
|
+
address (direction §7): null is "we could not ask", [] is "core" */
|
|
1595
|
+
'<div class="runs"><span class="rLbl">runs</span>' + tagsHtml(p.tags) + '</div>' +
|
|
1596
|
+
/* at most two doors (task 1003356): the site, and the hall where the
|
|
1597
|
+
viewer builds there and the hall answers at a different address */
|
|
1598
|
+
'<div class="rowDoors">' + (safeHttpUrl(p.origin)
|
|
1599
|
+
? '<a class="rowGo" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">view</a>'
|
|
1600
|
+
: '') + hallGlyphHtml(p, rel) + '</div>' +
|
|
1601
|
+
'</li>';
|
|
1572
1602
|
}).join(''));
|
|
1573
1603
|
}
|
|
1574
1604
|
|
|
@@ -1651,7 +1681,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1651
1681
|
}
|
|
1652
1682
|
if (row) {
|
|
1653
1683
|
row.hidden = !selfShown;
|
|
1654
|
-
$('selfPill').innerHTML = badge ? '
|
|
1684
|
+
$('selfPill').innerHTML = badge ? '<span class="pill">' + badge + '</span>' : '';
|
|
1655
1685
|
/* the hall glyph: member-gated (the badge says so) and address-gated */
|
|
1656
1686
|
var sh = $('selfHall');
|
|
1657
1687
|
if (sh) sh.hidden = !badge || !hallUrlOf(SELF_PROJECT);
|
|
@@ -1897,7 +1927,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1897
1927
|
}
|
|
1898
1928
|
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
1899
1929
|
if (ev.target.closest && ev.target.closest('a')) return;
|
|
1900
|
-
var tr = ev.target.closest && ev.target.closest('
|
|
1930
|
+
var tr = ev.target.closest && ev.target.closest('li');
|
|
1901
1931
|
if (!tr || !isPlainClick(ev)) return;
|
|
1902
1932
|
if (tr.id === 'selfRow') return openCard(SELF_PROJECT, selfRel(), null);
|
|
1903
1933
|
/* an owned row goes to MANAGE, exactly like its orb — two views, one job */
|
|
@@ -2016,7 +2046,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2016
2046
|
var rows = (d && d.leaderboard) || [];
|
|
2017
2047
|
if (!rows.length) return;
|
|
2018
2048
|
var real = rows.filter(function (b) { return !/-system$/.test(String(b.github_login || '')); });
|
|
2019
|
-
$('selfBuilders').textContent =
|
|
2049
|
+
$('selfBuilders').textContent = real.length === 1 ? '1 builder' : real.length + ' builders';
|
|
2020
2050
|
/* the card reads these too: the same payload answers the platform's
|
|
2021
2051
|
builder count and its owner — the archon row — by the same rule the
|
|
2022
2052
|
server-side enrichment applies to every other project */
|
|
@@ -2042,6 +2072,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2042
2072
|
var n = d.tasks.filter(function (t) { return Date.parse(t && t.shipped_at) >= since; }).length;
|
|
2043
2073
|
SELF_PROJECT.shipped_30d = n;
|
|
2044
2074
|
SELF_PROJECT.shipped_30d_capped = d.tasks.length >= 200 && n === d.tasks.length;
|
|
2075
|
+
/* the row's own fact, by the same rule every other row's is written */
|
|
2076
|
+
var sf = $('selfShipped');
|
|
2077
|
+
sf.textContent = shippedFigure(SELF_PROJECT) + ' shipped in 30 days';
|
|
2078
|
+
sf.hidden = false;
|
|
2045
2079
|
})
|
|
2046
2080
|
.catch(function () { /* unknown stays unknown: the figure hides */ });
|
|
2047
2081
|
}
|
|
@@ -2099,21 +2133,33 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2099
2133
|
|
|
2100
2134
|
$('onlyMine').addEventListener('change', applyMapFilter);
|
|
2101
2135
|
$('mapQ').addEventListener('input', applyMapFilter);
|
|
2102
|
-
/* the sort
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2136
|
+
/* ── the sort, off the column head (task 1003356) ──
|
|
2137
|
+
A ledger row has no columns to head, so aria-sort has no host: the two
|
|
2138
|
+
orders are toggle buttons in a group, pressed = the live order, and the
|
|
2139
|
+
sentence under the list states it for a screen reader that never saw the
|
|
2140
|
+
press. Pressing the pressed one reverses, exactly as the header did. */
|
|
2141
|
+
var SORT_WORDS = { name: ['a to z', 'z to a'], builders: ['fewest first', 'most first'] };
|
|
2142
|
+
function paintSort() {
|
|
2143
|
+
document.querySelectorAll('.tblSort button[data-sort]').forEach(function (b) {
|
|
2144
|
+
var on = b.getAttribute('data-sort') === mapSort.key;
|
|
2145
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
2146
|
+
var dir = b.querySelector('.dir');
|
|
2147
|
+
if (on && !dir) { dir = document.createElement('span'); dir.className = 'dir'; b.appendChild(document.createTextNode(' ')); b.appendChild(dir); }
|
|
2148
|
+
if (dir) { dir.hidden = !on; dir.textContent = on ? SORT_WORDS[mapSort.key][mapSort.dir === 1 ? 0 : 1] : ''; }
|
|
2149
|
+
});
|
|
2150
|
+
$('mapSorted').textContent = 'Sorted by ' + mapSort.key + ', ' +
|
|
2151
|
+
SORT_WORDS[mapSort.key][mapSort.dir === 1 ? 0 : 1] + '.';
|
|
2152
|
+
}
|
|
2153
|
+
document.querySelectorAll('.tblSort button[data-sort]').forEach(function (btn) {
|
|
2108
2154
|
btn.addEventListener('click', function () {
|
|
2109
|
-
var k =
|
|
2155
|
+
var k = btn.getAttribute('data-sort');
|
|
2110
2156
|
mapSort.dir = (mapSort.key === k) ? -mapSort.dir : 1;
|
|
2111
2157
|
mapSort.key = k;
|
|
2112
|
-
|
|
2113
|
-
th.setAttribute('aria-sort', mapSort.dir === 1 ? 'ascending' : 'descending');
|
|
2158
|
+
paintSort();
|
|
2114
2159
|
applyMapFilter();
|
|
2115
2160
|
});
|
|
2116
2161
|
});
|
|
2162
|
+
paintSort();
|
|
2117
2163
|
|
|
2118
2164
|
/* ══ THE LIVING SKY (task 1002762) ═════════════════════════════════════════
|
|
2119
2165
|
The retired gate's cluster, rewritten for the hub. Physics is the gate's
|
|
@@ -3447,6 +3493,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3447
3493
|
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
3494
|
|
|
3449
3495
|
var instanceId = null; /* set when a create lands / a resumed watch starts */
|
|
3496
|
+
/* where a project is ACTED on since task 1003355: its own page. A list row
|
|
3497
|
+
carries only view and manage now, so every pointer that names a control —
|
|
3498
|
+
attach an address, retry a snagged standup, bring a torn-down project
|
|
3499
|
+
back — must land there, not on the list, which has no such button. */
|
|
3500
|
+
function projectPageLink(label) {
|
|
3501
|
+
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
3502
|
+
'">' + label + '</a>';
|
|
3503
|
+
}
|
|
3450
3504
|
var live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
3451
3505
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
3452
3506
|
var lastStepsHtml = '';
|
|
@@ -3518,7 +3572,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3518
3572
|
function renderStepOauthApp() {
|
|
3519
3573
|
var domain = resolvedDomain();
|
|
3520
3574
|
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
|
|
3575
|
+
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
3576
|
}
|
|
3523
3577
|
/* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
|
|
3524
3578
|
manifests whose hook URL isn't publicly reachable, so a locally-hosted
|
|
@@ -3535,7 +3589,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3535
3589
|
address yet — says where to go instead of what the server said. */
|
|
3536
3590
|
var snag, raw = '';
|
|
3537
3591
|
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
|
|
3592
|
+
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
3593
|
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
3540
3594
|
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
|
|
3541
3595
|
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
@@ -3866,7 +3920,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3866
3920
|
answer at, so a verified answer can never arrive; say what IS true
|
|
3867
3921
|
instead of promising the hall (sign-in stays locked without one) */
|
|
3868
3922
|
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
|
|
3923
|
+
'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 ' +
|
|
3924
|
+
projectPageLink('its own page') + '; the steps below that need one unlock then.', '', flow));
|
|
3870
3925
|
return true;
|
|
3871
3926
|
}
|
|
3872
3927
|
if (inst.last_verified_up === false) {
|
|
@@ -3885,16 +3940,19 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3885
3940
|
return false;
|
|
3886
3941
|
} else if (s === 'error') {
|
|
3887
3942
|
/* 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
|
|
3943
|
+
false "keeps retrying" — retries stop, and the retry door is the project's
|
|
3944
|
+
own page (task 1003355 moved it off the row) */
|
|
3889
3945
|
setProgress(progressMarkup(1, 1,
|
|
3890
|
-
'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from
|
|
3946
|
+
'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from ' +
|
|
3947
|
+
projectPageLink('its own page') + '.', 'isErr'));
|
|
3891
3948
|
} else if (s === 'tearing_down') {
|
|
3892
3949
|
/* a deliberate end is calm, never styled as a failure (direction §5) —
|
|
3893
3950
|
and it speaks the teardown vocabulary, not the standup's (F22) */
|
|
3894
3951
|
setProgress(progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; ' +
|
|
3895
3952
|
(inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'), '', 'teardown'));
|
|
3896
3953
|
} else if (s === 'torn_down') {
|
|
3897
|
-
setProgress(progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from
|
|
3954
|
+
setProgress(progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from ' +
|
|
3955
|
+
projectPageLink('its own page') + ', or start another below.', '', 'teardown'));
|
|
3898
3956
|
return true;
|
|
3899
3957
|
} else {
|
|
3900
3958
|
setProgress(progressMarkup(0, 0, 'Filed. Watching for the standup to start…'));
|
|
@@ -4234,16 +4292,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4234
4292
|
return chipMarkup('', inst.status);
|
|
4235
4293
|
}
|
|
4236
4294
|
|
|
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
4295
|
/* client mirror of provisioning.DOMAIN_ATTACH_FROM */
|
|
4248
4296
|
/* the platform-assigned <slug>.<apex> for this instance, from the server-stamped
|
|
4249
4297
|
apex only — no host guessing here: the upgrade door below must never show for
|
|
@@ -4471,7 +4519,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4471
4519
|
: '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
4520
|
}
|
|
4473
4521
|
|
|
4474
|
-
|
|
4522
|
+
/* onManage: the same facts, but the pointers name the controls THIS surface
|
|
4523
|
+
has. Since task 1003355 the list row acts on nothing, so a row's sentence
|
|
4524
|
+
sends the owner to the project's own page and the manage page's says
|
|
4525
|
+
"below" — the ADR 0199 §8 rule that a refusal must name a reachable door. */
|
|
4526
|
+
function addressRow(inst, onManage) {
|
|
4475
4527
|
/* state FIRST, then domain: a torn-down or snagged project still holds its
|
|
4476
4528
|
domain (ADR 0181 keeps port + address), and the standup promise below
|
|
4477
4529
|
was reaching it — "answers once the instance is live" two lines above
|
|
@@ -4499,7 +4551,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4499
4551
|
available. Where there is no button the sentence stops at the full stop. */
|
|
4500
4552
|
if (inst.last_verified_up === false) {
|
|
4501
4553
|
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.' : '.');
|
|
4554
|
+
(canRestart(inst) ? (onManage ? ' — or restart it yourself below.' : ' — or restart it from its own page.') : '.');
|
|
4503
4555
|
}
|
|
4504
4556
|
if (inst.last_verified_up) return 'Its address is ' + link + ' — last answered ' + esc(relTime(verifiedAt(inst))) + '.';
|
|
4505
4557
|
return 'Its address is ' + link + ' — confirming it answers.';
|
|
@@ -4508,50 +4560,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4508
4560
|
return 'Address: <code>' + esc(inst.domain) + '</code> — it answers there once the instance is live.';
|
|
4509
4561
|
}
|
|
4510
4562
|
/* 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>';
|
|
4563
|
+
return '<span class="none">No address yet</span> — sign-in on this project can’t be set up until it has one. ' +
|
|
4564
|
+
(onManage ? 'Give it an address below.'
|
|
4565
|
+
: '<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
4566
|
}
|
|
4556
4567
|
|
|
4557
4568
|
function renderCard(inst) {
|
|
@@ -4561,7 +4572,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4561
4572
|
var iid = esc(String(inst.id));
|
|
4562
4573
|
var li = liFor(inst.id);
|
|
4563
4574
|
if (!li) return;
|
|
4564
|
-
if (cardAsking !== null && cardAsking === String(inst.id)) return; /* keep THIS card's open confirmation */
|
|
4565
4575
|
var gone = inst.status === 'torn_down';
|
|
4566
4576
|
li.className = 'proj' + (gone ? ' gone' : '');
|
|
4567
4577
|
var meta = [];
|
|
@@ -4596,73 +4606,38 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4596
4606
|
/* a live region: an action's result is announced where the pointer is,
|
|
4597
4607
|
not only painted (task 1003244) */
|
|
4598
4608
|
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 */
|
|
4609
|
+
/* TWO DOORS, never more (owner, 2026-08-29 — task 1003355): View opens the
|
|
4610
|
+
project itself, Manage opens its own page. Restart, try again, bring it
|
|
4611
|
+
back, the ender and the address all live there now, so a row SAYS what is
|
|
4612
|
+
true and opens a door — it no longer acts on the project. Both are real
|
|
4613
|
+
links, so middle/ctrl-click works; the delegated handler upgrades a plain
|
|
4614
|
+
click on Manage (task 1003041). */
|
|
4615
|
+
var acts = [];
|
|
4616
|
+
if (inst.status === 'active' && inst.domain) acts.push('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">View</a>');
|
|
4617
|
+
acts.push('<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>');
|
|
4618
|
+
body += '<div class="actions">' + acts.join('') + '</div>';
|
|
4619
|
+
/* sig() buckets recency per minute, so a live row re-renders on the
|
|
4620
|
+
heartbeat — a door that held focus must get it back after the swap */
|
|
4623
4621
|
var wasInside = li.contains(document.activeElement) && document.activeElement !== li;
|
|
4624
4622
|
var prefer = wasInside ? focusKeyOf(document.activeElement) : null;
|
|
4625
|
-
var focusBack = wasInside || li.hasAttribute('data-focus-back');
|
|
4626
|
-
li.removeAttribute('data-focus-back');
|
|
4627
4623
|
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);
|
|
4624
|
+
if (wasInside) focusCardControl(inst.id, prefer);
|
|
4634
4625
|
}
|
|
4635
4626
|
|
|
4636
4627
|
/* ── keeping focus on the card through a re-render (task 1003244) ── */
|
|
4637
4628
|
|
|
4638
|
-
/* which
|
|
4639
|
-
|
|
4629
|
+
/* which door had focus, as a selector the rebuilt row can answer. Manage is
|
|
4630
|
+
the one that carries a marker; View falls through to the .actions fallback,
|
|
4631
|
+
which is the same element whenever both doors render. */
|
|
4640
4632
|
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;
|
|
4633
|
+
return el && el.getAttribute && el.hasAttribute('data-manage') ? '[data-manage]' : null;
|
|
4647
4634
|
}
|
|
4648
4635
|
function focusCardControl(id, prefer) {
|
|
4649
4636
|
var li = liFor(id);
|
|
4650
4637
|
if (!li) return;
|
|
4651
|
-
var t = (prefer && li.querySelector(prefer)) ||
|
|
4652
|
-
li.querySelector('.actions a, .actions button') ||
|
|
4653
|
-
li.querySelector('details.attachFold summary');
|
|
4638
|
+
var t = (prefer && li.querySelector(prefer)) || li.querySelector('.actions a');
|
|
4654
4639
|
if (t) t.focus();
|
|
4655
4640
|
}
|
|
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
4641
|
|
|
4667
4642
|
function render(instances) {
|
|
4668
4643
|
$('loading').hidden = true;
|
|
@@ -4756,17 +4731,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4756
4731
|
}
|
|
4757
4732
|
timers[id] = setTimeout(tick, POLL_MS);
|
|
4758
4733
|
}
|
|
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
4734
|
|
|
4771
4735
|
/* ── settled-card refresh: the heartbeat overlay must reach the page.
|
|
4772
4736
|
A card at 'active' has no fast poll; once a minute (and on tab return)
|
|
@@ -4867,7 +4831,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4867
4831
|
if (server) rows.push(['server', server]);
|
|
4868
4832
|
$('mMeta').innerHTML = rows.map(ledgerRow).join('');
|
|
4869
4833
|
$('mMeta').hidden = !rows.length;
|
|
4870
|
-
var addr = addressRow(inst);
|
|
4834
|
+
var addr = addressRow(inst, true);
|
|
4871
4835
|
$('mAddr').innerHTML = addr ? ledgerRow(['address', addr]) : '';
|
|
4872
4836
|
$('mAddr').hidden = !addr;
|
|
4873
4837
|
/* a fact with no answer hides — never a dash (the card rule) */
|
|
@@ -4877,6 +4841,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4877
4841
|
it: the delete section reads only status + open_intent, both already in
|
|
4878
4842
|
hand, and a queued teardown must show up without a reload */
|
|
4879
4843
|
renderDelete(inst);
|
|
4844
|
+
/* its address (task 1003355) is fetch-free too — the control it carries
|
|
4845
|
+
reads status + domain + open_intent, all already in hand */
|
|
4846
|
+
renderAddress(inst);
|
|
4880
4847
|
/* same contract: the visibility section's in-flight line reads open_intent
|
|
4881
4848
|
only, so a queued settings push shows up on the tick without a reload */
|
|
4882
4849
|
renderVisState(inst);
|
|
@@ -4962,6 +4929,121 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4962
4929
|
if (t.hasAttribute('data-m-restart')) return mRestart();
|
|
4963
4930
|
});
|
|
4964
4931
|
|
|
4932
|
+
/* ── its address (task 1003355): the bring-your-own-domain control, moved off
|
|
4933
|
+
the list row when a row shrank to view and manage. Every server truth the
|
|
4934
|
+
card's fold carried comes with it: canAttachDomain mirrors
|
|
4935
|
+
DOMAIN_ATTACH_FROM, a snagged or torn-down project has nothing to point an
|
|
4936
|
+
address at, no second control renders while one is wiring, and the re-kick
|
|
4937
|
+
door (F19) and the correction door (task 1003237) keep their pre-filled
|
|
4938
|
+
shapes. Fetch-free, like every other section here, so the minute tick may
|
|
4939
|
+
repaint it. ── */
|
|
4940
|
+
|
|
4941
|
+
var mDomFor = null; /* the project the section is drawn for */
|
|
4942
|
+
var mDomNote = null; /* its last receipt — a repaint must not swallow it */
|
|
4943
|
+
|
|
4944
|
+
function domNote(html, cls) {
|
|
4945
|
+
mDomNote = html ? { html: html, cls: cls || '' } : null;
|
|
4946
|
+
var box = $('mDomMsg');
|
|
4947
|
+
if (!box) return;
|
|
4948
|
+
box.className = 'projMsg' + (cls ? ' ' + cls : '');
|
|
4949
|
+
box.innerHTML = html || '';
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
function renderAddress(inst) {
|
|
4953
|
+
if (mDomFor !== inst.id) { mDomFor = inst.id; mDomNote = null; }
|
|
4954
|
+
/* the server's mirror first, then the card's own call: a project that never
|
|
4955
|
+
stood up and one that is gone have nothing to point an address at */
|
|
4956
|
+
var offer = canAttachDomain(inst) && (inst.status === 'active' || inst.status === 'requested');
|
|
4957
|
+
$('mAddress').hidden = !offer;
|
|
4958
|
+
if (!offer) return;
|
|
4959
|
+
/* the strip already says "Wiring the address" — a second attach would only
|
|
4960
|
+
be refused (intent_conflict), so the control waits for the run to land */
|
|
4961
|
+
if (inst.open_intent && inst.open_intent.action === 'domain-attach') {
|
|
4962
|
+
$('mDom').innerHTML = '<div class="addr">Wiring the address' +
|
|
4963
|
+
(inst.domain ? ' for <b>' + esc(inst.domain) + '</b>' : '') + ' right now.</div>' +
|
|
4964
|
+
'<p class="finePrint">This page updates when it lands.</p>';
|
|
4965
|
+
return;
|
|
4966
|
+
}
|
|
4967
|
+
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
4968
|
+
/* the platform-assigned <slug>.<apex> is NOT a correction: moving off it is
|
|
4969
|
+
the upgrade this control already offered, and the field stays empty for
|
|
4970
|
+
the domain being brought */
|
|
4971
|
+
var byo = inst.domain && inst.domain !== assignedDefaultFor(inst);
|
|
4972
|
+
var repoint = !rekick && byo;
|
|
4973
|
+
var prefill = rekick || repoint;
|
|
4974
|
+
var lead = rekick ? 'The wiring for this address stopped. Send it again and the platform picks it up.'
|
|
4975
|
+
: repoint ? 'It answers on an address you brought. Point it at a different one by editing the name.'
|
|
4976
|
+
: inst.domain ? 'It has the address the platform assigned it. Point a domain you own at it instead.'
|
|
4977
|
+
: 'It has no address yet. Point a domain you own at it.';
|
|
4978
|
+
/* the minute tick must not eat a half-typed domain or drop the caret;
|
|
4979
|
+
defaultValue tells the founder's typing from the pre-fill (the card rule) */
|
|
4980
|
+
var old = $('mDom').querySelector('input[type=text]');
|
|
4981
|
+
var typed = old && old.value !== old.defaultValue ? old.value : '';
|
|
4982
|
+
var hadFocus = !!old && document.activeElement === old;
|
|
4983
|
+
$('mDom').innerHTML = '<p class="plain">' + lead + '</p>' +
|
|
4984
|
+
'<div class="attach">' +
|
|
4985
|
+
'<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) + '"' +
|
|
4986
|
+
(prefill ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
4987
|
+
'<button type="button" class="btn small" data-m-attach>' +
|
|
4988
|
+
(rekick ? 'Try the address again' : repoint ? 'Change the address' : 'Use this domain') + '</button></div>' +
|
|
4989
|
+
/* true in every state this renders in: a REQUESTED project never answered
|
|
4990
|
+
anywhere yet, so "stops answering" would be a claim about a thing that
|
|
4991
|
+
never happened (the F16 rule) */
|
|
4992
|
+
(repoint ? '<p class="attachNote">Changing it lets <code>' + esc(inst.domain) + '</code> go — this project won’t use that address any more.</p>' : '') +
|
|
4993
|
+
'<div class="projMsg' + (mDomNote && mDomNote.cls ? ' ' + mDomNote.cls : '') + '" id="mDomMsg" role="status" aria-live="polite">' +
|
|
4994
|
+
(mDomNote ? mDomNote.html : '') + '</div>';
|
|
4995
|
+
var field = $('mDomName');
|
|
4996
|
+
if (field && typed) field.value = typed;
|
|
4997
|
+
if (field && hadFocus) field.focus();
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
/* every refusal as the server worded it (the task 1002692 rule); the codes the
|
|
5001
|
+
page can say more precisely get their own sentence */
|
|
5002
|
+
function attachFailure(data, status) {
|
|
5003
|
+
var e = apiError(data, status);
|
|
5004
|
+
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') + '.';
|
|
5005
|
+
if (e.code === 'domain_taken') return esc(e.message || 'That address is already in use by another project.');
|
|
5006
|
+
if (e.code === 'domain_not_attachable') return esc(e.message || 'An address can be attached once the instance settles.');
|
|
5007
|
+
if (e.code === 'intent_conflict') {
|
|
5008
|
+
/* the address may still have been RECORDED (details.recorded) — say so */
|
|
5009
|
+
var t = esc(e.message || 'Another run holds the queue right now — re-send once it settles.');
|
|
5010
|
+
return e.details.recorded ? t + ' (The address itself is saved.)' : t;
|
|
5011
|
+
}
|
|
5012
|
+
return esc(e.message || 'The address could not be recorded — try again in a moment.');
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
function mAttach() {
|
|
5016
|
+
var inst = manageInst;
|
|
5017
|
+
if (!inst) return;
|
|
5018
|
+
var field = $('mDomName');
|
|
5019
|
+
var domain = field ? String(field.value || '').trim() : '';
|
|
5020
|
+
if (!domain) { domNote('Type the domain you own first — e.g. <code>myproject.example.com</code>.', 'err'); return; }
|
|
5021
|
+
var btn = $('mDom').querySelector('[data-m-attach]');
|
|
5022
|
+
if (btn) btn.disabled = true;
|
|
5023
|
+
domNote('Recording the address…');
|
|
5024
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(inst.id) + '/domain', {
|
|
5025
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
5026
|
+
body: JSON.stringify({ domain: domain })
|
|
5027
|
+
}).then(function (res) {
|
|
5028
|
+
if (res.status === 401) { domNote(signInAgain(), 'err'); return null; }
|
|
5029
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
5030
|
+
if (res.ok) { domNote(esc(data.message || 'Address recorded.'), 'ok'); return; }
|
|
5031
|
+
domNote(attachFailure(data, res.status), 'err');
|
|
5032
|
+
});
|
|
5033
|
+
}).catch(function () {
|
|
5034
|
+
domNote('Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
5035
|
+
}).then(function () {
|
|
5036
|
+
var b = $('mDom').querySelector('[data-m-attach]');
|
|
5037
|
+
if (b) b.disabled = false;
|
|
5038
|
+
loadManage(true, true); /* the address this page renders just changed */
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
$('mDom').addEventListener('click', function (ev) {
|
|
5043
|
+
var t = ev.target.closest && ev.target.closest('button');
|
|
5044
|
+
if (t && t.hasAttribute('data-m-attach')) mAttach();
|
|
5045
|
+
});
|
|
5046
|
+
|
|
4965
5047
|
/* an IN-FLIGHT project is watched here at the card's cadence — its own copy
|
|
4966
5048
|
says "this page updates when it starts", so it must. A settled project
|
|
4967
5049
|
keeps the C8 rule: entry + tab return, plus the request-free minute tick. */
|
|
@@ -5631,9 +5713,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5631
5713
|
function renderDelete(inst) {
|
|
5632
5714
|
if (mDelAsking && mDelAsking === inst.slug) return; /* keep THIS project's open confirmation */
|
|
5633
5715
|
if (inst.status === 'torn_down') {
|
|
5716
|
+
/* the revive door is on THIS page since task 1003355 — a pointer at the
|
|
5717
|
+
list would now send the owner to a row that only opens doors */
|
|
5634
5718
|
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
|
|
5719
|
+
'<p class="finePrint">Its repository, its database and its web address are all still here.' +
|
|
5720
|
+
(canRevive(inst) ? ' Bring it back with the button at the top of this page.' : '') + '</p>');
|
|
5637
5721
|
return;
|
|
5638
5722
|
}
|
|
5639
5723
|
if (inst.status === 'tearing_down') {
|
|
@@ -5675,6 +5759,22 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5675
5759
|
return !!(field && manageInst && field.value.trim() === manageInst.slug);
|
|
5676
5760
|
}
|
|
5677
5761
|
|
|
5762
|
+
/* every refusal as the server worded it, with the two codes the page can say
|
|
5763
|
+
more precisely spelled out. This vocabulary rode the list row's sender until
|
|
5764
|
+
task 1003355 made the ender the only teardown door — losing it would have
|
|
5765
|
+
turned two named refusals back into the generic "could not be queued" the
|
|
5766
|
+
tasks 1002691/1002692 walk was about. */
|
|
5767
|
+
function teardownFailure(data, status) {
|
|
5768
|
+
var e = apiError(data, status);
|
|
5769
|
+
if (e.code === 'intent_conflict') {
|
|
5770
|
+
return esc(e.message || ('A ' + (e.details.action || 'previous') + ' run is still ' + (e.details.state || 'open') + ' — retry in a moment.'));
|
|
5771
|
+
}
|
|
5772
|
+
if (e.code === 'not_teardownable') {
|
|
5773
|
+
return esc(e.message || ('Nothing to tear down right now — the instance is ' + (e.details.status || 'settled') + '.'));
|
|
5774
|
+
}
|
|
5775
|
+
return esc(e.message || 'The teardown could not be queued — try again in a moment.');
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5678
5778
|
function doDelete() {
|
|
5679
5779
|
var inst = manageInst;
|
|
5680
5780
|
/* the typed name is this page's own gate, never the server's — so a click
|
|
@@ -5696,10 +5796,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5696
5796
|
/* the still-true state first, then exactly what the server said */
|
|
5697
5797
|
renderDelete(inst);
|
|
5698
5798
|
$('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>');
|
|
5799
|
+
'<div class="projMsg err">' +
|
|
5800
|
+
(res.status === 401 ? signInAgain() : teardownFailure(data, res.status)) + '</div>');
|
|
5703
5801
|
});
|
|
5704
5802
|
}).catch(function () {
|
|
5705
5803
|
renderDelete(inst);
|
|
@@ -5722,109 +5820,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5722
5820
|
if (t.hasAttribute('data-del-no')) { mDelAsking = null; if (manageInst) renderDelete(manageInst); }
|
|
5723
5821
|
});
|
|
5724
5822
|
|
|
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
5823
|
/* ── restart an unreachable project (task 1003238). Its own route, because
|
|
5824
5824
|
PROVISION_ENQUEUE_FROM excludes 'active' — re-POSTing the slug the way
|
|
5825
5825
|
Try again / Bring it back do would be refused on every press. Every
|
|
5826
5826
|
refusal is shown as the server worded it: the server owns the gate, and
|
|
5827
|
-
a
|
|
5827
|
+
a page that invents its own reason drifts from it. mRestart() is the one
|
|
5828
|
+
sender since task 1003355 moved the door off the list row. ── */
|
|
5828
5829
|
|
|
5829
5830
|
function restartFailure(data, status) {
|
|
5830
5831
|
var e = apiError(data, status);
|
|
@@ -5832,84 +5833,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5832
5833
|
return e.message || 'The restart could not be queued — try again in a moment.';
|
|
5833
5834
|
}
|
|
5834
5835
|
|
|
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 */
|
|
5836
|
+
/* the list's one delegated handler. A row holds no sender since task 1003355
|
|
5837
|
+
— only its two doors — so the Manage link is all there is to upgrade: it is
|
|
5838
|
+
a real anchor, a modified click keeps its browser meaning and a plain one
|
|
5839
|
+
stays in-page (the orb-click contract). The address line's "give it an
|
|
5840
|
+
address" link carries the same marker, so it lands on the same page. */
|
|
5895
5841
|
$('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
5842
|
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'));
|
|
5843
|
+
if (!m || !isPlainClick(ev)) return;
|
|
5844
|
+
ev.preventDefault();
|
|
5845
|
+
goToManage(m.getAttribute('data-manage'));
|
|
5913
5846
|
});
|
|
5914
5847
|
|
|
5915
5848
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|