@bongos/core 1.19.516 → 1.19.518
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 +38 -33
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +2 -0
- package/docs/copy-inventory.md +128 -121
- package/docs/copy-registry.json +197 -134
- package/docs/module-api-changelog.md +4 -0
- package/modules/builder-settings/builder-needs.js +28 -2
- package/modules/hall-ui/public/collab.html +19 -0
- package/modules/hall-ui/public/collab.js +78 -0
- package/modules/hall-ui/public/drachmae.html +3 -0
- package/modules/hall-ui/public/drachmae.js +138 -2
- package/modules/hall-ui/public/hall-kit.js +45 -7
- package/modules/hall-ui/public/hall-render.js +6 -0
- package/modules/hall-ui/public/profile-charts.js +6 -13
- package/modules/hall-ui/public/profile.css +6 -35
- package/modules/hall-ui/public/shell.js +30 -1
- package/modules/hall-ui/public/style.css +35 -0
- package/modules/lifecycle/lifecycle.js +8 -0
- package/modules/lifecycle/mingle.js +17 -1
- package/modules/lifecycle/routes/mingle.js +5 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/me.js +39 -1
- package/src/module-api.js +1 -1
- package/tests/hall_kit_charts.mjs +49 -0
- package/tests/hall_reference_world.mjs +129 -0
- package/tests/mingle.mjs +26 -0
- package/tests/social_mode.mjs +181 -0
|
@@ -1469,5 +1469,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1469
1469
|
landed since 1.19.514 with no explicit bump. run 33797825811. (task 1002620)
|
|
1470
1470
|
1.19.516 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1471
1471
|
landed since 1.19.515 with no explicit bump. run 33798954769. (task 1002620)
|
|
1472
|
+
1.19.517 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1473
|
+
landed since 1.19.516 with no explicit bump. run 33799755382. (task 1002620)
|
|
1474
|
+
1.19.518 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1475
|
+
landed since 1.19.517 with no explicit bump. run 33801792364. (task 1002620)
|
|
1472
1476
|
---------------------------------------------------------------------------
|
|
1473
1477
|
```
|
|
@@ -191,7 +191,33 @@ function boardVotesNeed(ctx) {
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
// task 1003178 (C08, goal 1000075): an open mingle INTRODUCTION waiting on you.
|
|
195
|
+
// Third sibling of the two collab needs above, here for the same reason and with
|
|
196
|
+
// the same posture — an introduction nobody surfaces is merely stored, and the
|
|
197
|
+
// needs roster is how a collab feature reaches the whole hall instead of sitting
|
|
198
|
+
// on a page of its own (which is the thing C08's criterion refuses).
|
|
199
|
+
//
|
|
200
|
+
// 'info' and NEVER a demand, and the wording carries that: mingle is declinable
|
|
201
|
+
// without penalty (C07, ADR 0187 §5), so copy that pressured the reader would BE
|
|
202
|
+
// the penalty the decision forbids. The same argument the recommendation need
|
|
203
|
+
// makes, applied to a social invitation, where it matters more.
|
|
204
|
+
function mingleIntroductionsNeed(ctx) {
|
|
205
|
+
const n = Number(ctx.openMingleIntroductions) || 0;
|
|
206
|
+
if (n <= 0) return null;
|
|
207
|
+
const subject = n === 1 ? 'An introduction is' : `${n} introductions are`;
|
|
208
|
+
return {
|
|
209
|
+
id: 'mingle_introductions',
|
|
210
|
+
title: n === 1 ? 'Someone from another craft was introduced to you' : 'You have introductions waiting',
|
|
211
|
+
what: 'Mingle pairs builders across crafts — you opted in, and a pairing is waiting on your answer.',
|
|
212
|
+
state: 'action_needed',
|
|
213
|
+
severity: 'info',
|
|
214
|
+
message: `${subject} waiting on you. Saying no costs nothing at all — no rank, no karma, no streak, `
|
|
215
|
+
+ 'and it does not change how often you are paired again.',
|
|
216
|
+
action: { label: n === 1 ? 'See the introduction' : 'See the introductions', href: '/#/collab' },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const NEEDS = [artKeyNeed, helpRequestsNeed, taskRecommendationsNeed, boardVotesNeed, mingleIntroductionsNeed];
|
|
195
221
|
|
|
196
222
|
// ---- founding grace (task 1002739 / direction §5.6, idea 1000701) -----------
|
|
197
223
|
//
|
|
@@ -269,7 +295,7 @@ module.exports = {
|
|
|
269
295
|
computeNeeds,
|
|
270
296
|
// exported for tests / direct reuse
|
|
271
297
|
artKeyNeed,
|
|
272
|
-
helpRequestsNeed, taskRecommendationsNeed, boardVotesNeed,
|
|
298
|
+
helpRequestsNeed, taskRecommendationsNeed, boardVotesNeed, mingleIntroductionsNeed,
|
|
273
299
|
foundingGraceActive,
|
|
274
300
|
FOUNDING_GRACE_DAYS,
|
|
275
301
|
SETTINGS_PATH,
|
|
@@ -46,6 +46,25 @@
|
|
|
46
46
|
</div>
|
|
47
47
|
</section>
|
|
48
48
|
|
|
49
|
+
<!-- SOCIAL MODE ONLY (task 1003178 / C08). Not a page and not a nav item:
|
|
50
|
+
this panel is part of Collab, and it appears because the hall is in
|
|
51
|
+
social mode — the shell stamps data-social="on" on the document root
|
|
52
|
+
from /me, and collab.js reveals this section from that. With social
|
|
53
|
+
off the page is exactly what it was, which is the point: the hall
|
|
54
|
+
changes, it does not gain a destination. -->
|
|
55
|
+
<section class="scroll" id="cb-mingle-section" aria-labelledby="cb-mingle-h" hidden>
|
|
56
|
+
<h2 class="scroll__h" id="cb-mingle-h">Introductions</h2>
|
|
57
|
+
<p class="scroll__lede" id="cb-mingle-note">
|
|
58
|
+
Mingle pairs you with someone from a different craft. Saying no costs nothing
|
|
59
|
+
at all — no rank, no karma, no streak, and it does not change how often you
|
|
60
|
+
are paired again.
|
|
61
|
+
</p>
|
|
62
|
+
<ul class="ov-rows" id="cb-mingle"></ul>
|
|
63
|
+
<div class="empty-state" id="cb-mingle-empty" hidden>
|
|
64
|
+
<p>No introductions right now. You will see one here when a pairing round runs.</p>
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
49
68
|
<section class="scroll" aria-labelledby="cb-mine-h">
|
|
50
69
|
<h2 class="scroll__h" id="cb-mine-h">Your asks</h2>
|
|
51
70
|
<p class="scroll__lede">
|
|
@@ -143,6 +143,78 @@
|
|
|
143
143
|
? `Matching your craft: ${crafts.join(', ')}.`
|
|
144
144
|
: 'You have not set a craft, so only requests addressed to you by name reach you.';
|
|
145
145
|
}
|
|
146
|
+
|
|
147
|
+
// The social half. Best-effort and LAST: an introductions read that fails
|
|
148
|
+
// must never take the help requests down with it — those are the page.
|
|
149
|
+
try { await loadMingle(); } catch (_) { /* the panel stays as it was */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- social mode: introductions (task 1003178 / C08) ----------------------
|
|
153
|
+
//
|
|
154
|
+
// READ THE MODE OFF THE DOCUMENT, never re-derive it. The shell stamps
|
|
155
|
+
// data-social="on" from /me's resolved pair of switches (C07: the project
|
|
156
|
+
// enabled mingle AND this builder opted in). Asking the server again here would
|
|
157
|
+
// be a second answer to one question, and a page that decided for itself
|
|
158
|
+
// whether social was on could disagree with the rest of the hall.
|
|
159
|
+
const socialOn = () => document.documentElement.dataset.social === 'on';
|
|
160
|
+
|
|
161
|
+
// The other person in the pair — the row is written from the VIEWER's side, so
|
|
162
|
+
// it names who they were introduced to rather than echoing both ids back.
|
|
163
|
+
function otherOf(pair, meId) {
|
|
164
|
+
const lo = String(pair.builder_lo), hi = String(pair.builder_hi);
|
|
165
|
+
return String(meId) === lo
|
|
166
|
+
? { id: hi, login: pair.hi_login, name: pair.hi_name, craft: pair.craft_hi, myCraft: pair.craft_lo }
|
|
167
|
+
: { id: lo, login: pair.lo_login, name: pair.lo_name, craft: pair.craft_lo, myCraft: pair.craft_hi };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mingleRow(pair, meId) {
|
|
171
|
+
const o = otherOf(pair, meId);
|
|
172
|
+
const who = escapeHtml(o.name || o.login || `builder ${o.id}`);
|
|
173
|
+
return `<li class="ov-row">
|
|
174
|
+
<div class="ov-row__main">
|
|
175
|
+
<a class="ov-row__title" href="/builders/profile?id=${escapeHtml(String(o.id))}">${who}</a>
|
|
176
|
+
<p class="ov-row__sub">A ${escapeHtml(String(o.craft))} — you are a ${escapeHtml(String(o.myCraft))}. Mingle pairs across crafts.</p>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="ov-row__right">
|
|
179
|
+
<div class="ov-row__actions">
|
|
180
|
+
<button class="btn-accent" data-mingle="accepted" data-id="${escapeHtml(String(pair.id))}">Say yes</button>
|
|
181
|
+
<button class="btn-ghost" data-mingle="declined" data-id="${escapeHtml(String(pair.id))}">Not now</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
</li>`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function loadMingle() {
|
|
188
|
+
const section = $('#cb-mingle-section');
|
|
189
|
+
if (!section) return;
|
|
190
|
+
// Off is off: the section stays hidden and nothing is fetched. A page that
|
|
191
|
+
// fetched anyway would make "social is off" cost a request on every load.
|
|
192
|
+
if (!socialOn()) { section.hidden = true; return; }
|
|
193
|
+
section.hidden = false;
|
|
194
|
+
|
|
195
|
+
const r = await api.request('GET', '/mingle');
|
|
196
|
+
const pairs = (r.ok && r.data && r.data.open_pairs) || [];
|
|
197
|
+
const meId = (r.ok && r.data && r.data.you) || null;
|
|
198
|
+
paint('#cb-mingle', '#cb-mingle-empty', pairs.map((p) => mingleRow(p, meId)).join(''), pairs.length);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function decideMingle(id, decision) {
|
|
202
|
+
if (settling) return;
|
|
203
|
+
settling = true;
|
|
204
|
+
try {
|
|
205
|
+
const r = await api.request('POST', `/mingle/pairs/${id}/decide`, { decision });
|
|
206
|
+
if (!r.ok) {
|
|
207
|
+
toast(window.OTB.errMessage ? window.OTB.errMessage(r.data) : 'Could not answer that introduction.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// Neutral either way. "Declined" must not read as a failure — declining is
|
|
211
|
+
// free, and a toast that sounded disappointed would be the penalty C07's
|
|
212
|
+
// decision forbids, delivered by the UI instead of the schema.
|
|
213
|
+
toast(decision === 'accepted' ? 'Said yes — say hello.' : 'Not now. Nothing changes for you.');
|
|
214
|
+
await load();
|
|
215
|
+
} finally {
|
|
216
|
+
settling = false;
|
|
217
|
+
}
|
|
146
218
|
}
|
|
147
219
|
|
|
148
220
|
async function settle(id, status) {
|
|
@@ -162,6 +234,12 @@
|
|
|
162
234
|
}
|
|
163
235
|
|
|
164
236
|
document.addEventListener('click', (ev) => {
|
|
237
|
+
const mingleBtn = ev.target && ev.target.closest ? ev.target.closest('[data-mingle]') : null;
|
|
238
|
+
if (mingleBtn) {
|
|
239
|
+
ev.preventDefault();
|
|
240
|
+
decideMingle(Number(mingleBtn.getAttribute('data-id')), mingleBtn.getAttribute('data-mingle'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
165
243
|
const btn = ev.target && ev.target.closest ? ev.target.closest('[data-settle]') : null;
|
|
166
244
|
if (!btn) return;
|
|
167
245
|
ev.preventDefault();
|
|
@@ -102,6 +102,9 @@
|
|
|
102
102
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
103
103
|
<script src="/builders/palette.js" defer></script>
|
|
104
104
|
<script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
|
|
105
|
+
<!-- The chart primitive, for the treasury section (task 1003446). Before
|
|
106
|
+
drachmae.js, which reads window.OTBKit when it inserts that section. -->
|
|
107
|
+
<script src="/builders/hall-kit.js" defer></script>
|
|
105
108
|
<script src="/builders/drachmae.js?v=2026-09-02-drachmae-2" defer></script>
|
|
106
109
|
<script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
|
|
107
110
|
</body>
|
|
@@ -31,6 +31,15 @@
|
|
|
31
31
|
// The doc lives at docs/onboarding/, so relative links resolve against it.
|
|
32
32
|
const DOC_DIR = 'docs/onboarding/';
|
|
33
33
|
const BOUNTY_URL = '/api/gds/public/bounty-table';
|
|
34
|
+
// task 1003446: the treasury feed. NOT a new endpoint — this is the same
|
|
35
|
+
// /public/cost-summary the signed-out status page already draws as a
|
|
36
|
+
// cumulative line and the Work board already fetches to read ONE scalar off
|
|
37
|
+
// (`total_value_credits`). Its own route comment calls `daily[]` "the
|
|
38
|
+
// treasury chart's cumulative line"; the series was in the payload the whole
|
|
39
|
+
// time, and the stranger got the chart while the builder got the number.
|
|
40
|
+
const TREASURY_URL = '/api/gds/public/cost-summary';
|
|
41
|
+
const TREASURY_SECTION_ID = 'the-treasury';
|
|
42
|
+
const TREASURY_SECTION_TITLE = 'The treasury, so far';
|
|
34
43
|
const BOUNTY_SECTION_ID = 'security-bounty-rates';
|
|
35
44
|
const BOUNTY_SECTION_TITLE = 'Security bounty rates';
|
|
36
45
|
// The section the live table is inserted AHEAD of: the doc's closing links.
|
|
@@ -109,6 +118,129 @@
|
|
|
109
118
|
return true;
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
// ---- the treasury (task 1003446) -----------------------------------------
|
|
122
|
+
|
|
123
|
+
// One cumulative figure: the plot, the number it ends on, and the table twin
|
|
124
|
+
// that keeps it out of picture-only territory. `fmt` renders a value for a
|
|
125
|
+
// human; `rows` is the twin's own text, so the money and the credits can read
|
|
126
|
+
// differently without the chart knowing about either.
|
|
127
|
+
function treasuryFigure({ id, title, points, fmt, foot }) {
|
|
128
|
+
const kit = window.OTBKit;
|
|
129
|
+
const esc = window.OTBMdReader.escapeHtml;
|
|
130
|
+
if (!points.length) return '';
|
|
131
|
+
const last = points[points.length - 1];
|
|
132
|
+
const series = kit.timeStepSeriesHtml(points, {
|
|
133
|
+
x0: points[0].x,
|
|
134
|
+
x1: last.x,
|
|
135
|
+
yMax: last.value,
|
|
136
|
+
});
|
|
137
|
+
const { endX, endY } = series.geometry;
|
|
138
|
+
const day = (t) => new Date(t).toISOString().slice(0, 10);
|
|
139
|
+
// Every 7th day plus the last: a twin with one row per calendar day since
|
|
140
|
+
// May would be ~120 rows behind a disclosure nobody finishes reading.
|
|
141
|
+
const twinRows = points.filter((p, i) => i % 7 === 0 || i === points.length - 1);
|
|
142
|
+
return `<figure class="pchart" id="${esc(id)}">
|
|
143
|
+
<figcaption class="pchart__title">${esc(title)}</figcaption>
|
|
144
|
+
${kit.plotSvgHtml({
|
|
145
|
+
series: series.body,
|
|
146
|
+
// No readout is wired here, so no focus stop and no dead crosshair.
|
|
147
|
+
interactive: false,
|
|
148
|
+
ariaLabel: `${title}: ${fmt(last.value)} as of ${day(last.x)}`,
|
|
149
|
+
label: kit.plotValueLabelHtml(endX, endY - 8, fmt(last.value), 'end'),
|
|
150
|
+
leftTick: day(points[0].x),
|
|
151
|
+
rightTick: day(last.x),
|
|
152
|
+
})}
|
|
153
|
+
<details class="pchart__twin">
|
|
154
|
+
<summary>Read it as a table</summary>
|
|
155
|
+
<table class="ledger pchart__table"><thead><tr><th>Day</th><th>${esc(title)}</th></tr></thead><tbody>
|
|
156
|
+
${twinRows.map((p) => `<tr><td>${esc(day(p.x))}</td><td>${esc(fmt(p.value))}</td></tr>`).join('')}
|
|
157
|
+
</tbody></table>
|
|
158
|
+
</details>
|
|
159
|
+
<p class="pchart__foot">${esc(foot)}</p>
|
|
160
|
+
</figure>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// The live treasury as a real section of the article, ahead of the rate card.
|
|
164
|
+
// Returns false when the summary can't be read: the prose stays and no empty
|
|
165
|
+
// panel or outline entry that leads nowhere is rendered — the same contract
|
|
166
|
+
// insertBountySection keeps.
|
|
167
|
+
async function insertTreasurySection(article) {
|
|
168
|
+
if (!window.OTBKit) {
|
|
169
|
+
console.warn('[drachmae] OTBKit missing — the treasury section needs the chart primitive');
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
let payload;
|
|
173
|
+
try {
|
|
174
|
+
const r = await fetch(TREASURY_URL, { credentials: 'same-origin' });
|
|
175
|
+
if (!r.ok) throw new Error(`${TREASURY_URL} → ${r.status}`);
|
|
176
|
+
payload = await r.json();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.warn('[drachmae] cost summary unavailable:', err.message);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const esc = window.OTBMdReader.escapeHtml;
|
|
182
|
+
const kit = window.OTBKit;
|
|
183
|
+
const currency = (window.__BRANDING__ && window.__BRANDING__.currency
|
|
184
|
+
&& window.__BRANDING__.currency.label) || 'credits';
|
|
185
|
+
const Currency = currency.charAt(0).toUpperCase() + currency.slice(1);
|
|
186
|
+
const usd = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
|
|
187
|
+
const num = (n) => Number(n || 0).toLocaleString();
|
|
188
|
+
|
|
189
|
+
// The server already ran generate_series, so every calendar day is present
|
|
190
|
+
// and a flat run is a real flat run rather than a missing sample.
|
|
191
|
+
const spend = kit.cumulateByDay(payload.daily, { valueOf: (r) => r.amount_usd });
|
|
192
|
+
const earned = kit.cumulateByDay(payload.daily_credits, { valueOf: (r) => r.credits });
|
|
193
|
+
// Both series empty is a treasury with nothing in it yet — one honest line,
|
|
194
|
+
// not two empty plots.
|
|
195
|
+
if (!spend.length && !earned.length) return false;
|
|
196
|
+
|
|
197
|
+
const figures = [
|
|
198
|
+
treasuryFigure({
|
|
199
|
+
id: 'treasury-spend',
|
|
200
|
+
title: 'Spent, cumulative',
|
|
201
|
+
points: spend,
|
|
202
|
+
fmt: usd,
|
|
203
|
+
foot: 'Every recorded cost — API, infra, compute, art — added up day by day.',
|
|
204
|
+
}),
|
|
205
|
+
treasuryFigure({
|
|
206
|
+
id: 'treasury-earned',
|
|
207
|
+
title: `${Currency} awarded, cumulative`,
|
|
208
|
+
points: earned,
|
|
209
|
+
fmt: num,
|
|
210
|
+
foot: `What the project has paid builders, in ${esc(currency)}, day by day.`,
|
|
211
|
+
}),
|
|
212
|
+
].filter(Boolean).join('');
|
|
213
|
+
|
|
214
|
+
// Where the money went, as the kit's bar series rather than a fourth
|
|
215
|
+
// hand-rolled one. Categories arrive sorted biggest-first.
|
|
216
|
+
const byCategory = (payload.by_category || []).slice(0, 8);
|
|
217
|
+
const categories = byCategory.length
|
|
218
|
+
? `<h3 id="treasury-by-category">Where it went</h3>`
|
|
219
|
+
+ kit.barSeriesHtml(
|
|
220
|
+
byCategory.map((c) => ({ label: c.category, value: c.total_usd, valueLabel: usd(c.total_usd) })),
|
|
221
|
+
{ list: true, ariaLabel: 'Spend by category', valueWidth: '84px' },
|
|
222
|
+
)
|
|
223
|
+
: '';
|
|
224
|
+
|
|
225
|
+
const section = document.createElement('section');
|
|
226
|
+
section.innerHTML =
|
|
227
|
+
`<h2 id="${esc(TREASURY_SECTION_ID)}">${esc(TREASURY_SECTION_TITLE)}</h2>`
|
|
228
|
+
+ `<p>These are this project's real numbers, read from the server when you `
|
|
229
|
+
+ `opened this page. It has spent <strong>${esc(usd(payload.total_usd))}</strong> `
|
|
230
|
+
+ `and awarded <strong>${esc(num(payload.total_value_credits))} ${esc(currency)}</strong> `
|
|
231
|
+
+ `for the work that money bought.</p>`
|
|
232
|
+
+ `<div class="pcharts">${figures}</div>`
|
|
233
|
+
+ categories;
|
|
234
|
+
|
|
235
|
+
const bounty = article.querySelector(`#${BOUNTY_SECTION_ID}`);
|
|
236
|
+
const closing = [...article.querySelectorAll('h2')].find((h) => CLOSING_H2.test(h.textContent.trim()));
|
|
237
|
+
// Ahead of the rate card if it landed first, else ahead of the closing
|
|
238
|
+
// links, else appended: the doc keeps ending on "dig deeper" either way.
|
|
239
|
+
const before = (bounty && bounty.parentElement === article ? bounty : null) || closing;
|
|
240
|
+
if (before) article.insertBefore(section, before); else article.appendChild(section);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
112
244
|
// The outline is read from the RENDERED headings rather than the parser's raw
|
|
113
245
|
// text, so `*not*` and a [link](…) in a heading arrive as plain words, and so
|
|
114
246
|
// the live section inserted above is simply one more heading in order.
|
|
@@ -276,9 +408,13 @@
|
|
|
276
408
|
article.innerHTML = parsed.html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
277
409
|
article.removeAttribute('aria-busy');
|
|
278
410
|
show('article');
|
|
279
|
-
// Awaited before the outline is built so
|
|
280
|
-
// scrollspy
|
|
411
|
+
// Awaited before the outline is built so both live sections get their
|
|
412
|
+
// entries and scrollspy targets; the article itself is already on screen by
|
|
413
|
+
// now. The rate card goes in FIRST so the treasury can land ahead of it by
|
|
414
|
+
// id — the doc then reads prose, then what actually happened, then the
|
|
415
|
+
// published rates, then "dig deeper".
|
|
281
416
|
await insertBountySection(article);
|
|
417
|
+
await insertTreasurySection(article);
|
|
282
418
|
addAnchors(article);
|
|
283
419
|
setupTocToggle();
|
|
284
420
|
setupScrollspy(renderToc(collectHeadings(article)));
|
|
@@ -663,6 +663,35 @@
|
|
|
663
663
|
return chartNums(values).map((v) => (run += v));
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
// Timestamped rows → [{ x, value }] ascending, one point per calendar day
|
|
667
|
+
// that HAS a row, value cumulative. The third consumer of this shape put it
|
|
668
|
+
// here (task 1003446): the profile's works and credits lines, and
|
|
669
|
+
// /drachmae's treasury. Two callers is a coincidence; three is a primitive.
|
|
670
|
+
//
|
|
671
|
+
// `dateOf`/`valueOf` are the caller's, because only the caller knows which
|
|
672
|
+
// column is the date — `shipped_at` on an activity row, `recorded_at` on a
|
|
673
|
+
// credits row, `day` on a gap-filled daily feed. A row whose date will not
|
|
674
|
+
// parse is DROPPED rather than counted at the epoch, where it would stretch
|
|
675
|
+
// the axis back to 1970 and flatten every real point against the floor.
|
|
676
|
+
function cumulateByDay(rows, opts = {}) {
|
|
677
|
+
const dateOf = opts.dateOf || ((r) => r && r.day);
|
|
678
|
+
const valueOf = opts.valueOf || ((r) => r && r.value);
|
|
679
|
+
const byDay = new Map();
|
|
680
|
+
for (const r of rows || []) {
|
|
681
|
+
const t = Date.parse(dateOf(r));
|
|
682
|
+
if (!Number.isFinite(t)) continue;
|
|
683
|
+
// The epoch-day floor — UTC midnight, so two rows on one day land on one
|
|
684
|
+
// point. Same value as setUTCHours(0,0,0,0) without a Date per row, and
|
|
685
|
+
// the same expression profile-charts.js's own `midnight` used before this
|
|
686
|
+
// moved, so no series shifts by a day.
|
|
687
|
+
const d = Math.floor(t / 86400000) * 86400000;
|
|
688
|
+
byDay.set(d, (byDay.get(d) || 0) + (Number(valueOf(r)) || 0));
|
|
689
|
+
}
|
|
690
|
+
const days = [...byDay.keys()].sort((a, b) => a - b);
|
|
691
|
+
const totals = cumulate(days.map((d) => byDay.get(d)));
|
|
692
|
+
return days.map((d, i) => ({ x: d, value: totals[i] }));
|
|
693
|
+
}
|
|
694
|
+
|
|
666
695
|
// The line geometry, separated from the markup so a chart that needs the
|
|
667
696
|
// maths without this SVG can take just this. Returns { points, x, y, nums }.
|
|
668
697
|
//
|
|
@@ -867,21 +896,30 @@
|
|
|
867
896
|
return { body, xs, base, py, max };
|
|
868
897
|
}
|
|
869
898
|
|
|
870
|
-
// The plot itself: the series a geometry built, on the axis, with the
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
899
|
+
// The plot itself: the series a geometry built, on the axis, with the two end
|
|
900
|
+
// ticks — and, when the caller wires a readout, the hidden crosshair it moves
|
|
901
|
+
// plus the `tabindex` that makes an arrow-key-walkable chart reachable by tab.
|
|
902
|
+
//
|
|
903
|
+
// `interactive: false` (task 1003446) drops BOTH of those, and the pairing is
|
|
904
|
+
// the point: a `role="img"` with a `tabindex="0"` is a focus stop a keyboard
|
|
905
|
+
// reader lands on with nothing to do there, and a crosshair nothing moves is
|
|
906
|
+
// markup that only looks like a feature. A plot whose whole reading is its
|
|
907
|
+
// aria-label and its table twin says so.
|
|
874
908
|
function plotSvgHtml(opts = {}) {
|
|
875
909
|
const { width, height, pad, base } = plotBox(opts);
|
|
910
|
+
const interactive = opts.interactive !== false;
|
|
876
911
|
const tick = (x, text, anchor) => (text
|
|
877
912
|
? `<text class="kit-plot__tick" x="${x}" y="${height - 6}"`
|
|
878
913
|
+ `${anchor ? ` text-anchor="${anchor}"` : ''}>${esc(text)}</text>`
|
|
879
914
|
: '');
|
|
880
|
-
return `<svg class="kit-plot" viewBox="0 0 ${width} ${height}" role="img"
|
|
915
|
+
return `<svg class="kit-plot" viewBox="0 0 ${width} ${height}" role="img"`
|
|
916
|
+
+ (interactive ? ' tabindex="0"' : '')
|
|
881
917
|
+ ` aria-label="${esc(opts.ariaLabel || '')}">`
|
|
882
918
|
+ (opts.series || '')
|
|
883
919
|
+ `<line class="kit-plot__axis" x1="${pad.l}" y1="${base}" x2="${width - pad.r}" y2="${base}"></line>`
|
|
884
|
-
+
|
|
920
|
+
+ (interactive
|
|
921
|
+
? `<line class="kit-plot__hair" x1="0" y1="${pad.t}" x2="0" y2="${base}" style="visibility:hidden"></line>`
|
|
922
|
+
: '')
|
|
885
923
|
+ (opts.label || '')
|
|
886
924
|
+ tick(pad.l, opts.leftTick)
|
|
887
925
|
+ tick(width - pad.r, opts.rightTick, 'end')
|
|
@@ -918,7 +956,7 @@
|
|
|
918
956
|
// charts (task 1003445) — the ONE place the hall draws a series.
|
|
919
957
|
// linePoints/cumulate are the maths on their own, for a chart that wants
|
|
920
958
|
// the geometry without this SVG (the profile's timeChart).
|
|
921
|
-
sparklineHtml, barSeriesHtml, chartCardHtml, linePoints, cumulate,
|
|
959
|
+
sparklineHtml, barSeriesHtml, chartCardHtml, linePoints, cumulate, cumulateByDay,
|
|
922
960
|
// plotted charts: the axis frame + the two series shapes that ride it
|
|
923
961
|
plotSvgHtml, plotValueLabelHtml, timeStepSeriesHtml, timeStepGeometry, columnSeriesHtml,
|
|
924
962
|
// plotBox is exported so a caller mapping pointer x back into viewBox
|
|
@@ -205,6 +205,12 @@
|
|
|
205
205
|
rank: normalizeRank(builder?.rank),
|
|
206
206
|
graduated,
|
|
207
207
|
showWork: shipStepCurrent,
|
|
208
|
+
// task 1003178 (C08): the index owns its own /me, so it passes the social
|
|
209
|
+
// mode through too — otherwise the hall's busiest page would be the one
|
|
210
|
+
// page the mode never reached. Read off ctx.lastMeState (the live getter),
|
|
211
|
+
// not a local: renderNav takes (builder, onboarding) and is called from
|
|
212
|
+
// five sites, so there is no `me` in scope here.
|
|
213
|
+
social: (ctx.lastMeState && ctx.lastMeState.social) || null,
|
|
208
214
|
builder,
|
|
209
215
|
});
|
|
210
216
|
|
|
@@ -101,20 +101,13 @@
|
|
|
101
101
|
// point; a row with no parsable date is skipped, not zeroed. The lookback
|
|
102
102
|
// clips this for drawing (clipToWindow) rather than re-accumulating, so the
|
|
103
103
|
// running total stays true.
|
|
104
|
+
// The grouping AND the running total are the kit's since task 1003446, when
|
|
105
|
+
// /drachmae's treasury became the third caller of this shape. This wrapper
|
|
106
|
+
// survives only to keep the `{ day, value }` key this page's callers and
|
|
107
|
+
// clipToWindow already read; the arithmetic has one home.
|
|
104
108
|
function cumulativeByDay(rows, valueOf, dateOf = (r) => r && r.shipped_at) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const t = Date.parse(dateOf(r));
|
|
108
|
-
if (!Number.isFinite(t)) continue;
|
|
109
|
-
const d = midnight(t);
|
|
110
|
-
byDay.set(d, (byDay.get(d) || 0) + Number(valueOf(r)) || 0);
|
|
111
|
-
}
|
|
112
|
-
// The grouping is this function's; the running total is the kit's
|
|
113
|
-
// (task 1003445), so the hall has ONE accumulator rather than two that
|
|
114
|
-
// could disagree about what a hole in the series means.
|
|
115
|
-
const days = [...byDay.keys()].sort((a, b) => a - b);
|
|
116
|
-
const totals = window.OTBKit.cumulate(days.map((d) => byDay.get(d)));
|
|
117
|
-
return days.map((d, i) => ({ day: d, value: totals[i] }));
|
|
109
|
+
return window.OTBKit.cumulateByDay(rows, { dateOf, valueOf })
|
|
110
|
+
.map((p) => ({ day: p.x, value: p.value }));
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
// Clip a cumulative series to [windowStart, ∞) for drawing. The line must
|
|
@@ -119,41 +119,12 @@
|
|
|
119
119
|
(glass on the card, like settings' quote), the series in the accent and every
|
|
120
120
|
word in ink. The SVG scales with its card; the axis band is inside the
|
|
121
121
|
viewBox, so the card grows with its content and never scrolls. */
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.
|
|
128
|
-
margin: 0;
|
|
129
|
-
padding: var(--sp-xs) var(--sp-sm) var(--sp-xs);
|
|
130
|
-
background: var(--glass);
|
|
131
|
-
border: var(--line) solid var(--glass-edge);
|
|
132
|
-
border-radius: var(--radius-sm);
|
|
133
|
-
min-width: 0;
|
|
134
|
-
}
|
|
135
|
-
.pchart__title { margin: 0; font-size: 13.5px; font-weight: 500; color: var(--ink); }
|
|
136
|
-
.pchart__read { margin: 2px 0 var(--sp-hair); font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-soft); min-height: 1.4em; }
|
|
137
|
-
/* The plot itself — the svg, the line, the columns, the axis, the crosshair,
|
|
138
|
-
the ticks — is OTBKit's `.kit-plot*` in style.css (task 1003445). It was
|
|
139
|
-
written twice in this file's own script, and the goal's next charts want it
|
|
140
|
-
again. The dataviz spec moved verbatim; what stays below is this page's
|
|
141
|
-
FIGURE: the caption, the live readout, the table twin and the foot. */
|
|
142
|
-
.pchart__twin { margin-top: var(--sp-hair); }
|
|
143
|
-
.pchart__twin > summary { display: inline-flex; align-items: center; min-height: 24px; padding: 0 4px; margin: 0 -4px; font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-soft); cursor: pointer; border-radius: var(--radius-sm); }
|
|
144
|
-
.pchart__twin > summary:hover { color: var(--ink); background: var(--glass-strong); }
|
|
145
|
-
.pchart__table { margin-top: var(--sp-hair); font-size: 12.5px; }
|
|
146
|
-
.pchart__table td, .pchart__table th { padding: 3px 6px; }
|
|
147
|
-
.pchart__foot { margin: var(--sp-hair) 0 0; font-size: 12px; color: var(--ink-faint); }
|
|
148
|
-
.pchart__note { margin: 0; font-size: 14px; color: var(--ink-soft); }
|
|
149
|
-
/* the craft mix: label, bar, count — a row is its own table row */
|
|
150
|
-
/* The craft mix is OTBKit.barSeriesHtml (task 1003445) — the .pmix family was
|
|
151
|
-
the hall's third copy of one horizontal bar. Only the vertical air around it
|
|
152
|
-
is this page's; the row, the track and the 24px floor are the kit's. */
|
|
153
|
-
.pchart .kit-bars { margin: var(--sp-hair) 0; }
|
|
154
|
-
@media (max-width: 720px) {
|
|
155
|
-
.pcharts { grid-template-columns: minmax(0, 1fr); }
|
|
156
|
-
}
|
|
122
|
+
/* The plot is OTBKit's `.kit-plot*` (task 1003445) and the FIGURE around it —
|
|
123
|
+
`.pcharts` / `.pchart` / `__title` / `__read` / `__twin` / `__table` /
|
|
124
|
+
`__foot` — moved to style.css beside it in task 1003446, when /drachmae's
|
|
125
|
+
treasury became its second consumer. The names did not change: they are
|
|
126
|
+
spelled into profile.js AND drachmae.js. What stays in this sheet is the one
|
|
127
|
+
thing only this page has, the lookback control below. */
|
|
157
128
|
|
|
158
129
|
/* the lookback control (task 1003489, owner request) — one segmented row above
|
|
159
130
|
the grid, scoping every chart. The pressed side takes the accent TINT the
|
|
@@ -522,7 +522,32 @@
|
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
//
|
|
525
|
+
// SOCIAL MODE (task 1003178 / C08, goal 1000075). The owner's directive is that
|
|
526
|
+
// social is "scoped into the ENTIRE builders hall, not a single social tab", and
|
|
527
|
+
// the criterion says outright that a /social route with the same hall around it
|
|
528
|
+
// does NOT satisfy it. So social is a MODE stamped on the document root, beside
|
|
529
|
+
// data-theme and for the same reason: every hall page loads this shell, so one
|
|
530
|
+
// attribute lets any surface — a stylesheet, a page script, a widget — read the
|
|
531
|
+
// mode without a route, a nav item, or a second fetch.
|
|
532
|
+
//
|
|
533
|
+
// OFF is the default and the fallback. `/me.social` is null when lifecycle is
|
|
534
|
+
// disabled or the read failed, and null is OFF: a mode that cannot be read is
|
|
535
|
+
// not on. It is only ON when BOTH of C07's switches are (the project enabled
|
|
536
|
+
// mingle AND this builder opted in) — the server resolves that pair, this never
|
|
537
|
+
// re-derives it.
|
|
538
|
+
//
|
|
539
|
+
// Written as an attribute rather than kept in `access` on purpose: CSS can hit
|
|
540
|
+
// it (`:root[data-social="on"] …`), which is what lets a surface change how it
|
|
541
|
+
// READS without every page script learning about social.
|
|
542
|
+
function applySocial(social) {
|
|
543
|
+
var on = !!(social && social.enabled === true);
|
|
544
|
+
var root = document.documentElement;
|
|
545
|
+
if (on) root.dataset.social = 'on';
|
|
546
|
+
else delete root.dataset.social;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// applyAccess({ authed, rank, graduated, showWork, social }) — settle nav
|
|
550
|
+
// visibility and the social mode.
|
|
526
551
|
// Called by builders.js on the index (it owns /me there) and by the shell's own
|
|
527
552
|
// probe elsewhere. Cosmetic only; the server enforces (ADR 0016).
|
|
528
553
|
function applyAccess(state) {
|
|
@@ -531,6 +556,7 @@
|
|
|
531
556
|
if ('rank' in state) access.rank = state.rank ? String(state.rank).toLowerCase() : null;
|
|
532
557
|
if ('graduated' in state) access.graduated = !!state.graduated;
|
|
533
558
|
if ('showWork' in state) access.showWork = !!state.showWork;
|
|
559
|
+
if ('social' in state) applySocial(state.social);
|
|
534
560
|
if (state.builder) renderUser(state.builder);
|
|
535
561
|
}
|
|
536
562
|
document.querySelectorAll('#app-sidebar [data-gate]').forEach(function (el) {
|
|
@@ -744,6 +770,9 @@
|
|
|
744
770
|
rank: me.builder.rank,
|
|
745
771
|
graduated: !!(me.onboarding && me.onboarding.graduated),
|
|
746
772
|
showWork: showWork,
|
|
773
|
+
// task 1003178 (C08): the mode rides the identity every page already
|
|
774
|
+
// fetches, so no surface needs a route or a second call to know.
|
|
775
|
+
social: me.social || null,
|
|
747
776
|
builder: me.builder,
|
|
748
777
|
});
|
|
749
778
|
}).catch(function () { /* cosmetic only */ });
|
|
@@ -2047,6 +2047,41 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2047
2047
|
.kit-plot__end { font-family: var(--font-body); font-size: 11.5px; font-weight: 500; fill: var(--ink); }
|
|
2048
2048
|
.kit-plot__tick { font-family: var(--label-face); font-size: 10px; letter-spacing: var(--label-space); text-transform: var(--label-case); fill: var(--ink-faint); }
|
|
2049
2049
|
|
|
2050
|
+
/* The FIGURE a plot sits in — a card one tonal step down from the panel (glass
|
|
2051
|
+
on the card, like settings' quote), its caption, its live readout, its
|
|
2052
|
+
<details> table twin and its foot. Moved here from profile.css in task
|
|
2053
|
+
1003446, when /drachmae's treasury became the second consumer; the `.pchart`
|
|
2054
|
+
names are unchanged because they are spelled into profile.js and drachmae.js.
|
|
2055
|
+
The kit does NOT emit these — a page composes its own figure and the kit
|
|
2056
|
+
draws the series inside it. */
|
|
2057
|
+
.pcharts {
|
|
2058
|
+
display: grid;
|
|
2059
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2060
|
+
gap: var(--sp-sm);
|
|
2061
|
+
}
|
|
2062
|
+
.pchart {
|
|
2063
|
+
margin: 0;
|
|
2064
|
+
padding: var(--sp-xs) var(--sp-sm) var(--sp-xs);
|
|
2065
|
+
background: var(--glass);
|
|
2066
|
+
border: var(--line) solid var(--glass-edge);
|
|
2067
|
+
border-radius: var(--radius-sm);
|
|
2068
|
+
min-width: 0;
|
|
2069
|
+
}
|
|
2070
|
+
.pchart__title { margin: 0; font-size: 13.5px; font-weight: 500; color: var(--ink); }
|
|
2071
|
+
.pchart__read { margin: 2px 0 var(--sp-hair); font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-soft); min-height: 1.4em; }
|
|
2072
|
+
.pchart__twin { margin-top: var(--sp-hair); }
|
|
2073
|
+
.pchart__twin > summary { display: inline-flex; align-items: center; min-height: 24px; padding: 0 4px; margin: 0 -4px; font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-soft); cursor: pointer; border-radius: var(--radius-sm); }
|
|
2074
|
+
.pchart__twin > summary:hover { color: var(--ink); background: var(--glass-strong); }
|
|
2075
|
+
.pchart__table { margin-top: var(--sp-hair); font-size: 12.5px; }
|
|
2076
|
+
.pchart__table td, .pchart__table th { padding: 3px 6px; }
|
|
2077
|
+
.pchart__foot { margin: var(--sp-hair) 0 0; font-size: 12px; color: var(--ink-faint); }
|
|
2078
|
+
.pchart__note { margin: 0; font-size: 14px; color: var(--ink-soft); }
|
|
2079
|
+
/* The bar series inside a figure keeps the figure's own vertical air. */
|
|
2080
|
+
.pchart .kit-bars { margin: var(--sp-hair) 0; }
|
|
2081
|
+
@media (max-width: 720px) {
|
|
2082
|
+
.pcharts { grid-template-columns: minmax(0, 1fr); }
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2050
2085
|
.curve__kinds { display: flex; flex-direction: column; gap: 6px; margin-top: 16px; }
|
|
2051
2086
|
|
|
2052
2087
|
/* ---------- onboarding checklist ---------- */
|
|
@@ -22,6 +22,7 @@ const goalSuggest = require('./goal-suggest');
|
|
|
22
22
|
const helpRequests = require('./help-requests');
|
|
23
23
|
const taskRecommendations = require('./task-recommendations');
|
|
24
24
|
const activity = require('./activity');
|
|
25
|
+
const mingle = require('./mingle');
|
|
25
26
|
|
|
26
27
|
module.exports = {
|
|
27
28
|
// --- retired doorway leaks (ADR 0093 §3) ---------------------------------
|
|
@@ -52,6 +53,13 @@ module.exports = {
|
|
|
52
53
|
// builder, exposed the same way and for the same reason — a recommendation
|
|
53
54
|
// nobody surfaces is merely stored.
|
|
54
55
|
openTaskRecommendationsFor: (...a) => taskRecommendations.countOpenForRecipient(...a),
|
|
56
|
+
// task 1003178 (C08): social MODE, and the count of open introductions. The
|
|
57
|
+
// mode is the pair of switches C07 resolved (project enabled AND this builder
|
|
58
|
+
// opted in) — the kernel's /me carries it so every hall surface can read the
|
|
59
|
+
// mode WITHOUT importing this module, which is what makes social a mode rather
|
|
60
|
+
// than a page. The count feeds the needs roster, the same slot C01 and C03 use.
|
|
61
|
+
socialModeFor: (...a) => mingle.mingleStateFor(...a),
|
|
62
|
+
openMingleIntroductionsFor: (...a) => mingle.countOpenPairsFor(...a),
|
|
55
63
|
getTask: (...a) => db.getTask(...a),
|
|
56
64
|
// findTaskBySourceRef: the create-vector idempotency read (task 1003114 / R03
|
|
57
65
|
// of goal 1000074). copy-desk turns an artist's copy proposal into a task and
|