@bongos/core 1.24.36 → 1.24.37

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.
Files changed (71) hide show
  1. package/.bongos-core.json +90 -85
  2. package/docs/copy-inventory.md +58 -57
  3. package/docs/copy-registry.json +163 -166
  4. package/docs/module-api-changelog.md +2 -0
  5. package/docs/onboarding/primer.md +2 -0
  6. package/docs/page-inventory.json +34 -34
  7. package/docs/page-readings.json +684 -680
  8. package/modules/hall-ui/public/ai-studio.html +2 -2
  9. package/modules/hall-ui/public/atlas.html +2 -2
  10. package/modules/hall-ui/public/blockers.html +2 -2
  11. package/modules/hall-ui/public/board-room.html +2 -2
  12. package/modules/hall-ui/public/city.html +2 -2
  13. package/modules/hall-ui/public/collab.html +2 -2
  14. package/modules/hall-ui/public/commands.html +2 -2
  15. package/modules/hall-ui/public/copy-desk.html +2 -2
  16. package/modules/hall-ui/public/craft-mode.js +1 -1
  17. package/modules/hall-ui/public/deploy.html +2 -2
  18. package/modules/hall-ui/public/fleet-costs.js +3 -2
  19. package/modules/hall-ui/public/gate.html +2 -2
  20. package/modules/hall-ui/public/goal-map.html +2 -2
  21. package/modules/hall-ui/public/goals.html +2 -2
  22. package/modules/hall-ui/public/government.html +2 -2
  23. package/modules/hall-ui/public/idea.html +2 -2
  24. package/modules/hall-ui/public/ideas.html +2 -2
  25. package/modules/hall-ui/public/index.html +2 -2
  26. package/modules/hall-ui/public/market-watch-item.html +2 -2
  27. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  28. package/modules/hall-ui/public/market-watch.html +2 -2
  29. package/modules/hall-ui/public/modules.html +2 -2
  30. package/modules/hall-ui/public/money.html +92 -0
  31. package/modules/hall-ui/public/money.js +302 -0
  32. package/modules/hall-ui/public/money.states.json +15 -0
  33. package/modules/hall-ui/public/my-agents.html +2 -2
  34. package/modules/hall-ui/public/palette.js +9 -2
  35. package/modules/hall-ui/public/primer-live.js +18 -105
  36. package/modules/hall-ui/public/primer.html +7 -8
  37. package/modules/hall-ui/public/primer.js +16 -18
  38. package/modules/hall-ui/public/profile.html +2 -2
  39. package/modules/hall-ui/public/project-settings.css +2 -1
  40. package/modules/hall-ui/public/project-settings.html +6 -16
  41. package/modules/hall-ui/public/project-settings.js +2 -142
  42. package/modules/hall-ui/public/roadmap.html +2 -2
  43. package/modules/hall-ui/public/roster.html +2 -2
  44. package/modules/hall-ui/public/sessions.html +2 -2
  45. package/modules/hall-ui/public/settings.html +2 -2
  46. package/modules/hall-ui/public/shell.js +7 -5
  47. package/modules/hall-ui/public/street.html +2 -2
  48. package/modules/hall-ui/public/studio.html +2 -2
  49. package/modules/hall-ui/public/task.html +2 -2
  50. package/modules/hall-ui/public/thinking.html +2 -2
  51. package/modules/hall-ui/public/tweak-editor.html +2 -2
  52. package/modules/hall-ui/public/watch.html +2 -2
  53. package/modules/hall-ui/public/work.html +2 -2
  54. package/package-lock.json +2 -2
  55. package/package.json +1 -1
  56. package/release-notes.json +6 -0
  57. package/src/bongos/auth.js +0 -8
  58. package/src/bongos/serve-internal.js +15 -6
  59. package/src/module-api.js +1 -1
  60. package/tests/auth_resolve_failure.mjs +5 -4
  61. package/tests/fleet_cost_view.mjs +2 -18
  62. package/tests/hall_loading.mjs +1 -1
  63. package/tests/hall_money_page.mjs +272 -0
  64. package/tests/hall_nav.mjs +5 -4
  65. package/tests/hall_oversight_world.mjs +1 -1
  66. package/tests/hall_page_gate_map.mjs +3 -2
  67. package/tests/hall_reading_room.mjs +21 -62
  68. package/tests/nav_permission_atoms.mjs +1 -1
  69. package/modules/hall-ui/public/fleet.html +0 -69
  70. package/modules/hall-ui/public/fleet.js +0 -41
  71. package/modules/hall-ui/public/fleet.states.json +0 -14
