@bongos/core 1.19.331 → 1.19.332
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 +11 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +4 -0
- package/public-landing/projects.html +103 -23
- package/src/module-api.js +3 -1
- package/tests/projects_hub_pre_uat.mjs +53 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.332",
|
|
6
|
+
"core_contract": "1.19.332",
|
|
7
|
+
"source_commit": "e2808f7a5ce4556d6265f326004a0cc87f463841",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-25T00:
|
|
9
|
+
"built_at": "2026-08-25T00:18:07.252Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 324,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1811,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "e1c90c92977490868bc70a33b665c39305e4a5ec7c2b8af2f042df1ad8151de5",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4667,12 +4667,12 @@
|
|
|
4667
4667
|
{
|
|
4668
4668
|
"path": "package-lock.json",
|
|
4669
4669
|
"mode": "0000644",
|
|
4670
|
-
"sha256": "
|
|
4670
|
+
"sha256": "bc62624584d42e3c04dda858b69f3fdbb1c09db67d54c46b1e7601f004c2eca9"
|
|
4671
4671
|
},
|
|
4672
4672
|
{
|
|
4673
4673
|
"path": "package.json",
|
|
4674
4674
|
"mode": "0000644",
|
|
4675
|
-
"sha256": "
|
|
4675
|
+
"sha256": "be5885b562847832c030b3dcbed240eb204a1c648eb6835d0b3bddb107aec2f6"
|
|
4676
4676
|
},
|
|
4677
4677
|
{
|
|
4678
4678
|
"path": "public-docs/index.html",
|
|
@@ -4912,7 +4912,7 @@
|
|
|
4912
4912
|
{
|
|
4913
4913
|
"path": "public-landing/assets/cosmos.css",
|
|
4914
4914
|
"mode": "0000644",
|
|
4915
|
-
"sha256": "
|
|
4915
|
+
"sha256": "2a258be2790a6fadd1c2d9e190675c62e2b68dddece65f497b4e983c6d9b9146"
|
|
4916
4916
|
},
|
|
4917
4917
|
{
|
|
4918
4918
|
"path": "public-landing/assets/verro-medallion.png",
|
|
@@ -4932,7 +4932,7 @@
|
|
|
4932
4932
|
{
|
|
4933
4933
|
"path": "public-landing/projects.html",
|
|
4934
4934
|
"mode": "0000644",
|
|
4935
|
-
"sha256": "
|
|
4935
|
+
"sha256": "44474703a87cecfa8f80ecbca6ecfb03d4fc3ccf166fe26bc4c310f404f5f76a"
|
|
4936
4936
|
},
|
|
4937
4937
|
{
|
|
4938
4938
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -6452,7 +6452,7 @@
|
|
|
6452
6452
|
{
|
|
6453
6453
|
"path": "src/module-api.js",
|
|
6454
6454
|
"mode": "0000644",
|
|
6455
|
-
"sha256": "
|
|
6455
|
+
"sha256": "34ffbdddcf9e14262f980f9badc4f19eee6235321572aeb71809810eb09b32b5"
|
|
6456
6456
|
},
|
|
6457
6457
|
{
|
|
6458
6458
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8332,7 +8332,7 @@
|
|
|
8332
8332
|
{
|
|
8333
8333
|
"path": "tests/projects_hub_pre_uat.mjs",
|
|
8334
8334
|
"mode": "0000644",
|
|
8335
|
-
"sha256": "
|
|
8335
|
+
"sha256": "c30221f0cb0b23b7f18b2edffd26cb895038543b07ee87c7a401508131edfc1a"
|
|
8336
8336
|
},
|
|
8337
8337
|
{
|
|
8338
8338
|
"path": "tests/promote_goal_id.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.332",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.332",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.332",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -414,6 +414,10 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
414
414
|
/* ── page-level bits ── */
|
|
415
415
|
.alert{display:none;background:var(--err-soft);border:2px solid color-mix(in srgb,var(--err) 25%,transparent);color:var(--err);font-weight:600;font-size:14px;border-radius:14px;padding:12px 16px;margin-bottom:18px;}
|
|
416
416
|
.alert.on{display:block;}
|
|
417
|
+
/* the raw server text folds under the alert's plain sentence (task 1003244) */
|
|
418
|
+
.alert details{margin-top:8px;}
|
|
419
|
+
.alert summary{cursor:pointer;font-size:12.5px;font-weight:700;}
|
|
420
|
+
.alert details p{margin-top:6px;font-family:var(--font-mono);font-size:12px;font-weight:500;overflow-wrap:anywhere;}
|
|
417
421
|
.loading{font-size:14.5px;font-weight:600;color:var(--ink-faint);padding:14px 0;}
|
|
418
422
|
.empty{border:2px dashed var(--line);border-radius:18px;padding:22px;text-align:center;}
|
|
419
423
|
.empty p{font-size:14.5px;font-weight:600;color:var(--ink-soft);margin-bottom:14px;line-height:1.55;}
|
|
@@ -614,7 +614,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
614
614
|
stripFor/errMarkup/canRetry/canRevive the list card reads -->
|
|
615
615
|
<div id="mStrip"></div>
|
|
616
616
|
<div class="projMsg err" id="mErr" hidden></div>
|
|
617
|
-
<div class="projMsg" id="mMsg"></div>
|
|
617
|
+
<div class="projMsg" id="mMsg" role="status" aria-live="polite"></div>
|
|
618
618
|
<div class="actions" id="mActs" hidden></div>
|
|
619
619
|
</div>
|
|
620
620
|
|
|
@@ -683,7 +683,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
683
683
|
<div class="wrap viewWrap">
|
|
684
684
|
<div class="card">
|
|
685
685
|
<ol class="steps" aria-label="Creation steps">
|
|
686
|
-
<li class="now" data-step="1"><span class="snum">1</span><span class="slbl">Name</span></li>
|
|
686
|
+
<li class="now" data-step="1" aria-current="step"><span class="snum">1</span><span class="slbl">Name</span></li>
|
|
687
687
|
<li data-step="2"><span class="snum">2</span><span class="slbl">Type</span></li>
|
|
688
688
|
<li data-step="3"><span class="snum">3</span><span class="slbl">Home</span></li>
|
|
689
689
|
<li data-step="4"><span class="snum">4</span><span class="slbl">Address</span></li>
|
|
@@ -2400,16 +2400,27 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2400
2400
|
Six steps since task 1002772: gate 0 · name 1 · type 2 · home 3 ·
|
|
2401
2401
|
address 4 · review 5 · done 6 (the rail shows 1-5; done marks it all). */
|
|
2402
2402
|
var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-home', 4: 'panel-address', 5: 'panel-review', 6: 'panel-done' };
|
|
2403
|
+
/* the rail at the done panel (task 1003244): "done" is the Create step
|
|
2404
|
+
still running — the marker stays on 5 until the project actually
|
|
2405
|
+
answers, and only then does the whole rail read finished. aria-current
|
|
2406
|
+
tells a screen reader the same thing. */
|
|
2407
|
+
function paintRail(step) {
|
|
2408
|
+
var landed = step === 6 && !!(live && live.instStatus === 'active');
|
|
2409
|
+
var cur = step === 6 ? 5 : step;
|
|
2410
|
+
root.querySelectorAll('.steps li').forEach(function (li) {
|
|
2411
|
+
var n = Number(li.getAttribute('data-step'));
|
|
2412
|
+
var now = !landed && n === cur;
|
|
2413
|
+
li.classList.toggle('now', now);
|
|
2414
|
+
li.classList.toggle('done', landed || n < cur);
|
|
2415
|
+
if (now) li.setAttribute('aria-current', 'step'); else li.removeAttribute('aria-current');
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2403
2418
|
function show(step, moveFocus) {
|
|
2404
2419
|
state.step = step;
|
|
2405
2420
|
Object.keys(panels).forEach(function (k) {
|
|
2406
2421
|
$(panels[k]).classList.toggle('on', Number(k) === step);
|
|
2407
2422
|
});
|
|
2408
|
-
|
|
2409
|
-
var n = Number(li.getAttribute('data-step'));
|
|
2410
|
-
li.classList.toggle('now', n === step);
|
|
2411
|
-
li.classList.toggle('done', n < step || step === 6);
|
|
2412
|
-
});
|
|
2423
|
+
paintRail(step);
|
|
2413
2424
|
saveDraft();
|
|
2414
2425
|
/* the done step takes the whole card — the bongo pair steps aside */
|
|
2415
2426
|
root.querySelector('.createSplit').classList.toggle('wizDone', step === 6);
|
|
@@ -2782,14 +2793,24 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2782
2793
|
}
|
|
2783
2794
|
|
|
2784
2795
|
/* ── create ── */
|
|
2796
|
+
/* the mapped codes are headline-only; anything else gets a fixed owner
|
|
2797
|
+
sentence with the raw text folded under Technical details (direction
|
|
2798
|
+
§5.1, task 1003244) — operator text is never the headline */
|
|
2785
2799
|
function friendlyCreateError(err) {
|
|
2786
2800
|
var code = err && err.data && err.data.error && (err.data.error.code || '');
|
|
2787
2801
|
if (code === 'slug_taken') return 'That handle is already taken by another project. Pick a different one.';
|
|
2788
2802
|
if (code === 'bad_slug') return 'The handle didn’t pass the server’s checks — 2–40 lowercase letters, numbers, or dashes.';
|
|
2789
|
-
|
|
2803
|
+
/* both server branches: a reserved hostname, and any name under the platform's own apex */
|
|
2804
|
+
if (code === 'domain_reserved') return 'That address belongs to the platform’s own domain — your project already gets its own address there from its handle. Use a domain you own instead.';
|
|
2790
2805
|
if (code === 'domain_taken') return 'That address is already held by another project. Pick a different handle or domain.';
|
|
2791
2806
|
if (code === 'bad_domain') return 'The domain didn’t parse as a real hostname — use something like myproject.example.com.';
|
|
2792
|
-
return '
|
|
2807
|
+
if (code === 'repo_scope_required') return 'GitHub hasn’t granted repository access for this session. Sign in again — it asks for repository access — and try once more.';
|
|
2808
|
+
if (code === 'request_failed') return 'The platform couldn’t file the request. Nothing was created — try again in a moment.';
|
|
2809
|
+
return '';
|
|
2810
|
+
}
|
|
2811
|
+
function createFailureMarkup(err) {
|
|
2812
|
+
return 'We couldn’t file the request just now. Nothing was created — you can try again.' +
|
|
2813
|
+
'<details><summary>Technical details</summary><p>' + esc(err && err.message ? err.message : 'no detail came back') + '</p></details>';
|
|
2793
2814
|
}
|
|
2794
2815
|
|
|
2795
2816
|
/* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
|
|
@@ -2901,7 +2922,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2901
2922
|
$('h-home').className = 'hint err';
|
|
2902
2923
|
return;
|
|
2903
2924
|
}
|
|
2904
|
-
|
|
2925
|
+
var friendly = friendlyCreateError(err);
|
|
2926
|
+
if (friendly) alertBox.textContent = friendly;
|
|
2927
|
+
else alertBox.innerHTML = createFailureMarkup(err);
|
|
2905
2928
|
alertBox.classList.add('on');
|
|
2906
2929
|
});
|
|
2907
2930
|
});
|
|
@@ -2918,7 +2941,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2918
2941
|
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>';
|
|
2919
2942
|
|
|
2920
2943
|
var instanceId = null; /* set when a create lands / a resumed watch starts */
|
|
2921
|
-
var live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
|
|
2944
|
+
var live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
2922
2945
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
2923
2946
|
var lastStepsHtml = '';
|
|
2924
2947
|
var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall'];
|
|
@@ -3000,8 +3023,15 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3000
3023
|
return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.</p></li>';
|
|
3001
3024
|
}
|
|
3002
3025
|
if (live.manifest === 'error' || live.manifest === 'expired') {
|
|
3003
|
-
|
|
3004
|
-
|
|
3026
|
+
/* a plain sentence first; the raw route text folds under Technical
|
|
3027
|
+
details (task 1003244). The one refusal a founder can act on — no
|
|
3028
|
+
address yet — says where to go instead of what the server said. */
|
|
3029
|
+
var snag, raw = '';
|
|
3030
|
+
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
|
|
3031
|
+
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.';
|
|
3032
|
+
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
3033
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
|
|
3034
|
+
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
3005
3035
|
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
3006
3036
|
}
|
|
3007
3037
|
if (live.manifest === 'code_received') {
|
|
@@ -3186,6 +3216,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3186
3216
|
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
3187
3217
|
if (!res.ok) {
|
|
3188
3218
|
var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
|
|
3219
|
+
e.code = (d.error && d.error.code) || '';
|
|
3189
3220
|
throw e;
|
|
3190
3221
|
}
|
|
3191
3222
|
return d;
|
|
@@ -3206,6 +3237,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3206
3237
|
if (String(err && err.message) === 'signing in') return;
|
|
3207
3238
|
if (tab) { try { tab.close(); } catch (e) {} }
|
|
3208
3239
|
live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
3240
|
+
live.manifestCode = String((err && err.code) || '');
|
|
3209
3241
|
renderSteps();
|
|
3210
3242
|
});
|
|
3211
3243
|
}
|
|
@@ -3380,7 +3412,10 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3380
3412
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
3381
3413
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
3382
3414
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
3383
|
-
if (inst && inst.status !== live.instStatus) {
|
|
3415
|
+
if (inst && inst.status !== live.instStatus) {
|
|
3416
|
+
live.instStatus = inst.status; renderSteps();
|
|
3417
|
+
if (state.step === 6) paintRail(6); /* the rail finishes only when the project answers */
|
|
3418
|
+
}
|
|
3384
3419
|
var terminal = renderProgress(inst);
|
|
3385
3420
|
if (!terminal) pollTimer = setTimeout(tick, 5000);
|
|
3386
3421
|
else stopPolling();
|
|
@@ -3412,7 +3447,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3412
3447
|
instanceId = null;
|
|
3413
3448
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
3414
3449
|
manifestStartedAt = 0; manifestStale = false;
|
|
3415
|
-
live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
|
|
3450
|
+
live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
3416
3451
|
lastStepsHtml = '';
|
|
3417
3452
|
try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
3418
3453
|
state = { step: 1, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
|
|
@@ -3882,7 +3917,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3882
3917
|
: '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>');
|
|
3883
3918
|
}
|
|
3884
3919
|
var lm = lastMsg[inst.id];
|
|
3885
|
-
|
|
3920
|
+
/* a live region: an action's result is announced where the pointer is,
|
|
3921
|
+
not only painted (task 1003244) */
|
|
3922
|
+
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '" role="status" aria-live="polite">' + (lm ? lm.text : '') + '</div>';
|
|
3886
3923
|
/* every owned card opens its manage view (task 1003041) — a real link, so
|
|
3887
3924
|
middle/ctrl-click works; the delegated handler upgrades a plain click */
|
|
3888
3925
|
var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
|
|
@@ -3903,12 +3940,51 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3903
3940
|
var keepOpen = !!(oldFold && oldFold.open);
|
|
3904
3941
|
var typed = oldInput && oldInput.value !== oldInput.defaultValue ? oldInput.value : '';
|
|
3905
3942
|
var hadFocus = !!oldInput && document.activeElement === oldInput;
|
|
3943
|
+
/* focus anywhere else on the card (a door, a confirm, the fold's summary)
|
|
3944
|
+
comes back to the same control — or the nearest one — after the swap;
|
|
3945
|
+
quietActions() leaves a note when a disabled button already let go */
|
|
3946
|
+
var wasInside = li.contains(document.activeElement) && document.activeElement !== li;
|
|
3947
|
+
var prefer = wasInside ? focusKeyOf(document.activeElement) : null;
|
|
3948
|
+
var focusBack = wasInside || li.hasAttribute('data-focus-back');
|
|
3949
|
+
li.removeAttribute('data-focus-back');
|
|
3906
3950
|
li.innerHTML = orbFor(inst) + '<div class="projBody">' + body + '</div>';
|
|
3907
3951
|
var fold = li.querySelector('details.attachFold');
|
|
3908
3952
|
var input = fold ? fold.querySelector('input') : null;
|
|
3909
3953
|
if (fold && keepOpen) fold.open = true;
|
|
3910
3954
|
if (input && typed) input.value = typed;
|
|
3911
3955
|
if (input && hadFocus) input.focus();
|
|
3956
|
+
else if (focusBack) focusCardControl(inst.id, prefer);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
/* ── keeping focus on the card through a re-render (task 1003244) ── */
|
|
3960
|
+
|
|
3961
|
+
/* which control had focus, as a selector the rebuilt card can answer;
|
|
3962
|
+
a confirm's Yes/Keep lands back on the door that opened it */
|
|
3963
|
+
function focusKeyOf(el) {
|
|
3964
|
+
if (!el || !el.getAttribute) return null;
|
|
3965
|
+
if (el.tagName === 'SUMMARY') return 'details.attachFold summary';
|
|
3966
|
+
if (el.hasAttribute('data-teardown-yes') || el.hasAttribute('data-teardown-no')) return '[data-teardown]';
|
|
3967
|
+
var keys = ['data-manage', 'data-attach', 'data-retry', 'data-revive', 'data-teardown'];
|
|
3968
|
+
for (var i = 0; i < keys.length; i++) if (el.hasAttribute(keys[i])) return '[' + keys[i] + ']';
|
|
3969
|
+
return null;
|
|
3970
|
+
}
|
|
3971
|
+
function focusCardControl(id, prefer) {
|
|
3972
|
+
var li = liFor(id);
|
|
3973
|
+
if (!li) return;
|
|
3974
|
+
var t = (prefer && li.querySelector(prefer)) ||
|
|
3975
|
+
li.querySelector('.actions a, .actions button') ||
|
|
3976
|
+
li.querySelector('details.attachFold summary');
|
|
3977
|
+
if (t) t.focus();
|
|
3978
|
+
}
|
|
3979
|
+
/* an action in flight: the row stays put and its buttons go quiet, so
|
|
3980
|
+
nothing under the pointer is deleted mid-click; a button that held
|
|
3981
|
+
focus lets go when disabled, so the card remembers to take it back */
|
|
3982
|
+
function quietActions(id) {
|
|
3983
|
+
var act = $('act-' + id);
|
|
3984
|
+
if (!act) return;
|
|
3985
|
+
var li = liFor(id);
|
|
3986
|
+
if (li && act.contains(document.activeElement)) li.setAttribute('data-focus-back', '');
|
|
3987
|
+
act.querySelectorAll('button').forEach(function (b) { b.disabled = true; });
|
|
3912
3988
|
}
|
|
3913
3989
|
|
|
3914
3990
|
function render(instances) {
|
|
@@ -4465,16 +4541,20 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
4465
4541
|
var act = $('act-' + id);
|
|
4466
4542
|
if (!act) return;
|
|
4467
4543
|
cardAsking = String(id);
|
|
4468
|
-
act.innerHTML = '<div class="confirmRow">' +
|
|
4544
|
+
act.innerHTML = '<div class="confirmRow" tabindex="-1">' +
|
|
4469
4545
|
'<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
|
|
4470
4546
|
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
4471
4547
|
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
|
|
4548
|
+
/* the pressed button is gone — focus lands on the question itself, so
|
|
4549
|
+
the truth is read before Tab reaches Yes (task 1003244) */
|
|
4550
|
+
var row = act.firstChild;
|
|
4551
|
+
if (row && row.focus) row.focus();
|
|
4472
4552
|
}
|
|
4473
4553
|
function unAskTeardown(id) { cardAsking = null; var inst = byId[id]; if (inst) renderCard(inst); }
|
|
4474
4554
|
function doTeardown(id) {
|
|
4475
4555
|
cardAsking = null;
|
|
4476
|
-
|
|
4477
|
-
|
|
4556
|
+
quietActions(id);
|
|
4557
|
+
msg(id, 'Asking…');
|
|
4478
4558
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/teardown', {
|
|
4479
4559
|
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
4480
4560
|
}).then(function (res) {
|
|
@@ -4501,8 +4581,8 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
4501
4581
|
}).catch(function () {
|
|
4502
4582
|
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
4503
4583
|
}).then(function () {
|
|
4504
|
-
/*
|
|
4505
|
-
|
|
4584
|
+
/* wake the quieted row even when the state didn't change (renderCard
|
|
4585
|
+
also hands focus back), then pick up whatever DID change */
|
|
4506
4586
|
if (byId[id]) renderCard(byId[id]);
|
|
4507
4587
|
watchOnce(id);
|
|
4508
4588
|
});
|
|
@@ -4515,8 +4595,8 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
4515
4595
|
function reProvision(id, verb) {
|
|
4516
4596
|
var inst = byId[id];
|
|
4517
4597
|
if (!inst) return;
|
|
4518
|
-
|
|
4519
|
-
|
|
4598
|
+
quietActions(id);
|
|
4599
|
+
msg(id, 'Asking…');
|
|
4520
4600
|
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'standalone' };
|
|
4521
4601
|
if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
|
|
4522
4602
|
if (inst.target_ref) body.target_ref = inst.target_ref;
|
package/src/module-api.js
CHANGED
|
@@ -1089,8 +1089,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1089
1089
|
// landed since 1.19.329 with no explicit bump. run 32789596635. (task 1002620)
|
|
1090
1090
|
// 1.19.331 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1091
1091
|
// landed since 1.19.330 with no explicit bump. run 32791877167. (task 1002620)
|
|
1092
|
+
// 1.19.332 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1093
|
+
// landed since 1.19.331 with no explicit bump. run 32793033453. (task 1002620)
|
|
1092
1094
|
// ---------------------------------------------------------------------------
|
|
1093
|
-
const CORE_VERSION = '1.19.
|
|
1095
|
+
const CORE_VERSION = '1.19.332'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1094
1096
|
|
|
1095
1097
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1096
1098
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -309,3 +309,56 @@ test('decision 10 — both footers carry the open-source (AGPL-3.0) link, in lin
|
|
|
309
309
|
assert.ok(indexFoot.indexOf('<!--apex-only:end-->') < indexFoot.search(AGPL), 'index.html: after the apex-only block');
|
|
310
310
|
assert.match(COSMOS, /\.foot a\{color:var\(--link-ink\);font-weight:700;text-decoration:none;\}/, 'never the browser’s blue');
|
|
311
311
|
});
|
|
312
|
+
|
|
313
|
+
/* ── the should-fix remainder (task 1003244) ── */
|
|
314
|
+
test('an unmapped create failure gets a plain sentence with the raw text folded under Technical details', () => {
|
|
315
|
+
const friendly = fn('friendlyCreateError');
|
|
316
|
+
assert.match(friendly, /return '';\n\s*\}/, 'unmapped codes return nothing — the caller folds them');
|
|
317
|
+
assert.doesNotMatch(friendly, /Couldn’t file the request: ' \+/, 'the raw message is no longer the headline');
|
|
318
|
+
for (const code of ['slug_taken', 'bad_slug', 'domain_reserved', 'domain_taken', 'bad_domain', 'repo_scope_required', 'request_failed']) {
|
|
319
|
+
assert.match(friendly, new RegExp(`code === '${code}'`), `${code} stays headline-only`);
|
|
320
|
+
}
|
|
321
|
+
assert.match(friendly, /belongs to the platform’s own domain/, 'domain_reserved covers both server branches');
|
|
322
|
+
assert.match(fn('createFailureMarkup'), /Nothing was created — you can try again\.' \+\n\s*'<details><summary>Technical details<\/summary><p>' \+ esc\(err && err\.message/);
|
|
323
|
+
assert.match(SCRIPT, /if \(friendly\) alertBox\.textContent = friendly;\n\s*else alertBox\.innerHTML = createFailureMarkup\(err\);/);
|
|
324
|
+
assert.match(COSMOS, /\.alert details p\{[^}]*font-family:var\(--font-mono\)/, 'the raw text reads as code, inside the alert');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('a manifest error says a plain sentence, folds the route text, and sends a domainless project to its address', () => {
|
|
328
|
+
const steps = fn('renderStepOauthApp');
|
|
329
|
+
assert.doesNotMatch(steps, /'GitHub setup hit a snag' \+ \(live\.manifestMsg \? ': ' \+ esc\(live\.manifestMsg\)/, 'the raw route text is no longer the headline');
|
|
330
|
+
assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from <a href="\?view=mine">your projects page<\/a>/);
|
|
331
|
+
assert.match(steps, /\(raw \? '<details><summary>Technical details<\/summary><p>' \+ esc\(raw\) \+ '<\/p><\/details>' : ''\)/);
|
|
332
|
+
assert.match(SCRIPT, /e\.code = \(d\.error && d\.error\.code\) \|\| '';/, 'the route code travels with the error');
|
|
333
|
+
assert.match(SCRIPT, /live\.manifestCode = String\(\(err && err\.code\) \|\| ''\);/);
|
|
334
|
+
assert.equal((SCRIPT.match(/manifestMsg: '', manifestCode: '', appSlug: null/g) || []).length, 2, 'both resets carry the code');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('the rail at the done panel keeps its marker on Create until the project answers', () => {
|
|
338
|
+
const rail = fn('paintRail');
|
|
339
|
+
assert.match(rail, /var landed = step === 6 && !!\(live && live\.instStatus === 'active'\);/);
|
|
340
|
+
assert.match(rail, /var cur = step === 6 \? 5 : step;/);
|
|
341
|
+
assert.match(rail, /li\.classList\.toggle\('done', landed \|\| n < cur\);/);
|
|
342
|
+
assert.match(rail, /if \(now\) li\.setAttribute\('aria-current', 'step'\); else li\.removeAttribute\('aria-current'\);/);
|
|
343
|
+
assert.match(fn('show'), /paintRail\(step\);/, 'show() paints through it');
|
|
344
|
+
assert.doesNotMatch(fn('show'), /classList\.toggle\('done'/, 'and no longer marks the rail on its own');
|
|
345
|
+
assert.match(SCRIPT, /live\.instStatus = inst\.status; renderSteps\(\);\n\s*if \(state\.step === 6\) paintRail\(6\);/, 'the done-panel poll repaints when the status moves');
|
|
346
|
+
assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('card action results announce, nothing under the pointer is deleted, and focus stays on the card', () => {
|
|
350
|
+
assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the card slot is a live region');
|
|
351
|
+
assert.match(HUB, /<div class="projMsg" id="mMsg" role="status" aria-live="polite"><\/div>/, 'so is the manage page’s');
|
|
352
|
+
assert.doesNotMatch(SCRIPT, /act\.innerHTML = '<span class="projMsg">Asking…<\/span>'/, 'no action replaces the row it was pressed in');
|
|
353
|
+
assert.match(fn('doTeardown'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
|
|
354
|
+
assert.match(fn('reProvision'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
|
|
355
|
+
assert.match(fn('quietActions'), /act\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/);
|
|
356
|
+
assert.match(fn('quietActions'), /li\.setAttribute\('data-focus-back', ''\)/, 'a disabled button that held focus leaves a note');
|
|
357
|
+
assert.match(fn('askTeardown'), /<div class="confirmRow" tabindex="-1">/);
|
|
358
|
+
assert.match(fn('askTeardown'), /if \(row && row\.focus\) row\.focus\(\);/, 'the question takes focus when the button goes');
|
|
359
|
+
const card = fn('renderCard');
|
|
360
|
+
assert.match(card, /var wasInside = li\.contains\(document\.activeElement\) && document\.activeElement !== li;/);
|
|
361
|
+
assert.match(card, /else if \(focusBack\) focusCardControl\(inst\.id, prefer\);/);
|
|
362
|
+
assert.match(fn('focusKeyOf'), /data-teardown-yes'\) \|\| el\.hasAttribute\('data-teardown-no'\)\) return '\[data-teardown\]'/, 'Yes / Keep land back on the door that opened them');
|
|
363
|
+
assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a, \.actions button'\) \|\|\n\s*li\.querySelector\('details\.attachFold summary'\)/, 'nearest control as the fallback');
|
|
364
|
+
});
|