@bongos/core 1.21.66 → 1.21.67

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.
Files changed (49) hide show
  1. package/.bongos-core.json +111 -41
  2. package/docs/copy-inventory.md +414 -386
  3. package/docs/copy-registry.json +815 -537
  4. package/docs/file-map.md +1 -0
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +45 -12
  7. package/docs/page-readings.json +959 -907
  8. package/modules/platform-identity/routes/my-projects.js +1 -1
  9. package/modules/public-landing/public/account.html +1 -1
  10. package/modules/public-landing/public/assets/cosmos.css +12 -10
  11. package/modules/public-landing/public/contact.html +1 -1
  12. package/modules/public-landing/public/guild.html +1 -1
  13. package/modules/public-landing/public/index-cli.states.json +20 -0
  14. package/modules/public-landing/public/index.html +68 -1
  15. package/modules/public-landing/public/index.states.json +4 -4
  16. package/modules/public-landing/public/invitations-empty.states.json +56 -0
  17. package/modules/public-landing/public/invitations-picker.states.json +84 -0
  18. package/modules/public-landing/public/invitations.html +956 -0
  19. package/modules/public-landing/public/invitations.probes.json +430 -0
  20. package/modules/public-landing/public/invitations.states.json +473 -0
  21. package/modules/public-landing/public/privacy.html +1 -1
  22. package/modules/public-landing/public/projects.html +18 -543
  23. package/modules/public-landing/public/projects.probes.json +0 -29
  24. package/modules/public-landing/public/projects.states.json +0 -6
  25. package/modules/public-landing/public/terms.html +1 -1
  26. package/modules/ui-design/kit/fixtures/bongos-cli-latest-sample.json +10 -0
  27. package/modules/ui-design/kit/fixtures/featured-invitations.json +183 -0
  28. package/modules/ui-design/kit/fixtures/guilds-empty.json +1 -0
  29. package/modules/ui-design/kit/fixtures/me-guild-engagements-empty.json +1 -0
  30. package/modules/ui-design/kit/fixtures/me-guild-requests-empty.json +1 -0
  31. package/modules/ui-design/kit/fixtures/my-projects-applied.json +29 -0
  32. package/modules/ui-design/kit/fixtures/my-projects-invites-empty.json +1 -0
  33. package/modules/ui-design/kit/serve.js +5 -1
  34. package/package-lock.json +2 -2
  35. package/package.json +1 -1
  36. package/release-notes.json +10 -0
  37. package/src/module-api.js +1 -1
  38. package/src/platform-server.js +11 -0
  39. package/tests/hub_account_page.mjs +1 -1
  40. package/tests/hub_engagement_cards.mjs +44 -40
  41. package/tests/hub_guild_cards.mjs +24 -22
  42. package/tests/hub_invitations_page.mjs +414 -0
  43. package/tests/join_relay_outcome.mjs +1 -1
  44. package/tests/landing_legal_pages.mjs +8 -6
  45. package/tests/landing_page.mjs +125 -3
  46. package/tests/my_projects_surface.mjs +14 -6
  47. package/tests/public_landing_projects.mjs +12 -14
  48. package/tests/render_check.mjs +3 -2
  49. package/tests/ui_design_kit.mjs +3 -3
@@ -760,7 +760,7 @@ summary{min-height:24px;padding:3px 0;}
760
760
  account page, where the person's own settings live (ADR 0337 D4.3).
761
761
  Apex-only because the page is — /account is served only where
762
762
  platform-identity runs, so a child instance loses the item whole. -->
763
- <!--apex-only:start--><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->
763
+ <!--apex-only:start--><li class="bandCell"><a class="bandT" href="/invitations">Invitations</a></li><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->
764
764
  <!-- the honest hover target; the delegated click upgrades it to the POST -->
765
765
  <li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
766
766
  </ul>
@@ -955,26 +955,14 @@ summary{min-height:24px;padding:3px 0;}
955
955
  <ul class="projects" id="projectsList"></ul>
956
956
  <p class="finePrint">Buttons here ask the platform to act; each row reports only what was verified, and when. A standup keeps going even with this tab closed.</p>
957
957
 
