@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
@@ -1,431 +0,0 @@
1
- // drachmae.js — the in-hall reader for the "Drachmae & karma" doc (task 1472),
2
- // rebuilt as the hall's first REFERENCE room (task 1003473, goal 1000081).
3
- // Fetches the canonical markdown at /builders/drachmae-and-karma.md (mounted
4
- // from docs/onboarding/drachmae-and-karma.md — single source of truth, also the
5
- // one the primer links to) and renders it through the shared OTBMdReader, so
6
- // builders can read exactly how they get paid without leaving the hall.
7
- //
8
- // WHAT A REFERENCE OWES THAT THE PRIMER DOES NOT. People arrive at this page to
9
- // answer one question about how they get paid, so it is scanned and deep-linked
10
- // rather than read start to finish. Three things follow, and they are the whole
11
- // difference between this file and primer.js (the room itself — the layout, the
12
- // glass contents, the card — is room.css's, shared):
13
- // * the outline goes to h3 depth and the scrollspy lights the section AND
14
- // its part (the trail), with the phone toggle naming where the reader is;
15
- // * every h2/h3/h4 carries its own link (.room-anchor), which copies the
16
- // address on click, and a deep link lands after the async render;
17
- // * there is no progress hairline and no acknowledge gate: a reference has
18
- // no end to reach.
19
- //
20
- // task 2174: the red-team bounty rate card lives here. It is a published reward
21
- // SCHEDULE — reference material that reads the same for every builder — so it
22
- // belongs with the rest of the reward model. The doc's §"Red-team / security
23
- // bounty" describes it in prose; the LIVE numbers come from /public/bounty-table
24
- // and are inserted as a real section (h2 + table) ahead of the closing links,
25
- // so the outline and the scrollspy pick it up like any other section.
26
-
27
- (() => {
28
- 'use strict';
29
-
30
- const DOC_URL = '/builders/drachmae-and-karma.md';
31
- // The doc lives at docs/onboarding/, so relative links resolve against it.
32
- const DOC_DIR = 'docs/onboarding/';
33
- const BOUNTY_URL = '/api/bongos/public/bounty-table';
34
- // task 1003446: the treasury feed. NOT a new endpoint — this is the same
35
- // /public/cost-summary the signed-out status page already draws as a
36
- // cumulative line and the Work board already fetches to read ONE scalar off
37
- // (`total_value_credits`). Its own route comment calls `daily[]` "the
38
- // treasury chart's cumulative line"; the series was in the payload the whole
39
- // time, and the stranger got the chart while the builder got the number.
40
- const TREASURY_URL = '/api/bongos/public/cost-summary';
41
- const TREASURY_SECTION_ID = 'the-treasury';
42
- const TREASURY_SECTION_TITLE = 'The treasury, so far';
43
- const BOUNTY_SECTION_ID = 'security-bounty-rates';
44
- const BOUNTY_SECTION_TITLE = 'Security bounty rates';
45
- // The section the live table is inserted AHEAD of: the doc's closing links.
46
- // Matched by its opening words, so a retitled footer falls back to appending.
47
- const CLOSING_H2 = /^see it/i;
48
- // The y (viewport space) treated as "where the reader is looking": below the
49
- // sticky top bar, where the eye lands reading down a page.
50
- const READING_LINE_Y = 100;
51
- const LINK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 9.4a2.7 2.7 0 0 0 3.8 0l2.3-2.3a2.7 2.7 0 0 0-3.8-3.8L7.7 4.5"/><path d="M9.4 6.6a2.7 2.7 0 0 0-3.8 0L3.3 8.9a2.7 2.7 0 0 0 3.8 3.8l1.2-1.2"/></svg>';
52
-
53
- let repoBlobBase = null;
54
- let repoTreeBase = null;
55
-
56
- function $(sel) { return document.querySelector(sel); }
57
-
58
- // The shell's toast (style.css .toast[data-visible="1"]), the same contract
59
- // primer.js and copy-on-click.js speak; this page loads no dom-utils.js.
60
- function toast(msg) {
61
- const el = $('#toast');
62
- if (!el) return;
63
- el.textContent = msg;
64
- el.dataset.type = 'info';
65
- el.dataset.visible = '1';
66
- clearTimeout(toast._t);
67
- toast._t = setTimeout(() => { el.dataset.visible = '0'; }, 2400);
68
- }
69
-
70
- async function loadRepoInfoBases() {
71
- try {
72
- const r = await fetch('/api/bongos/public/repo-info', { credentials: 'same-origin' });
73
- if (!r.ok) return;
74
- const data = await r.json();
75
- if (data && typeof data.blob_base === 'string') repoBlobBase = data.blob_base;
76
- if (data && typeof data.tree_base === 'string') repoTreeBase = data.tree_base;
77
- } catch (_) { /* links degrade to the original href */ }
78
- }
79
-
80
- // The live rate card as a real section of the article, placed ahead of the
81
- // closing links so the doc still ends on "dig deeper". Returns false when the
82
- // schedule can't be read: the prose section stays, and no empty table or
83
- // outline entry that leads nowhere is rendered.
84
- async function insertBountySection(article) {
85
- let payload;
86
- try {
87
- const r = await fetch(BOUNTY_URL, { credentials: 'same-origin' });
88
- if (!r.ok) throw new Error(`${BOUNTY_URL} → ${r.status}`);
89
- payload = await r.json();
90
- } catch (err) {
91
- console.warn('[drachmae] bounty schedule unavailable:', err.message);
92
- return false;
93
- }
94
- const rows = Array.isArray(payload && payload.table) ? payload.table : [];
95
- if (!rows.length) return false;
96
-
97
- const esc = window.OTBMdReader.escapeHtml;
98
- const currency = payload.currency || 'credits';
99
- const section = document.createElement('section');
100
- section.innerHTML =
101
- `<h2 id="${esc(BOUNTY_SECTION_ID)}">${esc(BOUNTY_SECTION_TITLE)}</h2>` +
102
- `<p>Find a flaw in the system’s own walls and report it: rewards are paid in ` +
103
- `${esc(currency)} by severity. These are the live numbers, read from the server ` +
104
- `when you opened this page, so a tuned rate shows up here immediately.</p>` +
105
- '<table><thead><tr><th>Severity</th>' +
106
- `<th>${esc(currency.charAt(0).toUpperCase() + currency.slice(1))}</th>` +
107
- '<th>What counts</th></tr></thead><tbody>' +
108
- rows.map((r) => {
109
- // `overridden` means this instance tuned the tier off its default.
110
- const tuned = r.overridden ? ' <em>(tuned)</em>' : '';
111
- return `<tr><td><strong>${esc(r.severity || '')}</strong></td>` +
112
- `<td>${esc(String(r.drachmae ?? ''))}${tuned}</td>` +
113
- `<td>${esc(r.blurb || '')}</td></tr>`;
114
- }).join('') +
115
- '</tbody></table>';
116
- const closing = [...article.querySelectorAll('h2')].find((h) => CLOSING_H2.test(h.textContent.trim()));
117
- if (closing) article.insertBefore(section, closing); else article.appendChild(section);
118
- return true;
119
- }
120
-
121
- // ---- the treasury (task 1003446) -----------------------------------------
122
-
123
- // One cumulative figure: the plot, the number it ends on, and the table twin
124
- // that keeps it out of picture-only territory. `fmt` renders a value for a
125
- // human; `rows` is the twin's own text, so the money and the credits can read
126
- // differently without the chart knowing about either.
127
- function treasuryFigure({ id, title, points, fmt, foot }) {
128
- const kit = window.OTBKit;
129
- const esc = window.OTBMdReader.escapeHtml;
130
- if (!points.length) return '';
131
- const last = points[points.length - 1];
132
- const series = kit.timeStepSeriesHtml(points, {
133
- x0: points[0].x,
134
- x1: last.x,
135
- yMax: last.value,
136
- });
137
- const { endX, endY } = series.geometry;
138
- const day = (t) => new Date(t).toISOString().slice(0, 10);
139
- // Every 7th day plus the last: a twin with one row per calendar day since
140
- // May would be ~120 rows behind a disclosure nobody finishes reading.
141
- const twinRows = points.filter((p, i) => i % 7 === 0 || i === points.length - 1);
142
- return `<figure class="pchart" id="${esc(id)}">
143
- <figcaption class="pchart__title">${esc(title)}</figcaption>
144
- ${kit.plotSvgHtml({
145
- series: series.body,
146
- // No readout is wired here, so no focus stop and no dead crosshair.
147
- interactive: false,
148
- ariaLabel: `${title}: ${fmt(last.value)} as of ${day(last.x)}`,
149
- label: kit.plotValueLabelHtml(endX, endY - 8, fmt(last.value), 'end'),
150
- leftTick: day(points[0].x),
151
- rightTick: day(last.x),
152
- })}
153
- <details class="pchart__twin">
154
- <summary>Read it as a table</summary>
155
- <table class="ledger pchart__table"><thead><tr><th>Day</th><th>${esc(title)}</th></tr></thead><tbody>
156
- ${twinRows.map((p) => `<tr><td>${esc(day(p.x))}</td><td>${esc(fmt(p.value))}</td></tr>`).join('')}
157
- </tbody></table>
158
- </details>
159
- <p class="pchart__foot">${esc(foot)}</p>
160
- </figure>`;
161
- }
162
-
163
- // The live treasury as a real section of the article, ahead of the rate card.
164
- // Returns false when the summary can't be read: the prose stays and no empty
165
- // panel or outline entry that leads nowhere is rendered — the same contract
166
- // insertBountySection keeps.
167
- async function insertTreasurySection(article) {
168
- if (!window.OTBKit) {
169
- console.warn('[drachmae] OTBKit missing — the treasury section needs the chart primitive');
170
- return false;
171
- }
172
- let payload;
173
- try {
174
- const r = await fetch(TREASURY_URL, { credentials: 'same-origin' });
175
- if (!r.ok) throw new Error(`${TREASURY_URL} → ${r.status}`);
176
- payload = await r.json();
177
- } catch (err) {
178
- console.warn('[drachmae] cost summary unavailable:', err.message);
179
- return false;
180
- }
181
- const esc = window.OTBMdReader.escapeHtml;
182
- const kit = window.OTBKit;
183
- const currency = (window.__BRANDING__ && window.__BRANDING__.currency
184
- && window.__BRANDING__.currency.label) || 'credits';
185
- const Currency = currency.charAt(0).toUpperCase() + currency.slice(1);
186
- const usd = (n) => `$${Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`;
187
- const num = (n) => Number(n || 0).toLocaleString();
188
-
189
- // The server already ran generate_series, so every calendar day is present
190
- // and a flat run is a real flat run rather than a missing sample.
191
- const spend = kit.cumulateByDay(payload.daily, { valueOf: (r) => r.amount_usd });
192
- const earned = kit.cumulateByDay(payload.daily_credits, { valueOf: (r) => r.credits });
193
- // Both series empty is a treasury with nothing in it yet — one honest line,
194
- // not two empty plots.
195
- if (!spend.length && !earned.length) return false;
196
-
197
- const figures = [
198
- treasuryFigure({
199
- id: 'treasury-spend',
200
- title: 'Spent, cumulative',
201
- points: spend,
202
- fmt: usd,
203
- foot: 'Every recorded cost — API, infra, compute, art — added up day by day.',
204
- }),
205
- treasuryFigure({
206
- id: 'treasury-earned',
207
- title: `${Currency} awarded, cumulative`,
208
- points: earned,
209
- fmt: num,
210
- foot: `What the project has paid builders, in ${esc(currency)}, day by day.`,
211
- }),
212
- ].filter(Boolean).join('');
213
-
214
- // Where the money went, as the kit's bar series rather than a fourth
215
- // hand-rolled one. Categories arrive sorted biggest-first.
216
- const byCategory = (payload.by_category || []).slice(0, 8);
217
- const categories = byCategory.length
218
- ? `<h3 id="treasury-by-category">Where it went</h3>`
219
- + kit.barSeriesHtml(
220
- byCategory.map((c) => ({ label: c.category, value: c.total_usd, valueLabel: usd(c.total_usd) })),
221
- { list: true, ariaLabel: 'Spend by category', valueWidth: '84px' },
222
- )
223
- : '';
224
-
225
- const section = document.createElement('section');
226
- section.innerHTML =
227
- `<h2 id="${esc(TREASURY_SECTION_ID)}">${esc(TREASURY_SECTION_TITLE)}</h2>`
228
- + `<p>These are this project's real numbers, read from the server when you `
229
- + `opened this page. It has spent <strong>${esc(usd(payload.total_usd))}</strong> `
230
- + `and awarded <strong>${esc(num(payload.total_value_credits))} ${esc(currency)}</strong> `
231
- + `for the work that money bought.</p>`
232
- + `<div class="pcharts">${figures}</div>`
233
- + categories;
234
-
235
- const bounty = article.querySelector(`#${BOUNTY_SECTION_ID}`);
236
- const closing = [...article.querySelectorAll('h2')].find((h) => CLOSING_H2.test(h.textContent.trim()));
237
- // Ahead of the rate card if it landed first, else ahead of the closing
238
- // links, else appended: the doc keeps ending on "dig deeper" either way.
239
- const before = (bounty && bounty.parentElement === article ? bounty : null) || closing;
240
- if (before) article.insertBefore(section, before); else article.appendChild(section);
241
- return true;
242
- }
243
-
244
- // The outline is read from the RENDERED headings rather than the parser's raw
245
- // text, so `*not*` and a [link](…) in a heading arrive as plain words, and so
246
- // the live section inserted above is simply one more heading in order.
247
- function collectHeadings(article) {
248
- return [...article.querySelectorAll('h2[id], h3[id]')].map((el) => ({
249
- level: Number(el.tagName.charAt(1)), id: el.id, text: el.textContent.trim(),
250
- }));
251
- }
252
-
253
- // h2s as the list, each h3 nested under its part; no numerals (room.css turns
254
- // the room's counter off for a reference). Returns the flat list in document
255
- // order, each entry carrying its part's id and title, which is what the
256
- // scrollspy walks and what the phone toggle names.
257
- function renderToc(headings) {
258
- const list = $('#primer-toc-list');
259
- if (!list) return [];
260
- if (headings.length === 0) { list.innerHTML = '<li class="primer-toc__placeholder">(no sections)</li>'; return []; }
261
- const esc = window.OTBMdReader.escapeHtml;
262
- const link = (h) => `<a href="#${esc(h.id)}" data-section="${esc(h.id)}">${esc(h.text)}</a>`;
263
- const parts = [];
264
- for (const h of headings) {
265
- const last = parts[parts.length - 1];
266
- if (h.level === 3 && last) last.subs.push(h);
267
- else parts.push({ head: h, subs: [] });
268
- }
269
- list.innerHTML = parts.map((p) =>
270
- `<li>${link(p.head)}${p.subs.length
271
- ? `<ol class="primer-toc__sub">${p.subs.map((s) => `<li>${link(s)}</li>`).join('')}</ol>`
272
- : ''}</li>`
273
- ).join('');
274
- const flat = [];
275
- for (const p of parts) {
276
- flat.push({ id: p.head.id, parent: null, part: p.head.text });
277
- for (const s of p.subs) flat.push({ id: s.id, parent: p.head.id, part: p.head.text });
278
- }
279
- return flat;
280
- }
281
-
282
- // The active entry is the LAST heading whose top has scrolled past the
283
- // reading line, so it stays put through a section's paragraphs. Its part, when
284
- // the active entry is an h3, is the trail; the toggle's middle names the part.
285
- function setupScrollspy(sections) {
286
- if (sections.length === 0) return;
287
- const links = new Map();
288
- for (const a of document.querySelectorAll('.primer-toc__list a')) links.set(a.dataset.section, a);
289
- if (links.size === 0) return;
290
- const current = $('#primer-toc-current');
291
- let last = null;
292
- function update() {
293
- let active = null;
294
- for (const s of sections) {
295
- const el = document.getElementById(s.id);
296
- if (!el) continue;
297
- if (el.getBoundingClientRect().top <= READING_LINE_Y) active = s; else break;
298
- }
299
- const id = active ? active.id : null;
300
- if (id === last) return;
301
- last = id;
302
- const trail = active && active.parent;
303
- for (const [sid, a] of links) {
304
- a.classList.toggle('is-active', sid === id);
305
- a.classList.toggle('is-trail', !!trail && sid === trail);
306
- }
307
- if (current) current.textContent = active ? active.part : '';
308
- }
309
- window.addEventListener('scroll', update, { passive: true });
310
- window.addEventListener('resize', update, { passive: true });
311
- update();
312
- }
313
-
314
- // Every heading is an address. The link is the heading's own id, so it works
315
- // with the browser's hash navigation and no script; the click additionally
316
- // copies the absolute address, so a reader can hand a colleague the exact
317
- // answer, which is the reason a reference is deep-linked at all.
318
- function addAnchors(article) {
319
- for (const h of article.querySelectorAll('h2[id], h3[id], h4[id]')) {
320
- const a = document.createElement('a');
321
- a.className = 'room-anchor';
322
- a.href = `#${h.id}`;
323
- a.setAttribute('aria-label', `Link to this section: ${h.textContent.trim()}`);
324
- a.innerHTML = LINK_ICON;
325
- h.appendChild(a);
326
- }
327
- article.addEventListener('click', (e) => {
328
- const a = e.target.closest('a.room-anchor');
329
- if (!a) return;
330
- if (!(navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext)) return;
331
- const url = new URL(a.getAttribute('href'), window.location.href).href;
332
- navigator.clipboard.writeText(url).then(() => toast('Link copied.'), () => { /* the hash still navigated */ });
333
- });
334
- }
335
-
336
- // A deep link arrives before the article exists, so the browser's own jump
337
- // finds nothing and its :target was resolved against nothing. Once the
338
- // document and the live section are in the DOM the target is scrolled into
339
- // view (scroll-margin-top clears the top bar) and marked (.is-target) until
340
- // the next hash change hands the mark back to :target.
341
- function landOnHash() {
342
- let id = '';
343
- // a malformed percent-escape in the fragment throws; there is nothing to land on
344
- try { id = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { return; }
345
- const el = id && document.getElementById(id);
346
- if (!el) return;
347
- el.classList.add('is-target');
348
- el.scrollIntoView({ block: 'start' });
349
- window.addEventListener('hashchange', () => el.classList.remove('is-target'), { once: true });
350
- }
351
-
352
- function setupTocToggle() {
353
- const toc = $('#primer-toc');
354
- const btn = $('#primer-toc-toggle');
355
- if (!toc || !btn) return;
356
- btn.addEventListener('click', () => {
357
- const open = toc.classList.toggle('is-open');
358
- btn.setAttribute('aria-expanded', open ? 'true' : 'false');
359
- });
360
- toc.addEventListener('click', (e) => {
361
- const a = e.target.closest('a[data-section]');
362
- if (!a) return;
363
- if (window.matchMedia('(max-width: 760px)').matches) {
364
- toc.classList.remove('is-open');
365
- btn.setAttribute('aria-expanded', 'false');
366
- }
367
- });
368
- }
369
-
370
- // The three panels the article can resolve into (the primer's show()).
371
- function show(which) {
372
- for (const [id, on] of [['#primer-skeleton', which === 'loading'],
373
- ['#primer-article', which === 'article'],
374
- ['#primer-error', which === 'error']]) {
375
- const el = $(id);
376
- if (el) el.hidden = !on;
377
- }
378
- }
379
-
380
- async function boot() {
381
- const article = $('#primer-article');
382
- if (!article || !window.OTBMdReader) return;
383
- show('loading');
384
- let md;
385
- try {
386
- const [mdRes] = await Promise.all([
387
- fetch(DOC_URL, { credentials: 'same-origin' }),
388
- loadRepoInfoBases(),
389
- ]);
390
- if (!mdRes.ok) throw new Error(`${DOC_URL} → ${mdRes.status}`);
391
- md = await mdRes.text();
392
- } catch (err) {
393
- console.error('[drachmae] failed to load doc', err);
394
- article.removeAttribute('aria-busy');
395
- show('error');
396
- return;
397
- }
398
- const resolveLinkHref = window.OTBMdReader.makeResolveLinkHref({
399
- baseDir: DOC_DIR,
400
- getBlobBase: () => repoBlobBase,
401
- getTreeBase: () => repoTreeBase,
402
- diagramsHref: '/builders/diagrams',
403
- });
404
- const parsed = window.OTBMdReader.parseMarkdown(md, resolveLinkHref);
405
- // The doc's leading "# …" is the same page name .page-head--room already
406
- // renders. Two <h1>s fails one-header-main-h1, and the page head is the one
407
- // that exists before the fetch and survives its failure (task 1003307).
408
- article.innerHTML = parsed.html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/, '');
409
- article.removeAttribute('aria-busy');
410
- show('article');
411
- // Awaited before the outline is built so both live sections get their
412
- // entries and scrollspy targets; the article itself is already on screen by
413
- // now. The rate card goes in FIRST so the treasury can land ahead of it by
414
- // id — the doc then reads prose, then what actually happened, then the
415
- // published rates, then "dig deeper".
416
- await insertBountySection(article);
417
- await insertTreasurySection(article);
418
- addAnchors(article);
419
- setupTocToggle();
420
- setupScrollspy(renderToc(collectHeadings(article)));
421
- // After the fonts: a swap after the jump moves everything above the target
422
- // (measured: 300px short on a phone, past it on a desktop).
423
- (document.fonts ? document.fonts.ready : Promise.resolve()).then(landOnHash);
424
- }
425
-
426
- if (document.readyState === 'loading') {
427
- document.addEventListener('DOMContentLoaded', boot);
428
- } else {
429
- boot();
430
- }
431
- })();
@@ -1,58 +0,0 @@
1
- {
2
- "_": "Drachmae & karma — the hall's first REFERENCE room (task 1003473): the reading-room archetype (room.css) with the reference rules on (`room--reference`). Public (no page gate), so the signed-out render is the one most readers get. Measured at three widths in both modes rather than looked at once.",
3
- "page": "/builders/drachmae",
4
- "surface": "hall-ui",
5
- "stub": {
6
- "prefix": "/builders/"
7
- },
8
- "modeQuery": false,
9
- "_feeds": "The kit's own stub serves neither the markdown nor the bounty feed, so against it the page renders its SKELETON and then its FAILURE note — states the floors must hold in. The POPULATED document, the outline, the anchors and the live table need the hall-preview harness (node scripts/hall-preview/server.js --port 4620 --fixture-me, then this file with --base-in/--base-out pointed at it): it serves docs/onboarding/drachmae-and-karma.md and the checked-in public__bounty-table.json. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
10
- "ignoreRequests": [
11
- "/api/(gds|bongos)/me$",
12
- "/api/(gds|bongos)/me/",
13
- "/api/(gds|bongos)/goals",
14
- "/api/(gds|bongos)/inbox/",
15
- "/api/(gds|bongos)/versions"
16
- ],
17
- "ignoreConsole": [
18
- "/api/(gds|bongos)/",
19
- "\\b401\\b",
20
- "\\b403\\b",
21
- "\\b404\\b",
22
- "\\b501\\b",
23
- "Unauthorized",
24
- "Forbidden",
25
- "Not Found",
26
- "Not Implemented",
27
- "load failed"
28
- ],
29
- "_expect": "read pins the article, so this file is run against the harness (the kit stub can only ever show the failure note). jump is the deep link: the browser's own hash jump finds nothing on a page whose document arrives after load, so drachmae.js lands it, and the outline's lit entry with its trail, plus the phone toggle naming the part, are what the shot proves.",
30
- "states": {
31
- "read": {
32
- "auth": true,
33
- "actions": [["wait", 600]],
34
- "expect": { "visible": ["#primer-article", "#primer-toc"] }
35
- },
36
- "toc-open": {
37
- "auth": true,
38
- "actions": [["clickIfVisible", "#primer-toc-toggle"], ["wait", 250]],
39
- "_": "the room's one disclosure. Below 760px the contents collapse behind this toggle; above it the toggle is not rendered at all, so clickIfVisible is the right verb and the state is a no-op at 1440.",
40
- "expect": { "visible": ["#primer-toc"] }
41
- },
42
- "jump": {
43
- "auth": true,
44
- "url": "/builders/drachmae#how-karma-is-earned-and-lost",
45
- "actions": [["wait", 700]],
46
- "_": "a deep link to an h3 under Part 3. At 1440 the outline shows the entry lit and its part in the trail; at 390 the collapsed toggle reads the part's name.",
47
- "expect": { "visible": ["#primer-article", "#primer-toc"] }
48
- },
49
- "out": {
50
- "auth": false,
51
- "actions": [["wait", 600]]
52
- }
53
- },
54
- "reducedMotion": {
55
- "auth": true,
56
- "actions": [["wait", 600]]
57
- }
58
- }
@@ -1,69 +0,0 @@
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>Fleet costs — {{worldName}}</title>
7
- <meta name="description" content="What each hosted project costs this platform, and an estimate of what its owner pays for their own hosting.">
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">
14
- </head>
15
- <body data-page="fleet" data-page-title="Fleet costs">
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), so its sub-line went in task 1004829. -->
25
- <div class="page-head page-head--board">
26
- <h1 class="page-head__h">Fleet costs</h1>
27
- </div>
28
-
29
- <main id="hall-main">
30
- <!-- task 1005043: the page loader, shown until any section below is revealed. -->
31
- <p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…</p>
32
- <section class="scroll" id="fl-sealed" aria-labelledby="fl-sealed-h" hidden>
33
- <h2 class="scroll__h" id="fl-sealed-h">Restricted</h2>
34
- <p class="scroll__lede">This page opens at a higher rank.</p>
35
- </section>
36
-
37
- <section class="scroll" id="fl-body" aria-labelledby="fl-body-h" hidden>
38
- <h2 class="scroll__h" id="fl-body-h">What each project costs</h2>
39
- <!-- Two different bills, one column each (ADR 0327): a cloud host runs on its
40
- OWNER's account, so it costs this platform nothing and its owner pays their
41
- hosting provider directly. Nothing on this page is charged to anyone. -->
42
- <p class="scroll__lede">
43
- Two different bills, one column each. The first is what a project costs this
44
- platform. The second is an estimate of what its owner pays their own hosting
45
- provider, which bills them directly. A project with no recorded plan reads
46
- "not priced yet", never $0, and is left out of the total. A self-hosted
47
- project runs on its owner's own machine, so its bill is not visible to us.
48
- </p>
49
- <div id="fl-panel"><span class="kit-skel"></span></div>
50
- </section>
51
- </main>
52
-
53
- <footer class="app-foot" id="app-foot"></footer>
54
- </div>
55
- </div>
56
- </div>
57
-
58
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
59
-
60
- <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
61
- <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
62
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
63
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
64
- <script src="/builders/palette.js" defer></script>
65
- <script src="/builders/hall-kit.js"></script>
66
- <script src="/builders/fleet-costs.js"></script>
67
- <script src="/builders/fleet.js"></script>
68
- </body>
69
- </html>
@@ -1,41 +0,0 @@
1
- // fleet.js — the fleet cost page (task 1001949, ADR 0327): the chrome around
2
- // fleet-costs.js, which does all the drawing.
3
- //
4
- // One read, GET /provisioning/fleet/cost-ledger, gated on `provisioning.fleet.manage`
5
- // exactly as this page is. A 403 shows the sealed state rather than an empty table,
6
- // so a refusal is never mistaken for a fleet with nothing in it.
7
- (() => {
8
- 'use strict';
9
-
10
- const API = '/api/bongos';
11
- const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
12
- const { escapeHtml } = window.OTB;
13
- const $ = (id) => document.getElementById(id);
14
-
15
- function fail(message) {
16
- $('fl-body').hidden = false;
17
- $('fl-panel').innerHTML = `<p class="ps-invalid">${escapeHtml(message)}</p>`;
18
- }
19
-
20
- async function load() {
21
- let res;
22
- try {
23
- res = await api.request('GET', `${API}/provisioning/fleet/cost-ledger`);
24
- } catch (e) {
25
- return fail('Could not reach the server.');
26
- }
27
- if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
28
- if (res.status === 403) {
29
- $('fl-sealed').hidden = false;
30
- return;
31
- }
32
- if (!res.ok) {
33
- const err = (res.data && res.data.error) || {};
34
- return fail(err.message && err.message !== err.code ? err.message : 'Could not read the fleet costs.');
35
- }
36
- $('fl-body').hidden = false;
37
- $('fl-panel').innerHTML = window.OTBFleetCosts.ledgerHtml(res.data);
38
- }
39
-
40
- document.addEventListener('DOMContentLoaded', load);
41
- })();
@@ -1,14 +0,0 @@
1
- {
2
- "_": "FLEET COSTS (task 1001949, ADR 0327) through modules/ui-design/kit/render.js. The page reads ONE authenticated route, GET /provisioning/fleet/cost-ledger, gated on provisioning.fleet.manage, so against the kit's own stub it renders its failed-read state — a state the floors must hold in. To look at the populated ledger run it against the hall-preview harness, whose provisioning__fleet__cost-ledger.json fixture carries two cloud hosts with no recorded plan (every owner cost unpriced, 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/fleet.states.json --base-in http://localhost:4620 --base-out http://localhost:4620",
3
- "page": "/builders/fleet",
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
- "ledger": { "auth": true, "actions": [["wait", 800]], "expect": { "visible": ["#fl-body"] } },
11
- "out": { "auth": false, "actions": [["wait", 600]] }
12
- },
13
- "reducedMotion": { "auth": true, "actions": [["wait", 800]] }
14
- }
@@ -1,12 +0,0 @@
1
- {
2
- "_": "The founding mark (task 1004424) on the Ranks page: the Founding builders ledger (builder, rank, mark) a founded project shows, from the kit fixture founding-builders-sample.json. The page itself is ranks.states.json's; only the founding read differs. The never-founded case is ranks.states.json, where the section stays hidden.",
3
- "page": "/builders/ranks",
4
- "surface": "hall-ui",
5
- "stub": { "prefix": "/builders/", "fixtures": { "founding-builders": "founding-builders-sample" } },
6
- "modeQuery": false,
7
- "ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox/", "/api/(gds|bongos)/me/"],
8
- "ignoreConsole": ["/api/(gds|bongos)/me", "\\b401\\b", "Unauthorized", "404 \\(Not Found\\)", "goals load failed"],
9
- "states": {
10
- "founding": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#founding-scroll"] } }
11
- }
12
- }