@@ -0,0 +1,302 @@
1
+ // money.js — the owner's one money page (task 1005047; owner decision 2026-11-03,
2
+ // idea 1001312).
3
+ //
4
+ // WHY. What the project spent and paid out was drawn on three pages: Project
5
+ // settings ("Where the money went"), the Economy page and then the Primer ("The
6
+ // treasury, so far") and /fleet (what each hosted project costs). Each is a section
7
+ // here now, drawn by the same code that drew it there, moved whole:
8
+ // * #payouts — GET /project-account/outflow (+ /ledger-checks), the payout
9
+ // authority's read (payout.approve). From project-settings.js.
10
+ // * #the-treasury — GET /public/cost-summary, the same feed the status page's
11
+ // treasury chart draws. From primer-live.js (once drachmae.js).
12
+ // * #hosting — GET /provisioning/fleet/cost-ledger, drawn by fleet-costs.js.
13
+ // Its route exists only where the provisioning module is on and
14
+ // answers only `provisioning.fleet.manage`, as /fleet did.
15
+ // Each section stays hidden until its own read answers with something to draw, so a
16
+ // reader a read refuses, or a hall without the module, sees no empty panel. If none
17
+ // answers, one plain line says so instead of a page that never stops loading.
18
+ //
19
+ // The pure builders are exposed as window.OTBMoney so tests run the real drawing
20
+ // against the checked-in fixtures; the page boots only where its sections exist.
21
+ (() => {
22
+ 'use strict';
23
+
24
+ const API = '/api/bongos';
25
+ const TREASURY_URL = `${API}/public/cost-summary`;
26
+ const escapeHtml = (s) => window.OTB.escapeHtml(s);
27
+ const esc = escapeHtml;
28
+ const $ = (id) => document.getElementById(id);
29
+ const currencyLabel = () => (window.__BRANDING__ && window.__BRANDING__.currency
30
+ && window.__BRANDING__.currency.label) || 'credits';
31
+ const caps = (s) => s.charAt(0).toUpperCase() + s.slice(1);
32
+
33
+ // ---- where the money went (task 1003184, criterion money-outflow-visible) --------
34
+ //
35
+ // The owner's outflow view. The server sums every figure (modules/economy/outflow.js);
36
+ // this only draws it. It leads with what is left AFTER what is owed, because a balance
37
+ // that looks healthy while it owes three payouts is the trap the view exists to show.
38
+ // Any outcome but a 200 leaves the section hidden: the read is the payout authority's,
39
+ // and everyone else on this page has nothing to see here, not an error.
40
+
41
+ const money = (micro) => {
42
+ const usd = Number(micro || 0) / 1e6;
43
+ const abs = Math.abs(usd).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
44
+ return `${usd < 0 ? '−' : ''}$${abs}`;
45
+ };
46
+ const monthLabel = (ym) => {
47
+ const [y, m] = String(ym).split('-').map(Number);
48
+ return new Date(Date.UTC(y, m - 1, 1)).toLocaleDateString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' });
49
+ };
50
+ const dayLabel = (iso) => (iso ? new Date(iso).toLocaleDateString() : '');
51
+
52
+ function moneyFiguresHtml(out) {
53
+ const c = out.committed;
54
+ const after = out.balance.after_commitments_micro;
55
+ const fig = (label, value, note, tone) => `<div class="ps-money__fig${tone ? ` ps-money__fig--${tone}` : ''}">
56
+ <span class="ps-money__label">${escapeHtml(label)}</span>
57
+ <strong class="ps-money__value">${escapeHtml(value)}</strong>
58
+ <span class="ps-money__note">${escapeHtml(note)}</span>
59
+ </div>`;
60
+ const owedNote = c.owed_count === 1 ? '1 payment' : `${c.owed_count} payments`;
61
+ const apprNote = c.approved_count === 1 ? '1 approval' : `${c.approved_count} approvals`;
62
+ return `<div class="ps-money__figs">
63
+ ${fig('Balance', money(out.balance.available_micro), c.held_micro ? `after ${money(c.held_micro)} held` : 'on our record')}
64
+ ${fig('Owed but not paid', money(c.owed_micro), owedNote, c.owed_micro ? 'warn' : '')}
65
+ ${fig('Approved, not started', money(c.approved_micro), apprNote, c.approved_micro ? 'warn' : '')}
66
+ ${fig('Left after all of that', money(after), after < 0 ? 'the project owes more than it holds' : 'what is really free', after < 0 ? 'bad' : '')}
67
+ </div>${c.other_currency_count ? `<p class="ps-prov">${c.other_currency_count} more in a currency other than US dollars, not added in.</p>` : ''}`;
68
+ }
69
+
70
+ // The payout's latest try in words. A reason is either the processor's own sentence, shown
71
+ // as it is, or one of core's held codes (builder-payouts.js HELD_WORDS). The codes an owner
72
+ // can act on get a short phrase here; any other code is left out rather than shown as a slug.
73
+ const TRY_WORDS = { held: 'Waiting', failed: 'Did not go through' };
74
+ const REASON_WORDS = {
75
+ no_processor: 'no payment processor connected yet',
76
+ recipient_gated: 'this person cannot be paid right now',
77
+ guardian_cosign_missing: 'a guardian has not co-signed yet',
78
+ currency_not_supported: 'not in US dollars',
79
+ };
80
+ function lastTryWords(r) {
81
+ if (!r.last_outcome) return 'Not tried yet';
82
+ const reason = String(r.last_reason || '');
83
+ const why = REASON_WORDS[reason] || (/^[a-z_]+$/.test(reason) ? '' : reason);
84
+ return `${TRY_WORDS[r.last_outcome] || r.last_outcome}${why ? ` — ${why}` : ''}`;
85
+ }
86
+
87
+ function owedHtml(out) {
88
+ const rows = out.committed.owed;
89
+ if (!rows.length) return '';
90
+ return `<h3 class="ps-money__h">Owed but not paid</h3>
91
+ <div class="ps-money__wrap"><table class="ledger">
92
+ <thead><tr><th>Builder</th><th>Task</th><th>Amount</th><th>Owed since</th><th>Last try</th></tr></thead>
93
+ <tbody>${rows.map((r) => `<tr>
94
+ <td>${escapeHtml(r.builder_name || `builder ${r.builder_id}`)}</td>
95
+ <td><a href="/builders/task/${encodeURIComponent(r.task_id)}">${escapeHtml(r.task_title || `task ${r.task_id}`)}</a></td>
96
+ <td>${escapeHtml(money(r.amount_micro))}</td>
97
+ <td>${escapeHtml(dayLabel(r.since))}</td>
98
+ <td>${escapeHtml(lastTryWords(r))}</td>
99
+ </tr>`).join('')}</tbody>
100
+ </table></div>`;
101
+ }
102
+
103
+ function buildersHtml(out) {
104
+ const rows = out.by_builder;
105
+ if (!rows.length) return '';
106
+ return `<h3 class="ps-money__h">By builder</h3>
107
+ <div class="ps-money__wrap"><table class="ledger">
108
+ <thead><tr><th>Builder</th><th>Spent</th><th>Paid</th><th>Owed</th><th>Approved</th></tr></thead>
109
+ <tbody>${rows.map((b) => `<tr>
110
+ <td>${escapeHtml(b.builder_name || `builder ${b.builder_id}`)}</td>
111
+ <td>${escapeHtml(money(b.spend_micro))}</td>
112
+ <td>${escapeHtml(money(b.paid_micro))}</td>
113
+ <td>${escapeHtml(money(b.owed_micro))}</td>
114
+ <td>${escapeHtml(money(b.approved_micro))}</td>
115
+ </tr>`).join('')}</tbody>
116
+ </table></div>
117
+ <p class="ps-prov">Spent is the recorded cost of each builder’s sessions (API, compute). Paid, owed and approved are payouts.</p>`;
118
+ }
119
+
120
+ function burnHtml(out) {
121
+ const months = out.burn.months;
122
+ if (!months.some((m) => m.total_micro > 0)) return '';
123
+ const bars = window.OTBKit.barSeriesHtml(
124
+ months.map((m) => ({ label: monthLabel(m.month), value: m.total_micro, valueLabel: money(m.total_micro) })),
125
+ { list: true, ariaLabel: 'Money out by month', valueWidth: '96px' });
126
+ return `<h3 class="ps-money__h">Money out, by month</h3>${bars}
127
+ <p class="ps-prov">Each month is recorded spend (${escapeHtml(money(months.reduce((s, m) => s + m.spend_micro, 0)))} over these ${months.length} months) plus payouts and fees out of the project account. Read from the cost log and the account’s own record.</p>`;
128
+ }
129
+
130
+ // The daily check of our record against the processor's (task 1004912). The newest check
131
+ // in words, and on a difference each one with both amounts. The blocker it raises is public
132
+ // and carries no numbers, so this is where the owner reads them.
133
+ const CHECK_WORDS = {
134
+ clean: 'Our records and the payment processor’s agree.',
135
+ drift: 'Our records and the payment processor’s differ.',
136
+ unchecked: 'Not checked: no payment processor is connected yet, so there is nothing to compare with.',
137
+ error: 'Could not check: the payment processor could not be read.',
138
+ };
139
+ function ledgerCheckHtml(out) {
140
+ const c = out && out.checks && out.checks[0];
141
+ if (!c) return '';
142
+ const words = out.kind_words || {};
143
+ const amt = (v) => (v === null || v === undefined ? '—' : money(v));
144
+ const rows = (c.findings || []).map((f) => `<tr>
145
+ <td>${escapeHtml(words[f.kind] || f.kind)}</td>
146
+ <td>${escapeHtml(f.ref || 'the project balance')}</td>
147
+ <td>${escapeHtml(amt(f.ours_micro))}</td>
148
+ <td>${escapeHtml(amt(f.theirs_micro))}</td>
149
+ </tr>`).join('');
150
+ return `<h3 class="ps-money__h">Daily check against the payment processor</h3>
151
+ <p class="ps-prov">${escapeHtml(dayLabel(c.created_at))}: ${escapeHtml(CHECK_WORDS[c.outcome] || c.outcome)}${c.outcome === 'clean' || c.outcome === 'drift' ? ` ${c.refs_compared} ${c.refs_compared === 1 ? 'movement' : 'movements'} compared.` : ''}</p>
152
+ ${rows ? `<div class="ps-money__wrap"><table class="ledger">
153
+ <thead><tr><th>Difference</th><th>Reference</th><th>Our record</th><th>Processor</th></tr></thead>
154
+ <tbody>${rows}</tbody>
155
+ </table></div>` : ''}`;
156
+ }
157
+
158
+ // ---- the treasury (task 1003446) -----------------------------------------
159
+
160
+ // One cumulative figure: the plot, the number it ends on, and the table twin
161
+ // that keeps it out of picture-only territory.
162
+ function treasuryFigure({ id, title, points, fmt, foot }) {
163
+ const kit = window.OTBKit;
164
+ if (!points.length) return '';
165
+ const last = points[points.length - 1];
166
+ const series = kit.timeStepSeriesHtml(points, { x0: points[0].x, x1: last.x, yMax: last.value });
167
+ const { endX, endY } = series.geometry;
168
+ const day = (t) => new Date(t).toISOString().slice(0, 10);
169
+ // Every 7th day plus the last: one row per calendar day would be a table
170
+ // nobody finishes reading.
171
+ const twinRows = points.filter((p, i) => i % 7 === 0 || i === points.length - 1);
172
+ return `<figure class="pchart" id="${esc(id)}">
173
+ <figcaption class="pchart__title">${esc(title)}</figcaption>
174
+ ${kit.plotSvgHtml({
175
+ series: series.body,
176
+ // No readout is wired here, so no focus stop and no dead crosshair.
177
+ interactive: false,
178
+ ariaLabel: `${title}: ${fmt(last.value)} as of ${day(last.x)}`,
179
+ label: kit.plotValueLabelHtml(endX, endY - 8, fmt(last.value), 'end'),
180
+ leftTick: day(points[0].x),
181
+ rightTick: day(last.x),
182
+ })}
183
+ <details class="pchart__twin">
184
+ <summary>Read it as a table</summary>
185
+ <table class="ledger pchart__table"><thead><tr><th>Day</th><th>${esc(title)}</th></tr></thead><tbody>
186
+ ${twinRows.map((p) => `<tr><td>${esc(day(p.x))}</td><td>${esc(fmt(p.value))}</td></tr>`).join('')}
187
+ </tbody></table>
188
+ </details>
189
+ <p class="pchart__foot">${esc(foot)}</p>
190
+ </figure>`;
191
+ }
192
+
193
+ function treasuryHtml(payload) {
194
+ const kit = window.OTBKit;
195
+ if (!payload || !kit) return '';
196
+ const currency = currencyLabel();
197
+ const usd = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
198
+ const num = (n) => Number(n || 0).toLocaleString();
199
+ // The server already ran generate_series, so every calendar day is present
200
+ // and a flat run is a real flat run rather than a missing sample.
201
+ const spend = kit.cumulateByDay(payload.daily, { valueOf: (r) => r.amount_usd });
202
+ const earned = kit.cumulateByDay(payload.daily_credits, { valueOf: (r) => r.credits });
203
+ // Both series empty is a treasury with nothing in it yet: no section at all.
204
+ if (!spend.length && !earned.length) return '';
205
+ const figures = [
206
+ treasuryFigure({
207
+ id: 'treasury-spend', title: 'Spent, cumulative', points: spend, fmt: usd,
208
+ foot: 'Every recorded cost — API, infra, compute, art — added up day by day.',
209
+ }),
210
+ treasuryFigure({
211
+ id: 'treasury-earned', title: `${caps(currency)} awarded, cumulative`, points: earned, fmt: num,
212
+ foot: `What the project has awarded builders, in ${currency}, day by day.`,
213
+ }),
214
+ ].filter(Boolean).join('');
215
+ // Where the money went, as the kit's bar series. Categories arrive sorted
216
+ // biggest-first.
217
+ const byCategory = (payload.by_category || []).slice(0, 8);
218
+ const categories = byCategory.length
219
+ ? '<h3 class="ps-money__h" id="treasury-by-category">Spent, by kind</h3>'
220
+ + kit.barSeriesHtml(
221
+ byCategory.map((c) => ({ label: c.category, value: c.total_usd, valueLabel: usd(c.total_usd) })),
222
+ { list: true, ariaLabel: 'Spend by category', valueWidth: '84px' },
223
+ )
224
+ : '';
225
+ return '<p>These are this project’s real numbers, read from the server when you '
226
+ + `opened this page. It has spent <strong>${esc(usd(payload.total_usd))}</strong> `
227
+ + `and awarded <strong>${esc(num(payload.total_value_credits))} ${esc(currency)}</strong> `
228
+ + 'for the work that money bought.</p>'
229
+ + `<div class="pcharts">${figures}</div>`
230
+ + categories;
231
+ }
232
+
233
+ function payoutsHtml(out, checks) {
234
+ if (!out || !out.balance || !out.committed) return '';
235
+ return moneyFiguresHtml(out) + ledgerCheckHtml(checks) + owedHtml(out) + buildersHtml(out) + burnHtml(out);
236
+ }
237
+
238
+ // ---- the page -------------------------------------------------------------
239
+
240
+ // One GET, never a throw: any outcome but a 200 is "nothing here for you".
241
+ async function read(api, url) {
242
+ try {
243
+ const res = await api.request('GET', url);
244
+ return res.ok ? res.data : null;
245
+ } catch (_) { return null; }
246
+ }
247
+
248
+ function reveal(id, html) {
249
+ if (!html) return false;
250
+ $(`${id}-body`).innerHTML = html;
251
+ $(id).hidden = false;
252
+ return true;
253
+ }
254
+
255
+ async function loadPayouts(api) {
256
+ const out = await read(api, `${API}/project-account/outflow`);
257
+ if (!out || !out.balance || !out.committed) return false;
258
+ const checks = await read(api, `${API}/project-account/ledger-checks?limit=1`);
259
+ return reveal('payouts', payoutsHtml(out, checks));
260
+ }
261
+
262
+ async function loadTreasury(api) {
263
+ if (!window.OTBKit) {
264
+ console.warn('[money] OTBKit missing — the treasury section needs the chart primitive');
265
+ return false;
266
+ }
267
+ return reveal('the-treasury', treasuryHtml(await read(api, TREASURY_URL)));
268
+ }
269
+
270
+ async function loadHosting(api) {
271
+ const ledger = await read(api, `${API}/provisioning/fleet/cost-ledger`);
272
+ if (!ledger || !window.OTBFleetCosts) return false;
273
+ // its own scroll strip on a phone, as the payout tables have (hall-ui/CLAUDE.md)
274
+ return reveal('hosting', `<div class="ps-money__wrap">${window.OTBFleetCosts.ledgerHtml(ledger)}</div>`);
275
+ }
276
+
277
+ // A deep link (/money#hosting, or an old /fleet URL that serve-internal.js 302s
278
+ // here) arrives before its section is shown, so the browser's own jump found
279
+ // nothing. Once every section has settled, scroll the target into view.
280
+ function landOnHash() {
281
+ let id = '';
282
+ try { id = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { return; }
283
+ const el = id && document.getElementById(id);
284
+ if (el && !el.hidden) el.scrollIntoView({ block: 'start' });
285
+ }
286
+
287
+ async function boot() {
288
+ const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
289
+ const shown = await Promise.all([loadPayouts(api), loadTreasury(api), loadHosting(api)]);
290
+ if (!shown.some(Boolean)) $('money-empty').hidden = false;
291
+ landOnHash();
292
+ }
293
+
294
+ // `ready` settles when every section has (null where this is not the page).
295
+ let ready = null;
296
+ if (typeof document !== 'undefined' && document.getElementById && document.getElementById('payouts')) {
297
+ ready = document.readyState === 'loading'
298
+ ? new Promise((resolve) => document.addEventListener('DOMContentLoaded', () => resolve(boot())))
299
+ : boot();
300
+ }
301
+ window.OTBMoney = { payoutsHtml, treasuryHtml, lastTryWords, ready };
302
+ })();
@@ -0,0 +1,15 @@
1
+ {
2
+ "_": "MONEY (task 1005047; once FLEET COSTS, task 1001949, ADR 0327) through modules/ui-design/kit/render.js. The page reads three authenticated routes — GET /project-account/outflow (payout.approve), GET /public/cost-summary and GET /provisioning/fleet/cost-ledger (provisioning.fleet.manage) — each section hidden until its own read answers. Against the kit's own stub every read fails, so the page shows its one 'Nothing to show yet' line, a state the floors must hold in. To look at the populated page run it against the hall-preview harness, whose fixtures answer all three (the ledger's two cloud hosts carry no recorded plan, the state live is in today):\n\n node scripts/hall-preview/server.js --port 4620 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/money.states.json --base-in http://localhost:4620 --base-out http://localhost:4620",
3
+ "page": "/builders/money",
4
+ "surface": "hall-ui",
5
+ "stub": { "prefix": "/builders/" },
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/"],
8
+ "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b403\\b", "\\b404\\b", "Unauthorized", "Forbidden", "Not Found", "load failed"],
9
+ "states": {
10
+ "money": { "auth": true, "actions": [["wait", 800]], "expect": { "visible": ["main"] } },
11
+ "hosting": { "auth": true, "url": "/builders/money#hosting", "actions": [["wait", 800]] },
12
+ "out": { "auth": false, "actions": [["wait", 600]] }
13
+ },
14
+ "reducedMotion": { "auth": true, "actions": [["wait", 800]] }
15
+ }
@@ -83,8 +83,8 @@
83
83
 
