@bongos/core 1.19.781 → 1.19.783
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 +41 -36
- 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 +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +50 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +29 -27
- package/docs/copy-registry.json +86 -68
- package/docs/module-api-changelog.md +4 -0
- package/modules/builder-settings/builder-needs.js +9 -3
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/collab-lib.js +94 -2
- package/modules/hall-ui/public/collab.css +110 -5
- package/modules/hall-ui/public/collab.html +51 -37
- package/modules/hall-ui/public/collab.js +153 -60
- package/modules/hall-ui/public/shell.js +3 -1
- package/modules/lifecycle/help-requests.js +24 -2
- package/modules/lifecycle/routes/help-requests.js +11 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/start.js +26 -3
- package/src/module-api.js +1 -1
- package/tests/builder_needs.mjs +24 -0
- package/tests/collab_page.mjs +119 -0
- package/tests/hall_collab_world.mjs +7 -3
- package/tests/hall_nav.mjs +47 -0
- package/tests/help_requests.mjs +31 -0
- package/tests/social_mode.mjs +3 -1
- package/tests/start_inbox_count.mjs +112 -0
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
// modules/hall-ui/public/collab.js — the Collab page (/builders/collab).
|
|
2
2
|
// Task 1003172 / C02 of goal 1000075, criterion collab-page-matches-my-craft;
|
|
3
|
-
// rebuilt on the hall's row archetype by task 1003471 (goal 1000081)
|
|
3
|
+
// rebuilt on the hall's row archetype by task 1003471 (goal 1000081); made a
|
|
4
|
+
// THREE-BLOCK room of people by task 1003942 (owner direction: "way too text
|
|
5
|
+
// heavy, doesn't feel collaborative at all").
|
|
4
6
|
//
|
|
5
|
-
// The hall surface for C01's help requests.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The hall surface for C01's help requests, in three blocks. Top left, ASKED OF
|
|
8
|
+
// YOU: someone is waiting on a human answer, so it reads first. Top right, YOUR
|
|
9
|
+
// ASKS: a status check, with the two settle actions. Below, wide, ALL OPEN ASKS:
|
|
10
|
+
// everything anyone in the project has asked, which is what makes the page a
|
|
11
|
+
// room rather than an inbox. Each block shows its NEWEST THREE and a "show all"
|
|
12
|
+
// door to the rest; each head carries its count and the faces of who is asking.
|
|
10
13
|
//
|
|
11
|
-
// A row is the family's .ov-row: the
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// A row is the family's .ov-row--lead: the person's FACE first (collab-lib
|
|
15
|
+
// faceOf: the asker on what was asked of you and on the board, the addressee on
|
|
16
|
+
// your own asks), the ask's first sentence as the title and the rest as the sub
|
|
17
|
+
// (titleOf), the direction as a fact with a door to their profile, the task or
|
|
18
|
+
// goal as a chip, and the AGE as a state pill in the right column, so a glance
|
|
19
|
+
// down a list reads the ages in words and in the verification colours. Every
|
|
20
|
+
// rule that can be pure lives in collab-lib.js where a test can execute it.
|
|
17
21
|
//
|
|
18
22
|
// MATCHING IS THE SERVER'S JOB, not this file's. GET /help-requests/for-me derives
|
|
19
23
|
// the viewer's crafts from their own profile; sending a craft from here would be
|
|
@@ -33,6 +37,10 @@
|
|
|
33
37
|
const { escapeHtml, $, toast } = window.OTB;
|
|
34
38
|
const L = window.OTBCollabLib;
|
|
35
39
|
|
|
40
|
+
// The condensed view: the newest three, the rest behind "show all".
|
|
41
|
+
const SHOWN = 3;
|
|
42
|
+
const MAX_FACES = 6;
|
|
43
|
+
|
|
36
44
|
let settling = false;
|
|
37
45
|
|
|
38
46
|
// The age pill: the band's words, in the band's colour (collab.css .cb-age--*).
|
|
@@ -59,6 +67,27 @@
|
|
|
59
67
|
return `<a class="fact-chip" href="${escapeHtml(ctx.href)}">${escapeHtml(ctx.kind)} #${escapeHtml(String(ctx.id))}</a>`;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
// The face: the picture when the lib vouched for a src, else the initial on
|
|
71
|
+
// the glass; a craft wears its letter in the label face. Decorative on
|
|
72
|
+
// purpose (alt="", aria-hidden) — the name in the facts line is the door, and
|
|
73
|
+
// two doors to one profile on one row would be noise for a screen reader.
|
|
74
|
+
function faceHtml(face) {
|
|
75
|
+
if (!face) return '';
|
|
76
|
+
if (face.avatar) {
|
|
77
|
+
return `<img class="cb-face" src="${escapeHtml(face.avatar)}" alt="" loading="lazy" title="${escapeHtml(face.name)}">`;
|
|
78
|
+
}
|
|
79
|
+
const kind = face.kind === 'craft' ? ' cb-face--craft' : '';
|
|
80
|
+
return `<span class="cb-face cb-face--initial${kind}" aria-hidden="true" title="${escapeHtml(face.name)}">${escapeHtml(face.initial)}</span>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The stack in a block's head: each distinct asker once, capped, then +N.
|
|
84
|
+
function facesHtml(list) {
|
|
85
|
+
if (!list.length) return '';
|
|
86
|
+
const shown = list.slice(0, MAX_FACES).map(faceHtml).join('');
|
|
87
|
+
const more = list.length > MAX_FACES ? `<span class="cb-face cb-face--initial" aria-hidden="true">+${list.length - MAX_FACES}</span>` : '';
|
|
88
|
+
return shown + more;
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
// A name as a door to that builder's profile, or plain text when the server
|
|
63
92
|
// sent no id to link to.
|
|
64
93
|
function personLink(id, name) {
|
|
@@ -68,6 +97,18 @@
|
|
|
68
97
|
: escapeHtml(name);
|
|
69
98
|
}
|
|
70
99
|
|
|
100
|
+
// Splice a profile link over a person's name inside an escaped sentence. The
|
|
101
|
+
// replacement is a FUNCTION, not a string: String.prototype.replace treats
|
|
102
|
+
// `$&`, `$$`, "$`" and `$'` specially in a string replacement, and the name is
|
|
103
|
+
// builder-controlled (a display_name or login) — so a display name containing
|
|
104
|
+
// `$&` would duplicate or mangle the sentence around it. Not an injection
|
|
105
|
+
// (both halves are escaped either way), but a real way to corrupt someone
|
|
106
|
+
// else's row by choosing your own name.
|
|
107
|
+
function linkName(escapedLine, id, name) {
|
|
108
|
+
const link = personLink(id, name);
|
|
109
|
+
return (name && link) ? escapedLine.replace(escapeHtml(name), () => link) : escapedLine;
|
|
110
|
+
}
|
|
111
|
+
|
|
71
112
|
// DIRECTION, on the row (owner report, task 1003274): "<name> asked you by
|
|
72
113
|
// name" incoming, "You asked <name> by name" outgoing — so an outgoing ask can
|
|
73
114
|
// never be mistaken for an incoming one, which is exactly the mistake the
|
|
@@ -79,26 +120,32 @@
|
|
|
79
120
|
if (!line) return '';
|
|
80
121
|
const name = dir === 'in' ? L.askerOf(req) : (req.needs_builder_name || req.needs_builder_login);
|
|
81
122
|
const id = dir === 'in' ? req.requested_by : req.needs_builder_id;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
return `<span class="ov-fact">${linkName(escapeHtml(line), id, name)}</span>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The board's third-person line ("Alpha asked Beta by name"), both people as
|
|
127
|
+
// doors. The asker is linked first, then the addressee, each with the
|
|
128
|
+
// function replacer above.
|
|
129
|
+
function boardFact(req) {
|
|
130
|
+
const line = L.boardLine(req);
|
|
131
|
+
if (!line) return '';
|
|
132
|
+
let html = linkName(escapeHtml(line), req.requested_by, L.askerOf(req));
|
|
133
|
+
if (req.needs_builder_id != null) {
|
|
134
|
+
html = linkName(html, req.needs_builder_id, req.needs_builder_name || req.needs_builder_login);
|
|
135
|
+
}
|
|
92
136
|
return `<span class="ov-fact">${html}</span>`;
|
|
93
137
|
}
|
|
94
138
|
|
|
95
|
-
// A row for the "asked of you"
|
|
139
|
+
// A row for the "asked of you" block. There is deliberately NO "claim this"
|
|
96
140
|
// button: answering is a conversation, and inventing a reply thread here would
|
|
97
141
|
// be reimplementing the Agora, which this goal explicitly forbids (C06 owns the
|
|
98
142
|
// chat mirror). What the page owes a helper is the request, its age, its
|
|
99
|
-
// context, and who to answer.
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
// context, and who to answer — and their face. A by-name ask is the personal
|
|
144
|
+
// call on this reader, so its row is the LIT one (.cb-row--live).
|
|
145
|
+
function forMeRow(req, nowMs, extra) {
|
|
146
|
+
const live = req.needs_builder_id != null ? ' cb-row--live' : '';
|
|
147
|
+
return `<li class="ov-row ov-row--lead cb-row${live}" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
|
|
148
|
+
${faceHtml(L.faceOf(req, 'in'))}
|
|
102
149
|
<div class="ov-row__main">${titleHtml(req)}
|
|
103
150
|
<div class="ov-row__facts">${directionFact(req, 'in')}${contextChip(req)}</div>
|
|
104
151
|
</div>
|
|
@@ -106,11 +153,13 @@
|
|
|
106
153
|
</li>`;
|
|
107
154
|
}
|
|
108
155
|
|
|
109
|
-
// Your own ask, with the two settle actions. Only the
|
|
110
|
-
// server enforces it in the UPDATE's WHERE, this is
|
|
111
|
-
|
|
156
|
+
// Your own ask, led by who you asked, with the two settle actions. Only the
|
|
157
|
+
// asker can settle — the server enforces it in the UPDATE's WHERE, this is
|
|
158
|
+
// just the affordance.
|
|
159
|
+
function mineRow(req, nowMs, extra) {
|
|
112
160
|
const id = escapeHtml(String(req.id));
|
|
113
|
-
return `<li class="ov-row" data-id="${id}">
|
|
161
|
+
return `<li class="ov-row ov-row--lead cb-row" data-id="${id}"${extra ? ' data-extra hidden' : ''}>
|
|
162
|
+
${faceHtml(L.faceOf(req, 'out'))}
|
|
114
163
|
<div class="ov-row__main">${titleHtml(req)}
|
|
115
164
|
<div class="ov-row__facts">${directionFact(req, 'out')}${contextChip(req)}</div>
|
|
116
165
|
</div>
|
|
@@ -123,48 +172,79 @@
|
|
|
123
172
|
</li>`;
|
|
124
173
|
}
|
|
125
174
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
175
|
+
// A row on the board of everyone's asks: the asker's face, the third-person
|
|
176
|
+
// line, no actions — settling is the asker's, in their own block.
|
|
177
|
+
function allRow(req, nowMs, extra) {
|
|
178
|
+
return `<li class="ov-row ov-row--lead cb-row" data-id="${escapeHtml(String(req.id))}"${extra ? ' data-extra hidden' : ''}>
|
|
179
|
+
${faceHtml(L.faceOf(req, 'in'))}
|
|
180
|
+
<div class="ov-row__main">${titleHtml(req)}
|
|
181
|
+
<div class="ov-row__facts">${boardFact(req)}${contextChip(req)}</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="ov-row__right">${ageChip(req, nowMs)}${whenLine(req.created_at)}</div>
|
|
184
|
+
</li>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---- a block: count, faces, the newest three, the door to the rest -----------
|
|
188
|
+
//
|
|
189
|
+
// `key` names the block's ids (cb-<key>, cb-<key>-n, -faces, -empty, -more).
|
|
190
|
+
// Rows past SHOWN render with data-extra + hidden, and the button flips them —
|
|
191
|
+
// no re-render, no second fetch, no motion. The button says the whole number
|
|
192
|
+
// ("Show all 7"), so a reader knows what is behind the door before opening it.
|
|
193
|
+
function paintBlock(key, rows, rowFn, nowMs, faces) {
|
|
194
|
+
const list = rows.slice().sort(L.byNewestFirst);
|
|
195
|
+
const { shown, hidden } = L.splitShown(list, SHOWN);
|
|
196
|
+
const host = $(`#cb-${key}`);
|
|
197
|
+
if (host) {
|
|
198
|
+
host.innerHTML = shown.map((r) => rowFn(r, nowMs, false)).join('')
|
|
199
|
+
+ hidden.map((r) => rowFn(r, nowMs, true)).join('');
|
|
200
|
+
}
|
|
201
|
+
const n = $(`#cb-${key}-n`);
|
|
202
|
+
if (n) n.textContent = list.length ? String(list.length) : '';
|
|
203
|
+
const empty = $(`#cb-${key}-empty`);
|
|
204
|
+
if (empty) empty.hidden = list.length > 0;
|
|
205
|
+
const facesEl = $(`#cb-${key}-faces`);
|
|
206
|
+
if (facesEl) facesEl.innerHTML = faces ? facesHtml(L.askers(list)) : '';
|
|
207
|
+
const more = $(`#cb-${key}-more`);
|
|
208
|
+
if (more) {
|
|
209
|
+
more.hidden = hidden.length === 0;
|
|
210
|
+
more.setAttribute('aria-expanded', 'false');
|
|
211
|
+
more.textContent = `Show all ${list.length}`;
|
|
212
|
+
more.dataset.total = String(list.length);
|
|
134
213
|
}
|
|
135
|
-
groups.sort((a, b) => Number(b.id === 'you') - Number(a.id === 'you'));
|
|
136
|
-
return groups.map((g) => `<section class="ov-group">
|
|
137
|
-
<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(g.label)}</span><span class="ov-group__n">${g.rows.length}</span></div>
|
|
138
|
-
<ul class="ov-rows">${g.rows.map((r) => forMeRow(r, nowMs)).join('')}</ul>
|
|
139
|
-
</section>`).join('');
|
|
140
214
|
}
|
|
141
215
|
|
|
142
|
-
function
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
if (host)
|
|
146
|
-
|
|
216
|
+
function toggleMore(btn) {
|
|
217
|
+
const key = btn.id.replace(/^cb-|-more$/g, '');
|
|
218
|
+
const host = $(`#cb-${key}`);
|
|
219
|
+
if (!host) return;
|
|
220
|
+
const open = btn.getAttribute('aria-expanded') === 'true';
|
|
221
|
+
host.querySelectorAll('[data-extra]').forEach((el) => { el.hidden = open; });
|
|
222
|
+
btn.setAttribute('aria-expanded', open ? 'false' : 'true');
|
|
223
|
+
btn.textContent = open ? `Show all ${btn.dataset.total}` : 'Show fewer';
|
|
147
224
|
}
|
|
148
225
|
|
|
149
226
|
async function load() {
|
|
150
227
|
const nowMs = Date.now();
|
|
151
|
-
const [forMe, mine] = await Promise.all([
|
|
228
|
+
const [forMe, mine, all] = await Promise.all([
|
|
152
229
|
api.helpRequests.getHelpRequestsForMe({}),
|
|
153
230
|
api.helpRequests.getHelpRequestsMine({ query: { status: 'open' } }),
|
|
231
|
+
// The board read (task 1003942) — GET /help-requests, every open ask in the
|
|
232
|
+
// project. Through api.request because the generated client predates it.
|
|
233
|
+
api.request('GET', '/help-requests'),
|
|
154
234
|
]);
|
|
155
235
|
|
|
156
|
-
const
|
|
157
|
-
const
|
|
236
|
+
const rowsOf = (r) => ((r && r.ok && r.data && r.data.help_requests) || []);
|
|
237
|
+
const crafts = (forMe.ok && forMe.data && forMe.data.crafts) || [];
|
|
158
238
|
|
|
159
|
-
|
|
160
|
-
|
|
239
|
+
paintBlock('for-me', rowsOf(forMe), forMeRow, nowMs, true);
|
|
240
|
+
paintBlock('mine', rowsOf(mine), mineRow, nowMs, false);
|
|
241
|
+
paintBlock('all', rowsOf(all), allRow, nowMs, true);
|
|
161
242
|
|
|
162
243
|
const sub = $('#cb-sub');
|
|
163
244
|
if (sub) {
|
|
164
|
-
const crafts = (forMe.ok && forMe.data && forMe.data.crafts) || [];
|
|
165
245
|
sub.textContent = crafts.length
|
|
166
|
-
? `
|
|
167
|
-
: '
|
|
246
|
+
? `Builders asking builders. Asks to ${crafts.join(', ')}s, and to you by name, land in your block.`
|
|
247
|
+
: 'Builders asking builders. With no craft set, only asks addressed to you by name land in your block.';
|
|
168
248
|
}
|
|
169
249
|
|
|
170
250
|
// The social half. Best-effort and LAST: an introductions read that fails
|
|
@@ -192,8 +272,11 @@
|
|
|
192
272
|
|
|
193
273
|
function mingleRow(pair, meId) {
|
|
194
274
|
const o = otherOf(pair, meId);
|
|
195
|
-
const
|
|
196
|
-
|
|
275
|
+
const whoName = o.name || o.login || `builder ${o.id}`;
|
|
276
|
+
const who = escapeHtml(whoName);
|
|
277
|
+
const face = { kind: 'person', id: o.id, name: whoName, avatar: null, initial: L.initialOf(whoName) };
|
|
278
|
+
return `<li class="ov-row ov-row--lead cb-row">
|
|
279
|
+
${faceHtml(face)}
|
|
197
280
|
<div class="ov-row__main">
|
|
198
281
|
<a class="ov-row__title" href="/builders/profile?id=${escapeHtml(String(o.id))}">${who}</a>
|
|
199
282
|
<p class="ov-row__sub">A ${escapeHtml(String(o.craft))} — you are a ${escapeHtml(String(o.myCraft))}. Mingle pairs across crafts.</p>
|
|
@@ -207,6 +290,13 @@
|
|
|
207
290
|
</li>`;
|
|
208
291
|
}
|
|
209
292
|
|
|
293
|
+
function paint(hostSel, emptySel, html, n) {
|
|
294
|
+
const host = $(hostSel);
|
|
295
|
+
const empty = $(emptySel);
|
|
296
|
+
if (host) host.innerHTML = html;
|
|
297
|
+
if (empty) empty.hidden = n > 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
210
300
|
async function loadMingle() {
|
|
211
301
|
const section = $('#cb-mingle-section');
|
|
212
302
|
if (!section) return;
|
|
@@ -257,13 +347,16 @@
|
|
|
257
347
|
}
|
|
258
348
|
|
|
259
349
|
document.addEventListener('click', (ev) => {
|
|
260
|
-
const
|
|
350
|
+
const closest = (sel) => (ev.target && ev.target.closest ? ev.target.closest(sel) : null);
|
|
351
|
+
const more = closest('.cb-more');
|
|
352
|
+
if (more) { ev.preventDefault(); toggleMore(more); return; }
|
|
353
|
+
const mingleBtn = closest('[data-mingle]');
|
|
261
354
|
if (mingleBtn) {
|
|
262
355
|
ev.preventDefault();
|
|
263
356
|
decideMingle(Number(mingleBtn.getAttribute('data-id')), mingleBtn.getAttribute('data-mingle'));
|
|
264
357
|
return;
|
|
265
358
|
}
|
|
266
|
-
const btn =
|
|
359
|
+
const btn = closest('[data-settle]');
|
|
267
360
|
if (!btn) return;
|
|
268
361
|
ev.preventDefault();
|
|
269
362
|
settle(Number(btn.getAttribute('data-id')), btn.getAttribute('data-settle'));
|
|
@@ -254,7 +254,9 @@
|
|
|
254
254
|
// task 1003172 (C02, goal 1000075): help requests addressed to you. In
|
|
255
255
|
// Community because the group is picked by WHOSE data it is, and this is
|
|
256
256
|
// other builders reaching you — not your own numbers (that is You).
|
|
257
|
-
|
|
257
|
+
// task 1003942: the second consumer of the count badge. `help_requests` is
|
|
258
|
+
// the need id builder-needs.js emits with a `count`; applyCounts reads it.
|
|
259
|
+
{ id: 'collab', label: 'Collab', icon: 'leaderboard', href: '/collab', gate: 'signed', count: 'help_requests' },
|
|
258
260
|
// task 1003441: was the #leaderboard hash view on the home page, which is
|
|
259
261
|
// why this group read as one real page plus a link into index.html. Now a
|
|
260
262
|
// page, so no `view` key. Labelled Builders — 'roster' is the word
|
|
@@ -55,7 +55,9 @@ const COLS = 'id, requested_by, needs_craft, needs_builder_id, what_is_stuck, ta
|
|
|
55
55
|
// bare for the writes, whose RETURNING cannot reach a joined alias.
|
|
56
56
|
const LIST_COLS = `${COLS.split(', ').map((c) => `h.${c}`).join(', ')}, `
|
|
57
57
|
+ 'a.github_login AS requested_by_login, a.display_name AS requested_by_name, '
|
|
58
|
-
+ '
|
|
58
|
+
+ 'a.avatar_url AS requested_by_avatar_url, '
|
|
59
|
+
+ 'n.github_login AS needs_builder_login, n.display_name AS needs_builder_name, '
|
|
60
|
+
+ 'n.avatar_url AS needs_builder_avatar_url';
|
|
59
61
|
const LIST_FROM = 'FROM lifecycle_help_requests h '
|
|
60
62
|
+ 'LEFT JOIN builders a ON a.id = h.requested_by '
|
|
61
63
|
+ 'LEFT JOIN builders n ON n.id = h.needs_builder_id';
|
|
@@ -200,6 +202,26 @@ async function listMine({ builderId, status = null, limit = MAX_ROWS }, deps = {
|
|
|
200
202
|
return rows;
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
// The BOARD read (task 1003942, owner direction): every open ask in the project,
|
|
206
|
+
// newest first, for the Collab page's third block. The two reads above answer
|
|
207
|
+
// "what is addressed to me" and "what did I ask"; this one answers "what is
|
|
208
|
+
// anyone asking", which is what makes the page a room rather than an inbox.
|
|
209
|
+
// Any signed-in builder may read it: an ask is addressed to a craft or a person
|
|
210
|
+
// INSIDE this project, and every reader here is a member of it. It never takes
|
|
211
|
+
// a craft or a builder from the caller — there is nothing to narrow by, so the
|
|
212
|
+
// ?craft= hole C01's grader found cannot reopen here.
|
|
213
|
+
async function listOpenAll({ limit = MAX_ROWS } = {}, deps = {}) {
|
|
214
|
+
const activePool = deps.pool || pool;
|
|
215
|
+
const { rows } = await activePool.query(
|
|
216
|
+
`SELECT ${LIST_COLS} ${LIST_FROM}
|
|
217
|
+
WHERE h.status = 'open'
|
|
218
|
+
ORDER BY h.created_at DESC, h.id DESC
|
|
219
|
+
LIMIT $1`,
|
|
220
|
+
[capRows(limit)]
|
|
221
|
+
);
|
|
222
|
+
return rows;
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
async function getHelpRequest(id, deps = {}) {
|
|
204
226
|
const activePool = deps.pool || pool;
|
|
205
227
|
const { rows } = await activePool.query(`SELECT ${COLS} FROM lifecycle_help_requests WHERE id = $1`, [id]);
|
|
@@ -224,6 +246,6 @@ async function setHelpRequestStatus({ id, builderId, status }, deps = {}) {
|
|
|
224
246
|
module.exports = {
|
|
225
247
|
HELP_CRAFTS, helpCrafts, disciplineForCraft, craftForDiscipline, craftsForBuilder,
|
|
226
248
|
validateHelpRequest, MAX_STUCK, MAX_ROWS, capRows,
|
|
227
|
-
builderExists, createHelpRequest, listOpenForBuilder, listMine, getHelpRequest, setHelpRequestStatus,
|
|
249
|
+
builderExists, createHelpRequest, listOpenForBuilder, listMine, listOpenAll, getHelpRequest, setHelpRequestStatus,
|
|
228
250
|
countOpenForBuilder, craftMapSql,
|
|
229
251
|
};
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// read for "I am stuck" (task 1003171 / C01, goal 1000075; criterion
|
|
3
3
|
// collab-ask-for-help).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Five routes: ask, see what is addressed to me, see my own, see the whole open
|
|
6
|
+
// board (task 1003942), settle mine.
|
|
6
7
|
// The Collab PAGE is C02 and the matching rules beyond craft are its job — this
|
|
7
8
|
// surface deliberately stops at "the request is addressed and answerable".
|
|
8
9
|
//
|
|
@@ -88,6 +89,15 @@ module.exports = function buildHelpRequestsRouter() {
|
|
|
88
89
|
return res.json({ help_requests: rows, count: rows.length });
|
|
89
90
|
}, { errorCode: 'help_request_list_failed' }));
|
|
90
91
|
|
|
92
|
+
// The BOARD read (task 1003942): every open ask in the project, newest first,
|
|
93
|
+
// for the Collab page's "all open asks" block. rank: any-builder — an ask is
|
|
94
|
+
// addressed inside this project and every reader here is a member; the route
|
|
95
|
+
// takes no craft and no builder from the caller, so there is nothing to widen.
|
|
96
|
+
router.get('/help-requests', api.requireBuilder, asyncHandler('GET /help-requests', async (req, res) => {
|
|
97
|
+
const rows = await help.listOpenAll();
|
|
98
|
+
return res.json({ help_requests: rows, count: rows.length });
|
|
99
|
+
}, { errorCode: 'help_request_list_failed' }));
|
|
100
|
+
|
|
91
101
|
// Only the ASKER settles their own request: 'answered' when someone helped,
|
|
92
102
|
// 'withdrawn' when they no longer need it. Nobody is assigned by being asked —
|
|
93
103
|
// the same consent posture ADR 0187 §3 gives task recommendations.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.783",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.783",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.783",
|
|
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/scripts/gds/start.js
CHANGED
|
@@ -112,6 +112,30 @@ function formatCostRow(cost) {
|
|
|
112
112
|
return `${fmtUsd(spent)} logged spend · ${credits}c reward shipped`;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// inboxOpenCount — how many ideas are OPEN in the inbox, from a GET /inbox response.
|
|
116
|
+
//
|
|
117
|
+
// WHY THIS IS NOT ideas.length (task 1003454). GET /inbox answers
|
|
118
|
+
// { ideas, open_count }, where `ideas` is a PAGE (default 50 rows) and
|
|
119
|
+
// `open_count` is the real total, counted server-side under the same rank gate
|
|
120
|
+
// as the rows. Reading the array length therefore reported the page size: the
|
|
121
|
+
// card said "50 ideas in inbox" while the payload said 99, and once the queue
|
|
122
|
+
// passed one page the figure pinned at exactly 50 forever — a number that looks
|
|
123
|
+
// plausible, never moves, and understates the backlog the card exists to make
|
|
124
|
+
// visible. capture.js already destructured open_count correctly, which is what
|
|
125
|
+
// made this an oversight rather than a convention.
|
|
126
|
+
//
|
|
127
|
+
// The page length stays as a FALLBACK so an older core that predates open_count
|
|
128
|
+
// still renders a figure rather than "N/A". Returns null when neither is
|
|
129
|
+
// readable, which the callers render as "ideas: N/A".
|
|
130
|
+
function inboxOpenCount(inboxRes) {
|
|
131
|
+
if (!inboxRes || !inboxRes.ok) return null;
|
|
132
|
+
const data = inboxRes.data;
|
|
133
|
+
if (data && typeof data.open_count === 'number') return data.open_count;
|
|
134
|
+
if (data && Array.isArray(data.ideas)) return data.ideas.length;
|
|
135
|
+
if (data && typeof data.count === 'number') return data.count;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
115
139
|
function formatRecentRow({ recentShipped, inboxCount, blockerCount }) {
|
|
116
140
|
const bits = [];
|
|
117
141
|
// "1 ship in last 24h" — recent_shipped returns tasks shipped within
|
|
@@ -741,9 +765,7 @@ async function main() {
|
|
|
741
765
|
const progress = progressRes.ok && Array.isArray(progressRes.data?.progress) ? progressRes.data.progress : null;
|
|
742
766
|
const cost = costRes.ok ? costRes.data : null;
|
|
743
767
|
const recentShipped = recentRes.ok && Array.isArray(recentRes.data?.tasks) ? recentRes.data.tasks : null;
|
|
744
|
-
const inboxCount =
|
|
745
|
-
? inboxRes.data.ideas.length
|
|
746
|
-
: (inboxRes.ok && typeof inboxRes.data?.count === 'number' ? inboxRes.data.count : null);
|
|
768
|
+
const inboxCount = inboxOpenCount(inboxRes);
|
|
747
769
|
const blockerCount = blockersRes.ok && Array.isArray(blockersRes.data?.blockers)
|
|
748
770
|
? blockersRes.data.blockers.filter((b) => b.status === 'open' || !b.status).length
|
|
749
771
|
: null;
|
|
@@ -1023,6 +1045,7 @@ if (require.main === module) {
|
|
|
1023
1045
|
}
|
|
1024
1046
|
|
|
1025
1047
|
module.exports = {
|
|
1048
|
+
inboxOpenCount,
|
|
1026
1049
|
rankCrossDisciplineRecommendations,
|
|
1027
1050
|
rebaseWarningModel,
|
|
1028
1051
|
rebaseWarningMarkdown,
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.783'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/builder_needs.mjs
CHANGED
|
@@ -331,5 +331,29 @@ t('nothing waiting renders nothing at all', () => {
|
|
|
331
331
|
assert.equal(html, '');
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
// task 1003942: the help-requests need feeds the Collab nav badge, which reads
|
|
335
|
+
// the need's `count` (the field boardVotesNeed already carried) and follows its
|
|
336
|
+
// action to the page. Both were missing: no count, and a '/#/collab' hash href
|
|
337
|
+
// pointing at a view the index stopped routing when Collab became a page.
|
|
338
|
+
console.log('help_requests need carries a count and points at the page (task 1003942):');
|
|
339
|
+
|
|
340
|
+
t('open help requests state their number, machine-readable', () => {
|
|
341
|
+
const n = needs.helpRequestsNeed({ openHelpRequests: 4 });
|
|
342
|
+
assert.equal(n.count, 4);
|
|
343
|
+
assert.equal(n.state, 'action_needed');
|
|
344
|
+
assert.equal(needs.helpRequestsNeed({ openHelpRequests: 1 }).count, 1);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
t('the action goes to /collab, the page — not the retired hash view', () => {
|
|
348
|
+
assert.equal(needs.helpRequestsNeed({ openHelpRequests: 2 }).action.href, '/collab');
|
|
349
|
+
assert.equal(needs.taskRecommendationsNeed({ openTaskRecommendations: 1 }).action.href, '/collab');
|
|
350
|
+
assert.equal(needs.mingleIntroductionsNeed({ openMingleIntroductions: 1 }).action.href, '/collab');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
t('zero open requests is no need at all — nothing for a badge to count', () => {
|
|
354
|
+
assert.equal(needs.helpRequestsNeed({ openHelpRequests: 0 }), null);
|
|
355
|
+
assert.equal(needs.helpRequestsNeed({}), null);
|
|
356
|
+
});
|
|
357
|
+
|
|
334
358
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
335
359
|
process.exit(failed ? 1 : 0);
|