958
- <!-- ══ INVITATIONS · PROJECTS YOU BUILD ON · JOIN BY URL (task 1003774) ══
959
- Restores what the hall's deleted my-projects.js rendered (task 1003308,
960
- ADR 0141 §3). No Accept button — ADR 0335 retired that hub-only flip as
961
- a dead end; an invite is a link into the project instead. ══ -->
958
+ <!-- ══ PROJECTS YOU BUILD ON (task 1003774) · the way to INVITATIONS ══
959
+ The invitations (a project's own, a guild's, a guild's engagements, the
960
+ requests to a guild you own) and the join-by-address field have their own
961
+ page since task 1004276 (the owner's call, ADR 0337 D5): /invitations. ══ -->
962
962
  <div class="mineExtra">
963
- <section id="mineInvites" hidden aria-labelledby="mineInvitesH">
964
- <h2 id="mineInvitesH">Invitations</h2>
965
- <ul class="mineRows" id="invitesList"></ul>
966
- <!-- a guild inviting you (task 1004583; ADR 0336 D4 §3, ADR 0337 D4.4):
967
- each card says, BEFORE you accept, that your numbers count toward the
968
- guild's rounded totals, and offers the switch right there. -->
969
- <ul class="mineRows" id="guildInviteList"></ul>
970
- <!-- a guild you are in, engaging a project (task 1002303, R27; ADR 0365):
971
- "G is applying to P" and "P invited G", from GET /me/guild-engagements.
972
- Each card's Apply is the ordinary join request with `guild` added, so
973
- you apply as yourself, never as the guild. -->
974
- <ul class="mineRows" id="engageList"></ul>
975
- <!-- someone asking to join a guild you own (task 1004583): Accept / Decline.
976
- Their own counted switch rode their request, so this card has none. -->
977
- <ul class="mineRows" id="guildRequestList"></ul>
963
+ <section aria-labelledby="mineToInvitesH">
964
+ <h2 id="mineToInvitesH">Invitations</h2>
965
+ <p class="lede">Invitations from projects and guilds, and applying to a project by its address, have their own page. <a href="/invitations">Open your invitations</a></p>
978
966
  </section>
979
967
 
980
968
  <section aria-labelledby="mineBuildingH">
@@ -982,19 +970,6 @@ summary{min-height:24px;padding:3px 0;}
982
970
  <p class="mineEmpty" id="buildingEmpty" hidden>You’re not building on any other projects yet.</p>
983
971
  <ul class="mineRows" id="buildingList"></ul>
984
972
  </section>
985
-
986
- <section aria-labelledby="mineJoinH">
987
- <h2 id="mineJoinH">Join a project</h2>
988
- <p class="lede">Paste the project’s address — its owner sees your request in their own queue.</p>
989
- <div class="mineJoinRow">
990
- <label class="vh" for="joinOrigin">Project address</label>
991
- <input id="joinOrigin" type="url" placeholder="https://project.example.com" autocomplete="off">
992
- <button id="joinBtn" type="button" class="btn small">Request to join</button>
993
- </div>
994
- <label class="vh" for="joinNote">Add a note (optional)</label>
995
- <textarea id="joinNote" class="mineJoinNote" rows="2" maxlength="400" placeholder="A note for the owner (optional) — what you’d like to work on, who you are."></textarea>
996
- <p class="projMsg" id="joinStatus" role="status" aria-live="polite"></p>
997
- </section>
998
973
  </div>
999
974
  </section>
1000
975
  </div>
@@ -2548,9 +2523,9 @@ summary{min-height:24px;padding:3px 0;}
2548
2523
  $('signinTop').href = '/api/bongos/auth/web/start?return=/projects' + backPart + '&repos=1';
2549
2524
  /* the mine view is the only one that polls; it re-reads on entry so a
2550
2525
  card that changed while you were on the map is not stale — including
2551
- the membership + invitations lists (task 1003774), the same re-read
2526
+ the membership list (task 1003774), the same re-read
2552
2527
  refreshList() already does for owned instances */
2553
- if (v === 'mine' && me) { refreshList(); loadMemberships(); loadInvites(); }
2528
+ if (v === 'mine' && me) { refreshList(); loadMemberships(); }
2554
2529
  /* manage re-reads its project on entry — no timer lives here (C8) */
2555
2530
  if (v === 'manage') loadManage();
2556
2531
  /* owned chips refresh on re-entry, never on a timer — C8 keeps the map quiet */
@@ -3235,7 +3210,7 @@ summary{min-height:24px;padding:3px 0;}
3235
3210
  }
3236
3211
  if (res.ok && res.data && res.data.status === 'requested') {
3237
3212
  btn.textContent = 'Applied';
3238
- if (msg) { msg.className = 'projMsg ok'; msg.textContent = res.data.message || 'Requested.'; }
3213
+ if (msg) { msg.className = 'projMsg ok'; msg.textContent = res.data.message || 'Applied.'; }
3239
3214
  return;
3240
3215
  }
3241
3216
  btn.textContent = 'Apply to join';
@@ -3594,8 +3569,8 @@ summary{min-height:24px;padding:3px 0;}
3594
3569
  .catch(function () { ownedInstances = []; ownedByHost = {}; });
3595
3570
  }
3596
3571
 
