@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
@@ -2,7 +2,9 @@
2
2
  //
3
3
  // Fetches /builders/primer.md (canonical markdown from docs/onboarding/),
4
4
  // renders it through the SHARED reader (md-reader.js / window.OTBMdReader),
5
- // inlines the five onboarding diagrams at matching sections, builds a TOC from
5
+ // inlines the five onboarding diagrams in their chapters, adds the credits
6
+ // chapter's two live sections (primer-live.js), lands a deep link once all of
7
+ // that is in (task 1005046: the hall's one reading room), builds a TOC from
6
8
  // h2s with a scrollspy, drives a reading-progress hairline, and soft-gates the
7
9
  // footer ack button until the reader has scrolled past ~90% of the article.
8
10
  // Acking POSTs to /api/bongos/builders/me/onboarding/primer-read (same endpoint
@@ -37,17 +39,29 @@
37
39
  let repoBlobBase = null;
38
40
  let repoTreeBase = null;
39
41
 
40
- // Diagrams to inline at matching h2 sections. The keys are LOWERCASED h2
41
- // text (matched as substrings) — keep them tight so the wrong heading
42
- // doesn't pick up the wrong picture. The image paths resolve at
43
- // /builders/diagrams/* (mounted in server.js).
44
- const DIAGRAM_AT_HEADING = [
45
- { match: 'claim → ship lifecycle', src: '/builders/diagrams/01-task-lifecycle.png', alt: 'The full task lifecycle from backlog through shipped' },
46
- { match: 'rank ladder', src: '/builders/diagrams/02-rank-ladder.png', alt: 'The rank ladder — the ranks a builder can hold and how to rise' },
47
- { match: 'drachmae and karma', src: '/builders/diagrams/03-drachmae-karma.png', alt: 'How the reward currency and karma differ — pay for shipped work vs. reputation signals' },
48
- { match: 'get set up', 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 how code travels from your machine to prod' },
42
+ // The five onboarding diagrams, each inlined in the chapter it illustrates
43
+ // (task 1005046: the Diagrams page folded into this one, so this table is now
44
+ // the ONLY place a diagram is shown in the hall). Keyed by the chapter's h2 id
45
+ // — the slug md-reader.js gives it — not by a substring of its words, so a
46
+ // retitled chapter fails loudly in tests/hall_reading_world.mjs instead of
47
+ // silently losing its picture (the credits diagram did exactly that when the
48
+ // chapter's heading stopped matching 'drachmae and karma'). The image paths
49
+ // resolve at /builders/diagrams/* (serve-internal.js mounts the directory).
50
+ const DIAGRAM_AT_CHAPTER = [
51
+ { id: 'the-claim-ship-lifecycle', src: '/builders/diagrams/01-task-lifecycle.png', alt: 'The task lifecycle from backlog to shipped, with the step where credits land' },
52
+ { id: 'the-rank-ladder', src: '/builders/diagrams/02-rank-ladder.png', alt: 'The rank ladder: which ranks are live today and what each may do' },
53
+ { id: 'credits-and-karma', src: '/builders/diagrams/03-drachmae-karma.png', alt: 'Where credits and karma come from, and where each shows up' },
54
+ { id: 'how-versions-organise-the-work', src: '/builders/diagrams/05-versioning.png', alt: 'How a planning session seeds a version\'s task pool, and how a version closes' },
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' },
49
56
  ];
50
57
 
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
+ const LIVE_CHAPTER_ID = 'credits-and-karma';
64
+
51
65
  // -----------------------------------------------------------------------
52
66
  // Tiny utilities
53
67
  // -----------------------------------------------------------------------
@@ -57,13 +71,14 @@
57
71
  const MD = window.OTBMdReader;
58
72
  const escapeHtml = (s) => window.OTB.escapeHtml(s); // task 1004876: the old fallback escaped nothing
59
73
 
60
- // The shared resolver, plus the rewrites that are the primer's alone: the
61
- // economy doc has an in-hall reader page (task 1472), so a link to it stays in
62
- // the hall instead of bouncing the reader out to GitHub. And a link to a hall
74
+ // The shared resolver, plus the rewrites that are the primer's alone: a link
75
+ // written as a hall address (/builders/…) is a page in this hall, so it stays
76
+ // here instead of being dressed as a repo path on GitHub (task 1005046: the
77
+ // rank ladder sends the reader to their own standing). And a link to a hall
63
78
  // page's own file (../../modules/hall-ui/public/<page>.html) opens that page
64
79
  // here (task 1005069); on GitHub the same link shows the file.
65
- const HALL_PAGE = /^(?:\.\.\/)+modules\/hall-ui\/public\/([a-z0-9-]+)\.html$/;
66
80
  const resolveLinkHref = (() => {
81
+ const HALL_PAGE = /^(?:\.\.\/)+modules\/hall-ui\/public\/([a-z0-9-]+)\.html$/;
67
82
  if (!MD) return (h) => h;
68
83
  const base = MD.makeResolveLinkHref({
69
84
  baseDir: PRIMER_DIR,
@@ -71,7 +86,7 @@
71
86
  getTreeBase: () => repoTreeBase,
72
87
  });
73
88
  return (href) => {
74
- if (href === 'drachmae-and-karma.md') return '/builders/drachmae';
89
+ if (/^\/builders(?:[/#?]|$)/.test(String(href || ''))) return href;
75
90
  const page = HALL_PAGE.exec(href || '');
76
91
  return page ? '/builders/' + page[1] : base(href);
77
92
  };
@@ -97,19 +112,17 @@
97
112
  // -----------------------------------------------------------------------
98
113
 
99
114
  function inlineDiagrams(articleEl) {
100
- const h2s = articleEl.querySelectorAll('h2');
101
- for (const h of h2s) {
102
- const txt = h.textContent.toLowerCase();
103
- const match = DIAGRAM_AT_HEADING.find((d) => txt.includes(d.match));
104
- if (!match) continue;
115
+ for (const d of DIAGRAM_AT_CHAPTER) {
116
+ const h = articleEl.querySelector(`h2[id="${d.id}"]`);
117
+ if (!h) continue;
105
118
  const fig = document.createElement('figure');
106
119
  fig.className = 'primer-figure';
107
120
  const img = document.createElement('img');
108
- img.src = match.src;
109
- img.alt = match.alt;
121
+ img.src = d.src;
122
+ img.alt = d.alt;
110
123
  img.loading = 'lazy';
111
124
  const cap = document.createElement('figcaption');
112
- cap.textContent = match.alt;
125
+ cap.textContent = d.alt;
113
126
  fig.appendChild(img);
114
127
  fig.appendChild(cap);
115
128
  // Place the figure AFTER the h2's first paragraph so it sits inside
@@ -124,6 +137,61 @@
124
137
  }
125
138
  }
126
139
 
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
+ // removed outright if its feed cannot be read, so the chapter never shows an
143
+ // empty panel or an outline entry that leads nowhere.
144
+ //
145
+ // FETCHED ON APPROACH, not on load. The Primer is the hall's most-read page
146
+ // and three old URLs now land on it, while most readers never scroll to the
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
152
+ // landing: after the fills on a deep link, at once otherwise.
153
+ const LIVE_HASHES = new Set([LIVE_CHAPTER_ID, 'security-bounty-rates']);
154
+ function insertLiveSections(articleEl) {
155
+ const live = window.OTBPrimerLive;
156
+ const h = articleEl.querySelector(`h2[id="${LIVE_CHAPTER_ID}"]`);
157
+ if (!live || !h) return Promise.resolve();
158
+ let next = h.nextElementSibling;
159
+ while (next && next.tagName !== 'H2') next = next.nextElementSibling;
160
+ const place = (el) => { if (next) articleEl.insertBefore(el, next); else articleEl.appendChild(el); };
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
+ const bounty = live.placeholder('bounty');
164
+ place(bounty);
165
+ let started = null;
166
+ const fill = () => {
167
+ if (!started) started = Promise.allSettled([live.fillBounty(bounty)]);
168
+ return started;
169
+ };
170
+ let hash = '';
171
+ try { hash = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { hash = ''; }
172
+ if (LIVE_HASHES.has(hash) || typeof window.IntersectionObserver !== 'function') return fill();
173
+ const io = new window.IntersectionObserver((entries) => {
174
+ if (!entries.some((e) => e.isIntersecting)) return;
175
+ io.disconnect();
176
+ fill();
177
+ }, { rootMargin: '100% 0px' });
178
+ io.observe(bounty);
179
+ return Promise.resolve();
180
+ }
181
+
182
+ // A deep link — /primer#the-rank-ladder, or an old /ranks, /drachmae or
183
+ // /diagrams URL that serve-internal.js 302s here — arrives before the article
184
+ // exists, so the browser's own jump found nothing. Once the document and its
185
+ // live sections are in, scroll the target into view. After the fonts: a swap
186
+ // after the jump moves everything above the target.
187
+ function landOnHash() {
188
+ let id = '';
189
+ // a malformed percent-escape in the fragment throws; there is nothing to land on
190
+ try { id = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { return; }
191
+ const el = id && document.getElementById(id);
192
+ if (el) el.scrollIntoView({ block: 'start' });
193
+ }
194
+
127
195
  // -----------------------------------------------------------------------
128
196
  // TOC — built from h2 headings. The numeral is drawn by room.css's counter,
129
197
  // so the markup carries only the title.
@@ -430,12 +498,14 @@
430
498
  article.removeAttribute('aria-busy');
431
499
  show('article');
432
500
  inlineDiagrams(article);
501
+ const live = insertLiveSections(article);
433
502
  const tocSections = renderToc(headings);
434
503
  setupTocToggle();
435
504
  setupScrollspy(tocSections);
436
505
  setupProgressBar(article, onArticleProgress);
437
506
  setupAck();
438
507
  checkAlreadyAcked();
508
+ live.then(() => (document.fonts ? document.fonts.ready : null)).then(landOnHash);
439
509
  }
440
510
 
441
511
  if (document.readyState === 'loading') {
@@ -2,7 +2,9 @@
2
2
  "_": "The hall's primer through modules/ui-design/kit/render.js (task 1003306). Written with the reading-room re-skin so the room's failure state and its table of contents are MEASURED at 1440/390/320 in both modes, not just looked at.",
3
3
  "page": "/builders/primer",
4
4
  "surface": "hall-ui",
5
- "stub": { "prefix": "/builders/" },
5
+ "stub": {
6
+ "prefix": "/builders/"
7
+ },
6
8
  "modeQuery": false,
7
9
  "_feeds": "The kit's stub carries the public-landing fixtures, not the hall's, and it serves no /builders/primer.md at all — so the room renders its SKELETON and then its FAILURE note here, which is the point: the two states before the document are the ones nobody looks at, and they have to hold the floors too. The populated document is the hall-preview harness's job: node scripts/hall-preview/server.js --port 4612 --fixture-me, then this same file with --base-in/--base-out pointed at it.",
8
10
  "ignoreRequests": [
@@ -11,7 +13,10 @@
11
13
  "/api/(gds|bongos)/goals",
12
14
  "/api/(gds|bongos)/inbox/",
13
15
  "/api/(gds|bongos)/public/repo-info",
14
- "/builders/primer.md"
16
+ "/builders/primer.md",
17
+ "/api/(gds|bongos)/public/cost-summary",
18
+ "/api/(gds|bongos)/public/bounty-table",
19
+ "/builders/diagrams/"
15
20
  ],
16
21
  "ignoreConsole": [
17
22
  "/api/(gds|bongos)/",
@@ -25,14 +30,56 @@
25
30
  ],
26
31
  "_expect": "The expectations name only what is true under BOTH bases. Under the kit stub there is no primer.md, so the failure note renders; under the harness the real document does. Pinning either one would make this file pass in one place and fail in the other, which is how a states file stops being run against the harness at all.",
27
32
  "states": {
28
- "in": { "auth": true, "expect": { "visible": ["#hall-main", "#primer-toc"] } },
33
+ "in": {
34
+ "auth": true,
35
+ "expect": {
36
+ "visible": [
37
+ "#hall-main",
38
+ "#primer-toc"
39
+ ]
40
+ }
41
+ },
29
42
  "toc-open": {
30
43
  "auth": true,
31
- "actions": [["clickIfVisible", "#primer-toc-toggle"], ["wait", 250]],
44
+ "actions": [
45
+ [
46
+ "clickIfVisible",
47
+ "#primer-toc-toggle"
48
+ ],
49
+ [
50
+ "wait",
51
+ 250
52
+ ]
53
+ ],
32
54
  "_": "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.",
33
- "expect": { "visible": ["#primer-toc"] }
55
+ "expect": {
56
+ "visible": [
57
+ "#primer-toc"
58
+ ]
59
+ }
60
+ },
61
+ "jump": {
62
+ "auth": true,
63
+ "url": "/builders/primer#credits-and-karma",
64
+ "actions": [
65
+ [
66
+ "wait",
67
+ 800
68
+ ]
69
+ ],
70
+ "_": "where an old /drachmae link lands (task 1005046: it 302s here). Under the harness the credits chapter is in view with its diagram and the two live sections below; under the kit stub it is the failure note again, which the floors must hold in too.",
71
+ "expect": {
72
+ "visible": [
73
+ "#hall-main",
74
+ "#primer-toc"
75
+ ]
76
+ }
34
77
  },
35
- "out": { "auth": false }
78
+ "out": {
79
+ "auth": false
80
+ }
36
81
  },
37
- "reducedMotion": { "auth": true }
82
+ "reducedMotion": {
83
+ "auth": true
84
+ }
38
85
  }
@@ -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-10-30-market-watch"></script>
211
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
210
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
211
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
212
212
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
213
213
  <script src="/builders/palette.js" defer></script>
214
214
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -247,7 +247,8 @@
247
247
  }
248
248
  .ps-ship__choice input { margin-top: 4px; flex: 0 0 auto; }
249
249
 
250
- /* Where the money went (task 1003184). Four lead figures that wrap two-by-two on a
250
+ /* Where the money went (task 1003184), drawn on money.html since task 1005047, which
251
+ loads this sheet for it. Four lead figures that wrap two-by-two on a
251
252
  phone; the tables scroll inside their own strip, never the page (hall-ui/CLAUDE.md). */
252
253
  .ps-money__figs {
253
254
  display: grid;
@@ -49,19 +49,9 @@
49
49
  server resolved it through, and markup that could drift from that is the
50
50
  thing this page exists to end. -->
51
51
  <div id="ps-rows"><span class="kit-skel"></span></div>
52
- </section>
53
- <!-- Where the money went (task 1003184, criterion money-outflow-visible). Hidden
54
- unless GET /project-account/outflow answers — it is the payout authority's
55
- read (payout.approve, the owner), so every other reader of this page never
56
- sees an empty panel. Written by project-settings.js; every figure is the
57
- server's, summed from records that already exist. -->
58
- <section class="scroll" id="ps-money" aria-labelledby="ps-money-h" hidden>
59
- <h2 class="scroll__h" id="ps-money-h">Where the money went</h2>
60
- <p class="scroll__lede">
61
- What this project has spent, what each builder has been paid, and what it has
62
- promised but not paid yet. A balance only means something after those promises.
63
- </p>
64
- <div id="ps-money-body"></div>
52
+ <!-- task 1005047: the money figures this page drew ("Where the money went",
53
+ task 1003184) moved to the one money page; this page keeps its knobs. -->
54
+ <p class="ps-prov" id="ps-money-link">What this project has spent and paid out, its treasury and its hosting bills are on the <a href="/builders/money">Money</a> page.</p>
65
55
  </section>
66
56
  <!-- Danger zone (task 1004570): where this project's OWNER takes it offline or
67
57
  disconnects it. Both controls live on the platform that hosts the project,
@@ -86,11 +76,11 @@
86
76
 
87
77
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
88
78
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
89
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
90
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
79
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
80
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
91
81
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
92
82
  <script src="/builders/palette.js" defer></script>
93
83
  <script src="/builders/hall-kit.js"></script>
94
- <script src="/builders/project-settings.js?v=2026-11-02-outflow"></script>
84
+ <script src="/builders/project-settings.js?v=2026-11-04-money"></script>
95
85
  </body>
96
86
  </html>
@@ -417,148 +417,8 @@
417
417
  $('ps-danger').hidden = false;
418
418
  }
419
419
 
420
- // ---- where the money went (task 1003184, criterion money-outflow-visible) --------
421
- //
422
- // The owner's outflow view. The server sums every figure (modules/economy/outflow.js);
423
- // this only draws it. It leads with what is left AFTER what is owed, because a balance
424
- // that looks healthy while it owes three payouts is the trap the view exists to show.
425
- // Any outcome but a 200 leaves the section hidden: the read is the payout authority's,
426
- // and everyone else on this page has nothing to see here, not an error.
427
-
428
- const money = (micro) => {
429
- const usd = Number(micro || 0) / 1e6;
430
- const abs = Math.abs(usd).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
431
- return `${usd < 0 ? '−' : ''}$${abs}`;
432
- };
433
- const monthLabel = (ym) => {
434
- const [y, m] = String(ym).split('-').map(Number);
435
- return new Date(Date.UTC(y, m - 1, 1)).toLocaleDateString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' });
436
- };
437
- const dayLabel = (iso) => (iso ? new Date(iso).toLocaleDateString() : '');
438
-
439
- function moneyFiguresHtml(out) {
440
- const c = out.committed;
441
- const after = out.balance.after_commitments_micro;
442
- const fig = (label, value, note, tone) => `<div class="ps-money__fig${tone ? ` ps-money__fig--${tone}` : ''}">
443
- <span class="ps-money__label">${escapeHtml(label)}</span>
444
- <strong class="ps-money__value">${escapeHtml(value)}</strong>
445
- <span class="ps-money__note">${escapeHtml(note)}</span>
446
- </div>`;
447
- const owedNote = c.owed_count === 1 ? '1 payment' : `${c.owed_count} payments`;
448
- const apprNote = c.approved_count === 1 ? '1 approval' : `${c.approved_count} approvals`;
449
- return `<div class="ps-money__figs">
450
- ${fig('Balance', money(out.balance.available_micro), c.held_micro ? `after ${money(c.held_micro)} held` : 'on our record')}
451
- ${fig('Owed but not paid', money(c.owed_micro), owedNote, c.owed_micro ? 'warn' : '')}
452
- ${fig('Approved, not started', money(c.approved_micro), apprNote, c.approved_micro ? 'warn' : '')}
453
- ${fig('Left after all of that', money(after), after < 0 ? 'the project owes more than it holds' : 'what is really free', after < 0 ? 'bad' : '')}
454
- </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>` : ''}`;
455
- }
456
-
457
- // The payout's latest try in words. A reason is either the processor's own sentence, shown
458
- // as it is, or one of core's held codes (builder-payouts.js HELD_WORDS). The codes an owner
459
- // can act on get a short phrase here; any other code is left out rather than shown as a slug.
460
- const TRY_WORDS = { held: 'Waiting', failed: 'Did not go through' };
461
- const REASON_WORDS = {
462
- no_processor: 'no payment processor connected yet',
463
- recipient_gated: 'this person cannot be paid right now',
464
- guardian_cosign_missing: 'a guardian has not co-signed yet',
465
- currency_not_supported: 'not in US dollars',
466
- };
467
- function lastTryWords(r) {
468
- if (!r.last_outcome) return 'Not tried yet';
469
- const reason = String(r.last_reason || '');
470
- const why = REASON_WORDS[reason] || (/^[a-z_]+$/.test(reason) ? '' : reason);
471
- return `${TRY_WORDS[r.last_outcome] || r.last_outcome}${why ? ` — ${why}` : ''}`;
472
- }
473
-
474
- function owedHtml(out) {
475
- const rows = out.committed.owed;
476
- if (!rows.length) return '';
477
- return `<h3 class="ps-money__h">Owed but not paid</h3>
478
- <div class="ps-money__wrap"><table class="ledger">
479
- <thead><tr><th>Builder</th><th>Task</th><th>Amount</th><th>Owed since</th><th>Last try</th></tr></thead>
480
- <tbody>${rows.map((r) => `<tr>
481
- <td>${escapeHtml(r.builder_name || `builder ${r.builder_id}`)}</td>
482
- <td><a href="/builders/task/${encodeURIComponent(r.task_id)}">${escapeHtml(r.task_title || `task ${r.task_id}`)}</a></td>
483
- <td>${escapeHtml(money(r.amount_micro))}</td>
484
- <td>${escapeHtml(dayLabel(r.since))}</td>
485
- <td>${escapeHtml(lastTryWords(r))}</td>
486
- </tr>`).join('')}</tbody>
487
- </table></div>`;
488
- }
489
-
490
- function buildersHtml(out) {
491
- const rows = out.by_builder;
492
- if (!rows.length) return '';
493
- return `<h3 class="ps-money__h">By builder</h3>
494
- <div class="ps-money__wrap"><table class="ledger">
495
- <thead><tr><th>Builder</th><th>Spent</th><th>Paid</th><th>Owed</th><th>Approved</th></tr></thead>
496
- <tbody>${rows.map((b) => `<tr>
497
- <td>${escapeHtml(b.builder_name || `builder ${b.builder_id}`)}</td>
498
- <td>${escapeHtml(money(b.spend_micro))}</td>
499
- <td>${escapeHtml(money(b.paid_micro))}</td>
500
- <td>${escapeHtml(money(b.owed_micro))}</td>
501
- <td>${escapeHtml(money(b.approved_micro))}</td>
502
- </tr>`).join('')}</tbody>
503
- </table></div>
504
- <p class="ps-prov">Spent is the recorded cost of each builder’s sessions (API, compute). Paid, owed and approved are payouts.</p>`;
505
- }
506
-
507
- function burnHtml(out) {
508
- const months = out.burn.months;
509
- if (!months.some((m) => m.total_micro > 0)) return '';
510
- const bars = window.OTBKit.barSeriesHtml(
511
- months.map((m) => ({ label: monthLabel(m.month), value: m.total_micro, valueLabel: money(m.total_micro) })),
512
- { list: true, ariaLabel: 'Money out by month', valueWidth: '96px' });
513
- return `<h3 class="ps-money__h">Money out, by month</h3>${bars}
514
- <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>`;
515
- }
516
-
517
- // The daily check of our record against the processor's (task 1004912). The newest check
518
- // in words, and on a difference each one with both amounts. The blocker it raises is public
519
- // and carries no numbers, so this is where the owner reads them.
520
- const CHECK_WORDS = {
521
- clean: 'Our records and the payment processor’s agree.',
522
- drift: 'Our records and the payment processor’s differ.',
523
- unchecked: 'Not checked: no payment processor is connected yet, so there is nothing to compare with.',
524
- error: 'Could not check: the payment processor could not be read.',
525
- };
526
- function ledgerCheckHtml(out) {
527
- const c = out && out.checks && out.checks[0];
528
- if (!c) return '';
529
- const words = out.kind_words || {};
530
- const amt = (v) => (v === null || v === undefined ? '—' : money(v));
531
- const rows = (c.findings || []).map((f) => `<tr>
532
- <td>${escapeHtml(words[f.kind] || f.kind)}</td>
533
- <td>${escapeHtml(f.ref || 'the project balance')}</td>
534
- <td>${escapeHtml(amt(f.ours_micro))}</td>
535
- <td>${escapeHtml(amt(f.theirs_micro))}</td>
536
- </tr>`).join('');
537
- return `<h3 class="ps-money__h">Daily check against the payment processor</h3>
538
- <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>
539
- ${rows ? `<div class="ps-money__wrap"><table class="ledger">
540
- <thead><tr><th>Difference</th><th>Reference</th><th>Our record</th><th>Processor</th></tr></thead>
541
- <tbody>${rows}</tbody>
542
- </table></div>` : ''}`;
543
- }
544
-
545
- async function loadMoney() {
546
- let out = null;
547
- try {
548
- const res = await api.request('GET', `${API}/project-account/outflow`);
549
- out = res.ok ? res.data : null;
550
- } catch (_) { out = null; }
551
- if (!out || !out.balance || !out.committed) return;
552
- let checks = null;
553
- try {
554
- const res = await api.request('GET', `${API}/project-account/ledger-checks?limit=1`);
555
- checks = res.ok ? res.data : null;
556
- } catch (_) { checks = null; }
557
- $('ps-money-body').innerHTML = moneyFiguresHtml(out) + ledgerCheckHtml(checks) + owedHtml(out) + buildersHtml(out) + burnHtml(out);
558
- $('ps-money').hidden = false;
559
- }
560
-
420
+ // task 1005047: "Where the money went" (task 1003184) moved to money.js, the one
421
+ // money page; the section's link is in project-settings.html.
561
422
  load();
562
- loadMoney();
563
423
  loadDanger();
564
424
  })();
@@ -43,8 +43,8 @@
43
43
 
44
44
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
45
45
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
46
- <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
47
- <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
46
+ <script src="/builders/craft-mode.js?v=2026-11-04-money"></script>
47
+ <script src="/builders/shell.js?v=2026-11-04-money"></script>
48
48
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
49
49
  <script src="/builders/palette.js" defer></script>
50
50
  <!-- The chart kit and the timeline's series builders (task 1003447). Both MUST