84
84
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
85
85
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
86
- <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
87
- <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
86
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
87
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
88
88
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
89
89
  <script src="/builders/palette.js" defer></script>
90
90
  <script src="/builders/gov-living.js?v=2026-10-10-real1"></script>
@@ -76,6 +76,14 @@
76
76
  { label: 'Constitution', href: '/government#constitution' },
77
77
  { label: 'Ranks & Permissions', href: '/government#ranks' },
78
78
  ] },
79
+ // task 1005047: the owner's one money page, a section per kind of money. The
80
+ // treasury lived in the Primer (and before that on the Economy page) until then,
81
+ // so 'Treasury' still finds it.
82
+ { parent: 'Money', gate: 'nav-money', items: [
83
+ { label: 'Where the money went', href: '/money#payouts' },
84
+ { label: 'Treasury', href: '/money#the-treasury' },
85
+ { label: 'Hosting costs (Fleet)', href: '/money#hosting' },
86
+ ] },
79
87
  { parent: 'My agents', gate: 'nav-my-agents', items: [
80
88
  { label: 'How it talks to you', href: '/my-agents#talk' },
81
89
  { label: 'How your agents work', href: '/my-agents#work' },
@@ -86,14 +94,13 @@
86
94
  // task 1005046: the Primer is the one reading room, so the rank ladder, the
87
95
  // economy and the diagrams that had pages of their own are its chapters
88
96
  // now. Each row is a chapter's h2 slug (md-reader.js slugify over
89
- // docs/onboarding/primer.md) or one of the credits chapter's live sections
97
+ // docs/onboarding/primer.md) or the credits chapter's live section
90
98
  // (primer-live.js SECTIONS); 'Economy' rides the label so the word a
91
99
  // builder used to type still finds the place.
92
100
  { parent: 'Primer', gate: 'nav-primer', items: [
93
101
  { label: 'Task lifecycle', href: '/primer#the-claim-ship-lifecycle' },
94
102
  { label: 'Rank ladder', href: '/primer#the-rank-ladder' },
95
103
  { label: 'Credits and karma (Economy)', href: '/primer#credits-and-karma' },
96
- { label: 'Treasury', href: '/primer#the-treasury' },
97
104
  { label: 'Security bounty rates', href: '/primer#security-bounty-rates' },
98
105
  { label: 'Versions', href: '/primer#how-versions-organise-the-work' },
99
106
  ] },
@@ -1,37 +1,33 @@
1
- // primer-live.js — the two LIVE sections of the Primer's "Credits and karma"
2
- // chapter: the project's treasury (what it has spent, and what it has awarded
3
- // builders for that spend) and the published security bounty rates.
1
+ // primer-live.js — the LIVE section of the Primer's "Credits and karma" chapter:
2
+ // the published security bounty rates.
4
3
  //
5
4
  // Task 1005046 (owner decision 2026-11-03, idea 1001311): the Economy page
6
5
  // (/drachmae) folded into the Primer so credits and karma are explained once.
7
- // Its prose became the chapter; these two sections are the part that was never
8
- // prose — numbers read from the server when the reader opens the page — so they
9
- // moved here whole, from drachmae.js (tasks 2174 and 1003446), as a small
10
- // module primer.js calls rather than more lines in primer.js.
6
+ // Its prose became the chapter; its two live sections — numbers read from the
7
+ // server when the reader opens the page — moved here whole, from drachmae.js
8
+ // (tasks 2174 and 1003446), as a small module primer.js calls.
11
9
  //
12
- // The contract each section keeps, unchanged from the Economy page:
13
- // * it reads an endpoint that already existed — /public/cost-summary (the
14
- // status page's treasury chart) and /public/bounty-table — no new route;
15
- // * every mark is drawn by the shared chart kit (hall-kit.js, window.OTBKit);
10
+ // Task 1005047 (owner decision 2026-11-03, idea 1001312): the other one, "The
11
+ // treasury, so far", is a money figure, and every money figure the hall draws is
12
+ // on the owner's one money page now (money.js, #the-treasury). The chapter links
13
+ // there. The bounty rate card stays: it is the rule for an award, not a sum.
14
+ //
15
+ // The contract the section keeps, unchanged from the Economy page:
16
+ // * it reads an endpoint that already existed — /public/bounty-table;
16
17
  // * the currency word is the branding pack's, never a hardcoded unit;
17
18
  // * a feed that cannot be read degrades to NOTHING — the section is removed,
18
- // never left as an empty panel or an outline entry that leads nowhere.
19
- // What is new: each section is placed the moment the article renders, with a
20
- // Loading… line, so the reader sees that something is on its way.
19
+ // never left as an empty panel or an outline entry that leads nowhere;
20
+ // * it is placed the moment the article renders, with a Loading… line, so the
21
+ // reader sees that something is on its way.
21
22
 
22
23
  (() => {
23
24
  'use strict';
24
25
 
25
26
  const BOUNTY_URL = '/api/bongos/public/bounty-table';
26
- // NOT a new endpoint: the same /public/cost-summary the signed-out status page
27
- // draws as a cumulative line. Its `daily[]` is "the treasury chart's
28
- // cumulative line"; the series was in the payload all along.
29
- const TREASURY_URL = '/api/bongos/public/cost-summary';
30
27
 
31
- // The two sections' addresses. Kept from the Economy page, so a link someone
32
- // copied there (/drachmae#the-treasury) names the same section here.
28
+ // The section's address, kept from the Economy page, so a link someone copied
29
+ // there (/drachmae#security-bounty-rates) names the same section here.
33
30
  const SECTIONS = {
34
- treasury: { id: 'the-treasury', title: 'The treasury, so far' },
35
31
  bounty: { id: 'security-bounty-rates', title: 'Security bounty rates' },
36
32
  };
37
33
 
@@ -101,88 +97,5 @@
101
97
  return settle(section, bountyHtml(await readJson(BOUNTY_URL)));
102
98
  }
103
99
 
104
- // ---- the treasury (task 1003446) -----------------------------------------
105
-
106
- // One cumulative figure: the plot, the number it ends on, and the table twin
107
- // that keeps it out of picture-only territory.
108
- function treasuryFigure({ id, title, points, fmt, foot }) {
109
- const kit = window.OTBKit;
110
- if (!points.length) return '';
111
- const last = points[points.length - 1];
112
- const series = kit.timeStepSeriesHtml(points, { x0: points[0].x, x1: last.x, yMax: last.value });
113
- const { endX, endY } = series.geometry;
114
- const day = (t) => new Date(t).toISOString().slice(0, 10);
115
- // Every 7th day plus the last: one row per calendar day would be a table
116
- // nobody finishes reading.
117
- const twinRows = points.filter((p, i) => i % 7 === 0 || i === points.length - 1);
118
- return `<figure class="pchart" id="${esc(id)}">
119
- <figcaption class="pchart__title">${esc(title)}</figcaption>
120
- ${kit.plotSvgHtml({
121
- series: series.body,
122
- // No readout is wired here, so no focus stop and no dead crosshair.
123
- interactive: false,
124
- ariaLabel: `${title}: ${fmt(last.value)} as of ${day(last.x)}`,
125
- label: kit.plotValueLabelHtml(endX, endY - 8, fmt(last.value), 'end'),
126
- leftTick: day(points[0].x),
127
- rightTick: day(last.x),
128
- })}
129
- <details class="pchart__twin">
130
- <summary>Read it as a table</summary>
131
- <table class="ledger pchart__table"><thead><tr><th>Day</th><th>${esc(title)}</th></tr></thead><tbody>
132
- ${twinRows.map((p) => `<tr><td>${esc(day(p.x))}</td><td>${esc(fmt(p.value))}</td></tr>`).join('')}
133
- </tbody></table>
134
- </details>
135
- <p class="pchart__foot">${esc(foot)}</p>
136
- </figure>`;
137
- }
138
-
139
- function treasuryHtml(payload) {
140
- const kit = window.OTBKit;
141
- if (!payload || !kit) return '';
142
- const currency = currencyLabel();
143
- const usd = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
144
- const num = (n) => Number(n || 0).toLocaleString();
145
- // The server already ran generate_series, so every calendar day is present
146
- // and a flat run is a real flat run rather than a missing sample.
147
- const spend = kit.cumulateByDay(payload.daily, { valueOf: (r) => r.amount_usd });
148
- const earned = kit.cumulateByDay(payload.daily_credits, { valueOf: (r) => r.credits });
149
- // Both series empty is a treasury with nothing in it yet: no section at all.
150
- if (!spend.length && !earned.length) return '';
151
- const figures = [
152
- treasuryFigure({
153
- id: 'treasury-spend', title: 'Spent, cumulative', points: spend, fmt: usd,
154
- foot: 'Every recorded cost — API, infra, compute, art — added up day by day.',
155
- }),
156
- treasuryFigure({
157
- id: 'treasury-earned', title: `${caps(currency)} awarded, cumulative`, points: earned, fmt: num,
158
- foot: `What the project has awarded builders, in ${currency}, day by day.`,
159
- }),
160
- ].filter(Boolean).join('');
161
- // Where the money went, as the kit's bar series. Categories arrive sorted
162
- // biggest-first.
163
- const byCategory = (payload.by_category || []).slice(0, 8);
164
- const categories = byCategory.length
165
- ? '<h4 id="treasury-by-category">Where it went</h4>'
166
- + kit.barSeriesHtml(
167
- byCategory.map((c) => ({ label: c.category, value: c.total_usd, valueLabel: usd(c.total_usd) })),
168
- { list: true, ariaLabel: 'Spend by category', valueWidth: '84px' },
169
- )
170
- : '';
171
- return '<p>These are this project’s real numbers, read from the server when you '
172
- + `opened this page. It has spent <strong>${esc(usd(payload.total_usd))}</strong> `
173
- + `and awarded <strong>${esc(num(payload.total_value_credits))} ${esc(currency)}</strong> `
174
- + 'for the work that money bought.</p>'
175
- + `<div class="pcharts">${figures}</div>`
176
- + categories;
177
- }
178
-
179
- async function fillTreasury(section) {
180
- if (!window.OTBKit) {
181
- console.warn('[primer] OTBKit missing — the treasury section needs the chart primitive');
182
- return settle(section, '');
183
- }
184
- return settle(section, treasuryHtml(await readJson(TREASURY_URL)));
185
- }
186
-
187
- window.OTBPrimerLive = { SECTIONS, placeholder, fillBounty, fillTreasury, bountyHtml, treasuryHtml };
100
+ window.OTBPrimerLive = { SECTIONS, placeholder, fillBounty, bountyHtml };
188
101
  })();
@@ -112,19 +112,18 @@
112
112
 
113
113
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
114
114
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
115
- <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
116
- <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
115
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
116
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
117
117
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
118
  <script src="/builders/palette.js" defer></script>
119
119
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
120
120
  it rather than carrying a second copy of the grammar. -->
121
121
  <script src="/builders/md-reader.js" defer></script>
122
- <!-- The credits chapter's live sections (task 1005046, from the retired Economy
123
- page): the chart kit first, then the module that draws with it, both before
124
- primer.js, which places them once the article renders. -->
125
- <script src="/builders/hall-kit.js" defer></script>
126
- <script src="/builders/primer-live.js?v=2026-11-04-reading-room" defer></script>
127
- <script src="/builders/primer.js?v=2026-11-04-reading-room-lazy" defer></script>
122
+ <!-- The credits chapter's live section (task 1005046, from the retired Economy
123
+ page), before primer.js, which places it once the article renders. Its chart
124
+ kit went with the treasury to the money page (task 1005047). -->
125
+ <script src="/builders/primer-live.js?v=2026-11-04-money" defer></script>
126
+ <script src="/builders/primer.js?v=2026-11-04-money" defer></script>
128
127
  <script src="/builders/diagram-viewer.js?v=2026-11-04-reading-room" defer></script>
