@bongos/core 1.23.14 → 1.23.15
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 +77 -42
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +22 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +924 -878
- package/docs/copy-registry.json +1796 -1325
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +8 -3
- package/docs/page-readings.json +575 -555
- package/modules/platform-identity/builder-of-the-week.js +180 -0
- package/modules/platform-identity/migrations/platform_identity_032_weekly_features.sql +53 -0
- package/modules/platform-identity/routes/community-leaderboard.js +25 -1
- package/modules/public-landing/public/account.html +2 -1
- package/modules/public-landing/public/community-week-none.states.json +16 -0
- package/modules/public-landing/public/community.html +142 -13
- package/modules/public-landing/public/community.states.json +6 -4
- package/modules/public-landing/public/connections.html +2 -1
- package/modules/public-landing/public/contact.html +2 -1
- package/modules/public-landing/public/guild.html +2 -1
- package/modules/public-landing/public/index.html +2 -1
- package/modules/public-landing/public/invitations.html +2 -1
- package/modules/public-landing/public/privacy.html +2 -1
- package/modules/public-landing/public/projects.html +2 -1
- package/modules/public-landing/public/terms.html +2 -1
- package/modules/ui-design/kit/fixtures/community-week-none.json +6 -0
- package/modules/ui-design/kit/fixtures/community-week.json +11 -0
- package/modules/ui-design/kit/serve.js +8 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +5 -5
- package/tests/builder_of_the_week.mjs +266 -0
- package/tests/builder_of_the_week_db.mjs +166 -0
- package/tests/hub_account_page.mjs +2 -2
- package/tests/hub_community_page.mjs +136 -25
- package/tests/landing_legal_pages.mjs +2 -2
- package/tests/privacy_matrix.mjs +53 -8
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// modules/platform-identity/builder-of-the-week.js — Builder of the Week (community
|
|
2
|
+
// spec D5, docs/specs/bongos-v1-community-tab.md; task 1002981, goal 1000110 CT5).
|
|
3
|
+
// The read behind GET /community/builder-of-the-week, for the hub's /community/week
|
|
4
|
+
// page and the line above the leaderboard.
|
|
5
|
+
//
|
|
6
|
+
// THE RULE (owner-signed 2026-08-16; the week decided 2026-10-27): of the builders
|
|
7
|
+
// the leaderboard may show (rollupVisibleSql), the one who earned the most credits
|
|
8
|
+
// across every project in the last FULL week — Monday 00:00 to Monday 00:00 UTC —
|
|
9
|
+
// among those who shipped at least one work in it. Ties go to more works shipped,
|
|
10
|
+
// then more karma, then handle/login so the answer is stable.
|
|
11
|
+
//
|
|
12
|
+
// THE WEEK IS READ FROM HISTORY, NOT FROM THE LIVE ROWS. Per (project, builder) it is
|
|
13
|
+
// the last snapshot at-or-before the week's end minus the last at-or-before its start
|
|
14
|
+
// (activity-snapshots.js), summed across projects. With no snapshot before the week, a
|
|
15
|
+
// row counts from zero only if the hub first saw it during the week (first_seen_at,
|
|
16
|
+
// migration 032; owner 2026-10-27): the leaderboard's missing-baseline rule would let a
|
|
17
|
+
// builder last seen before snapshots began win one week with their whole lifetime, and
|
|
18
|
+
// a crown is never corrected. A returning builder's first week back is the cost. A snapshot's reported_at never runs
|
|
19
|
+
// backwards, so once the week has ended its numbers cannot move, and whoever asks
|
|
20
|
+
// first gets the same answer.
|
|
21
|
+
//
|
|
22
|
+
// CROWNED AT RENDER, NO CRON (spec D5). The first read after Monday 00:00 UTC decides
|
|
23
|
+
// the week and writes it to platform_identity_weekly_features (migration 032) with
|
|
24
|
+
// ON CONFLICT DO NOTHING; every later read finds the row. A week nobody won is written
|
|
25
|
+
// too (github_id NULL), so it stays empty. Nothing updates a row.
|
|
26
|
+
//
|
|
27
|
+
// WHO IS SHOWN is decided again on every read: a winner who has since hidden their
|
|
28
|
+
// numbers or gone private is not drawn, on the crown or in the past weeks, and their
|
|
29
|
+
// week is not given to anyone else (spec D8). A hidden winner and an empty week look
|
|
30
|
+
// the same. Viewer-independent; github_id never crosses the projection.
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const { pool: defaultPool } = require('../../src/module-api');
|
|
34
|
+
const { rollupVisibleSql } = require('./account-visibility');
|
|
35
|
+
|
|
36
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
37
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
38
|
+
// How many weeks before the current one the past-weeks list shows.
|
|
39
|
+
const ARCHIVE_WEEKS = 12;
|
|
40
|
+
|
|
41
|
+
// The Monday 00:00 UTC that opens the week `now` is in.
|
|
42
|
+
function mondayOf(now) {
|
|
43
|
+
const midnight = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
44
|
+
const sinceMonday = (new Date(midnight).getUTCDay() + 6) % 7;
|
|
45
|
+
return new Date(midnight - sinceMonday * DAY_MS);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The week the crown covers at `now`: the last full week before `now`'s own.
|
|
49
|
+
function crownedWeek(now = new Date()) {
|
|
50
|
+
const end = mondayOf(now);
|
|
51
|
+
return { start: new Date(end.getTime() - WEEK_MS), end };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isoDate = (d) => d.toISOString().slice(0, 10);
|
|
55
|
+
|
|
56
|
+
// The week's winner among eligible builders, or null. Arithmetic is bigint for the
|
|
57
|
+
// leaderboard's reason: the columns are instance-reported int4.
|
|
58
|
+
async function computeWinner(start, end, pool) {
|
|
59
|
+
const { rows } = await pool.query(
|
|
60
|
+
`WITH act AS (
|
|
61
|
+
SELECT a.client_id, a.github_id, a.first_seen_at, pa.handle, pa.github_login
|
|
62
|
+
FROM platform_identity_builder_project_activity a
|
|
63
|
+
JOIN platform_identity_accounts pa ON pa.github_id = a.github_id
|
|
64
|
+
WHERE ${rollupVisibleSql('pa')}
|
|
65
|
+
), per_project AS (
|
|
66
|
+
SELECT act.github_id, act.handle, act.github_login,
|
|
67
|
+
COALESCE(e.credits, 0)::bigint - COALESCE(b.credits, 0) AS credits,
|
|
68
|
+
COALESCE(e.tasks_shipped, 0)::bigint - COALESCE(b.tasks_shipped, 0) AS tasks_shipped,
|
|
69
|
+
COALESCE(e.karma, 0)::bigint - COALESCE(b.karma, 0) AS karma
|
|
70
|
+
FROM act
|
|
71
|
+
LEFT JOIN LATERAL (
|
|
72
|
+
SELECT s.credits, s.tasks_shipped, s.karma
|
|
73
|
+
FROM platform_identity_activity_snapshots s
|
|
74
|
+
WHERE s.client_id = act.client_id AND s.github_id = act.github_id
|
|
75
|
+
AND s.reported_at <= $2::timestamptz
|
|
76
|
+
ORDER BY s.reported_at DESC, s.id DESC
|
|
77
|
+
LIMIT 1
|
|
78
|
+
) e ON true
|
|
79
|
+
LEFT JOIN LATERAL (
|
|
80
|
+
SELECT s.credits, s.tasks_shipped, s.karma
|
|
81
|
+
FROM platform_identity_activity_snapshots s
|
|
82
|
+
WHERE s.client_id = act.client_id AND s.github_id = act.github_id
|
|
83
|
+
AND s.reported_at <= $1::timestamptz
|
|
84
|
+
ORDER BY s.reported_at DESC, s.id DESC
|
|
85
|
+
LIMIT 1
|
|
86
|
+
) b ON true
|
|
87
|
+
WHERE b.credits IS NOT NULL OR act.first_seen_at >= $1::timestamptz
|
|
88
|
+
)
|
|
89
|
+
SELECT github_id, sum(credits)::bigint AS credits, sum(tasks_shipped)::bigint AS tasks_shipped
|
|
90
|
+
FROM per_project
|
|
91
|
+
GROUP BY github_id, handle, github_login
|
|
92
|
+
HAVING sum(tasks_shipped) >= 1
|
|
93
|
+
ORDER BY sum(credits) DESC, sum(tasks_shipped) DESC, sum(karma) DESC,
|
|
94
|
+
handle COLLATE "C", github_login COLLATE "C"
|
|
95
|
+
LIMIT 1`,
|
|
96
|
+
[start, end],
|
|
97
|
+
);
|
|
98
|
+
return rows[0] || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// One crowning per week per process: concurrent first reads share it, and a week
|
|
102
|
+
// already on record is not asked about again.
|
|
103
|
+
const crowning = new Map();
|
|
104
|
+
let lastCrowned = null;
|
|
105
|
+
|
|
106
|
+
async function crownWeek(start, end, pool) {
|
|
107
|
+
const week = isoDate(start);
|
|
108
|
+
const { rows } = await pool.query(
|
|
109
|
+
'SELECT 1 FROM platform_identity_weekly_features WHERE week_start = $1::date',
|
|
110
|
+
[week],
|
|
111
|
+
);
|
|
112
|
+
if (rows.length) return;
|
|
113
|
+
const winner = await computeWinner(start, end, pool);
|
|
114
|
+
await pool.query(
|
|
115
|
+
`INSERT INTO platform_identity_weekly_features (week_start, github_id, credits_delta, ships_delta)
|
|
116
|
+
VALUES ($1::date, $2::bigint, $3::bigint, $4::bigint)
|
|
117
|
+
ON CONFLICT (week_start) DO NOTHING`,
|
|
118
|
+
[week, winner ? winner.github_id : null, winner ? winner.credits : 0, winner ? winner.tasks_shipped : 0],
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ensureCrowned(start, end, pool) {
|
|
123
|
+
const week = isoDate(start);
|
|
124
|
+
if (lastCrowned && lastCrowned.week === week && lastCrowned.pool === pool) return Promise.resolve();
|
|
125
|
+
const inFlight = crowning.get(week);
|
|
126
|
+
if (inFlight && inFlight.pool === pool) return inFlight.promise;
|
|
127
|
+
const entry = { pool };
|
|
128
|
+
entry.promise = crownWeek(start, end, pool)
|
|
129
|
+
.then(() => { lastCrowned = { week, pool }; })
|
|
130
|
+
.finally(() => { if (crowning.get(week) === entry) crowning.delete(week); });
|
|
131
|
+
crowning.set(week, entry);
|
|
132
|
+
return entry.promise;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function clearCrownMemo() {
|
|
136
|
+
crowning.clear();
|
|
137
|
+
lastCrowned = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// readBuilderOfTheWeek — crowns the last full week if nobody has yet, then reads it and
|
|
141
|
+
// the weeks before it. Returns { week: { start, end }, winner, past }: dates as
|
|
142
|
+
// YYYY-MM-DD (end is the Monday the week closed on), winner null when nobody is to be
|
|
143
|
+
// shown, past newest first. `now` is injectable so a test can pin the week.
|
|
144
|
+
async function readBuilderOfTheWeek({ now = new Date(), pool = defaultPool } = {}) {
|
|
145
|
+
const { start, end } = crownedWeek(now);
|
|
146
|
+
await ensureCrowned(start, end, pool);
|
|
147
|
+
const { rows } = await pool.query(
|
|
148
|
+
`SELECT to_char(f.week_start, 'YYYY-MM-DD') AS week_start, f.credits_delta, f.ships_delta,
|
|
149
|
+
pa.handle, pa.github_login, pa.display_name, pa.avatar_url
|
|
150
|
+
FROM platform_identity_weekly_features f
|
|
151
|
+
JOIN platform_identity_accounts pa ON pa.github_id = f.github_id
|
|
152
|
+
WHERE f.week_start <= $1::date
|
|
153
|
+
AND ${rollupVisibleSql('pa')}
|
|
154
|
+
ORDER BY f.week_start DESC
|
|
155
|
+
LIMIT $2`,
|
|
156
|
+
[isoDate(start), ARCHIVE_WEEKS + 1],
|
|
157
|
+
);
|
|
158
|
+
const shown = rows.map((r) => ({
|
|
159
|
+
week_start: r.week_start,
|
|
160
|
+
handle: r.handle,
|
|
161
|
+
github_login: r.github_login,
|
|
162
|
+
display_name: r.display_name,
|
|
163
|
+
avatar_url: r.avatar_url,
|
|
164
|
+
credits: Number(r.credits_delta),
|
|
165
|
+
tasks_shipped: Number(r.ships_delta),
|
|
166
|
+
}));
|
|
167
|
+
const isThisWeek = shown.length > 0 && shown[0].week_start === isoDate(start);
|
|
168
|
+
return {
|
|
169
|
+
week: { start: isoDate(start), end: isoDate(end) },
|
|
170
|
+
winner: isThisWeek ? shown[0] : null,
|
|
171
|
+
past: (isThisWeek ? shown.slice(1) : shown).slice(0, ARCHIVE_WEEKS),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
readBuilderOfTheWeek,
|
|
177
|
+
crownedWeek,
|
|
178
|
+
clearCrownMemo,
|
|
179
|
+
ARCHIVE_WEEKS,
|
|
180
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- platform_identity_032_weekly_features.sql — Builder of the Week, one row per crowned
|
|
2
|
+
-- week (community spec D5, docs/specs/bongos-v1-community-tab.md; task 1002981).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. The crown is computed at render, with no cron: the first read of
|
|
5
|
+
-- GET /community/builder-of-the-week after Monday 00:00 UTC decides the week that just
|
|
6
|
+
-- ended and writes it here. The row is what pins the crown — a builder who goes public
|
|
7
|
+
-- or reports late cannot take a week that was already decided — and what the page's
|
|
8
|
+
-- past-weeks list reads.
|
|
9
|
+
--
|
|
10
|
+
-- THE SHAPE CARRIES THE RULES (owner, 2026-10-27):
|
|
11
|
+
-- * THE KEY IS week_start, the Monday (UTC) the crowned week began, so a week has one
|
|
12
|
+
-- row. The writer is INSERT ... ON CONFLICT (week_start) DO NOTHING, and nothing
|
|
13
|
+
-- updates a row: the first decision stands.
|
|
14
|
+
-- * github_id IS NULL when nobody qualified (no eligible builder shipped a work that
|
|
15
|
+
-- week). The empty week is recorded too, so it stays empty.
|
|
16
|
+
-- * credits_delta / ships_delta are the week's numbers as decided, bigint like the
|
|
17
|
+
-- leaderboard's arithmetic (builder-of-the-week.js).
|
|
18
|
+
-- * NO FOREIGN KEY to platform_identity_accounts, like every github_id column in this
|
|
19
|
+
-- module. Whether a winner may be SHOWN is decided at read time by
|
|
20
|
+
-- rollupVisibleSql (account-visibility.js), never by this row.
|
|
21
|
+
--
|
|
22
|
+
-- AND first_seen_at ON THE ACTIVITY ROWS (owner, 2026-10-27). A week counts a
|
|
23
|
+
-- (project, builder) with no snapshot before it only if the hub first saw that row
|
|
24
|
+
-- during the week: otherwise a builder last seen before snapshots began would bring
|
|
25
|
+
-- their whole lifetime into one week and keep the crown for good. Added with NO
|
|
26
|
+
-- default first, so every existing row stays NULL ("seen before this, history
|
|
27
|
+
-- unknown"); only then the default, so a new row stamps its own first sighting.
|
|
28
|
+
--
|
|
29
|
+
-- NOTHING HERE IS AUTHORITY (ADR 0016 / ADR 0141): descriptive, public analytics.
|
|
30
|
+
--
|
|
31
|
+
-- Additive + platform_identity_-namespaced (ADR 0083 §5). A CREATE and a nullable ADD COLUMN, so the
|
|
32
|
+
-- previous release still runs against this schema. Idempotent.
|
|
33
|
+
|
|
34
|
+
BEGIN;
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS platform_identity_weekly_features (
|
|
37
|
+
week_start date PRIMARY KEY,
|
|
38
|
+
github_id bigint,
|
|
39
|
+
credits_delta bigint NOT NULL,
|
|
40
|
+
ships_delta bigint NOT NULL,
|
|
41
|
+
crowned_at timestamptz NOT NULL DEFAULT now()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
ALTER TABLE platform_identity_builder_project_activity ADD COLUMN IF NOT EXISTS first_seen_at timestamptz;
|
|
45
|
+
ALTER TABLE platform_identity_builder_project_activity ALTER COLUMN first_seen_at SET DEFAULT now();
|
|
46
|
+
|
|
47
|
+
COMMENT ON COLUMN platform_identity_builder_project_activity.first_seen_at IS
|
|
48
|
+
'When the hub first saw this (project, builder) row; NULL for rows older than migration 032. Builder of the Week counts a row with no snapshot before the week only if it was first seen during the week (task 1002981).';
|
|
49
|
+
|
|
50
|
+
COMMENT ON TABLE platform_identity_weekly_features IS
|
|
51
|
+
'Builder of the Week, one row per week, keyed on the Monday (UTC) it began (community spec D5, task 1002981). Written once by the first read after the week ends, never updated; github_id NULL = nobody qualified. Whether a winner is shown is decided at read time by the leaderboard''s eligibility predicate.';
|
|
52
|
+
|
|
53
|
+
COMMIT;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// ../community-leaderboard.js; this file is the HTTP shell — validation, the
|
|
7
7
|
// per-IP read cap, the response envelope. It never reads a session, so the
|
|
8
8
|
// board is the same for every viewer.
|
|
9
|
+
// It also serves GET /community/builder-of-the-week (spec D5, task 1002981), the
|
|
10
|
+
// board's weekly crown, from ../builder-of-the-week.js on the same terms.
|
|
9
11
|
//
|
|
10
12
|
// Reaches core only through the doorway (ADR 0083).
|
|
11
13
|
'use strict';
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
const express = require('express');
|
|
14
16
|
const api = require('../../../src/module-api');
|
|
15
17
|
const leaderboard = require('../community-leaderboard');
|
|
18
|
+
const builderOfTheWeek = require('../builder-of-the-week');
|
|
16
19
|
const { createReadRateLimit } = require('../read-rate-limit');
|
|
17
20
|
|
|
18
21
|
const { pool, corsPublicGet } = api;
|
|
@@ -20,6 +23,9 @@ const { pool, corsPublicGet } = api;
|
|
|
20
23
|
const READ_RATE_LIMIT = 60; // per IP per minute — the community-search budget
|
|
21
24
|
const READ_RATE_SCOPE = 'community-leaderboard-read';
|
|
22
25
|
const leaderboardReadRateLimit = createReadRateLimit({ scope: READ_RATE_SCOPE, limit: READ_RATE_LIMIT });
|
|
26
|
+
// Builder of the Week spends its own budget (ADR 0209: never a shared one).
|
|
27
|
+
const WEEK_READ_RATE_SCOPE = 'builder-of-the-week-read';
|
|
28
|
+
const weekReadRateLimit = createReadRateLimit({ scope: WEEK_READ_RATE_SCOPE, limit: READ_RATE_LIMIT });
|
|
23
29
|
|
|
24
30
|
module.exports = function communityLeaderboardRoutes() {
|
|
25
31
|
const router = express.Router();
|
|
@@ -63,7 +69,25 @@ module.exports = function communityLeaderboardRoutes() {
|
|
|
63
69
|
}
|
|
64
70
|
});
|
|
65
71
|
|
|
72
|
+
router.use('/community/builder-of-the-week', corsPublicGet('/community/builder-of-the-week'));
|
|
73
|
+
|
|
74
|
+
// rank: public — Builder of the Week (spec D5; task 1002981): the last full
|
|
75
|
+
// week's winner and the weeks before it. Takes no parameters. The first read
|
|
76
|
+
// after Monday 00:00 UTC also records the week (crown at render, no cron), at
|
|
77
|
+
// most one row per week. Viewer-independent; no-store so a winner who hides
|
|
78
|
+
// leaves the page on the next read (ADR 0171 D5).
|
|
79
|
+
router.get('/community/builder-of-the-week', weekReadRateLimit, async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const out = await builderOfTheWeek.readBuilderOfTheWeek({ pool });
|
|
82
|
+
res.set('Cache-Control', 'no-store');
|
|
83
|
+
res.json(out);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
api.logger('platform-identity').error({ err: err && err.message }, 'GET /community/builder-of-the-week failed');
|
|
86
|
+
res.fail('builder_of_the_week_failed', { status: 500, message: 'internal error' });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
66
90
|
return router;
|
|
67
91
|
};
|
|
68
92
|
|
|
69
|
-
module.exports._internals = { READ_RATE_LIMIT, READ_RATE_SCOPE };
|
|
93
|
+
module.exports._internals = { READ_RATE_LIMIT, READ_RATE_SCOPE, WEEK_READ_RATE_SCOPE };
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
<div class="bandIn">
|
|
119
119
|
<ul class="bandRow">
|
|
120
120
|
<li class="bandCell"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
|
|
121
|
+
<li class="bandCell"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li>
|
|
121
122
|
<li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
|
|
122
123
|
<li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
|
|
123
124
|
<li class="bandCell knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
@@ -131,7 +132,7 @@
|
|
|
131
132
|
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
132
133
|
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
|
|
133
134
|
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
134
|
-
<li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
135
|
+
<li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
135
136
|
</ul>
|
|
136
137
|
</div>
|
|
137
138
|
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The builder of the week when the last week has no one to show (task 1002981): nobody qualified, or the winner hides their numbers — the two look the same (spec D8). A hub's first week, so no past weeks either (fixtures/community-week-none.json). The page says so in one line; the board draws no crown line.",
|
|
3
|
+
"page": "/community",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modes": ["dark"],
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"stub": { "fixtures": { "community-week": "community-week-none" } },
|
|
8
|
+
"ignoreRequests": ["/api/(gds|bongos)/me$", "/my-projects", "/me/guild-", "/scouting$"],
|
|
9
|
+
"ignoreConsole": ["/api/(gds|bongos)/me", "/scouting", "\\b40[13]\\b", "Unauthorized", "Forbidden"],
|
|
10
|
+
"ignoreControls": [".vh"],
|
|
11
|
+
"states": {
|
|
12
|
+
"out-week-none": { "auth": false, "url": "/community/week", "actions": [["wait", 400]], "expect": { "visible": ["#week", "#weekNone", "#weekRuleH"], "hidden": ["#weekCrown", "#weekLoading", "#weekFailed", "#pastH"], "count": [["#weekPast .commRow", 0]], "text": [["#weekNone", "No builder of the week for 19–25 Oct 2026."]] } },
|
|
13
|
+
"out-board-no-crown": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#board", "#boardList"], "hidden": ["#boardCrown"] } }
|
|
14
|
+
},
|
|
15
|
+
"reducedMotion": { "auth": false, "actions": [["wait", 400]] }
|
|
16
|
+
}
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<!-- THE COMMUNITY PAGES (task 1003363; ADR 0217 D1, ADR 0337 D1/D3/D4.1, the community
|
|
7
7
|
spec docs/specs/bongos-v1-community-tab.md D1/D3/D4/D8). Who is building, across every
|
|
8
|
-
project, as
|
|
9
|
-
from this one file: /community is the leaderboard, /community/
|
|
10
|
-
|
|
8
|
+
project, as four sub-pages of the bar's Community menu (owner, 2026-10-27), all served
|
|
9
|
+
from this one file: /community is the leaderboard, /community/week the builder of the
|
|
10
|
+
week (task 1002981, spec D5), /community/find the builder search, /community/recruiters
|
|
11
|
+
the builders who hide their numbers but are open to recruiters.
|
|
11
12
|
The script shows the section the path names and marks it current in the menu.
|
|
12
|
-
VIEWING IS UNGATED (ADR 0217 D1): the leaderboard
|
|
13
|
-
viewer, signed in or not.
|
|
14
|
-
(modules/platform-identity/routes/community-leaderboard.js,
|
|
13
|
+
VIEWING IS UNGATED (ADR 0217 D1): the leaderboard, the builder of the week and the search
|
|
14
|
+
are the same for every viewer, signed in or not. All three reads are public,
|
|
15
|
+
viewer-independent routes (modules/platform-identity/routes/community-leaderboard.js,
|
|
16
|
+
which also serves the builder of the week, and community-search.js),
|
|
15
17
|
which decide WHO appears through account-visibility.js; these pages add no rule of
|
|
16
18
|
their own. The one viewer-relative page is the recruiter list: drawn only from a 200 of
|
|
17
19
|
GET /scouting, whose door (a project owner, or project.curate) is the server's, never
|
|
@@ -90,7 +92,28 @@
|
|
|
90
92
|
.commDoor p{margin:0 0 18px;font-size:15px;line-height:1.55;color:var(--ink-soft);}
|
|
91
93
|
.commDoor .gh{width:18px;height:18px;fill:currentColor;}
|
|
92
94
|
.commDoor .btn{text-transform:none;}
|
|
95
|
+
/* Builder of the week (task 1002981): one line on the board, then its own page — the
|
|
96
|
+
crown, the rule, and the weeks before it as board rows with the week where the place is */
|
|
97
|
+
.commCrownLine{margin:-6px 0 10px;max-width:62ch;font-size:14.5px;line-height:1.5;color:var(--ink-soft);}
|
|
98
|
+
.commCrownLine a{display:inline-block;min-height:24px;padding:3px 0;line-height:18px;color:var(--ink);
|
|
99
|
+
text-decoration:underline;text-decoration-color:var(--accent);text-underline-offset:4px;}
|
|
100
|
+
.commCrownLine a:focus-visible{outline:2px solid var(--focus);outline-offset:3px;}
|
|
101
|
+
.commCrownLine strong{font-weight:500;color:var(--ink);}
|
|
102
|
+
.commCrown{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:14px 40px;
|
|
103
|
+
padding:0 0 26px;margin:0 0 6px;border-bottom:1px solid var(--rule-soft);}
|
|
104
|
+
.crownWhen{grid-column:1/-1;margin:0;font-family:var(--label-face);font-size:12.5px;color:var(--ink-faint);}
|
|
105
|
+
.crownWho{display:flex;align-items:center;gap:16px;min-width:0;}
|
|
106
|
+
.crownWho .connAv{width:64px;height:64px;}
|
|
107
|
+
.crownName{font-family:var(--font-display);font-weight:300;font-size:clamp(26px,3.4vw,34px);letter-spacing:-.02em;
|
|
108
|
+
line-height:1.1;color:var(--ink);overflow-wrap:anywhere;}
|
|
109
|
+
.crownNums{display:flex;gap:0 36px;text-align:right;}
|
|
110
|
+
.commSub{margin:24px 0 4px;font-family:var(--font-body);font-size:14.5px;font-weight:500;color:var(--ink);}
|
|
111
|
+
.weekRows .commRow{grid-template-columns:6.5rem 36px minmax(0,1fr) auto;}
|
|
112
|
+
.weekRows .commPlace{font-family:var(--label-face);}
|
|
93
113
|
@media (max-width:480px){
|
|
114
|
+
.commCrown{grid-template-columns:minmax(0,1fr);}
|
|
115
|
+
.crownNums{text-align:left;}
|
|
116
|
+
.weekRows .commRow{grid-template-columns:7ch 32px minmax(0,1fr) auto;}
|
|
94
117
|
.commRow,.commRows.canRecruit .commRow{grid-template-columns:3ch 32px minmax(0,1fr) auto;gap:0 10px;}
|
|
95
118
|
.commRow .connAv{width:32px;height:32px;}
|
|
96
119
|
.commNum{font-size:19px;}
|
|
@@ -132,6 +155,7 @@
|
|
|
132
155
|
<div class="bandIn">
|
|
133
156
|
<ul class="bandRow">
|
|
134
157
|
<li class="bandCell"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
|
|
158
|
+
<li class="bandCell"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li>
|
|
135
159
|
<li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
|
|
136
160
|
<li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
|
|
137
161
|
<li class="bandCell knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
@@ -145,7 +169,7 @@
|
|
|
145
169
|
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
146
170
|
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
|
|
147
171
|
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
148
|
-
<li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
172
|
+
<li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/week">Builder of the week</a><span class="bandC">the top earner of each week, and how it is chosen</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
149
173
|
</ul>
|
|
150
174
|
</div>
|
|
151
175
|
</div>
|
|
@@ -164,14 +188,15 @@
|
|
|
164
188
|
<main class="acct" id="comm">
|
|
165
189
|
<h1 class="vh">Community</h1>
|
|
166
190
|
|
|
167
|
-
<!--
|
|
168
|
-
the path names (/community, /community/find, /community/recruiters). -->
|
|
191
|
+
<!-- FOUR SUB-PAGES, ONE FILE: every section is real markup; the script un-hides the one
|
|
192
|
+
the path names (/community, /community/week, /community/find, /community/recruiters). -->
|
|
169
193
|
<div class="mineExtra">
|
|
170
194
|
<!-- THE LEADERBOARD (spec D3): GET /community/leaderboard, one window and one craft
|
|
171
195
|
at a time. The server ranks; this page draws its rows in the order and with the
|
|
172
196
|
`place` it was given, and every count it prints is the length of a list. -->
|
|
173
197
|
<section id="board" data-view="leaderboard" aria-labelledby="boardH">
|
|
174
198
|
<h2 id="boardH">Leaderboard</h2>
|
|
199
|
+
<p class="commCrownLine" id="boardCrown" hidden></p>
|
|
175
200
|
<div class="commFilters">
|
|
176
201
|
<div class="commTabs" id="craftTabs" role="group" aria-label="Role">
|
|
177
202
|
<button class="commTab" type="button" data-craft="" aria-pressed="true">Everyone</button>
|
|
@@ -199,6 +224,23 @@
|
|
|
199
224
|
<p class="commNote" id="boardFine">A project reports a builder’s numbers when they sign in or ship there, so a window counts work when it was reported, not when it was done.</p>
|
|
200
225
|
</section>
|
|
201
226
|
|
|
227
|
+
<!-- BUILDER OF THE WEEK (spec D5, task 1002981): GET /community/builder-of-the-week, the
|
|
228
|
+
last full week's winner and the weeks before it. The server decides the week and
|
|
229
|
+
records it (builder-of-the-week.js); this page draws what it was sent, the same for
|
|
230
|
+
every viewer, and states the rule in full (the spec: the formula is public). -->
|
|
231
|
+
<section id="week" data-view="week" aria-labelledby="weekH" hidden>
|
|
232
|
+
<h2 id="weekH">Builder of the week</h2>
|
|
233
|
+
<p class="commMsg" id="weekLoading" role="status">Reading the builder of the week…</p>
|
|
234
|
+
<p class="projMsg err" id="weekFailed" role="alert" hidden></p>
|
|
235
|
+
<div class="commCrown" id="weekCrown" hidden></div>
|
|
236
|
+
<p class="commMsg" id="weekNone" hidden></p>
|
|
237
|
+
<h3 class="commSub" id="weekRuleH">How it is chosen</h3>
|
|
238
|
+
<p class="commNote">The builder who earned the most {{currency}} across every project in one week, Monday to Sunday (UTC), among the builders who shipped at least one work that week. A tie goes to whoever shipped more works, then to whoever earned more karma. Builders who hide their numbers or keep their account private are not counted.</p>
|
|
239
|
+
<p class="commNote">Each week is decided once, the first time anyone opens this page after it ends, and it does not change after that. A project reports a builder’s numbers when they sign in or ship there, so a week counts work when it was reported, not when it was done.</p>
|
|
240
|
+
<h3 class="commSub" id="pastH" hidden>Past weeks</h3>
|
|
241
|
+
<ol class="commRows weekRows" id="weekPast" aria-labelledby="pastH"></ol>
|
|
242
|
+
</section>
|
|
243
|
+
|
|
202
244
|
<!-- THE SEARCH (spec D4): GET /community/search, a name and an optional craft. A
|
|
203
245
|
builder who hides their numbers is still found by name, and drops out once a
|
|
204
246
|
craft is chosen — the server's rule (builder-search.js), never this page's. -->
|
|
@@ -262,10 +304,11 @@
|
|
|
262
304
|
var QUERY_MIN = 2;
|
|
263
305
|
var QUERY_MAX = 100;
|
|
264
306
|
var CRAFT_WORD = { engineer: 'engineer', artist: 'artist', ideator: 'ideator', ui: 'designer' };
|
|
265
|
-
// The
|
|
307
|
+
// The four sub-pages this file is served as (platform-server.js's /community routes);
|
|
266
308
|
// the path picks one, and its menu items are marked current.
|
|
267
309
|
var VIEWS = {
|
|
268
310
|
leaderboard: { path: '/community', section: 'board', title: 'Leaderboard' },
|
|
311
|
+
week: { path: '/community/week', section: 'week', title: 'Builder of the week' },
|
|
269
312
|
find: { path: '/community/find', section: 'find', title: 'Find a builder' },
|
|
270
313
|
recruiters: { path: '/community/recruiters', section: 'recruit', title: 'Open to recruiters' },
|
|
271
314
|
};
|
|
@@ -311,7 +354,7 @@
|
|
|
311
354
|
return what + ' could not be read. Reload the page to try again.';
|
|
312
355
|
}
|
|
313
356
|
function viewOf(pathname) {
|
|
314
|
-
var m = String(pathname || '').replace(/\/+$/, '').match(/^\/community(?:\/(find|recruiters))?$/);
|
|
357
|
+
var m = String(pathname || '').replace(/\/+$/, '').match(/^\/community(?:\/(week|find|recruiters))?$/);
|
|
315
358
|
return m ? (m[1] || 'leaderboard') : 'leaderboard';
|
|
316
359
|
}
|
|
317
360
|
|
|
@@ -442,6 +485,91 @@
|
|
|
442
485
|
loadBoard();
|
|
443
486
|
});
|
|
444
487
|
|
|
488
|
+
// ── the builder of the week (task 1002981, spec D5) ──────────────────────────
|
|
489
|
+
// The server decides each week and records it; this draws the answer as sent. On the
|
|
490
|
+
// board it is one line, and a failed or empty read draws nothing there.
|
|
491
|
+
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
492
|
+
// "19–25 Oct" for the Monday a week began on (UTC), the year added when asked
|
|
493
|
+
function weekSpan(start, withYear) {
|
|
494
|
+
var a = new Date(String(start) + 'T00:00:00Z');
|
|
495
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(start)) || !isFinite(a.getTime())) return '';
|
|
496
|
+
var b = new Date(a.getTime() + 6 * 24 * 60 * 60 * 1000);
|
|
497
|
+
var year = function (d) { return withYear ? ' ' + d.getUTCFullYear() : ''; };
|
|
498
|
+
if (a.getUTCMonth() === b.getUTCMonth()) return a.getUTCDate() + '–' + b.getUTCDate() + ' ' + MONTHS[b.getUTCMonth()] + year(b);
|
|
499
|
+
var first = a.getUTCDate() + ' ' + MONTHS[a.getUTCMonth()] + (a.getUTCFullYear() === b.getUTCFullYear() ? '' : year(a));
|
|
500
|
+
return first + ' – ' + b.getUTCDate() + ' ' + MONTHS[b.getUTCMonth()] + year(b);
|
|
501
|
+
}
|
|
502
|
+
function crownLine(r) {
|
|
503
|
+
var line = $('boardCrown');
|
|
504
|
+
var w = r.ok ? r.data.winner : null;
|
|
505
|
+
line.textContent = '';
|
|
506
|
+
line.hidden = !w;
|
|
507
|
+
if (!w) return;
|
|
508
|
+
var a = el('a', null, 'Builder of the week');
|
|
509
|
+
a.setAttribute('href', '/community/week');
|
|
510
|
+
line.appendChild(a);
|
|
511
|
+
line.appendChild(el('span', null, ': '));
|
|
512
|
+
line.appendChild(el('strong', null, w.display_name || w.handle || w.github_login || 'Builder'));
|
|
513
|
+
var span = weekSpan(r.data.week && r.data.week.start, false);
|
|
514
|
+
line.appendChild(el('span', null, ', ' + num(w.credits).toLocaleString() + ' ' + UNIT + (span ? ', ' + span : '')));
|
|
515
|
+
}
|
|
516
|
+
function stat(value, unit) {
|
|
517
|
+
var d = el('div');
|
|
518
|
+
d.appendChild(el('span', 'commNum', value));
|
|
519
|
+
d.appendChild(el('span', 'commUnit', unit));
|
|
520
|
+
return d;
|
|
521
|
+
}
|
|
522
|
+
function paintCrown(w, span) {
|
|
523
|
+
var box = $('weekCrown');
|
|
524
|
+
box.textContent = '';
|
|
525
|
+
if (span) box.appendChild(el('p', 'crownWhen', span));
|
|
526
|
+
var who = el('div', 'crownWho');
|
|
527
|
+
who.appendChild(face(w));
|
|
528
|
+
var main = el('div', 'mineRow__main');
|
|
529
|
+
main.appendChild(el('div', 'crownName', w.display_name || w.handle || w.github_login || 'Builder'));
|
|
530
|
+
if (w.handle) main.appendChild(el('span', 'mineRow__host', '@' + w.handle));
|
|
531
|
+
who.appendChild(main);
|
|
532
|
+
box.appendChild(who);
|
|
533
|
+
var nums = el('div', 'crownNums');
|
|
534
|
+
var ships = num(w.tasks_shipped);
|
|
535
|
+
nums.appendChild(stat(num(w.credits).toLocaleString(), UNIT));
|
|
536
|
+
nums.appendChild(stat(String(ships), ships === 1 ? 'work shipped' : 'works shipped'));
|
|
537
|
+
box.appendChild(nums);
|
|
538
|
+
box.hidden = false;
|
|
539
|
+
}
|
|
540
|
+
function pastRow(p) {
|
|
541
|
+
var li = el('li', 'commRow');
|
|
542
|
+
li.appendChild(el('span', 'commPlace', weekSpan(p.week_start, false)));
|
|
543
|
+
li.appendChild(face(p));
|
|
544
|
+
var main = nameBlock(p, p.handle ? '@' + p.handle : '');
|
|
545
|
+
main.appendChild(el('p', 'mineRow__hint', plural(num(p.tasks_shipped), 'work') + ' shipped'));
|
|
546
|
+
li.appendChild(main);
|
|
547
|
+
var credits = el('div', 'commCredits');
|
|
548
|
+
credits.appendChild(el('span', 'commNum', num(p.credits).toLocaleString()));
|
|
549
|
+
credits.appendChild(el('span', 'commUnit', UNIT));
|
|
550
|
+
li.appendChild(credits);
|
|
551
|
+
return li;
|
|
552
|
+
}
|
|
553
|
+
function loadWeek() {
|
|
554
|
+
return read('/community/builder-of-the-week').then(function (r) {
|
|
555
|
+
$('weekLoading').hidden = true;
|
|
556
|
+
if (!r.ok) {
|
|
557
|
+
$('weekFailed').textContent = failText(r.status, 'The builder of the week');
|
|
558
|
+
$('weekFailed').hidden = false;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
var span = weekSpan(r.data.week && r.data.week.start, true);
|
|
562
|
+
if (r.data.winner) paintCrown(r.data.winner, span);
|
|
563
|
+
else {
|
|
564
|
+
$('weekNone').textContent = span ? 'No builder of the week for ' + span + '.' : 'No builder of the week yet.';
|
|
565
|
+
$('weekNone').hidden = false;
|
|
566
|
+
}
|
|
567
|
+
var list = $('weekPast');
|
|
568
|
+
arr(r.data.past).forEach(function (p) { if (p) list.appendChild(pastRow(p)); });
|
|
569
|
+
$('pastH').hidden = !list.children.length;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
445
573
|
// ── the search ──────────────────────────────────────────────────────────────
|
|
446
574
|
function say(text, kind) {
|
|
447
575
|
var s = $('findStatus');
|
|
@@ -570,8 +698,9 @@
|
|
|
570
698
|
});
|
|
571
699
|
document.title = document.title + ' · ' + view.title;
|
|
572
700
|
$('signinTop').setAttribute('href', '/api/bongos/auth/web/start?return=' + view.path + '&repos=1');
|
|
573
|
-
if (VIEW === 'leaderboard') loadBoard();
|
|
574
|
-
if (VIEW === '
|
|
701
|
+
if (VIEW === 'leaderboard') { loadBoard(); read('/community/builder-of-the-week').then(crownLine); }
|
|
702
|
+
if (VIEW === 'week') loadWeek();
|
|
703
|
+
else if (VIEW === 'recruiters') bar.ready.then(function (me) { if (me) loadRecruit(); else showRecruit('recruitDoor'); });
|
|
575
704
|
else bar.ready.then(function (me) { if (me) loadOwned(me, $(VIEW === 'find' ? 'findFor' : 'boardFor')); });
|
|
576
705
|
})();
|
|
577
706
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "The community pages' states for modules/ui-design/kit/render.js (task 1003363; ADR 0217 D1, ADR 0337 D4.1): one file served as
|
|
2
|
+
"_": "The community pages' states for modules/ui-design/kit/render.js (task 1003363; ADR 0217 D1, ADR 0337 D4.1): one file served as four sub-pages of the bar's Community menu, each state naming its `url`. The leaderboard, the builder of the week and the search are the same for every viewer, from the stub's fixtures (community-leaderboard.json: six builders all time, fewer in each window; community-week.json: a winner and four past weeks, task 1002981; community-search.json); community-week-none.states.json is a week nobody won. The recruiter list signed out is a sign-in door, and signed in it is the default fixture's 403 (most builders own no project); community-recruiter.states.json is a project owner's list, community-empty.states.json a board with nobody on it. Dark only, like every apex page (ADR 0204).",
|
|
3
3
|
"page": "/community",
|
|
4
4
|
"surface": "public-landing",
|
|
5
5
|
"modes": ["dark"],
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
"ignoreConsole": ["/api/(gds|bongos)/me", "/scouting", "\\b40[13]\\b", "Unauthorized", "Forbidden"],
|
|
9
9
|
"ignoreControls": [".vh"],
|
|
10
10
|
"states": {
|
|
11
|
-
"out": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#board", "#boardList", "#boardWindow", "#signinTop", "#projTrig"], "hidden": ["#find", "#recruit", "#who", "#boardLoading", "#boardFailed", "#boardEmpty"], "count": [["#boardList .commRow", 6]], "value": [["#boardWindow", "all"]], "attr": [["#craftTabs [data-craft=\"\"]", "aria-pressed", "true"], ["#commTrig", "aria-current", "true"], ["#menuCommunity a[href=\"/community\"]", "aria-current", "page"]], "text": [["#boardList .commRow:first-child .mineRow__name", "Priya Nair"], ["#boardList .commRow:first-child .commNum", "18,420"], ["#boardList .commRow:first-child .mineRow__hint", "212 works shipped · 64 karma"], ["#boardNote", "Credits earned across every project, since each builder started."]] } },
|
|
11
|
+
"out": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#board", "#boardList", "#boardWindow", "#signinTop", "#projTrig", "#boardCrown"], "hidden": ["#week", "#find", "#recruit", "#who", "#boardLoading", "#boardFailed", "#boardEmpty"], "href": [["#boardCrown a", "/community/week"]], "count": [["#boardList .commRow", 6]], "value": [["#boardWindow", "all"]], "attr": [["#craftTabs [data-craft=\"\"]", "aria-pressed", "true"], ["#commTrig", "aria-current", "true"], ["#menuCommunity a[href=\"/community\"]", "aria-current", "page"]], "text": [["#boardList .commRow:first-child .mineRow__name", "Priya Nair"], ["#boardList .commRow:first-child .commNum", "18,420"], ["#boardList .commRow:first-child .mineRow__hint", "212 works shipped · 64 karma"], ["#boardNote", "Credits earned across every project, since each builder started."], ["#boardCrown", "Builder of the week: Ada Okafor, 1,240 credits, 19–25 Oct"]] } },
|
|
12
12
|
"out-30d": { "auth": false, "actions": [["wait", 400], ["select", "#boardWindow", "30d"], ["wait", 300]], "expect": { "count": [["#boardList .commRow", 3]], "value": [["#boardWindow", "30d"]], "text": [["#boardList .commRow:first-child .mineRow__name", "Ada Okafor"], ["#boardNote", "Credits earned in the last 30 days, across every project."]] } },
|
|
13
13
|
"out-artists": { "auth": false, "actions": [["wait", 400], ["click", "#craftTabs [data-craft=\"artist\"]"], ["wait", 300]], "expect": { "count": [["#boardList .commRow", 1]], "text": [["#boardList .commRow:first-child .mineRow__name", "Ada Okafor"], ["#boardList .commRow:first-child .commPlace", "1"]] } },
|
|
14
|
-
"out-community-band": { "auth": false, "actions": [["wait", 400], ["clickIfVisible", "#commTrig"], ["wait", 300]], "expect": { "hidden": ["#menuCommunity .knowCell"], "count": [["#menuCommunity .bandCell:not([hidden])",
|
|
15
|
-
"in-community-band": { "auth": true, "actions": [["wait", 400], ["clickIfVisible", "#commTrig"], ["wait", 300]], "expect": { "count": [["#menuCommunity .bandCell:not([hidden])",
|
|
14
|
+
"out-community-band": { "auth": false, "actions": [["wait", 400], ["clickIfVisible", "#commTrig"], ["wait", 300]], "expect": { "hidden": ["#menuCommunity .knowCell"], "count": [["#menuCommunity .bandCell:not([hidden])", 4]], "attr": [["#menuCommunity a[href=\"/community\"]", "aria-current", "page"]] } },
|
|
15
|
+
"in-community-band": { "auth": true, "actions": [["wait", 400], ["clickIfVisible", "#commTrig"], ["wait", 300]], "expect": { "count": [["#menuCommunity .bandCell:not([hidden])", 5]], "href": [["#menuCommunity .knowCell a", "/connections#connSuggest"]] } },
|
|
16
16
|
"in-projects-band": { "auth": true, "actions": [["wait", 400], ["click", "#projTrig"], ["wait", 300]], "expect": { "visible": ["#menuProjects"], "count": [["#menuProjects .knowCell:not([hidden])", 1]] } },
|
|
17
17
|
"out-projects-band": { "auth": false, "actions": [["wait", 400], ["click", "#projTrig"], ["wait", 300]], "expect": { "visible": ["#menuProjects"], "attr": [["#menuProjects a[href=\"/community\"]", "aria-current", "page"]] } },
|
|
18
|
+
"out-week": { "auth": false, "url": "/community/week", "actions": [["wait", 400]], "expect": { "visible": ["#week", "#weekCrown", "#pastH", "#weekPast"], "hidden": ["#board", "#find", "#recruit", "#weekLoading", "#weekFailed", "#weekNone"], "count": [["#weekPast .commRow", 4]], "attr": [["#menuCommunity a[href=\"/community/week\"]", "aria-current", "page"]], "text": [["#weekCrown .crownWhen", "19–25 Oct 2026"], ["#weekCrown .crownName", "Ada Okafor"], ["#weekCrown .crownNums div:first-child .commNum", "1,240"], ["#weekCrown .crownNums div:last-child .commUnit", "works shipped"], ["#weekPast .commRow:first-child .commPlace", "12–18 Oct"], ["#weekPast .commRow:nth-child(3) .commPlace", "28 Sep – 4 Oct"]] } },
|
|
19
|
+
"in-week": { "auth": true, "url": "/community/week", "actions": [["wait", 600]], "expect": { "visible": ["#week", "#weekCrown", "#who"], "hidden": ["#board", "#signinTop", "#weekFailed"], "count": [["#weekPast .commRow", 4]] } },
|
|
18
20
|
"out-find": { "auth": false, "url": "/community/find", "actions": [["wait", 400]], "expect": { "visible": ["#find", "#findQ", "#findCraft", "#findBtn"], "hidden": ["#board", "#recruit"], "attr": [["#menuCommunity a[href=\"/community/find\"]", "aria-current", "page"]] } },
|
|
19
21
|
"out-find-search": { "auth": false, "url": "/community/find", "actions": [["wait", 400], ["fill", "#findQ", "pri"], ["click", "#findBtn"], ["wait", 300]], "expect": { "count": [["#findList .mineRow", 2]], "text": [["#findStatus", "2 builders found."], ["#findList .mineRow:first-child .mineRow__hint", "engineer · designer"]] } },
|
|
20
22
|
"out-recruiters": { "auth": false, "url": "/community/recruiters", "actions": [["wait", 400]], "expect": { "visible": ["#recruit", "#recruitDoor", "#recruitSignin"], "hidden": ["#board", "#find", "#recruitLoading", "#recruitClosed"], "count": [["#recruitList .mineRow", 0]], "attr": [["#menuCommunity a[href=\"/community/recruiters\"]", "aria-current", "page"]] } },
|