@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.
- package/.bongos-core.json +90 -85
- package/docs/copy-inventory.md +58 -57
- package/docs/copy-registry.json +163 -166
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/primer.md +2 -0
- package/docs/page-inventory.json +34 -34
- package/docs/page-readings.json +684 -680
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/fleet-costs.js +3 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -2
- package/modules/hall-ui/public/market-watch-item.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch.html +2 -2
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/money.html +92 -0
- package/modules/hall-ui/public/money.js +302 -0
- package/modules/hall-ui/public/money.states.json +15 -0
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/palette.js +9 -2
- package/modules/hall-ui/public/primer-live.js +18 -105
- package/modules/hall-ui/public/primer.html +7 -8
- package/modules/hall-ui/public/primer.js +16 -18
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.css +2 -1
- package/modules/hall-ui/public/project-settings.html +6 -16
- package/modules/hall-ui/public/project-settings.js +2 -142
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +7 -5
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/work.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/auth.js +0 -8
- package/src/bongos/serve-internal.js +15 -6
- package/src/module-api.js +1 -1
- package/tests/auth_resolve_failure.mjs +5 -4
- package/tests/fleet_cost_view.mjs +2 -18
- package/tests/hall_loading.mjs +1 -1
- package/tests/hall_money_page.mjs +272 -0
- package/tests/hall_nav.mjs +5 -4
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_page_gate_map.mjs +3 -2
- package/tests/hall_reading_room.mjs +21 -62
- package/tests/nav_permission_atoms.mjs +1 -1
- package/modules/hall-ui/public/fleet.html +0 -69
- package/modules/hall-ui/public/fleet.js +0 -41
- 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-
|
|
87
|
-
<script src="/builders/shell.js?v=2026-11-04-
|
|
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
|
|
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
|
|
2
|
-
//
|
|
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;
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
-
//
|
|
20
|
-
//
|
|
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
|
|
32
|
-
//
|
|
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
|
-
|
|
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-
|
|
116
|
-
<script src="/builders/shell.js?v=2026-11-04-
|
|
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
|
|
123
|
-
page)
|
|
124
|
-
|
|
125
|
-
<script src="/builders/
|
|
126
|
-
<script src="/builders/primer
|
|
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
|
|
59
|
-
//
|
|
60
|
-
// the
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
|
141
|
-
// END of that chapter — ahead of the next h2 —
|
|
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
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
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, '
|
|
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
|
-
|
|
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.
|
|
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-
|
|
211
|
-
<script src="/builders/shell.js?v=2026-11-04-
|
|
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)
|
|
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;
|