129
128
  <script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
130
129
  </body>
@@ -55,11 +55,11 @@
55
55
  { id: 'get-set-up-on-your-machine', src: '/builders/diagrams/04-architecture.png', alt: 'The complete architecture: your machine holds the worker, the code and your local memory; the server is the authority and production; GitHub holds the repo — and the path a change takes to production' },
56
56
  ];
57
57
 
58
- // The chapter the two live sections close (task 1005046, moved from the
59
- // retired /drachmae page): what the treasury has actually spent and paid, and
60
- // the published security bounty rates. They go at the end of this chapter,
61
- // ahead of the next one, as h3s — so the table of contents stays one line per
62
- // chapter and a deep link (#the-treasury, #security-bounty-rates) still works.
58
+ // The chapter the live section closes (task 1005046, moved from the retired
59
+ // /drachmae page): the published security bounty rates. It goes at the end of
60
+ // this chapter, ahead of the next one, as an h3 — so the table of contents stays
61
+ // one line per chapter and a deep link (#security-bounty-rates) still works.
62
+ // (The treasury it once sat beside is on the money page since task 1005047.)
63
63
  const LIVE_CHAPTER_ID = 'credits-and-karma';
64
64
 
65
65
  // -----------------------------------------------------------------------
@@ -137,21 +137,20 @@
137
137
  }
