@bongos/core 1.19.538 → 1.19.539

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.
@@ -1525,5 +1525,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1525
1525
  landed since 1.19.536 with no explicit bump. run 33888812605. (task 1002620)
1526
1526
  1.19.538 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1527
1527
  landed since 1.19.537 with no explicit bump. run 33890191596. (task 1002620)
1528
+ 1.19.539 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1529
+ landed since 1.19.538 with no explicit bump. run 33892948885. (task 1002620)
1528
1530
  ---------------------------------------------------------------------------
1529
1531
  ```
@@ -411,6 +411,16 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
411
411
  task 1003355 moved it to the project's own page, where the section's heading
412
412
  is the disclosure and the row it left carries only its two doors. */
413
413
  .attachNote{margin:8px 0 0;font-size:13px;line-height:1.45;color:var(--ink-faint);}
414
+ /* invites filed in this session (task 1002346): a hairline ledger under the pair
415
+ above — one line per person, the name in the body voice, what happened next to
416
+ it in the faint ink. Shared by the wizard's step and the project's own page. */
417
+ .inviteList{list-style:none;display:grid;margin:12px 0 0;padding:0;}
418
+ .inviteList:empty{display:none;}
419
+ .inviteList>li{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;padding:10px 0 11px;border-top:1px solid var(--rule-soft);}
420
+ .inviteList>li:first-child{border-top:0;}
421
+ .inviteList>li::before{content:"";grid-column:1;grid-row:1;width:7px;height:7px;margin-top:7px;border-radius:50%;border:1px solid var(--accent);background:var(--bg);}
422
+ .inviteList b{grid-column:2;font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
423
+ .inviteList small{grid-column:2;font-family:var(--font-body);font-size:13px;font-weight:400;line-height:1.45;color:var(--ink-faint);}
414
424
  .attachNote code{font-family:var(--font-mono);}
415
425
  .projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
416
426
  .projMsg.ok{color:var(--ok);}
@@ -1516,6 +1526,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
1516
1526
  #view-new #wizSignin:hover{background:color-mix(in srgb,var(--ink) 88%,var(--bg));}
1517
1527
  /* the error alert: the error ink between two hairlines, no plate */
1518
1528
  #view-new .alert{background:none;border:0;border-top:1px solid var(--err);border-bottom:1px solid var(--err);border-radius:0;padding:12px 0;font-weight:400;}
1529
+ /* the invite step (task 1002346): the page's own input+button pair, wearing the
1530
+ wizard's hairline field rather than the plated one — the .attach width has to
1531
+ give way to the row's flex, or #view-new's own width:100% overflows it */
1532
+ #view-new .attach{gap:10px 14px;margin-top:14px;align-items:center;}
1533
+ #view-new .attach input[type=text]{width:auto;flex:1;min-width:220px;}
1534
+ #view-new .inviteBox{margin-top:34px;}
1535
+ #view-new .inviteBox .finePrint{margin-top:10px;}
1536
+ #view-new #wizInviteLater{margin-top:14px;}
1519
1537
  /* one fold for the whole wizard: 840 is where the page's bands and ledger stack
1520
1538
  (cosmos's createSplit collapse), so the rail folds with them, not before them */
1521
1539
  @media (max-width:840px){
@@ -849,6 +849,27 @@ summary{min-height:24px;padding:3px 0;}
849
849
  </details>
850
850
  </div>
851
851
 
852
+ <!-- live since task 1002346 (BV1.R27): the way to invite a builder AFTER
853
+ creation. The wizard's invite step is skippable, and — the mModules
854
+ argument beside it — an optional step is a default rather than a dead end
855
+ only if there is a way back. The SAME widget on the SAME route
856
+ (POST /projects/invite), admitted here by the project's own ownership
857
+ (ADR 0248); like its neighbour above, nothing is pushed to the project and
858
+ nothing restarts, so there is no "what your project runs" read-back line. -->
859
+ <div class="card mSec" id="mInvite">
860
+ <h2>Invite a builder</h2>
861
+ <div id="mInv">
862
+ <p class="finePrint">By GitHub username. They get an invitation to accept from their own projects page, and they need to have signed in here at least once.</p>
863
+ <div class="attach">
864
+ <label class="vh" for="mInviteWho">GitHub username</label>
865
+ <input type="text" id="mInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
866
+ <button type="button" class="btn small" data-invite-send="mInvite">Invite</button>
867
+ </div>
868
+ <ul class="inviteList" id="mInviteList"></ul>
869
+ <div class="projMsg" id="mInviteMsg" role="status" aria-live="polite"></div>
870
+ </div>
871
+ </div>
872
+
852
873
  <!-- the planet picker (task 1003347; direction record §16, v1 the bands): the
853
874
  owner's own choice of how the platform draws this project — the sky,
854
875
  the list, this page's head and the card read the same row. The pick is
@@ -1203,7 +1224,29 @@ summary{min-height:24px;padding:3px 0;}
1203
1224
  <!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
1204
1225
  and a live region that is itself replaced never announces -->
1205
1226
  <div id="liveProgress" role="status" aria-live="polite"></div>
1206
- <h2 class="stepsHead">Your steps — the parts only you can play</h2>
1227
+ <!-- the invite step (task 1002346, BV1.R27) — OPTIONAL, and it stands HERE
1228
+ rather than in the numbered rail because an invite needs a project that
1229
+ already exists: the catalog row a client_id resolves is projected at
1230
+ create (ADR 0246), so before the Create step there is nothing to invite
1231
+ anyone TO. The whole reasoning is ADR 0249. Hidden until the created
1232
+ record carries an id and a slug; the script is the only thing that
1233
+ shows it. -->
1234
+ <div class="inviteBox" id="wizInvite" hidden>
1235
+ <h2 class="stepsHead">Invite builders</h2>
1236
+ <p class="finePrint">Optional — invite people by GitHub username. Each one gets an invitation to accept from their own projects page, and they need to have signed in here at least once. Skip this and invite anyone later; nothing about your project waits on it.</p>
1237
+ <div class="attach">
1238
+ <label class="vh" for="wizInviteWho">GitHub username</label>
1239
+ <input type="text" id="wizInviteWho" placeholder="GitHub username" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
1240
+ <button type="button" class="btn small" data-invite-send="wizInvite">Invite</button>
1241
+ <button type="button" class="btn small ghost" data-invite-skip="wizInvite">Skip</button>
1242
+ </div>
1243
+ <ul class="inviteList" id="wizInviteList"></ul>
1244
+ <div class="projMsg" id="wizInviteMsg" role="status" aria-live="polite"></div>
1245
+ </div>
1246
+ <!-- the way back, the doneModules idiom: an optional step is a default
1247
+ rather than a dead end only if the panel names where it lives after -->
1248
+ <p class="samePath" id="wizInviteLater"></p>
1249
+ <h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play</h2>
1207
1250
  <ol class="humanSteps" id="humanSteps"></ol>
1208
1251
  <!-- the modules step was optional (task 1002342, BV1.R23) — one line
1209
1252
  saying where it lives afterwards, so a skip is never a dead end -->
@@ -4098,6 +4141,14 @@ summary{min-height:24px;padding:3px 0;}
4098
4141
  skippable, so this panel names where it lives afterwards rather than
4099
4142
  leaving an owner to go looking for it. */
4100
4143
  $('doneModules').innerHTML = 'Add or remove modules later in ' + projectPageLink('the project’s settings') + '.';
4144
+ /* the invite step (task 1002346, BV1.R27). The CREATED RECORD is the proof
4145
+ the widget needs and the only one this panel has: no signed-in identity,
4146
+ no filed instance or no slug ⇒ nothing to invite to, and the step stays
4147
+ hidden. It is deliberately NOT keyed on instanceId, which startProgress
4148
+ sets only AFTER this runs. */
4149
+ var created = createdRecord();
4150
+ setInviteSlug('wizInvite', (me && created && created.id && created.slug) ? created.slug : '');
4151
+ $('wizInviteLater').innerHTML = 'You can invite builders any time from ' + projectPageLink('the project’s own page') + '.';
4101
4152
  renderSteps();
4102
4153
  }
4103
4154
 
@@ -5280,6 +5331,11 @@ summary{min-height:24px;padding:3px 0;}
5280
5331
  progress survives the tick because only a different project re-renders it */
5281
5332
  renderPick(inst);
5282
5333
  renderCardPick(inst);
5334
+ /* invite a builder (task 1002346): fetch-free too — the widget needs only the
5335
+ slug, and the API is the wall. This read is own-scoped and 404s for anyone
5336
+ who is not the owner, so reaching renderManage at all IS the ownership
5337
+ answer the widget does not ask for itself. */
5338
+ setInviteSlug('mInvite', inst.slug || '');
5283
5339
  /* the page names its project — the heading holds #mName; and in the tab */
5284
5340
  document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
5285
5341
  /* the card's own story, on the project's own page (task 1003232): the
@@ -6692,6 +6748,169 @@ summary{min-height:24px;padding:3px 0;}
6692
6748
  goToManage(m.getAttribute('data-manage'));
6693
6749
  });
6694
6750
 
6751
+ /* ══ invite a builder (task 1002346, BV1.R27; goal 1000046 criterion C6, ADR 0249)
6752
+ ONE widget, mounted twice: the creation wizard's optional invite step (on the
6753
+ done panel — an invite needs a project that already exists) and the project's
6754
+ own page, so a skipped step is a default rather than a dead end.
6755
+
6756
+ THE ROUTE IS USED AS IT STANDS: POST /projects/invite { client_id, github_login },
6757
+ where client_id IS the project's slug — ADR 0246 projects a control-plane project
6758
+ onto the catalog under its slug — and the project's owner is admitted by ADR 0248's
6759
+ ownership arm. Nothing here is a second gate; the server owns the answer.
6760
+
6761
+ TWO QUESTIONS THIS WIDGET DELIBERATELY DOES NOT ASK: "is anyone signed in" and
6762
+ "is this their project". Each caller already knows, and hands over a slug or
6763
+ nothing — which is the ONE door in (setInviteSlug). A widget that re-derived
6764
+ ownership on the page would be a second, weaker copy of a server rule.
6765
+
6766
+ THE REFUSALS ARE SAID IN THE ROUTE'S OWN VOCABULARY, and the 403 is said
6767
+ CAREFULLY: it is byte-identical whether the project is real, stealth, or absent
6768
+ (ADR 0248), so this page may never word it as "it exists but you may not" — that
6769
+ would hand back the membership oracle the route equalised away.
6770
+
6771
+ IT NEVER BLOCKS CREATION. The project is filed before the step is shown at all;
6772
+ a refusal leaves the box exactly as it was, ready to try again.
6773
+
6774
+ WHAT IT PUBLISHES OF AN INVITEE is the 201 row's own membership_kind and the
6775
+ login the inviter typed — never the github_id the row carries, which belongs to
6776
+ someone who has accepted nothing yet.
6777
+ Proof: ../../../tests/project_invite_ui.mjs ══ */
6778
+
6779
+ var INVITE_MOUNTS = {
6780
+ wizInvite: { who: 'wizInviteWho', list: 'wizInviteList', msg: 'wizInviteMsg', after: 'doneStepsHead' },
6781
+ mInvite: { who: 'mInviteWho', list: 'mInviteList', msg: 'mInviteMsg', after: null }
6782
+ };
6783
+ var inviteSlug = {}; /* mount id → the slug it invites to; '' means no step at all */
6784
+ var inviteRows = {}; /* mount id → the invites filed on THIS project this session */
6785
+ var inviteBusy = {}; /* a send is in flight — the mount is inert until it answers */
6786
+
6787
+ /* setInviteSlug(id, slug) — the only door in. A falsy slug closes the mount, which
6788
+ is how "signed out", "not the owner" and "no project yet" all reach this widget.
6789
+ A DIFFERENT slug starts clean: the pending lines belonged to the project they
6790
+ were filed on, and following them to another one would be a lie. */
6791
+ function setInviteSlug(id, slug) {
6792
+ var m = INVITE_MOUNTS[id];
6793
+ if (!m) return;
6794
+ var next = slug || '';
6795
+ if (inviteSlug[id] !== next) {
6796
+ inviteSlug[id] = next;
6797
+ inviteRows[id] = [];
6798
+ inviteSay(id, '', '');
6799
+ }
6800
+ var box = $(id);
6801
+ if (box) box.hidden = !next;
6802
+ renderInviteRows(id);
6803
+ }
6804
+
6805
+ /* One line per invite this session filed: the login the inviter typed, and the
6806
+ WORD THE ROW ITSELF CARRIES — so a membership_kind other than 'pending' shows
6807
+ up here rather than being papered over as one. */
6808
+ function renderInviteRows(id) {
6809
+ var m = INVITE_MOUNTS[id];
6810
+ var list = m && $(m.list);
6811
+ if (!list) return;
6812
+ list.innerHTML = (inviteRows[id] || []).map(function (r) {
6813
+ var said = r.kind === 'pending' ? 'invited — waiting for them to accept.' : r.kind;
6814
+ return '<li><b>' + esc(r.login) + '</b><small>' + esc(said) + '</small></li>';
6815
+ }).join('');
6816
+ }
6817
+
6818
+ function inviteSay(id, text, cls, isHtml) {
6819
+ var m = INVITE_MOUNTS[id];
6820
+ var el = m && $(m.msg);
6821
+ if (!el) return;
6822
+ el.className = 'projMsg' + (cls ? ' ' + cls : '');
6823
+ if (isHtml) el.innerHTML = text; else el.textContent = text;
6824
+ }
6825
+
6826
+ /* inviteRefusal(code, status, message) — the route's refusals as one sentence
6827
+ each. PURE, so the harness reads it directly. A code this page does not know
6828
+ falls through to the SERVER'S own words rather than an invented reason. */
6829
+ function inviteRefusal(code, status, message) {
6830
+ if (status === 404 && code === 'account_not_found') return 'That builder hasn’t signed in here yet — ask them to sign in once, then invite them.';
6831
+ if (status === 404 && code === 'unknown_project') return 'This project isn’t on the map yet — give it a moment and try again.';
6832
+ /* deliberately says nothing about the project: see the header */
6833
+ if (status === 403) return 'This account can’t invite people here.';
6834
+ return message || 'The invitation couldn’t be sent — try again in a moment.';
6835
+ }
6836
+
6837
+ /* doInvite(id) — one builder, one request. */
6838
+ function doInvite(id) {
6839
+ var m = INVITE_MOUNTS[id];
6840
+ var slug = inviteSlug[id] || '';
6841
+ if (!m || !slug || inviteBusy[id]) return;
6842
+ var field = $(m.who);
6843
+ var login = String((field && field.value) || '').replace(/^\s+|\s+$/g, '');
6844
+ if (!login) {
6845
+ inviteSay(id, 'Type the GitHub username of the person you want to invite.', 'err');
6846
+ if (field && field.focus) field.focus();
6847
+ return;
6848
+ }
6849
+ inviteBusy[id] = true;
6850
+ inviteSay(id, 'Sending the invitation…', '');
6851
+ fetch(API + '/projects/invite', {
6852
+ method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
6853
+ body: JSON.stringify({ client_id: slug, github_login: login })
6854
+ }).then(function (res) {
6855
+ return jsonOrEmpty(res).then(function (data) {
6856
+ inviteBusy[id] = false;
6857
+ if (res.ok) {
6858
+ if (data && data.invited && data.row) {
6859
+ inviteRows[id] = (inviteRows[id] || []).concat([{ login: login, kind: String(data.row.membership_kind || 'pending') }]);
6860
+ renderInviteRows(id);
6861
+ if (field) { field.value = ''; if (field.focus) field.focus(); }
6862
+ inviteSay(id, login + ' is invited — they accept it from their own projects page.', 'ok');
6863
+ return;
6864
+ }
6865
+ /* 200 { invited: false, alreadyMember: true } — not a failure, and not a
6866
+ pending invite either: they are already on the project */
6867
+ if (field) field.value = '';
6868
+ inviteSay(id, login + ' is already on this project.', 'ok');
6869
+ return;
6870
+ }
6871
+ if (res.status === 401) { inviteSay(id, signInAgain(), 'err', true); return; }
6872
+ var e = apiError(data, res.status);
6873
+ inviteSay(id, inviteRefusal(e.code, res.status, e.message), 'err');
6874
+ });
6875
+ }).catch(function () {
6876
+ inviteBusy[id] = false;
6877
+ inviteSay(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
6878
+ });
6879
+ }
6880
+
6881
+ /* skipInvite(id) — the step steps aside having sent NOTHING. Focus moves on
6882
+ rather than being stranded inside the box that just went away. */
6883
+ function skipInvite(id) {
6884
+ var m = INVITE_MOUNTS[id];
6885
+ if (!m || inviteBusy[id]) return;
6886
+ var box = $(id);
6887
+ if (box) box.hidden = true;
6888
+ inviteSay(id, '', '');
6889
+ var after = m.after && $(m.after);
6890
+ if (after && after.focus) { after.setAttribute('tabindex', '-1'); after.focus(); }
6891
+ }
6892
+
6893
+ /* One delegated pair per mount: the buttons name their own mount, so the two
6894
+ surfaces share every line of behaviour above without sharing state. */
6895
+ Object.keys(INVITE_MOUNTS).forEach(function (id) {
6896
+ var box = $(id);
6897
+ if (box) {
6898
+ box.addEventListener('click', function (ev) {
6899
+ var t = ev.target.closest && ev.target.closest('[data-invite-send],[data-invite-skip]');
6900
+ if (!t) return;
6901
+ if (t.hasAttribute('data-invite-skip')) skipInvite(id); else doInvite(id);
6902
+ });
6903
+ }
6904
+ var field = $(INVITE_MOUNTS[id].who);
6905
+ if (field) {
6906
+ field.addEventListener('keydown', function (ev) {
6907
+ if (ev.key !== 'Enter') return;
6908
+ ev.preventDefault();
6909
+ doInvite(id);
6910
+ });
6911
+ }
6912
+ });
6913
+
6695
6914
  /* ── boot: who am I → my instances ────────────────────────────────────── */
6696
6915
 
6697
6916
  function renderWho() {
@@ -22,6 +22,7 @@
22
22
  "in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
23
23
  "in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
24
24
  "in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
25
+ "in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
25
26
  "in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
26
27
  "in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
27
28
  "in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.538",
3
+ "version": "1.19.539",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.538",
9
+ "version": "1.19.539",
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.538",
3
+ "version": "1.19.539",
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",
package/src/module-api.js CHANGED
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
49
49
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
50
50
  // the entry to that file. Look for a version's history there, not here.
51
51
  // ---------------------------------------------------------------------------
52
- const CORE_VERSION = '1.19.538'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
52
+ const CORE_VERSION = '1.19.539'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
53
53
 
54
54
  // A namespaced logger so a module's log lines are attributable + consistent.
55
55
  // Usage: const log = api.logger('dev-box'); log.info('mounted');