@bongos/core 1.21.55 → 1.21.57
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 +54 -29
- package/docs/copy-inventory.md +62 -24
- package/docs/copy-registry.json +456 -89
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +32 -4
- package/docs/page-readings.json +421 -354
- package/modules/hall-ui/public/street-draw.js +1 -3
- package/modules/hall-ui/public/street.css +0 -1
- package/modules/hall-ui/public/street.html +0 -1
- package/modules/hall-ui/public/street.js +0 -2
- package/modules/hall-ui/records/governor-street.md +10 -0
- package/modules/public-landing/public/guild.html +571 -0
- package/modules/public-landing/public/guild.probes.json +53 -0
- package/modules/public-landing/public/guild.states.json +38 -0
- package/modules/ui-design/kit/fixtures/guilds.json +73 -0
- package/modules/ui-design/kit/serve.js +46 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +13 -0
- package/tests/hall_matter_card.mjs +1 -1
- package/tests/hall_street.mjs +12 -3
- package/tests/hub_guild_page.mjs +537 -0
- package/tests/privacy_matrix.mjs +126 -14
- package/tests/render_check.mjs +2 -2
- package/tests/ui_design_kit.mjs +3 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The guild page's interaction contract for modules/ui-design/kit/probe.js (task 1002301): signed out, the public page and the sign-in doors pointing back at the guild; signed in, the member's two switches driven end to end against the stub's held guilds (serve.js keeps fixtures/guilds.json for the run and refuses what PATCH /guilds/:slug/membership refuses), so a save the page re-reads comes back saved. The probes are a SEQUENCE over one stub: the prompt's yes lists the builder, later probes put them back off, and the 'not now' probe runs in a fresh browser (no remembered answer). A probe with `continue` keeps the previous page.",
|
|
3
|
+
"page": "/guild",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"probes": [
|
|
6
|
+
{ "name": "signed out: the public guild, and both sign-in doors return to it", "auth": false, "width": 1440, "url": "/g/forge-crew",
|
|
7
|
+
"steps": [["wait", 400]],
|
|
8
|
+
"expect": { "visible": ["#guildBody", "#signinTop"], "hidden": ["#gYou", "#missing"], "count": [["#gRoster li", 4]],
|
|
9
|
+
"href": [["#signinTop", "/api/bongos/auth/web/start?return=/g/forge-crew&repos=1"], ["#missingSignin", "/api/bongos/auth/web/start?return=/g/forge-crew&repos=1"]] } },
|
|
10
|
+
{ "name": "signed out: an unlisted guild is 'no guild here', and sign-in returns to it", "auth": false, "width": 1440, "url": "/g/night-shift",
|
|
11
|
+
"steps": [["wait", 400]],
|
|
12
|
+
"expect": { "visible": ["#missing", "#missingSignin"], "hidden": ["#guildBody"],
|
|
13
|
+
"href": [["#missingSignin", "/api/bongos/auth/web/start?return=/g/night-shift&repos=1"]] } },
|
|
14
|
+
{ "name": "a path that cannot be a slug is 'no guild here'", "auth": false, "width": 1440, "url": "/g/-bad-",
|
|
15
|
+
"steps": [["wait", 400]],
|
|
16
|
+
"expect": { "visible": ["#missing"], "hidden": ["#guildBody"] } },
|
|
17
|
+
{ "name": "signed in, a member not on the public page: asked first, the pair waits", "auth": true, "width": 1440, "url": "/g/forge-crew",
|
|
18
|
+
"steps": [["wait", 500]],
|
|
19
|
+
"expect": { "visible": ["#gYou", "#showAsk", "#askYes", "#askNo"], "hidden": ["#shownBand"], "count": [["#gRoster li", 6]],
|
|
20
|
+
"text": [["#countWhat", "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."]] } },
|
|
21
|
+
{ "name": "show me: the switch saves, the prompt gives way to the pair, set to Show me", "continue": true,
|
|
22
|
+
"steps": [["click", "#askYes"], ["wait", 500]],
|
|
23
|
+
"expect": { "text": [["#youMsg", "Saved — you’re listed on its public page."]], "hidden": ["#showAsk"], "visible": ["#shownBand"], "checked": "input[name=shown][value=on]" } },
|
|
24
|
+
{ "name": "nothing changed saves nothing", "continue": true,
|
|
25
|
+
"steps": [["click", "#youSave"], ["wait", 150]],
|
|
26
|
+
"expect": { "text": [["#youMsg", "Nothing has changed."]] } },
|
|
27
|
+
{ "name": "picking Leave them out says what that means before anything is saved", "continue": true,
|
|
28
|
+
"steps": [["click", "#countBand .aOpt:nth-child(2)"], ["wait", 150]],
|
|
29
|
+
"expect": { "checked": "input[name=counted][value=off]", "text": [["#countWhat", "Your numbers are left out of this guild’s totals, from its next weekly count."], ["#youMsg", "Nothing has changed."]] } },
|
|
30
|
+
{ "name": "both switches in one save: only what moved is sent, and the page re-reads", "continue": true,
|
|
31
|
+
"steps": [["click", "#shownBand .aOpt:nth-child(2)"], ["click", "#youSave"], ["wait", 500]],
|
|
32
|
+
"expect": { "text": [["#youMsg", "Saved — you’re off its public page, and your numbers are left out of its totals."]], "checked": "input[name=shown][value=off]", "hidden": ["#showAsk"] } },
|
|
33
|
+
{ "name": "a fresh visit, off the public page again: asked again; not now keeps you off and shows the pair", "auth": true, "width": 1440, "url": "/g/forge-crew",
|
|
34
|
+
"steps": [["wait", 500], ["click", "#askNo"], ["wait", 150]],
|
|
35
|
+
"expect": { "hidden": ["#showAsk"], "visible": ["#shownBand"], "checked": "input[name=shown][value=off]",
|
|
36
|
+
"text": [["#youMsg", "You’re staying off its public page. You can change that here at any time."]] } },
|
|
37
|
+
{ "name": "the owner of an unlisted guild: no public page, so no pair and no record", "auth": true, "width": 1440, "url": "/g/night-shift",
|
|
38
|
+
"steps": [["wait", 500]],
|
|
39
|
+
"expect": { "visible": ["#gYou", "#shownUnlisted"], "hidden": ["#shownBand", "#showAsk", "#gFigs"], "text": [["#gKicker", "Guild · unlisted"], ["#gYouSide", "You own this guild. Only you see this, and only you can change it."]] } },
|
|
40
|
+
{ "name": "a public guild you are not in: the public page, nothing of yours", "auth": true, "width": 1440, "url": "/g/open-hands",
|
|
41
|
+
"steps": [["wait", 500]],
|
|
42
|
+
"expect": { "visible": ["#guildBody"], "hidden": ["#gYou", "#missing"], "text": [["#gRecordNote", "Not counted yet. A guild’s record is counted once a week."]] } },
|
|
43
|
+
{ "name": "the hub's bar: the chip names the person, both bands closed, the page starts under the bar", "auth": true, "width": 1440, "url": "/g/open-hands",
|
|
44
|
+
"steps": [["wait", 400]],
|
|
45
|
+
"expect": { "expanded": false, "top": [["#guild", 64]], "visible": ["#projTrig", "#who"], "hidden": ["#signinTop", "#menuProjects", "#menuProfile"], "text": [["#whoLogin", "builder"]], "count": [["#mapQ", 0], ["[aria-current=\"page\"]", 0]] } },
|
|
46
|
+
{ "name": "Enter opens Projects and lands on the first item; Escape closes and returns focus", "continue": true,
|
|
47
|
+
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150], ["press", "Escape"], ["wait", 100]],
|
|
48
|
+
"expect": { "closed": "#menuProjects", "activeId": "projTrig", "expanded": false } },
|
|
49
|
+
{ "name": "phone width: the bar stays one line and nothing scrolls sideways", "auth": true, "width": 320, "url": "/g/forge-crew",
|
|
50
|
+
"steps": [["wait", 500]],
|
|
51
|
+
"expect": { "noOverflow": true, "visible": ["#guildBody"] } }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The guild page's states for modules/ui-design/kit/render.js (task 1002301, R25; ADR 0336 D4, ADR 0337 D4.4). One shell serves every /g/<slug>; the stub answers three guilds from fixtures/guilds.json. Signed out: forge-crew is the public page (four listed members, the rounded record), and night-shift — unlisted — is the same 'no guild here' an unknown slug gets, with sign-in. Signed in as the fixture builder: forge-crew is the members' view (everyone, six, with the prompt to be listed because they are a member who is not on its public page; the pair for that switch waits until the prompt is answered), night-shift is their unlisted guild (no public page, no published record), open-hands is a public guild they are not in (the public view, not counted yet), and an unknown slug is 'no guild here' without the sign-in. Dark only, like every apex page (ADR 0204).",
|
|
3
|
+
"page": "/guild",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modes": ["dark"],
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/guilds/[a-z0-9-]+(/members)?$"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "/api/(gds|bongos)/guilds/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found"],
|
|
9
|
+
"ignoreControls": [".vh"],
|
|
10
|
+
"states": {
|
|
11
|
+
"out-public": { "url": "/g/forge-crew", "auth": false, "actions": [["wait", 400]],
|
|
12
|
+
"expect": { "visible": ["#guildBody", "#gName", "#gRoster", "#gFigs", "#signinTop"], "hidden": ["#gYou", "#missing", "#loading", "#who"],
|
|
13
|
+
"text": [["#gName", "Forge Crew"], ["#gKicker", "Guild"], ["#gMeta", "4 members · since Sep 2026"], ["#tShipped", "100+"], ["#tCredits", "2,000+"], ["#tKarma", "50+"], ["#tProjects", "under 10"], ["#gRecordNote", "Counted on 1 Oct 2026. Projects are the public projects its listed members build on."]],
|
|
14
|
+
"count": [["#gRoster li", 4]] } },
|
|
15
|
+
"out-unlisted": { "url": "/g/night-shift", "auth": false, "actions": [["wait", 400]],
|
|
16
|
+
"expect": { "visible": ["#missing", "#missingSignin"], "hidden": ["#guildBody", "#loading"],
|
|
17
|
+
"text": [["#missingWhy", "There is no public guild at this address. If it is an unlisted guild you belong to, sign in to see it."]] } },
|
|
18
|
+
"in-member": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 500]],
|
|
19
|
+
"expect": { "visible": ["#guildBody", "#gYou", "#showAsk", "#countWhat", "#who"], "hidden": ["#shownBand", "#shownUnlisted", "#missing", "#signinTop"],
|
|
20
|
+
"text": [["#gKicker", "Guild · public"], ["#gMeta", "6 members · 4 on the public page · since Sep 2026"], ["#gYouSide", "You’re a member of this guild. Only you see this, and only you can change it."]],
|
|
21
|
+
"checked": "input[name=shown][value=off]",
|
|
22
|
+
"count": [["#gRoster li", 6]] } },
|
|
23
|
+
"in-unlisted-owner": { "url": "/g/night-shift", "auth": true, "actions": [["wait", 500]],
|
|
24
|
+
"expect": { "visible": ["#guildBody", "#gYou", "#shownUnlisted"], "hidden": ["#showAsk", "#shownBand", "#gFigs", "#gDesc"],
|
|
25
|
+
"text": [["#gKicker", "Guild · unlisted"], ["#shownUnlisted", "This guild is unlisted, so it has no public page. If you make it public, you’ll be listed on it as its owner, and each member will be asked before they are."], ["#gMeta", "3 members"], ["#gRecordNote", "This guild is unlisted, so its record isn’t published anywhere."], ["#countWhat", "Your numbers are left out of this guild’s totals, from its next weekly count."]],
|
|
26
|
+
"checked": "input[name=counted][value=off]" } },
|
|
27
|
+
"in-not-member": { "url": "/g/open-hands", "auth": true, "actions": [["wait", 500]],
|
|
28
|
+
"expect": { "visible": ["#guildBody", "#gRoster"], "hidden": ["#gYou", "#missing"],
|
|
29
|
+
"text": [["#gKicker", "Guild"], ["#gMeta", "2 members · since Oct 2026"], ["#tShipped", "—"], ["#gRecordNote", "Not counted yet. A guild’s record is counted once a week."]],
|
|
30
|
+
"count": [["#gRoster li", 2]] } },
|
|
31
|
+
"in-missing": { "url": "/g/no-such-guild", "auth": true, "actions": [["wait", 500]],
|
|
32
|
+
"expect": { "visible": ["#missing"], "hidden": ["#guildBody", "#missingSignin"],
|
|
33
|
+
"text": [["#missingWhy", "There is no guild at this address that you can see. A guild that is unlisted is shown only to its members."]] } },
|
|
34
|
+
"in-profile-band": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 500], ["click", "#who"], ["wait", 300]],
|
|
35
|
+
"expect": { "visible": ["#menuProfile"], "hidden": ["#menuProjects"], "text": [["#bandWhoLogin", "builder"]] } }
|
|
36
|
+
},
|
|
37
|
+
"reducedMotion": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 400]] }
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"forge-crew": {
|
|
3
|
+
"public": {
|
|
4
|
+
"guild": {
|
|
5
|
+
"slug": "forge-crew",
|
|
6
|
+
"name": "Forge Crew",
|
|
7
|
+
"description": "Builders who like the unglamorous parts: migrations, test harnesses, the deploy path. We pick up the work nobody else claims.",
|
|
8
|
+
"created_at": "2026-09-12T10:20:00.000Z"
|
|
9
|
+
},
|
|
10
|
+
"members": [
|
|
11
|
+
{ "handle": "mara-quinn", "display_name": "Mara Quinn", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
|
|
12
|
+
{ "handle": "ines-duarte", "display_name": "Ines Duarte", "avatar_url": null, "membership_kind": "member" },
|
|
13
|
+
{ "handle": "kip", "display_name": "Kip", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" },
|
|
14
|
+
{ "handle": "theo-park", "display_name": "Theo Park", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" }
|
|
15
|
+
],
|
|
16
|
+
"size": 4,
|
|
17
|
+
"totals": { "credits": 2000, "works_shipped": 100, "karma": 50, "projects": 0, "as_of": "2026-10-01T03:00:00.000Z" }
|
|
18
|
+
},
|
|
19
|
+
"members": {
|
|
20
|
+
"guild": {
|
|
21
|
+
"slug": "forge-crew",
|
|
22
|
+
"name": "Forge Crew",
|
|
23
|
+
"description": "Builders who like the unglamorous parts: migrations, test harnesses, the deploy path. We pick up the work nobody else claims.",
|
|
24
|
+
"visibility": "public"
|
|
25
|
+
},
|
|
26
|
+
"you": { "membership_kind": "member", "shown_publicly": false, "counted_in_totals": true },
|
|
27
|
+
"members": [
|
|
28
|
+
{ "handle": "mara-quinn", "display_name": "Mara Quinn", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
|
|
29
|
+
{ "handle": "builder", "display_name": "A Builder", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" },
|
|
30
|
+
{ "handle": "ines-duarte", "display_name": "Ines Duarte", "avatar_url": null, "membership_kind": "member" },
|
|
31
|
+
{ "handle": "kip", "display_name": "Kip", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" },
|
|
32
|
+
{ "handle": "sol-reyes", "display_name": "Sol Reyes", "avatar_url": null, "membership_kind": "member" },
|
|
33
|
+
{ "handle": "theo-park", "display_name": "Theo Park", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" }
|
|
34
|
+
],
|
|
35
|
+
"count": 6
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"night-shift": {
|
|
39
|
+
"public": null,
|
|
40
|
+
"members": {
|
|
41
|
+
"guild": {
|
|
42
|
+
"slug": "night-shift",
|
|
43
|
+
"name": "Night Shift",
|
|
44
|
+
"description": "",
|
|
45
|
+
"visibility": "unlisted"
|
|
46
|
+
},
|
|
47
|
+
"you": { "membership_kind": "owner", "shown_publicly": false, "counted_in_totals": false },
|
|
48
|
+
"members": [
|
|
49
|
+
{ "handle": "builder", "display_name": "A Builder", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
|
|
50
|
+
{ "handle": "ines-duarte", "display_name": "Ines Duarte", "avatar_url": null, "membership_kind": "member" },
|
|
51
|
+
{ "handle": "sol-reyes", "display_name": "Sol Reyes", "avatar_url": null, "membership_kind": "member" }
|
|
52
|
+
],
|
|
53
|
+
"count": 3
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"open-hands": {
|
|
57
|
+
"public": {
|
|
58
|
+
"guild": {
|
|
59
|
+
"slug": "open-hands",
|
|
60
|
+
"name": "Open Hands",
|
|
61
|
+
"description": "A newer guild for first-time builders. Ask anything.",
|
|
62
|
+
"created_at": "2026-10-03T16:05:00.000Z"
|
|
63
|
+
},
|
|
64
|
+
"members": [
|
|
65
|
+
{ "handle": "ada-ng", "display_name": "Ada Ng", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
|
|
66
|
+
{ "handle": "ruth-okafor", "display_name": "Ruth Okafor", "avatar_url": null, "membership_kind": "member" }
|
|
67
|
+
],
|
|
68
|
+
"size": 2,
|
|
69
|
+
"totals": null
|
|
70
|
+
},
|
|
71
|
+
"members": null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -436,6 +436,44 @@ function handlePatch(res, body) {
|
|
|
436
436
|
return json(res, 200, { handle: h, previous_handle: previous || null });
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
// The hub's guild page (task 1002301): GET /guilds/:slug (public), GET /guilds/:slug/members
|
|
440
|
+
// and PATCH /guilds/:slug/membership (own-scope), answered from ONE set of guilds held for the
|
|
441
|
+
// run (guilds.json, or the file a states file's stub.fixtures names), keyed by slug, each with
|
|
442
|
+
// the public read's body (null = the 404 an unlisted or unknown guild answers) and the
|
|
443
|
+
// signed-in builder's members read (null = not a member: the same 404). The PATCH mirrors the
|
|
444
|
+
// route's schema — the two booleans, at least one — so a save the page re-reads comes back saved.
|
|
445
|
+
let GUILDS = null;
|
|
446
|
+
function guildsHeld() {
|
|
447
|
+
if (!GUILDS) {
|
|
448
|
+
const name = 'guilds';
|
|
449
|
+
GUILDS = JSON.parse(fs.readFileSync(path.join(FIXTURES, (ALIAS[name] || name) + '.json'), 'utf8'));
|
|
450
|
+
}
|
|
451
|
+
return GUILDS;
|
|
452
|
+
}
|
|
453
|
+
// a root-relative avatar is this stub's own (/__kit/avatar.svg), absolutised as me() does
|
|
454
|
+
function withAvatars(body, origin) {
|
|
455
|
+
if (!body || !Array.isArray(body.members)) return body;
|
|
456
|
+
return { ...body, members: body.members.map((m) => (typeof m.avatar_url === 'string' && m.avatar_url.startsWith('/') ? { ...m, avatar_url: origin + m.avatar_url } : m)) };
|
|
457
|
+
}
|
|
458
|
+
const PAGE_ALIAS = { g: 'guild' };
|
|
459
|
+
const GUILD_NOT_FOUND ={ error: { code: 'guild_not_found', message: 'guild_not_found' } };
|
|
460
|
+
function guildRead(res, slug, half, origin) {
|
|
461
|
+
const g = guildsHeld()[String(slug).toLowerCase()];
|
|
462
|
+
const body = g && g[half];
|
|
463
|
+
return body ? json(res, 200, withAvatars(body, origin)) : json(res, 404, GUILD_NOT_FOUND);
|
|
464
|
+
}
|
|
465
|
+
function guildMembershipPatch(res, slug, body) {
|
|
466
|
+
const keys = Object.keys(body);
|
|
467
|
+
const bad = keys.find((k) => !['shown_publicly', 'counted_in_totals'].includes(k));
|
|
468
|
+
if (bad) return refuse(res, 400, 'validation_failed', `unknown field: ${bad}`);
|
|
469
|
+
for (const k of keys) if (typeof body[k] !== 'boolean') return refuse(res, 400, 'validation_failed', `${k} must be a boolean`);
|
|
470
|
+
if (!keys.length) return refuse(res, 400, 'nothing_to_update', 'send shown_publicly and/or counted_in_totals');
|
|
471
|
+
const g = guildsHeld()[String(slug).toLowerCase()];
|
|
472
|
+
if (!g || !g.members) return json(res, 404, GUILD_NOT_FOUND);
|
|
473
|
+
Object.assign(g.members.you, body);
|
|
474
|
+
return json(res, 200, { shown_publicly: g.members.you.shown_publicly, counted_in_totals: g.members.you.counted_in_totals });
|
|
475
|
+
}
|
|
476
|
+
|
|
439
477
|
const server = http.createServer((req, res) => {
|
|
440
478
|
const u = new URL(req.url, 'http://x');
|
|
441
479
|
const p = u.pathname;
|
|
@@ -453,6 +491,10 @@ const server = http.createServer((req, res) => {
|
|
|
453
491
|
if (rest === '/me/cross-project' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => crossProjectPatch(res, body)) : json(res, 401, UNAUTH);
|
|
454
492
|
if (rest === '/me/profile' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => profilePatch(res, body)) : json(res, 401, UNAUTH);
|
|
455
493
|
if (rest === '/me/handle' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => handlePatch(res, body)) : json(res, 401, UNAUTH);
|
|
494
|
+
const guild = rest.match(/^\/guilds\/([^/]+)(\/members|\/membership)?$/);
|
|
495
|
+
if (guild && !guild[2] && req.method === 'GET') return guildRead(res, guild[1], 'public', origin);
|
|
496
|
+
if (guild && guild[2] === '/members' && req.method === 'GET') return AUTH ? guildRead(res, guild[1], 'members', origin) : json(res, 401, UNAUTH);
|
|
497
|
+
if (guild && guild[2] === '/membership' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => guildMembershipPatch(res, guild[1], body)) : json(res, 401, UNAUTH);
|
|
456
498
|
if (rest === '/provisioning/instances' && req.method === 'GET') return AUTH ? instances(res, null, false) : json(res, 401, UNAUTH);
|
|
457
499
|
// the planet-physics questions' pre-create check (task 1004418): nothing stops here —
|
|
458
500
|
// the kit carries no screen. Of the soft-limit table it mirrors only the three hide
|
|
@@ -550,7 +592,10 @@ const server = http.createServer((req, res) => {
|
|
|
550
592
|
// TASK_DETAIL_PAGE_RE / IDEA_DETAIL_PAGE_RE routes). Without this the states
|
|
551
593
|
// for those pages could only be rendered at a URL production never serves, and
|
|
552
594
|
// a page script that reads its id out of location.pathname would see none.
|
|
553
|
-
|
|
595
|
+
// A namespace word may name its page differently: the hub serves /g/<slug> from
|
|
596
|
+
// guild.html (platform-server.js's /g/:slug route, task 1002301).
|
|
597
|
+
const base = under2 === '' ? 'index' : (fs.existsSync(path.join(DIR, under2 + '.html')) ? under2 : under2.replace(/\/[^/]+$/, ''));
|
|
598
|
+
const name = PAGE_ALIAS[base] && fs.existsSync(path.join(DIR, PAGE_ALIAS[base] + '.html')) ? PAGE_ALIAS[base] : base;
|
|
554
599
|
if (!name.includes('/') && fs.existsSync(path.join(DIR, name + '.html'))) {
|
|
555
600
|
try { res.writeHead(200, { 'content-type': MIME['.html'], 'cache-control': 'no-store' }); return res.end(page(name + '.html').html); }
|
|
556
601
|
catch (e) { res.writeHead(500, { 'content-type': 'text/plain' }); return res.end('render failed: ' + e.stack); }
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.57",
|
|
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.57",
|
|
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.57",
|
|
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
|
@@ -9102,5 +9102,17 @@
|
|
|
9102
9102
|
"id": "1004580",
|
|
9103
9103
|
"text": "Work categories can now be managed right on the Rules floor of the Governor City, with no trip to the old settings page. Each category is its own line showing how many goals use it. You can rename it, retire it or bring it bac"
|
|
9104
9104
|
}
|
|
9105
|
+
],
|
|
9106
|
+
"1.21.56": [
|
|
9107
|
+
{
|
|
9108
|
+
"id": "1004581",
|
|
9109
|
+
"text": "The street no longer shows the small grey line of words above the buildings. The owner wants the city to read at a glance from its look alone, and the line was also wrong in light mode. It is gone in light and dark and on phon"
|
|
9110
|
+
}
|
|
9111
|
+
],
|
|
9112
|
+
"1.21.57": [
|
|
9113
|
+
{
|
|
9114
|
+
"id": "1002301",
|
|
9115
|
+
"text": "Guilds now have their own page on cloudbongos.com — who is in it and what it has shipped together — and members can choose whether they appear on it and count toward its totals."
|
|
9116
|
+
}
|
|
9105
9117
|
]
|
|
9106
9118
|
}
|
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.57'; // 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');
|
package/src/platform-server.js
CHANGED
|
@@ -305,6 +305,19 @@ function buildPlatformApp() {
|
|
|
305
305
|
if (!html) return next();
|
|
306
306
|
res.type('html').send(html);
|
|
307
307
|
});
|
|
308
|
+
// THE GUILD PAGE (task 1002301, R25; ADR 0336 D4/D6): /g/<slug>, the namespace
|
|
309
|
+
// D6 reserved. Hub-only for the /account reason — every read it makes is a
|
|
310
|
+
// platform-identity route. ONE shell for every slug: the page reads the public
|
|
311
|
+
// guild and, signed in, the members' own-scope read, so whether a guild exists
|
|
312
|
+
// (and whether an unlisted one is yours) is answered by those routes and never
|
|
313
|
+
// by this one. A segment that cannot be a slug falls through to the 404 tail.
|
|
314
|
+
app.get('/g/:slug', (req, res, next) => {
|
|
315
|
+
if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
|
|
316
|
+
if (!/^[A-Za-z0-9-]{2,39}$/.test(req.params.slug)) return next();
|
|
317
|
+
const html = landingPage('guild.html');
|
|
318
|
+
if (!html) return next();
|
|
319
|
+
res.type('html').send(html);
|
|
320
|
+
});
|
|
308
321
|
// Assets fall through to the 404 tail when no module claims the apex root — the
|
|
309
322
|
// handler is not mounted at all rather than pointed at a missing directory.
|
|
310
323
|
const landingAssets = landingDir() && express.static(landingDir(), { index: false, fallthrough: true });
|
|
@@ -245,7 +245,7 @@ function fakeDom() {
|
|
|
245
245
|
get innerHTML() { throw new Error('the card must not read innerHTML'); },
|
|
246
246
|
set innerHTML(v) { throw new Error('the card must not write innerHTML'); },
|
|
247
247
|
});
|
|
248
|
-
const ids = ['streetMode', 'streetModeIcon', 'streetModeWord', 'streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', '
|
|
248
|
+
const ids = ['streetMode', 'streetModeIcon', 'streetModeWord', 'streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardOpen', 'streetCardClose', 'streetToast'];
|
|
249
249
|
for (const id of ids) mk('x').setAttribute('id', id);
|
|
250
250
|
byId.get('streetToast').hidden = true; // as the page ships it
|
|
251
251
|
const document = { documentElement: mk('html'), getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
|
package/tests/hall_street.mjs
CHANGED
|
@@ -382,9 +382,18 @@ test('the windows light (evening) only when the docket was read and nothing need
|
|
|
382
382
|
assert.equal(model({ docket: docket([matter({ state: 'moving' })]) }).evening, true);
|
|
383
383
|
assert.equal(model({ docket: docket([matter({ state: 'needs' })]) }).evening, false);
|
|
384
384
|
const m = model({ docket: docket([matter({ state: 'moving' }), matter({ id: 'x:2', state: 'settled' })]), earnings: { days: [{ day: '2026-10-01', credits: 24 }] } });
|
|
385
|
-
assert.equal(m.hint, '', 'the hint gives way to the evening');
|
|
386
385
|
assert.equal(m.eveningLine, 'this month · 1 moving · 1 settled · $24 to your share');
|
|
387
|
-
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('the street carries no legend hint line (owner decision 2026-10-04, task 1004581)', () => {
|
|
389
|
+
for (const m of [model(), model({ docket: docket([matter()]) }), model({ docket: docket([matter({ state: 'moving' })]) })]) {
|
|
390
|
+
assert.equal('hint' in m, false, 'the model has no hint field');
|
|
391
|
+
}
|
|
392
|
+
assert.equal('HINT' in S, false, 'street-draw exports no HINT');
|
|
393
|
+
for (const f of ['street.html', 'street.css', 'street.js', 'street-draw.js']) {
|
|
394
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public', f), 'utf8');
|
|
395
|
+
assert.doesNotMatch(src, /street-hint|streetHint|orange waits on you/, f + ' carries no hint line');
|
|
396
|
+
}
|
|
388
397
|
});
|
|
389
398
|
|
|
390
399
|
// ── districts: one door per open goal ───────────────────────────────────────
|
|
@@ -539,7 +548,7 @@ function fakeDom() {
|
|
|
539
548
|
};
|
|
540
549
|
const focused = { el: null };
|
|
541
550
|
const html = mk('html');
|
|
542
|
-
const ids = ['streetMode', 'streetModeIcon', 'streetModeWord', 'streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', '
|
|
551
|
+
const ids = ['streetMode', 'streetModeIcon', 'streetModeWord', 'streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardOpen', 'streetCardClose', 'streetToast'];
|
|
543
552
|
for (const id of ids) mk('x').setAttribute('id', id);
|
|
544
553
|
const document = { documentElement: html, getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
|
|
545
554
|
return { document, byId, html, focused };
|