138
138
  }
139
139
 
140
- // The live sections of the credits chapter (primer-live.js). Placed at the
141
- // END of that chapter — ahead of the next h2 — each with a Loading… line, and
140
+ // The live section of the credits chapter (primer-live.js). Placed at the
141
+ // END of that chapter — ahead of the next h2 — with a Loading… line, and
142
142
  // removed outright if its feed cannot be read, so the chapter never shows an
143
143
  // empty panel or an outline entry that leads nowhere.
144
144
  //
145
145
  // FETCHED ON APPROACH, not on load. The Primer is the hall's most-read page
146
146
  // and three old URLs now land on it, while most readers never scroll to the
147
- // treasury; firing /public/cost-summary (a day-by-day cumulative query) and
148
- // /public/bounty-table on every open would spend that on readers who never
149
- // see it. So the two reads start when the reader comes within a screen of
150
- // the sections — or at once when the address points at the credits chapter
151
- // or either section (an old /drachmae link), so a deep link lands on their
152
- // final layout. Resolves when there is nothing left to wait for before
147
+ // rate card; firing /public/bounty-table on every open would spend that on
148
+ // readers who never see it. So the read starts when the reader comes within a
149
+ // screen of the section — or at once when the address points at the credits
150
+ // chapter or the section itself (an old /drachmae link), so a deep link lands
151
+ // on its final layout. Resolves when there is nothing left to wait for before
153
152
  // landing: after the fills on a deep link, at once otherwise.