3597
- /* ── invitations / building-on rows + join-by-URL (task 1003774) ──
3598
- Same row shape for both lists: a name, its host in the mono face, and one
3572
+ /* ── the building-on rows (task 1003774; the invitation rows moved to
3573
+ /invitations with task 1004276) ── Same row shape as that page's lists: a name, its host in the mono face, and one
3599
3574
  action on the right. textContent-only, matching the deleted hall page
3600
3575
  this restores. */
3601
3576
  function mineEl(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
@@ -3653,472 +3628,10 @@ summary{min-height:24px;padding:3px 0;}
3653
3628
  $('buildingEmpty').hidden = !!(rows && rows.length);
3654
3629
  }
3655
3630
 
3656
- /* Each invitation renders as a way IN, never an Accept button: the hub's own
3657
- accept flip never admits anyone to the project's own instance (ADR 0335)
3658
- — signing in there is what actually accepts it (task 1002969). Since task
3659
- 1002818 (ADR 0353) the row is the project's own notice of its invite and the
3660
- hub has no accept route at all. Decline still works; it only clears the
3661
- hub's own pending row. */
3662
- function renderInvitesList(invites) {
3663
- var ul = $('invitesList');
3664
- ul.textContent = '';
3665
- if (!invites || !invites.length) { paintInvitesSection(); return; }
3666
- invites.forEach(function (inv) {
3667
- var row = mineRow(inv);
3668
- var hint = mineEl('p', 'mineRow__hint');
3669
- hint.textContent = 'Sign in there to accept.';
3670
- row.querySelector('.mineRow__main').appendChild(hint);
3671
- var actions = mineEl('div', 'mineRow__actions');
3672
- var href = safeHttpUrl(inv.origin);
3673
- if (href) {
3674
- var open = document.createElement('a');
3675
- open.className = 'btn small';
3676
- open.href = href;
3677
- open.target = '_blank';
3678
- open.rel = 'noopener';
3679
- open.textContent = 'Open project →';
3680
- actions.appendChild(open);
3681
- }
3682
- var decline = document.createElement('button');
3683
- decline.type = 'button';
3684
- decline.className = 'btn small ghost';
3685
- decline.textContent = 'Decline';
3686
- decline.addEventListener('click', function () {
3687
- postThenRemoveRow(
3688
- API + '/my-projects/invites/' + encodeURIComponent(inv.client_id) + '/decline', row, decline, null,
3689
- paintInvitesSection
3690
- );
3691
- });
3692
- actions.appendChild(decline);
3693
- row.appendChild(actions);
3694
- ul.appendChild(row);
3695
- });
3696
- paintInvitesSection();
3697
- }
3698
-
3699
- /* the Invitations section holds four lists — the projects' own invites, a
3700
- guild's invites to you, your guilds' engagements, and the join requests to a
3701
- guild you own — and shows while any of them has a row */
3702
- var INVITE_LISTS = ['invitesList', 'guildInviteList', 'engageList', 'guildRequestList'];
3703
- function paintInvitesSection() {
3704
- $('mineInvites').hidden = !INVITE_LISTS.some(function (id) { return $(id).children.length; });
3705
- }
3706
-
3707
- function loadInvites() {
3708
- return Promise.all([
3709
- fetch(API + '/my-projects/invites', { credentials: 'same-origin' })
3710
- .then(function (res) { return res.ok ? res.json() : { invites: [] }; })
3711
- .then(function (d) { renderInvitesList(d.invites || []); })
3712
- .catch(function () { renderInvitesList([]); }),
3713
- loadEngagements(),
3714
- loadGuildQueue()
3715
- ]);
3716
- }
3717
-
3718
- /* ── a guild you are in, engaging a project (task 1002303, R27; ADR 0365) ──
3719
- GET /me/guild-engagements names what the guilds you are in have going: "G is
3720
- applying to P" (the guild's owner put it forward) and "P invited G" (the
3721
- project invited the guild; its hall sends each member on the guild's public
3722
- page their own invite, and the rest of the guild hears it here). The read only
3723
- names a project that is on the map and public, and leaves out one you already
3724
- build on, are invited to, or have applied to. Each card's Apply is the SAME
3725
- relay as everything else in this view — postJoinRequest — with `guild` added:
3726
- you apply as yourself, never as the guild, with your own note if you like. A
3727
- guild that no longer holds for you (you left it, or its 30 days ran out) is a
3728
- 409 and nothing is relayed; the card then offers the plain application.
3729
- textContent / createElement only. Proof: tests/hub_engagement_cards.mjs */
3730
- var ENGAGE_GONE = 'That guild application has ended — you can still apply on your own.';
3731
- function engageLine(e) {
3732
- var g = e.guild.name || e.guild.slug;
3733
- var p = (e.project && e.project.name) || hostOf(e.project.origin);
3734
- return e.kind === 'invited' ? p + ' invited ' + g : g + ' is applying to ' + p + ' — apply with them?';
3735
- }
3736
- function sessionExpired(el) {
3737
- el.className = 'projMsg err';
3738
- el.textContent = 'Your session expired — ';
3739
- var a = document.createElement('a');
3740
- a.href = API + '/auth/web/start?return=/projects%3Fview%3Dmine&repos=1';
3741
- a.textContent = 'sign in again';
3742
- el.appendChild(a);
3743
- el.appendChild(document.createTextNode('.'));
3744
- }
3745
- function engageRow(e) {
3746
- var origin = e.project.origin;
3747
- var slug = e.guild.slug;
3748
- var row = mineRow({ name: engageLine(e), origin: origin });
3749
- row.className = 'mineRow engageRow';
3750
- var main = row.querySelector('.mineRow__main');
3751
- var hint = mineEl('p', 'mineRow__hint');
3752
- hint.textContent = 'You apply as yourself — add a note for the owner if you like.';
3753
- main.appendChild(hint);
3754
- var note = document.createElement('textarea');
3755
- note.className = 'mineJoinNote';
3756
- note.rows = 2;
3757
- note.maxLength = 400;
3758
- note.placeholder = 'A note for the owner (optional) — what you’d like to work on, who you are.';
3759
- note.setAttribute('aria-label', 'A note for the owner (optional)');
3760
- note.hidden = true;
3761
- main.appendChild(note);
3762
- var msg = mineEl('p', 'projMsg');
3763
- msg.setAttribute('role', 'status');
3764
- msg.setAttribute('aria-live', 'polite');
3765
- main.appendChild(msg);
3766
- var actions = mineEl('div', 'mineRow__actions');
3767
- var noteBtn = document.createElement('button');
3768
- noteBtn.type = 'button';
3769
- noteBtn.className = 'linklike';
3770
- noteBtn.textContent = 'Add a note';
3771
- noteBtn.addEventListener('click', function () { note.hidden = false; noteBtn.hidden = true; note.focus(); });
3772
- var apply = document.createElement('button');
3773
- apply.type = 'button';
3774
- apply.className = 'btn small';
3775
- apply.textContent = 'Apply';
3776
- /* with the guild until the server says it no longer holds, then on your own */
3777
- var withGuild = true;
3778
- function idle() { apply.disabled = false; apply.textContent = withGuild ? 'Apply' : 'Apply on my own'; }
3779
- apply.addEventListener('click', function () {
3780
- apply.disabled = true;
3781
- apply.textContent = 'Applying…';
3782
- msg.className = 'projMsg';
3783
- msg.textContent = '';
3784
- postJoinRequest(origin, (note.value || '').trim() || null, withGuild ? slug : null).then(function (res) {
3785
- if (res.unauthenticated) { idle(); sessionExpired(msg); return; }
3786
- if (res.ok && res.data && res.data.status === 'requested') {
3787
- /* no longer pressable, so no longer the accent (the One Warm Thing rule) */
3788
- apply.className = 'btn small ghost';
3789
- apply.textContent = 'Applied';
3790
- hint.hidden = true;
3791
- note.hidden = true;
3792
- noteBtn.hidden = true;
3793
- msg.className = 'projMsg ok';
3794
- msg.textContent = res.data.message || 'Requested.';
3795
- return;
3796
- }
3797
- var err = apiError(res.data, res.status);
3798
- if (res.status === 409 && err.code === 'guild_engagement_gone') {
3799
- withGuild = false;
3800
- idle();
3801
- msg.textContent = ENGAGE_GONE;
3802
- return;
3803
- }
3804
- idle();
3805
- /* the relay's own answer (invite_only, already_member …) in its own words */
3806
- msg.className = 'projMsg' + (res.ok ? '' : ' err');
3807
- msg.textContent = (res.ok && res.data && res.data.message) || err.message || 'Could not reach that project — try again in a moment.';
3808
- }).catch(function () {
3809
- idle();
3810
- msg.className = 'projMsg err';
3811
- msg.textContent = 'Couldn’t reach the platform — check your connection and try again.';
3812
- });
3813
- });
3814
- actions.appendChild(noteBtn);
3815
- actions.appendChild(apply);
3816
- row.appendChild(actions);
3817
- return row;
3818
- }
3819
- /* a row this page cannot act on (an unknown kind, no guild to name, no address
3820
- to apply to) is dropped rather than drawn as a card whose button fails */
3821
- function renderEngagements(rows) {
3822
- var ul = $('engageList');
3823
- ul.textContent = '';
3824
- (rows || []).forEach(function (e) {
3825
- if (!e || (e.kind !== 'apply' && e.kind !== 'invited')) return;
3826
- if (!e.guild || typeof e.guild.slug !== 'string' || !e.guild.slug) return;
3827
- if (!e.project || !safeHttpUrl(e.project.origin)) return;
3828
- ul.appendChild(engageRow(e));
3829
- });
3830
- paintInvitesSection();
3831
- }
3832
- function loadEngagements() {
3833
- return fetch(API + '/me/guild-engagements', { credentials: 'same-origin' })
3834
- .then(function (res) { return res.ok ? res.json() : { engagements: [] }; })
3835
- .then(function (d) { renderEngagements(d.engagements || []); })
3836
- .catch(function () { renderEngagements([]); });
3837
- }
3838
-
3839
- /* ── a guild inviting you, and a join request to a guild you own (task 1004583;
3840
- ADR 0336 D3/D4 §3, ADR 0337 D4.4) ──
3841
- GET /me/guild-requests names the invites addressed to you, each carrying its
3842
- card: the guild's name, description and CURRENT visibility. Accept echoes back
3843
- the visibility this card showed — the consent is to what the card said, so a
3844
- guild that changed since is a 409 and the card re-draws for a fresh decision —
3845
- and the counted_in_totals choice made on the card. The D4.4 notice is said for
3846
- the choice as it stands, BEFORE accept, in guild.html's own words (the test
3847
- holds the two pages to one sentence). The guilds you OWN answer their join
3848
- requests here too, from the same read's owner_incoming — one request for
3849
- every guild you own, never one per guild. The requester's own switch rode
3850
- their request, so the owner's card has none. A decline is silent on both sides (204, the other side is never
3851
- told). textContent / createElement only. Proof: tests/hub_guild_cards.mjs */
3852
- var GUILD_COUNTED_ON = 'Your numbers count toward this guild’s rounded totals — even while your record is hidden or your account is private. ' +
3853
- 'They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. ' +
3854
- 'Leave them out of any guild you’d rather not share them with.';
3855
- var GUILD_COUNTED_OFF = 'Your numbers are left out of this guild’s totals, from its next weekly count.';
3856
- var GUILD_SHOWN = {
3857
- public: 'It’s public: joining lists your name and picture on its page while your account is public. You can turn that off there.',
3858
- unlisted: 'It’s unlisted: it has no public page, so joining lists you nowhere.'
3859
- };
3860
- var GUILD_OFFLINE = 'Couldn’t reach the platform — check your connection and try again.';
3861
- /* a POST on the cookie session; a 204 (decline) has no body to read */
3862
- function guildPost(path, body) {
3863
- var init = { method: 'POST', credentials: 'same-origin' };
3864
- if (body) { init.headers = { 'Content-Type': 'application/json' }; init.body = JSON.stringify(body); }
3865
- return fetch(API + path, init).then(function (res) {
3866
- return (res.status === 204 ? Promise.resolve({}) : jsonOrEmpty(res)).then(function (data) {
3867
- return { ok: res.ok, status: res.status, data: data };
3868
- });
3869
- });
3870
- }
3871
- function guildButton(label, cls) {
3872
- var b = document.createElement('button');
3873
- b.type = 'button';
3874
- b.className = cls;
3875
- b.textContent = label;
3876
- return b;
3877
- }
3878
- /* the row's status line, quiet until it has something to say */
3879
- function guildMsg(main) {
3880
- var msg = mineEl('p', 'projMsg');
3881
- msg.setAttribute('role', 'status');
3882
- msg.setAttribute('aria-live', 'polite');
3883
- main.appendChild(msg);
3884
- return msg;
3885
- }
3886
- /* Decline: silent to the other side. The row goes; a failure keeps it and says so. */
3887
- function guildDecline(row, id, btns, msg) {
3888
- btns.forEach(function (b) { b.disabled = true; });
3889
- msg.className = 'projMsg';
3890
- msg.textContent = '';
3891
- guildPost('/guild-requests/' + encodeURIComponent(id) + '/decline').then(function (res) {
3892
- if (res.ok) { row.remove(); paintInvitesSection(); return; }
3893
- btns.forEach(function (b) { b.disabled = false; });
3894
- if (res.status === 401) { sessionExpired(msg); return; }
3895
- msg.className = 'projMsg err';
3896
- msg.textContent = res.status === 404 ? 'That has already been answered, or it ran out.' : 'Couldn’t decline just now — try again in a moment.';
3897
- }).catch(function () {
3898
- btns.forEach(function (b) { b.disabled = false; });
3899
- msg.className = 'projMsg err';
3900
- msg.textContent = GUILD_OFFLINE;
3901
- });
3902
- }
3903
-
3904
- function guildInviteRow(inv) {
3905
- var name = inv.name || inv.slug;
3906
- var vis = inv.visibility;
3907
- var row = mineRow({ name: 'You’re invited to join ' + name });
3908
- row.className = 'mineRow guildRow guildRow--invite';
3909
- var main = row.querySelector('.mineRow__main');
3910
- if (inv.description) {
3911
- var desc = mineEl('p', 'guildRow__desc');
3912
- desc.textContent = inv.description;
3913
- main.appendChild(desc);
3914
- }
3915
- var shown = mineEl('p', 'mineRow__hint');
3916
- main.appendChild(shown);
3917
- /* the counted switch — the guild page's pair, in its words, defaulting on as
3918
- the membership does; the notice under it is said for the choice as it stands */
3919
- var band = mineEl('div', 'guildCount');
3920
- var label = mineEl('span', 'guildCount__label');
3921
- label.id = 'guildCount-' + inv.id;
3922
- label.textContent = 'In this guild’s totals';
3923
- band.appendChild(label);
3924
- var group = mineEl('div', 'guildCount__opts');
3925
- group.setAttribute('role', 'radiogroup');
3926
- group.setAttribute('aria-labelledby', label.id);
3927
- var radios = [['on', 'Count my numbers', 'They add to this guild’s totals.'], ['off', 'Leave them out', 'The totals add up everyone else.']].map(function (o) {
3928
- var opt = mineEl('label', 'guildCount__opt');
3929
- var input = document.createElement('input');
3930
- input.type = 'radio';
3931
- input.name = 'guildCounted-' + inv.id;
3932
- input.value = o[0];
3933
- input.checked = o[0] === 'on';
3934
- var text = mineEl('span');
3935
- var b = mineEl('b');
3936
- b.textContent = o[1];
3937
- text.appendChild(b);
3938
- text.appendChild(document.createTextNode(o[2]));
3939
- opt.appendChild(input);
3940
- opt.appendChild(text);
3941
- group.appendChild(opt);
3942
- return input;
3943
- });
3944
- band.appendChild(group);
3945
- var what = mineEl('p', 'guildCount__what');
3946
- band.appendChild(what);
3947
- main.appendChild(band);
3948
- var msg = guildMsg(main);
3949
- function counted() { return !radios[1].checked; }
3950
- function paintWhat() { what.textContent = counted() ? GUILD_COUNTED_ON : GUILD_COUNTED_OFF; }
3951
- radios.forEach(function (r) { r.addEventListener('change', paintWhat); });
3952
- paintWhat();
3953
-
3954
- var actions = mineEl('div', 'mineRow__actions');
3955
- var page = document.createElement('a');
3956
- page.className = 'linklike';
3957
- page.href = '/g/' + encodeURIComponent(inv.slug);
3958
- page.textContent = 'See its page';
3959
- function paintShown() { shown.textContent = GUILD_SHOWN[vis]; page.hidden = vis !== 'public'; }
3960
- paintShown();
3961
- var accept = guildButton('Accept', 'btn small');
3962
- var decline = guildButton('Decline', 'btn small ghost');
3963
- function idle() {
3964
- accept.disabled = false;
3965
- decline.disabled = false;
3966
- accept.textContent = 'Accept';
3967
- }
3968
- /* an invite that is gone (withdrawn, run out, answered elsewhere) cannot be
3969
- answered again: say so and leave nothing pressable */
3970
- function ended() {
3971
- accept.hidden = true;
3972
- decline.hidden = true;
3973
- radios.forEach(function (r) { r.disabled = true; });
3974
- msg.className = 'projMsg';
3975
- msg.textContent = 'That invite has ended — it was withdrawn, or it ran out.';
3976
- }
3977
- accept.addEventListener('click', function () {
3978
- var on = counted();
3979
- accept.disabled = true;
3980
- decline.disabled = true;
3981
- accept.textContent = 'Joining…';
3982
- msg.className = 'projMsg';
3983
- msg.textContent = '';
3984
- guildPost('/guild-requests/' + encodeURIComponent(inv.id) + '/accept', { visibility: vis, counted_in_totals: on }).then(function (res) {
3985
- if (res.ok) {
3986
- /* no longer pressable, so no longer the accent (the One Warm Thing rule) */
3987
- accept.className = 'btn small ghost';
3988
- accept.textContent = 'Joined';
3989
- decline.hidden = true;
3990
- radios.forEach(function (r) { r.disabled = true; });
3991
- page.hidden = false;
3992
- msg.className = 'projMsg ok';
3993
- msg.textContent = 'You’ve joined ' + name + (on ? ', and your numbers count toward its totals.' : ', and your numbers are left out of its totals.');
3994
- return;
3995
- }
3996
- if (res.status === 401) { idle(); sessionExpired(msg); return; }
3997
- var err = apiError(res.data, res.status);
3998
- if (res.status === 409 && err.code === 'guild_visibility_changed' && err.details && GUILD_SHOWN[err.details.visibility]) {
3999
- vis = err.details.visibility;
4000
- paintShown();
4001
- idle();
4002
- msg.className = 'projMsg err';
4003
- msg.textContent = 'This guild changed since the card was drawn: it’s ' + vis + ' now. Read it again, then accept.';
4004
- return;
4005
- }
4006
- if (res.status === 404) { ended(); return; }
4007
- idle();
4008
- msg.className = 'projMsg err';
4009
- msg.textContent = 'Couldn’t accept just now — try again in a moment.';
4010
- }).catch(function () {
4011
- idle();
4012
- msg.className = 'projMsg err';
4013
- msg.textContent = GUILD_OFFLINE;
4014
- });
4015
- });
4016
- decline.addEventListener('click', function () { guildDecline(row, inv.id, [accept, decline], msg); });
4017
- actions.appendChild(page);
4018
- actions.appendChild(accept);
4019
- actions.appendChild(decline);
4020
- row.appendChild(actions);
4021
- return row;
4022
- }
4023
- /* a row id is a bigserial (digits, within the bigint range the route checks);
4024
- a card with no id, no slug or a visibility this page cannot say is dropped
4025
- rather than drawn with a button that fails */
4026
- function guildRowId(r) {
4027
- return !!r && typeof r.id === 'string' && /^\d{1,19}$/.test(r.id) && (r.id.length < 19 || r.id <= '9223372036854775807');
4028
- }
4029
- function renderGuildInvites(rows) {
4030
- var ul = $('guildInviteList');
4031
- ul.textContent = '';
4032
- (rows || []).forEach(function (inv) {
4033
- if (!guildRowId(inv) || typeof inv.slug !== 'string' || !inv.slug || !GUILD_SHOWN[inv.visibility]) return;
4034
- ul.appendChild(guildInviteRow(inv));
4035
- });
4036
- paintInvitesSection();
4037
- }
4038
-
4039
- function guildRequestRow(r) {
4040
- var who = r.display_name || '@' + r.handle;
4041
- var row = mineRow({ name: who + ' asked to join ' + (r.guild.name || r.guild.slug) });
4042
- row.className = 'mineRow guildRow';
4043
- var main = row.querySelector('.mineRow__main');
4044
- /* the handle under a display name, never twice */
4045
- if (r.display_name) {
4046
- var handle = mineEl('span', 'mineRow__host');
4047
- handle.textContent = '@' + r.handle;
4048
- main.appendChild(handle);
4049
- }
4050
- var hint = mineEl('p', 'mineRow__hint');
4051
- hint.textContent = 'Accepting makes them a member. A decline is never told to them.';
4052
- main.appendChild(hint);
4053
- var msg = guildMsg(main);
4054
- var actions = mineEl('div', 'mineRow__actions');
4055
- var accept = guildButton('Accept', 'btn small');
4056
- var decline = guildButton('Decline', 'btn small ghost');
4057
- accept.addEventListener('click', function () {
4058
- accept.disabled = true;
4059
- decline.disabled = true;
4060
- accept.textContent = 'Accepting…';
4061
- msg.className = 'projMsg';
4062
- msg.textContent = '';
4063
- guildPost('/guild-requests/' + encodeURIComponent(r.id) + '/accept').then(function (res) {
4064
- if (res.ok) {
4065
- accept.className = 'btn small ghost';
4066
- accept.textContent = 'Accepted';
4067
- decline.hidden = true;
4068
- hint.hidden = true;
4069
- msg.className = 'projMsg ok';
4070
- msg.textContent = who + ' is in ' + (r.guild.name || r.guild.slug) + ' now.';
4071
- return;
4072
- }
4073
- accept.textContent = 'Accept';
4074
- if (res.status === 404) {
4075
- accept.hidden = true;
4076
- decline.hidden = true;
4077
- msg.className = 'projMsg';
4078
- msg.textContent = 'That request has ended — it was withdrawn, it ran out, or their account is closed.';
4079
- return;
4080
- }
4081
- accept.disabled = false;
4082
- decline.disabled = false;
4083
- if (res.status === 401) { sessionExpired(msg); return; }
4084
- msg.className = 'projMsg err';
4085
- msg.textContent = 'Couldn’t accept just now — try again in a moment.';
4086
- }).catch(function () {
4087
- accept.disabled = false;
4088
- decline.disabled = false;
4089
- accept.textContent = 'Accept';
4090
- msg.className = 'projMsg err';
4091
- msg.textContent = GUILD_OFFLINE;
4092
- });
4093
- });
4094
- decline.addEventListener('click', function () { guildDecline(row, r.id, [accept, decline], msg); });
4095
- actions.appendChild(accept);
4096
- actions.appendChild(decline);
4097
- row.appendChild(actions);
4098
- return row;
4099
- }
4100
- function renderGuildRequests(rows) {
4101
- var ul = $('guildRequestList');
4102
- ul.textContent = '';
4103
- (rows || []).forEach(function (r) {
4104
- if (!guildRowId(r) || typeof r.handle !== 'string' || !r.handle) return;
4105
- if (!r.guild || typeof r.guild.slug !== 'string' || !r.guild.slug) return;
4106
- ul.appendChild(guildRequestRow(r));
4107
- });
4108
- paintInvitesSection();
4109
- }
4110
- /* ONE read for both lists: the invites to you, and the requests waiting on you
4111
- as the owner of a guild; a read that fails is silent, as the other lists' are */
4112
- function loadGuildQueue() {
4113
- return fetch(API + '/me/guild-requests', { credentials: 'same-origin' })
4114
- .then(function (res) { return res.ok ? res.json() : {}; })
4115
- .then(function (d) { renderGuildInvites(d.incoming || []); renderGuildRequests(d.owner_incoming || []); })
4116
- .catch(function () { renderGuildInvites([]); renderGuildRequests([]); });
4117
- }
4118
-
4119
- /* the one relay both the map's per-card Apply (submitApply) and the
4120
- address-typed join field drive — POST /my-projects/join, the 401 case and
4121
- the 'requested' echo update kept in ONE place (task 1003774). Keys the
3631
+ /* the relay the map's per-card Apply (submitApply) drives — POST
3632
+ /my-projects/join, the 401 case and the 'requested' echo update kept in ONE
3633
+ place (task 1003774). /invitations carries the same relay for its join field
3634
+ and guild cards, without the echo (it has no map to keep Applied on). Keys the
4122
3635
  echo off the server's OWN resolved origin where the response carries one:
4123
3636
  it need not match what the visitor typed byte-for-byte (a trailing
4124
3637
  slash, a different case) to be the same project the map's catalog names.
@@ -4143,44 +3656,6 @@ summary{min-height:24px;padding:3px 0;}
4143
3656
  });
4144
3657
  }
4145
3658
 
4146
- /* Join by URL, with the applicant's own optional note (task 1002330). */
4147
- $('joinBtn').addEventListener('click', function () {
4148
- var origin = ($('joinOrigin').value || '').trim();
4149
- var status = $('joinStatus');
4150
- if (!origin) { status.className = 'projMsg err'; status.textContent = 'Enter a project’s address.'; return; }
4151
- /* a bare host ("grove.cloudbongos.com") never resolves — the server's own
4152
- origin lookup is an exact match against the registered https:// form —
4153
- and would otherwise surface as the generic "could not reach" message
4154
- for what is really a typo. Assume https:// rather than making the
4155
- visitor retype it. */
4156
- if (!/^https?:\/\//i.test(origin)) origin = 'https://' + origin;
4157
- var note = ($('joinNote').value || '').trim();
4158
- var btn = $('joinBtn');
4159
- btn.disabled = true;
4160
- status.className = 'projMsg';
4161
- status.textContent = 'Requesting…';
4162
- postJoinRequest(origin, note).then(function (res) {
4163
- btn.disabled = false;
4164
- if (res.unauthenticated) { status.className = 'projMsg err'; status.innerHTML = signInAgain(); return; }
4165
- if (res.ok && res.data && res.data.message) {
4166
- status.className = 'projMsg' + (res.data.status === 'requested' ? ' ok' : '');
4167
- status.textContent = res.data.message;
4168
- if (res.data.status === 'requested') {
4169
- $('joinOrigin').value = '';
4170
- $('joinNote').value = '';
4171
- }
4172
- } else {
4173
- var e = apiError(res.data, res.status);
4174
- status.className = 'projMsg err';
4175
- status.textContent = e.message || 'Could not reach that project — try again in a moment.';
4176
- }
4177
- }).catch(function () {
4178
- btn.disabled = false;
4179
- status.className = 'projMsg err';
4180
- status.textContent = 'Couldn’t reach the platform — check your connection and try again.';
4181
- });
4182
- });
4183
-
4184
3659
  $('onlyMine').addEventListener('change', applyMapFilter);
4185
3660
  $('mapQ').addEventListener('input', applyMapFilter);
4186
3661
  /* ── the sort, off the column head (task 1003356) ──
@@ -11158,7 +10633,7 @@ summary{min-height:24px;padding:3px 0;}
11158
10633
  $('gate').hidden = true;
11159
10634
  $('mine').hidden = false;
11160
10635
  load();
11161
- return Promise.all([loadMemberships(), loadOwnedInstances(), loadInvites()]).then(openHandedProject);
10636
+ return Promise.all([loadMemberships(), loadOwnedInstances()]).then(openHandedProject);
11162
10637
  })
11163
10638
  .catch(function (err) {
11164
10639
  me = null;