@bongos/core 1.24.35 → 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 (112) hide show
  1. package/.bongos-core.json +128 -168
  2. package/.claude/skills/merge-mode/SKILL.md +1 -1
  3. package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +1 -1
  4. package/docs/copy-inventory.md +164 -219
  5. package/docs/copy-registry.json +323 -892
  6. package/docs/module-api-changelog.md +4 -0
  7. package/docs/onboarding/diagrams/README.md +1 -2
  8. package/docs/onboarding/primer.md +66 -22
  9. package/docs/page-inventory.json +39 -133
  10. package/docs/page-readings.json +882 -1346
  11. package/docs/recipes/ui-look-before-you-ship.md +2 -2
  12. package/modules/design-styles/skills/style/SKILL.md +2 -2
  13. package/modules/hall-ui/public/ai-studio.html +2 -2
  14. package/modules/hall-ui/public/atlas.html +2 -2
  15. package/modules/hall-ui/public/blockers.html +2 -2
  16. package/modules/hall-ui/public/board-room.html +2 -2
  17. package/modules/hall-ui/public/builders.js +8 -6
  18. package/modules/hall-ui/public/city.html +2 -2
  19. package/modules/hall-ui/public/collab.html +2 -2
  20. package/modules/hall-ui/public/commands.html +2 -2
  21. package/modules/hall-ui/public/copy-desk.html +2 -2
  22. package/modules/hall-ui/public/craft-mode.js +2 -2
  23. package/modules/hall-ui/public/deploy.html +2 -2
  24. package/modules/hall-ui/public/diagram-viewer.css +1 -1
  25. package/modules/hall-ui/public/diagram-viewer.js +5 -5
  26. package/modules/hall-ui/public/fleet-costs.js +3 -2
  27. package/modules/hall-ui/public/founding-mark.js +3 -2
  28. package/modules/hall-ui/public/gate.html +2 -2
  29. package/modules/hall-ui/public/goal-map.html +2 -2
  30. package/modules/hall-ui/public/goals.html +2 -2
  31. package/modules/hall-ui/public/government.html +2 -2
  32. package/modules/hall-ui/public/hall-render.js +62 -1
  33. package/modules/hall-ui/public/hall-widgets.js +11 -1
  34. package/modules/hall-ui/public/idea.html +2 -2
  35. package/modules/hall-ui/public/ideas.html +2 -2
  36. package/modules/hall-ui/public/index.html +4 -4
  37. package/modules/hall-ui/public/market-watch-item.html +2 -2
  38. package/modules/hall-ui/public/market-watch-rules.html +2 -2
  39. package/modules/hall-ui/public/market-watch.html +2 -2
  40. package/modules/hall-ui/public/modules.html +2 -2
  41. package/modules/hall-ui/public/money.html +92 -0
  42. package/modules/hall-ui/public/money.js +302 -0
  43. package/modules/hall-ui/public/money.states.json +15 -0
  44. package/modules/hall-ui/public/my-agents.html +2 -2
  45. package/modules/hall-ui/public/palette.js +22 -0
  46. package/modules/hall-ui/public/primer-live.js +101 -0
  47. package/modules/hall-ui/public/primer.html +10 -6
  48. package/modules/hall-ui/public/primer.js +93 -23
  49. package/modules/hall-ui/public/primer.states.json +54 -7
  50. package/modules/hall-ui/public/profile.html +2 -2
  51. package/modules/hall-ui/public/project-settings.css +2 -1
  52. package/modules/hall-ui/public/project-settings.html +6 -16
  53. package/modules/hall-ui/public/project-settings.js +2 -142
  54. package/modules/hall-ui/public/roadmap.html +2 -2
  55. package/modules/hall-ui/public/room.css +23 -121
  56. package/modules/hall-ui/public/roster.html +2 -2
  57. package/modules/hall-ui/public/sessions.html +2 -2
  58. package/modules/hall-ui/public/settings.html +2 -2
  59. package/modules/hall-ui/public/shell.js +21 -26
  60. package/modules/hall-ui/public/street.html +2 -2
  61. package/modules/hall-ui/public/studio.html +2 -2
  62. package/modules/hall-ui/public/style.css +8 -55
  63. package/modules/hall-ui/public/task.html +2 -2
  64. package/modules/hall-ui/public/thinking.html +2 -2
  65. package/modules/hall-ui/public/tweak-editor.html +2 -2
  66. package/modules/hall-ui/public/watch.html +2 -2
  67. package/modules/hall-ui/public/work.html +2 -2
  68. package/modules/hall-ui/records/founding-mark.md +4 -4
  69. package/modules/hall-ui/records/reading-rooms.md +2 -0
  70. package/modules/hall-ui/records/reference-room.md +2 -0
  71. package/modules/onboarding/founding-mode.js +6 -2
  72. package/modules/ui-design/kit/render.js +1 -1
  73. package/modules/ui-design/styles/README.md +2 -2
  74. package/package-lock.json +2 -2
  75. package/package.json +1 -1
  76. package/release-notes.json +12 -0
  77. package/scripts/bongos/gen-hall-sitemap.js +3 -0
  78. package/scripts/hall-preview/server.js +6 -11
  79. package/src/bongos/auth.js +0 -8
  80. package/src/bongos/serve-internal.js +43 -43
  81. package/src/module-api.js +1 -1
  82. package/tests/auth_resolve_failure.mjs +5 -4
  83. package/tests/craft_mode_home.mjs +6 -2
  84. package/tests/fleet_cost_view.mjs +2 -18
  85. package/tests/founding_builders.mjs +6 -5
  86. package/tests/founding_mode.mjs +7 -4
  87. package/tests/hall_board_world.mjs +44 -1
  88. package/tests/hall_fullscreen.mjs +1 -1
  89. package/tests/hall_loading.mjs +1 -1
  90. package/tests/hall_money_page.mjs +272 -0
  91. package/tests/hall_nav.mjs +29 -11
  92. package/tests/hall_oversight_world.mjs +1 -1
  93. package/tests/hall_page_gate_map.mjs +17 -7
  94. package/tests/hall_reading_room.mjs +385 -0
  95. package/tests/hall_reading_world.mjs +36 -30
  96. package/tests/nav_permission_atoms.mjs +3 -2
  97. package/tests/rank_tier_single_source.mjs +0 -1
  98. package/tests/ui_design_kit.mjs +1 -1
  99. package/modules/hall-ui/public/diagrams.css +0 -45
  100. package/modules/hall-ui/public/diagrams.html +0 -106
  101. package/modules/hall-ui/public/diagrams.states.json +0 -28
  102. package/modules/hall-ui/public/drachmae.html +0 -110
  103. package/modules/hall-ui/public/drachmae.js +0 -431
  104. package/modules/hall-ui/public/drachmae.states.json +0 -58
  105. package/modules/hall-ui/public/fleet.html +0 -69
  106. package/modules/hall-ui/public/fleet.js +0 -41
  107. package/modules/hall-ui/public/fleet.states.json +0 -14
  108. package/modules/hall-ui/public/ranks-founding.states.json +0 -12
  109. package/modules/hall-ui/public/ranks.html +0 -83
  110. package/modules/hall-ui/public/ranks.js +0 -229
  111. package/modules/hall-ui/public/ranks.states.json +0 -15
  112. package/tests/hall_reference_world.mjs +0 -352