154
- const LIVE_HASHES = new Set([LIVE_CHAPTER_ID, 'the-treasury', 'security-bounty-rates']);
153
+ const LIVE_HASHES = new Set([LIVE_CHAPTER_ID, 'security-bounty-rates']);
155
154
  function insertLiveSections(articleEl) {
156
155
  const live = window.OTBPrimerLive;
157
156
  const h = articleEl.querySelector(`h2[id="${LIVE_CHAPTER_ID}"]`);
@@ -159,13 +158,13 @@
159
158
  let next = h.nextElementSibling;
160
159
  while (next && next.tagName !== 'H2') next = next.nextElementSibling;
161
160
  const place = (el) => { if (next) articleEl.insertBefore(el, next); else articleEl.appendChild(el); };
162
- const treasury = live.placeholder('treasury');
161
+ // task 1005047: the treasury left for the money page (money.js); the chapter's
162
+ // prose links there, so the bounty rate card is the one live section.
163
163
  const bounty = live.placeholder('bounty');
164
- place(treasury);
165
164
  place(bounty);
166
165
  let started = null;
167
166
  const fill = () => {
168
- if (!started) started = Promise.allSettled([live.fillTreasury(treasury), live.fillBounty(bounty)]);
167
+ if (!started) started = Promise.allSettled([live.fillBounty(bounty)]);
169
168
  return started;
170
169
  };
171
170
  let hash = '';
@@ -176,7 +175,6 @@
176
175
  io.disconnect();
177
176
  fill();
178
177
  }, { rootMargin: '100% 0px' });
179
- io.observe(treasury);
180
178
  io.observe(bounty);
181
179
  return Promise.resolve();
182
180
  }
@@ -207,8 +207,8 @@
207
207
  chrome exists before the page script runs. -->
208
208
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
209
209
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
210
- <script src="/builders/craft-mode.js?v=2026-11-04-reading-room"></script>
211
- <script src="/builders/shell.js?v=2026-11-04-reading-room"></script>
210
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
211
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
212
212
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
213
213
  <script src="/builders/palette.js" defer></script>
214
214
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -247,7 +247,8 @@
247
247
  }
248
248
  .ps-ship__choice input { margin-top: 4px; flex: 0 0 auto; }
249
249
 
250
- /* Where the money went (task 1003184). Four lead figures that wrap two-by-two on a
250
+ /* Where the money went (task 1003184), drawn on money.html since task 1005047, which
251
+ loads this sheet for it. Four lead figures that wrap two-by-two on a
251
252
  phone; the tables scroll inside their own strip, never the page (hall-ui/CLAUDE.md). */
252
253
  .ps-money__figs {
253
254
  display: grid;