@bongos/core 1.21.62 → 1.21.63
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 +40 -30
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +7 -3
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +301 -301
- package/docs/copy-registry.json +342 -342
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +687 -670
- package/modules/platform-identity/guilds.js +32 -0
- package/modules/platform-identity/routes/guilds.js +10 -3
- package/modules/public-landing/public/assets/cosmos.css +29 -3
- package/modules/public-landing/public/projects.html +294 -4
- package/modules/public-landing/public/projects.probes.json +18 -0
- package/modules/public-landing/public/projects.states.json +3 -0
- package/modules/ui-design/kit/fixtures/guilds.json +7 -0
- package/modules/ui-design/kit/fixtures/me__guild-requests.json +9 -0
- package/modules/ui-design/kit/serve.js +41 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/guilds_api.mjs +31 -0
- package/tests/guilds_db.mjs +7 -0
- package/tests/hub_engagement_cards.mjs +9 -6
- package/tests/hub_guild_cards.mjs +392 -0
|
@@ -647,6 +647,37 @@ async function listGuildRequests({ guildId, ownerGithubId }, { pool = defaultPoo
|
|
|
647
647
|
return { incoming, outgoing };
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
+
// The join requests waiting on the caller as an OWNER, across every guild they
|
|
651
|
+
// own, in ONE statement (task 1004583): the hub's Invitations reads this rather
|
|
652
|
+
// than one queue per owned guild. listGuildRequests' request leg keyed on the
|
|
653
|
+
// caller instead of one guild — a requester floors on consent alone, the owner
|
|
654
|
+
// test re-asserted per row — and its seen_at stamp. Each row names its guild.
|
|
655
|
+
async function listOwnedRequests({ githubId }, { pool = defaultPool } = {}) {
|
|
656
|
+
const { rows } = await pool.query(
|
|
657
|
+
`SELECT r.id::text AS id, r.created_at, r.seen_at, g.slug, g.name, ${PERSON_COLUMNS}
|
|
658
|
+
FROM platform_identity_guild_requests r
|
|
659
|
+
JOIN platform_identity_guilds g ON g.id = r.guild_id
|
|
660
|
+
JOIN platform_identity_accounts pa ON pa.github_id = r.github_id
|
|
661
|
+
WHERE r.kind = 'request' AND r.status = 'pending'
|
|
662
|
+
AND r.created_at > now() - make_interval(days => $2::int)
|
|
663
|
+
AND ${ownerExistsSql('r.guild_id', '$1')}
|
|
664
|
+
AND ${accountActiveSql('pa')}
|
|
665
|
+
ORDER BY r.created_at DESC, r.id DESC
|
|
666
|
+
LIMIT $3`,
|
|
667
|
+
[githubId, REQUEST_EXPIRY_DAYS, LIST_MAX],
|
|
668
|
+
);
|
|
669
|
+
const unseen = rows.filter((r) => !r.seen_at).map((r) => r.id);
|
|
670
|
+
if (unseen.length) {
|
|
671
|
+
await pool.query(
|
|
672
|
+
`UPDATE platform_identity_guild_requests r SET seen_at = now()
|
|
673
|
+
WHERE r.id = ANY($1::bigint[]) AND r.kind = 'request' AND r.seen_at IS NULL
|
|
674
|
+
AND ${ownerExistsSql('r.guild_id', '$2')}`,
|
|
675
|
+
[unseen, githubId],
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return rows.map(({ slug, name, ...r }) => ({ ...r, guild: { slug, name } }));
|
|
679
|
+
}
|
|
680
|
+
|
|
650
681
|
// ---------------------------------------------------------------------------
|
|
651
682
|
// The public read (D4)
|
|
652
683
|
// ---------------------------------------------------------------------------
|
|
@@ -707,5 +738,6 @@ module.exports = {
|
|
|
707
738
|
cancelRequest,
|
|
708
739
|
listMyRequests,
|
|
709
740
|
listGuildRequests,
|
|
741
|
+
listOwnedRequests,
|
|
710
742
|
getPublicGuild,
|
|
711
743
|
};
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// POST /guilds create {slug, name, description?, visibility}
|
|
13
13
|
// GET /me/guilds my guilds, with my own two switches
|
|
14
14
|
// GET /me/guild-requests invites to me (the card) + requests I sent
|
|
15
|
+
// + requests to the guilds I own (owner_incoming)
|
|
15
16
|
// GET /guilds/:slug/members the members' roster (members only; 404 else)
|
|
16
17
|
// PATCH /guilds/:slug/membership my switches {shown_publicly?, counted_in_totals?}
|
|
17
18
|
// DELETE /guilds/:slug/membership leave (the owner transfers first, or is last and dissolves)
|
|
@@ -154,13 +155,19 @@ module.exports = function guildRoutes() {
|
|
|
154
155
|
}));
|
|
155
156
|
|
|
156
157
|
// rank: any-builder — MY queue: invites addressed to me, each carrying the
|
|
157
|
-
// card (the guild's current visibility, which an accept must echo back),
|
|
158
|
-
//
|
|
158
|
+
// card (the guild's current visibility, which an accept must echo back), the
|
|
159
|
+
// join requests I sent, and the join requests waiting on me as the owner of a
|
|
160
|
+
// guild, each naming its guild (task 1004583 — one read for the hub, never one
|
|
161
|
+
// per owned guild). Stamps seen_at on the invites and on those requests.
|
|
159
162
|
router.get('/me/guild-requests', requireBuilder, handle('GET /me/guild-requests', 'guild_requests_read_failed', async (req, res) => {
|
|
160
163
|
const me = await actingAccount(req.builder);
|
|
161
164
|
if (me.fail) return res.fail(...me.fail);
|
|
162
165
|
const { incoming, outgoing } = await guilds.listMyRequests({ githubId: me.githubId }, { pool });
|
|
163
|
-
|
|
166
|
+
const ownerIncoming = await guilds.listOwnedRequests({ githubId: me.githubId }, { pool });
|
|
167
|
+
res.json({
|
|
168
|
+
incoming, outgoing, owner_incoming: ownerIncoming,
|
|
169
|
+
incoming_count: incoming.length, outgoing_count: outgoing.length, owner_incoming_count: ownerIncoming.length,
|
|
170
|
+
});
|
|
164
171
|
}));
|
|
165
172
|
|
|
166
173
|
// rank: any-builder — the members' roster, for members only (the own-scope
|
|
@@ -1325,13 +1325,39 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1325
1325
|
.mineJoinRow input::placeholder{color:var(--ink-faint);}
|
|
1326
1326
|
.mineJoinNote{display:block;width:100%;box-sizing:border-box;margin-top:10px;min-height:64px;padding:12px 16px;font:inherit;font-size:14px;line-height:1.5;color:var(--ink);background:var(--bg-card);border:1px solid var(--ctl-line);border-radius:14px;resize:vertical;}
|
|
1327
1327
|
.mineJoinNote::placeholder{color:var(--ink-faint);}
|
|
1328
|
-
/* the guild cards (task 1002303, R27):
|
|
1329
|
-
hairline between
|
|
1328
|
+
/* the guild cards (task 1002303, R27): more lists under the invitations, one
|
|
1329
|
+
hairline between any two that have rows; each card's note waits behind
|
|
1330
1330
|
"Add a note", and its status line takes no room until it has something to say */
|
|
1331
|
-
.mineRows:not(:empty)
|
|
1331
|
+
.mineRows:not(:empty)~.mineRows .mineRow:first-child{border-top:1px solid var(--rule-soft);}
|
|
1332
1332
|
.engageRow .mineJoinNote{max-width:62ch;}
|
|
1333
1333
|
.engageRow .projMsg{margin-top:6px;min-height:0;}
|
|
1334
1334
|
.engageRow .projMsg:empty{margin:0;}
|
|
1335
|
+
/* a guild's invite to you and a join request to your guild (task 1004583). The
|
|
1336
|
+
invite carries the guild page's counted pair (guild.html's .aOpt voice, set
|
|
1337
|
+
tighter for a row) with the D4.4 notice under it, said before accept. */
|
|
1338
|
+
.guildRow .projMsg{margin-top:6px;min-height:0;}
|
|
1339
|
+
.guildRow .projMsg:empty{margin:0;}
|
|
1340
|
+
/* the invite's buttons sit at its foot, after the notice they answer */
|
|
1341
|
+
.guildRow--invite .mineRow__actions{align-self:flex-end;}
|
|
1342
|
+
.guildRow__desc{margin:4px 0 0;font-size:14px;line-height:1.5;color:var(--ink-soft);max-width:62ch;}
|
|
1343
|
+
.guildCount{display:grid;gap:8px;margin-top:12px;max-width:66ch;}
|
|
1344
|
+
.guildCount__label{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
1345
|
+
.guildCount__opts{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
|
|
1346
|
+
.guildCount__opt{position:relative;display:block;margin:0;padding:12px 16px 14px;border-left:1px solid var(--rule-soft);cursor:pointer;}
|
|
1347
|
+
.guildCount__opt:first-child{border-left:0;padding-left:0;}
|
|
1348
|
+
.guildCount__opt input{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);}
|
|
1349
|
+
.guildCount__opt>span{display:block;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
1350
|
+
.guildCount__opt b{display:flex;align-items:center;gap:10px;margin-bottom:4px;font-family:var(--font-body);font-weight:500;font-size:14.5px;color:var(--ink);}
|
|
1351
|
+
.guildCount__opt b::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
|
|
1352
|
+
.guildCount__opt:has(input:checked) b::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
1353
|
+
.guildCount__opt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
1354
|
+
.guildCount__opt:has(input:disabled){cursor:default;}
|
|
1355
|
+
.guildCount__what{margin:0;font-size:13px;line-height:1.5;color:var(--ink-soft);max-width:70ch;}
|
|
1356
|
+
@media (max-width:480px){
|
|
1357
|
+
.guildCount__opts{grid-template-columns:1fr;}
|
|
1358
|
+
.guildCount__opt{padding:10px 0 12px;border-left:0;border-top:1px solid var(--rule-soft);}
|
|
1359
|
+
.guildCount__opt:first-child{border-top:0;}
|
|
1360
|
+
}
|
|
1335
1361
|
|
|
1336
1362
|
/* ── the ledger row: planet · name and chip · the facts · the rail · the
|
|
1337
1363
|
doors. The planet and the doors are fixed widths; the three middle tracks
|
|
@@ -963,11 +963,18 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
963
963
|
<section id="mineInvites" hidden aria-labelledby="mineInvitesH">
|
|
964
964
|
<h2 id="mineInvitesH">Invitations</h2>
|
|
965
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>
|
|
966
970
|
<!-- a guild you are in, engaging a project (task 1002303, R27; ADR 0365):
|
|
967
971
|
"G is applying to P" and "P invited G", from GET /me/guild-engagements.
|
|
968
972
|
Each card's Apply is the ordinary join request with `guild` added, so
|
|
969
973
|
you apply as yourself, never as the guild. -->
|
|
970
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>
|
|
971
978
|
</section>
|
|
972
979
|
|
|
973
980
|
<section aria-labelledby="mineBuildingH">
|
|
@@ -3689,10 +3696,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3689
3696
|
paintInvitesSection();
|
|
3690
3697
|
}
|
|
3691
3698
|
|
|
3692
|
-
/* the Invitations section holds
|
|
3693
|
-
guild
|
|
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'];
|
|
3694
3703
|
function paintInvitesSection() {
|
|
3695
|
-
$('mineInvites').hidden =
|
|
3704
|
+
$('mineInvites').hidden = !INVITE_LISTS.some(function (id) { return $(id).children.length; });
|
|
3696
3705
|
}
|
|
3697
3706
|
|
|
3698
3707
|
function loadInvites() {
|
|
@@ -3701,7 +3710,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3701
3710
|
.then(function (res) { return res.ok ? res.json() : { invites: [] }; })
|
|
3702
3711
|
.then(function (d) { renderInvitesList(d.invites || []); })
|
|
3703
3712
|
.catch(function () { renderInvitesList([]); }),
|
|
3704
|
-
loadEngagements()
|
|
3713
|
+
loadEngagements(),
|
|
3714
|
+
loadGuildQueue()
|
|
3705
3715
|
]);
|
|
3706
3716
|
}
|
|
3707
3717
|
|
|
@@ -3826,6 +3836,286 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3826
3836
|
.catch(function () { renderEngagements([]); });
|
|
3827
3837
|
}
|
|
3828
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
|
+
|
|
3829
4119
|
/* the one relay both the map's per-card Apply (submitApply) and the
|
|
3830
4120
|
address-typed join field drive — POST /my-projects/join, the 401 case and
|
|
3831
4121
|
the 'requested' echo update kept in ONE place (task 1003774). Keys the
|
|
@@ -187,6 +187,24 @@
|
|
|
187
187
|
{ "name": "guild cards: the invited card applies on its own, with no note", "continue": true,
|
|
188
188
|
"steps": [["click","#engageList .engageRow:nth-child(2) .btn"],["wait",400]],
|
|
189
189
|
"expect": { "text": [["#engageList .engageRow:nth-child(2) .btn", "Applied"]], "hasClass": [["#engageList .engageRow:nth-child(2) .projMsg", "ok"]] } },
|
|
190
|
+
{"name": "guild invite cards (task 1004583): each says what joining shows and, before accept, that your numbers count — with the switch right there, set to count", "auth": true, "width": 1440, "url": "/projects?view=mine",
|
|
191
|
+
"steps": [["wait",900]],
|
|
192
|
+
"expect": {"visible": ["#guildInviteList", "#guildInviteList .guildRow:first-child .guildCount"], "count": [["#guildInviteList .guildRow", 2], ["#guildInviteList .guildRow:first-child a.linklike:not([hidden])", 1], ["#guildInviteList .guildRow:nth-child(2) a.linklike:not([hidden])", 0]], "text": [["#guildInviteList .guildRow:first-child .mineRow__name", "You’re invited to join Open Hands"], ["#guildInviteList .guildRow:first-child .guildCount__what", "Your numbers count toward this guild’s rounded totals — even while your record is hidden or your account is private. They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. Leave them out of any guild you’d rather not share them with."]], "checked": "#guildInviteList .guildRow:first-child input[value=\"on\"]"} },
|
|
193
|
+
{"name": "guild invite: Leave them out says what that means before anything is sent", "continue": true,
|
|
194
|
+
"steps": [["click","#guildInviteList .guildRow:first-child .guildCount__opt:nth-child(2)"],["wait",150]],
|
|
195
|
+
"expect": {"checked": "#guildInviteList .guildRow:first-child input[value=\"off\"]", "text": [["#guildInviteList .guildRow:first-child .guildCount__what", "Your numbers are left out of this guild’s totals, from its next weekly count."]]} },
|
|
196
|
+
{"name": "guild invite: Accept sends the card's visibility and the switch (the stub refuses an accept without the card's visibility) and the card says where your numbers went", "continue": true,
|
|
197
|
+
"steps": [["click","#guildInviteList .guildRow:first-child .btn"],["wait",400]],
|
|
198
|
+
"expect": {"hasClass": [["#guildInviteList .guildRow:first-child .projMsg", "ok"], ["#guildInviteList .guildRow:first-child .btn", "ghost"]], "text": [["#guildInviteList .guildRow:first-child .btn", "Joined"], ["#guildInviteList .guildRow:first-child .projMsg", "You’ve joined Open Hands, and your numbers are left out of its totals."]]} },
|
|
199
|
+
{"name": "guild invite: Decline drops the card, silently", "continue": true,
|
|
200
|
+
"steps": [["click","#guildInviteList .guildRow:nth-child(2) .btn.ghost"],["wait",400]],
|
|
201
|
+
"expect": {"count": [["#guildInviteList .guildRow", 1]]} },
|
|
202
|
+
{"name": "join request (task 1004583): the owner of Night Shift accepts one and declines the other, from the hub", "continue": true,
|
|
203
|
+
"steps": [["click","#guildRequestList .guildRow:first-child .btn"],["wait",400],["click","#guildRequestList .guildRow:nth-child(2) .btn.ghost"],["wait",400]],
|
|
204
|
+
"expect": {"count": [["#guildRequestList .guildRow", 1]], "hasClass": [["#guildRequestList .guildRow:first-child .projMsg", "ok"]], "text": [["#guildRequestList .guildRow:first-child .btn", "Accepted"], ["#guildRequestList .guildRow:first-child .projMsg", "Tomás Ek is in Night Shift now."]]} },
|
|
205
|
+
{"name": "guild cards at 320: the switch stacks and nothing scrolls sideways", "auth": true, "width": 320, "url": "/projects?view=mine",
|
|
206
|
+
"steps": [["wait",900]],
|
|
207
|
+
"expect": {"noOverflow": true, "visible": ["#guildInviteList .guildRow:first-child .guildCount"]} },
|
|
190
208
|
{ "name": "invite a guild (task 1002303, R27): a /g/ address gives one pending line per member on its public page, each a link to the hall", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
191
209
|
"steps": [["wait",900],["fill","#mInviteGuild","/g/forge-crew"],["click","#mInvite [data-invite-guild]"],["wait",400]],
|
|
192
210
|
"expect": { "count": [["#mInviteList li", 4]], "value": [["#mInviteGuild", ""]],
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
"in-mine-engagements": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["focus","#engageList .engageRow:first-child .btn"],["wait",200]], "expect": { "visible": ["#mineInvites", "#invitesList", "#engageList"], "count": [["#engageList .engageRow", 2], ["#engageList .engageRow .mineJoinNote:not([hidden])", 0]], "text": [["#engageList .engageRow:nth-child(1) .mineRow__name", "Forge Crew is applying to Tidepool — apply with them?"], ["#engageList .engageRow:nth-child(1) .mineRow__host", "tidepool.cloudbongos.com"], ["#engageList .engageRow:nth-child(2) .mineRow__name", "Lantern Works invited Forge Crew"], ["#engageList .engageRow:nth-child(1) .mineRow__hint", "You apply as yourself — add a note for the owner if you like."], ["#engageList .engageRow:nth-child(1) .btn", "Apply"]] } },
|
|
31
31
|
"in-mine-engagement-note": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","I like the boring parts too: migrations and the deploy path."],["wait",150]], "expect": { "visible": ["#engageList .engageRow:first-child .mineJoinNote"], "hidden": ["#engageList .engageRow:first-child .linklike"], "value": [["#engageList .engageRow:first-child .mineJoinNote", "I like the boring parts too: migrations and the deploy path."]] } },
|
|
32
32
|
"in-mine-engagement-applied": { "auth": true, "url": "/projects?view=mine", "actions": [["wait",900],["click","#engageList .engageRow:first-child .linklike"],["fill","#engageList .engageRow:first-child .mineJoinNote","I like the boring parts too: migrations and the deploy path."],["click","#engageList .engageRow:first-child .btn"],["wait",400]], "expect": { "hidden": ["#engageList .engageRow:first-child .mineJoinNote", "#engageList .engageRow:first-child .linklike"], "hasClass": [["#engageList .engageRow:first-child .projMsg", "ok"], ["#engageList .engageRow:first-child .btn", "ghost"]], "text": [["#engageList .engageRow:first-child .btn", "Applied"], ["#engageList .engageRow:first-child .projMsg", "Requested access. The project’s owner admits new builders; once approved, sign in to enter."], ["#engageList .engageRow:nth-child(2) .btn", "Apply"]] } },
|
|
33
|
+
"in-mine-guild-cards": {"auth": true, "url": "/projects?view=mine", "actions": [["wait", 900], ["focus", "#guildInviteList .guildRow:first-child .btn"], ["wait", 200]], "expect": {"visible": ["#mineInvites", "#guildInviteList", "#guildRequestList", "#guildInviteList .guildRow:first-child .guildCount"], "count": [["#guildInviteList .guildRow", 2], ["#guildRequestList .guildRow", 2], ["#guildInviteList .guildRow .guildCount", 2], ["#guildRequestList .guildRow .guildCount", 0]], "text": [["#guildInviteList .guildRow:nth-child(1) .mineRow__name", "You’re invited to join Open Hands"], ["#guildInviteList .guildRow:nth-child(2) .mineRow__name", "You’re invited to join Quiet Room"], ["#guildInviteList .guildRow:nth-child(1) .guildCount__what", "Your numbers count toward this guild’s rounded totals — even while your record is hidden or your account is private. They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. Leave them out of any guild you’d rather not share them with."], ["#guildRequestList .guildRow:nth-child(1) .mineRow__name", "Tomás Ek asked to join Night Shift"], ["#guildRequestList .guildRow:nth-child(2) .mineRow__name", "@kai-lowe asked to join Night Shift"]], "checked": "#guildInviteList .guildRow:first-child input[value=\"on\"]"}},
|
|
34
|
+
"in-mine-guild-count-off": {"auth": true, "url": "/projects?view=mine", "actions": [["wait", 900], ["click", "#guildInviteList .guildRow:first-child .guildCount__opt:nth-child(2)"], ["wait", 150]], "expect": {"checked": "#guildInviteList .guildRow:first-child input[value=\"off\"]", "text": [["#guildInviteList .guildRow:first-child .guildCount__what", "Your numbers are left out of this guild’s totals, from its next weekly count."], ["#guildInviteList .guildRow:first-child .btn", "Accept"]]}},
|
|
35
|
+
"in-mine-guild-joined": {"auth": true, "url": "/projects?view=mine", "actions": [["wait", 900], ["click", "#guildInviteList .guildRow:first-child .guildCount__opt:nth-child(2)"], ["click", "#guildInviteList .guildRow:first-child .btn"], ["wait", 400]], "expect": {"hidden": ["#guildInviteList .guildRow:first-child .btn.ghost + .btn"], "hasClass": [["#guildInviteList .guildRow:first-child .projMsg", "ok"], ["#guildInviteList .guildRow:first-child .btn", "ghost"]], "text": [["#guildInviteList .guildRow:first-child .btn", "Joined"], ["#guildInviteList .guildRow:first-child .projMsg", "You’ve joined Open Hands, and your numbers are left out of its totals."]]}},
|
|
33
36
|
"in-manage-app-live": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["focus","#mApp a"]], "expect": {"visible":["#mAppCard","#mApp .appStat","#mApp .appVars"],"hidden":[]} },
|
|
34
37
|
"in-manage-app-building": { "auth": true, "url": "/projects?view=manage&id=102", "actions": [["wait",900],["focus","#mApp a"]], "expect": {"visible":["#mAppCard","#mApp .appStages"],"hidden":[]} },
|
|
35
38
|
"in-manage-app-failed": { "auth": true, "url": "/projects?view=manage&id=105", "actions": [["wait",900],["focus","#mApp .appRetryBtn"]], "expect": {"visible":["#mAppCard","#mApp p.err","#mApp .appRetryBtn"],"hidden":["#mApp .appRetry"]} },
|
|
@@ -51,6 +51,13 @@
|
|
|
51
51
|
{ "handle": "sol-reyes", "display_name": "Sol Reyes", "avatar_url": null, "membership_kind": "member" }
|
|
52
52
|
],
|
|
53
53
|
"count": 3
|
|
54
|
+
},
|
|
55
|
+
"requests": {
|
|
56
|
+
"incoming": [
|
|
57
|
+
{ "id": "601", "created_at": "2026-10-04T19:20:00.000Z", "seen_at": null, "handle": "tomas-ek", "display_name": "Tomás Ek", "avatar_url": null },
|
|
58
|
+
{ "id": "602", "created_at": "2026-10-02T11:05:00.000Z", "seen_at": null, "handle": "kai-lowe", "display_name": null, "avatar_url": null }
|
|
59
|
+
],
|
|
60
|
+
"outgoing": []
|
|
54
61
|
}
|
|
55
62
|
},
|
|
56
63
|
"open-hands": {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"incoming": [
|
|
3
|
+
{ "id": "501", "created_at": "2026-10-04T15:10:00.000Z", "seen_at": null, "slug": "open-hands", "name": "Open Hands", "description": "A newer guild for first-time builders. Ask anything.", "visibility": "public" },
|
|
4
|
+
{ "id": "502", "created_at": "2026-10-03T08:45:00.000Z", "seen_at": "2026-10-03T09:00:00.000Z", "slug": "quiet-room", "name": "Quiet Room", "description": "", "visibility": "unlisted" }
|
|
5
|
+
],
|
|
6
|
+
"outgoing": [],
|
|
7
|
+
"incoming_count": 2,
|
|
8
|
+
"outgoing_count": 0
|
|
9
|
+
}
|
|
@@ -512,6 +512,44 @@ function guildInvitePost(res, body) {
|
|
|
512
512
|
const invites = (g.public.members || []).map((m) => ({ handle: m.handle, display_name: m.display_name || null, invite_url: 'https://mercury.example.test/builders/watch?invite=' + encodeURIComponent(m.handle) }));
|
|
513
513
|
return json(res, 200, { invited: false, guild: { slug: g.public.guild.slug, name: g.public.guild.name }, invites, count: invites.length });
|
|
514
514
|
}
|
|
515
|
+
// The guild cards in the mine view's invitations (task 1004583): GET /me/guild-requests is
|
|
516
|
+
// the invites to the builder (me__guild-requests.json, each card with the guild's CURRENT
|
|
517
|
+
// visibility) plus `owner_incoming`, the join requests to every held guild they own (its
|
|
518
|
+
// `requests` in guilds.json), each naming its guild — one read, as the route answers it. The
|
|
519
|
+
// stub mirrors the routes' refusals: an invitee's accept must echo the card's visibility (400
|
|
520
|
+
// without one, 409 with the guild's current one when it differs), only `visibility` and
|
|
521
|
+
// `counted_in_totals` are taken, a row the caller may not answer is one 404, and a decline is
|
|
522
|
+
// a SILENT 204. Nothing is written: a reload draws the same cards, as the join relay's
|
|
523
|
+
// 'requested' does.
|
|
524
|
+
function ownedRequests() {
|
|
525
|
+
return Object.values(guildsHeld()).filter((g) => g.members && g.members.you.membership_kind === 'owner')
|
|
526
|
+
.flatMap((g) => ((g.requests && g.requests.incoming) || []).map((r) => ({ ...r, guild: { slug: g.members.guild.slug, name: g.members.guild.name } })));
|
|
527
|
+
}
|
|
528
|
+
function myGuildRequests(res) {
|
|
529
|
+
const q = heldFixture('me__guild-requests');
|
|
530
|
+
const owner = ownedRequests();
|
|
531
|
+
return json(res, 200, { ...q, owner_incoming: owner, owner_incoming_count: owner.length });
|
|
532
|
+
}
|
|
533
|
+
function guildRequestAnswer(res, id, verb, body) {
|
|
534
|
+
const notFound = () => refuse(res, 404, 'guild_request_not_found', 'guild_request_not_found');
|
|
535
|
+
if (!/^\d{1,19}$/.test(id)) return notFound();
|
|
536
|
+
const bad = Object.keys(body).find((k) => !['visibility', 'counted_in_totals'].includes(k));
|
|
537
|
+
if (bad) return refuse(res, 400, 'validation_failed', `unknown field: ${bad}`);
|
|
538
|
+
if (body.visibility !== undefined && !['public', 'unlisted'].includes(body.visibility)) return refuse(res, 400, 'validation_failed', 'visibility must be public or unlisted');
|
|
539
|
+
if (body.counted_in_totals !== undefined && typeof body.counted_in_totals !== 'boolean') return refuse(res, 400, 'validation_failed', 'counted_in_totals must be a boolean');
|
|
540
|
+
const invite = (heldFixture('me__guild-requests').incoming || []).find((r) => r.id === id);
|
|
541
|
+
const request = Object.values(guildsHeld()).some((g) => g.members && g.members.you.membership_kind === 'owner'
|
|
542
|
+
&& ((g.requests && g.requests.incoming) || []).some((r) => r.id === id));
|
|
543
|
+
if (!invite && !request) return notFound();
|
|
544
|
+
if (verb === 'decline') { res.writeHead(204, { 'cache-control': 'no-store' }); return res.end(); }
|
|
545
|
+
if (invite) {
|
|
546
|
+
if (!body.visibility) return refuse(res, 400, 'guild_card_visibility_required', 'send the visibility your invite card showed');
|
|
547
|
+
const held = guildsHeld()[invite.slug];
|
|
548
|
+
const now = (held && held.members && held.members.guild.visibility) || invite.visibility;
|
|
549
|
+
if (body.visibility !== now) return json(res, 409, { error: { code: 'guild_visibility_changed', message: 'guild_visibility_changed', details: { visibility: now } } });
|
|
550
|
+
}
|
|
551
|
+
return json(res, 200, { status: 'accepted' });
|
|
552
|
+
}
|
|
515
553
|
function guildEngagementLive(slug, origin) {
|
|
516
554
|
const g = typeof slug === 'string' && guildsHeld()[slug.toLowerCase()];
|
|
517
555
|
if (!g || !g.members) return false;
|
|
@@ -540,6 +578,9 @@ const server = http.createServer((req, res) => {
|
|
|
540
578
|
if (guild && guild[2] === '/members' && req.method === 'GET') return AUTH ? guildRead(res, guild[1], 'members', origin) : json(res, 401, UNAUTH);
|
|
541
579
|
if (guild && guild[2] === '/membership' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => guildMembershipPatch(res, guild[1], body)) : json(res, 401, UNAUTH);
|
|
542
580
|
if (guild && guild[2] === '/engagements' && req.method === 'POST') return AUTH ? readBody(req, (body) => guildEngagementPost(res, guild[1], body)) : json(res, 401, UNAUTH);
|
|
581
|
+
if (rest === '/me/guild-requests' && req.method === 'GET') return AUTH ? myGuildRequests(res) : json(res, 401, UNAUTH);
|
|
582
|
+
const answer = rest.match(/^\/guild-requests\/([^/]+)\/(accept|decline)$/);
|
|
583
|
+
if (answer && req.method === 'POST') return AUTH ? readBody(req, (body) => guildRequestAnswer(res, answer[1], answer[2], body)) : json(res, 401, UNAUTH);
|
|
543
584
|
if (rest === '/provisioning/instances' && req.method === 'GET') return AUTH ? instances(res, null, false) : json(res, 401, UNAUTH);
|
|
544
585
|
// the planet-physics questions' pre-create check (task 1004418): nothing stops here —
|
|
545
586
|
// the kit carries no screen. Of the soft-limit table it mirrors only the three hide
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.63",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.63",
|
|
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.21.
|
|
3
|
+
"version": "1.21.63",
|
|
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/release-notes.json
CHANGED
|
@@ -9148,5 +9148,11 @@
|
|
|
9148
9148
|
"id": "1004584",
|
|
9149
9149
|
"text": "Shipping finished tasks now works for projects that install Bongos as a package, so their work lands instead of getting stuck halfway."
|
|
9150
9150
|
}
|
|
9151
|
+
],
|
|
9152
|
+
"1.21.63": [
|
|
9153
|
+
{
|
|
9154
|
+
"id": "1004583",
|
|
9155
|
+
"text": "Builders can now accept or decline a guild invite from their projects page, choosing first whether their numbers count toward the guild's totals, and guild owners can answer join requests there too."
|
|
9156
|
+
}
|
|
9151
9157
|
]
|
|
9152
9158
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.63'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|