@@ -0,0 +1,92 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Money — {{worldName}}</title>
7
+ <meta name="description" content="Where this project's money went, the treasury so far, and what each hosted project costs, on one page for the people who steer the project.">
8
+ <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-09-23-changelist1">
12
+ <link rel="stylesheet" href="/builders/oversight.css">
13
+ <link rel="stylesheet" href="/builders/project-settings.css?v=2026-11-02-outflow">
14
+ </head>
15
+ <body data-page="money" data-page-title="Money">
16
+
17
+ <div class="app">
18
+ <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
19
+ <div class="app-scrim" id="app-scrim" hidden></div>
20
+ <div class="app-body">
21
+ <header class="app-topbar" id="app-topbar"></header>
22
+ <div class="app-content">
23
+ <!-- The page's one h1, for screen readers: no page shows its head (task
24
+ 1004836). -->
25
+ <div class="page-head page-head--board">
26
+ <h1 class="page-head__h">Money</h1>
27
+ </div>
28
+
29
+ <!-- task 1005047 (owner decision 2026-11-03, idea 1001312): the owner's one money
30
+ page. Every money figure the hall draws is drawn here and nowhere else in the
31
+ hall: what was spent and paid out (from Project settings), the treasury (from
32
+ the Economy page, then the Primer) and the hosting bills (from /fleet, which
33
+ now redirects to #hosting). Each section is hidden until its own read answers,
34
+ so a reader the read refuses, or a hall without the module behind it, sees no
35
+ empty panel. Written by money.js. -->
36
+ <main id="hall-main">
37
+ <!-- task 1005043: the page loader, shown until any section below is revealed. -->
38
+ <p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…</p>
39
+
40
+ <section class="scroll" id="payouts" aria-labelledby="payouts-h" hidden>
41
+ <h2 class="scroll__h" id="payouts-h">Where the money went</h2>
42
+ <p class="scroll__lede">
43
+ What this project has spent, what each builder has been paid, and what it has
44
+ promised but not paid yet. A balance only means something after those promises.
45
+ </p>
46
+ <div id="payouts-body"></div>
47
+ </section>
48
+
49
+ <section class="scroll" id="the-treasury" aria-labelledby="the-treasury-h" hidden>
50
+ <h2 class="scroll__h" id="the-treasury-h">The treasury, so far</h2>
51
+ <div id="the-treasury-body"></div>
52
+ </section>
53
+
54
+ <section class="scroll" id="hosting" aria-labelledby="hosting-h" hidden>
55
+ <h2 class="scroll__h" id="hosting-h">What each project costs</h2>
56
+ <!-- Two different bills, one column each (ADR 0327): a cloud host runs on its
57
+ OWNER's account, so it costs this platform nothing and its owner pays their
58
+ hosting provider directly. Nothing in this section is charged to anyone. -->
59
+ <p class="scroll__lede">
60
+ Two different bills, one column each. The first is what a project costs this
61
+ platform. The second is an estimate of what its owner pays their own hosting
62
+ provider, which bills them directly. A project with no recorded plan reads
63
+ "not priced yet", never $0, and is left out of the total. A self-hosted
64
+ project runs on its owner's own machine, so its bill is not visible to us.
65
+ </p>
66
+ <div id="hosting-body"></div>
67
+ </section>
68
+
69
+ <section class="scroll" id="money-empty" aria-labelledby="money-empty-h" hidden>
70
+ <h2 class="scroll__h" id="money-empty-h">Nothing to show yet</h2>
71
+ <p class="scroll__lede">No money has been recorded on this project yet, or none of it is yours to see.</p>
72
+ </section>
73
+ </main>
74
+
75
+ <footer class="app-foot" id="app-foot"></footer>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
81
+
82
+ <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
83
+ <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
84
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
85
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
86
+ <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
87
+ <script src="/builders/palette.js" defer></script>
88
+ <script src="/builders/hall-kit.js"></script>
89
+ <script src="/builders/fleet-costs.js"></script>
90
+ <script src="/builders/money.js"></script>
91
+ </body>
92
+ </html>
@@ -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-10-30-market-watch"></script>
87
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></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>
@@ -54,6 +54,7 @@
54
54
  // government → government.js makeTabs ids (two rooms since ADR 0266)
55
55
  // settings → settings.js RAIL_GROUPS
56
56
  // my-agents → my-agents.js ANCHORS (the agent settings left Settings, task 1005044)
57
+ // primer → primer.md's h2 slugs + primer-live.js SECTIONS (task 1005046)
57
58
  var SECTIONS = [
58
59
  { parent: 'Accomplishments', gate: 'nav-standing', items: [
59
60
  { label: 'Work', href: '/#accomplishments-work' },
@@ -75,6 +76,14 @@
75
76
  { label: 'Constitution', href: '/government#constitution' },
76
77
  { label: 'Ranks & Permissions', href: '/government#ranks' },
77
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
+ ] },
78
87
  { parent: 'My agents', gate: 'nav-my-agents', items: [
79
88
  { label: 'How it talks to you', href: '/my-agents#talk' },
80
89
  { label: 'How your agents work', href: '/my-agents#work' },
@@ -82,6 +91,19 @@
82
91
  { label: 'Skill voices', href: '/my-agents#skills' },
83
92
  { label: 'What it knows', href: '/my-agents#knows' },
84
93
  ] },
94
+ // task 1005046: the Primer is the one reading room, so the rank ladder, the
95
+ // economy and the diagrams that had pages of their own are its chapters
96
+ // now. Each row is a chapter's h2 slug (md-reader.js slugify over
97
+ // docs/onboarding/primer.md) or the credits chapter's live section
98
+ // (primer-live.js SECTIONS); 'Economy' rides the label so the word a
99
+ // builder used to type still finds the place.
100
+ { parent: 'Primer', gate: 'nav-primer', items: [
101
+ { label: 'Task lifecycle', href: '/primer#the-claim-ship-lifecycle' },
102
+ { label: 'Rank ladder', href: '/primer#the-rank-ladder' },
103
+ { label: 'Credits and karma (Economy)', href: '/primer#credits-and-karma' },
104
+ { label: 'Security bounty rates', href: '/primer#security-bounty-rates' },
105
+ { label: 'Versions', href: '/primer#how-versions-organise-the-work' },
106
+ ] },
85
107
  { parent: 'Settings', gate: 'shell-user', items: [
86
108
  { label: 'Account', href: '/settings#account' },
87
109
  { label: 'Access', href: '/settings#access' },
@@ -0,0 +1,101 @@
1
+ // primer-live.js — the LIVE section of the Primer's "Credits and karma" chapter:
2
+ // the published security bounty rates.
3
+ //
4
+ // Task 1005046 (owner decision 2026-11-03, idea 1001311): the Economy page
5
+ // (/drachmae) folded into the Primer so credits and karma are explained once.
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.
9
+ //
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;
17
+ // * the currency word is the branding pack's, never a hardcoded unit;
18
+ // * a feed that cannot be read degrades to NOTHING — the section is removed,
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.
22
+
23
+ (() => {
24
+ 'use strict';
25
+
26
+ const BOUNTY_URL = '/api/bongos/public/bounty-table';
27
+
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.
30
+ const SECTIONS = {
31
+ bounty: { id: 'security-bounty-rates', title: 'Security bounty rates' },
32
+ };
33
+
34
+ const esc = (s) => window.OTB.escapeHtml(s);
35
+ const currencyLabel = () => (window.__BRANDING__ && window.__BRANDING__.currency
36
+ && window.__BRANDING__.currency.label) || 'credits';
37
+ const caps = (s) => s.charAt(0).toUpperCase() + s.slice(1);
38
+
39
+ // A section with its heading and a Loading… line, ready to be placed. The h3
40
+ // carries the address from the start, so a deep link has somewhere to land
41
+ // even before the numbers arrive.
42
+ function headingHtml(kind) {
43
+ const def = SECTIONS[kind];
44
+ return `<h3 id="${esc(def.id)}">${esc(def.title)}</h3>`;
45
+ }
46
+ function placeholder(kind) {
47
+ const section = document.createElement('section');
48
+ section.className = 'primer-live';
49
+ section.dataset.live = kind;
50
+ section.setAttribute('aria-busy', 'true');
51
+ section.innerHTML = headingHtml(kind) + '<p class="hall-loading" role="status">Loading…</p>';
52
+ return section;
53
+ }
54
+
55
+ // Replace the Loading… line with the real body, or take the section away.
56
+ function settle(section, bodyHtml) {
57
+ if (!bodyHtml) { section.remove(); return false; }
58
+ section.innerHTML = headingHtml(section.dataset.live) + bodyHtml;
59
+ section.removeAttribute('aria-busy');
60
+ return true;
61
+ }
62
+
63
+ async function readJson(url) {
64
+ try {
65
+ const r = await fetch(url, { credentials: 'same-origin' });
66
+ if (!r.ok) throw new Error(`${url} → ${r.status}`);
67
+ return await r.json();
68
+ } catch (err) {
69
+ console.warn('[primer] live section unavailable:', err.message);
70
+ return null;
71
+ }
72
+ }
73
+
74
+ // ---- the security bounty rate card (task 2174) ---------------------------
75
+
76
+ function bountyHtml(payload) {
77
+ const rows = Array.isArray(payload && payload.table) ? payload.table : [];
78
+ if (!rows.length) return '';
79
+ const currency = payload.currency || currencyLabel();
80
+ return `<p>Find a flaw in the system’s own walls and report it: rewards are paid in `
81
+ + `${esc(currency)} by severity. These are the live numbers, read from the server `
82
+ + 'when you opened this page, so a tuned rate shows up here immediately.</p>'
83
+ + '<table><thead><tr><th>Severity</th>'
84
+ + `<th>${esc(caps(currency))}</th>`
85
+ + '<th>What counts</th></tr></thead><tbody>'
86
+ + rows.map((r) => {
87
+ // `overridden` means this project tuned the tier off its default.
88
+ const tuned = r.overridden ? ' <em>(tuned)</em>' : '';
89
+ return `<tr><td><strong>${esc(r.severity || '')}</strong></td>`
90
+ + `<td>${esc(String(r.drachmae ?? ''))}${tuned}</td>`
91
+ + `<td>${esc(r.blurb || '')}</td></tr>`;
92
+ }).join('')
93
+ + '</tbody></table>';
94
+ }
95
+
96
+ async function fillBounty(section) {
97
+ return settle(section, bountyHtml(await readJson(BOUNTY_URL)));
98
+ }
99
+
100
+ window.OTBPrimerLive = { SECTIONS, placeholder, fillBounty, bountyHtml };
101
+ })();
@@ -4,14 +4,14 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Primer — {{worldName}}</title>
7
- <meta name="description" content="The guide a new builder reads on day one. What {{productName}} is, how the build system works, the claim-to-ship lifecycle, the rank ladder, and {{currency}} and karma.">
7
+ <meta name="description" content="The guide a new builder reads on day one. What {{productName}} is, how the build system works, the claim-to-ship lifecycle, the rank ladder, {{currency}} and karma, and how versions organise the work, each with its diagram.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
12
  <link rel="stylesheet" href="/builders/room.css">
13
13
  <link rel="stylesheet" href="/builders/primer.css?v=2026-07-02-shell1">
14
- <link rel="stylesheet" href="/builders/diagram-viewer.css?v=2026-06-14-diagviewer-1">
14
+ <link rel="stylesheet" href="/builders/diagram-viewer.css?v=2026-11-04-reading-room">
15
15
  </head>
16
16
  <!-- `room` opts this page into the reading-room archetype's composition tokens
17
17
  (room.css). They sit on the body, never in a page :root — the branding
@@ -112,15 +112,19 @@
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-10-30-market-watch"></script>
116
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></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
- <script src="/builders/primer.js?v=2026-05-29-primer-1" defer></script>
123
- <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" 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>
127
+ <script src="/builders/diagram-viewer.js?v=2026-11-04-reading-room" defer></script>
124
128
  <script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
125
129
  </body>
126
130
  </html>