@bongos/core 1.24.10 → 1.24.12

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 (96) hide show
  1. package/.bongos-core.json +171 -86
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +12 -0
  4. package/clients/bongos-client/index.cjs +12 -0
  5. package/clients/bongos-client/index.d.ts +18 -1
  6. package/clients/bongos-client/index.mjs +12 -0
  7. package/docs/api/openapi.json +304 -5
  8. package/docs/api-reference.md +14 -4
  9. package/docs/architecture.md +4 -0
  10. package/docs/copy-inventory.md +154 -76
  11. package/docs/copy-registry.json +923 -125
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +68 -4
  14. package/docs/page-readings.json +873 -819
  15. package/modules/government/decision-rules.js +1 -0
  16. package/modules/hall-ui/public/ai-studio.html +1 -1
  17. package/modules/hall-ui/public/atlas.html +1 -1
  18. package/modules/hall-ui/public/blockers.html +1 -1
  19. package/modules/hall-ui/public/board-room.html +1 -1
  20. package/modules/hall-ui/public/city.html +1 -1
  21. package/modules/hall-ui/public/collab.html +1 -1
  22. package/modules/hall-ui/public/commands.html +1 -1
  23. package/modules/hall-ui/public/copy-desk.html +1 -1
  24. package/modules/hall-ui/public/craft-mode.js +2 -2
  25. package/modules/hall-ui/public/deploy.html +1 -1
  26. package/modules/hall-ui/public/diagrams.html +1 -1
  27. package/modules/hall-ui/public/drachmae.html +1 -1
  28. package/modules/hall-ui/public/fleet.html +1 -1
  29. package/modules/hall-ui/public/gate.html +1 -1
  30. package/modules/hall-ui/public/goal-map.html +1 -1
  31. package/modules/hall-ui/public/goals.html +1 -1
  32. package/modules/hall-ui/public/government.html +1 -1
  33. package/modules/hall-ui/public/idea.html +1 -1
  34. package/modules/hall-ui/public/ideas.html +1 -1
  35. package/modules/hall-ui/public/index.html +1 -1
  36. package/modules/hall-ui/public/market-watch-item.html +83 -0
  37. package/modules/hall-ui/public/market-watch-item.js +150 -0
  38. package/modules/hall-ui/public/market-watch-item.states.json +143 -0
  39. package/modules/hall-ui/public/market-watch-lib.js +243 -0
  40. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  41. package/modules/hall-ui/public/market-watch-settings.html +1 -1
  42. package/modules/hall-ui/public/market-watch.css +169 -0
  43. package/modules/hall-ui/public/market-watch.html +111 -0
  44. package/modules/hall-ui/public/market-watch.js +129 -0
  45. package/modules/hall-ui/public/market-watch.states.json +14 -0
  46. package/modules/hall-ui/public/modules.html +1 -1
  47. package/modules/hall-ui/public/my-agents.html +1 -1
  48. package/modules/hall-ui/public/primer.html +1 -1
  49. package/modules/hall-ui/public/profile.html +1 -1
  50. package/modules/hall-ui/public/project-settings.css +12 -0
  51. package/modules/hall-ui/public/project-settings.html +1 -1
  52. package/modules/hall-ui/public/project-settings.js +63 -1
  53. package/modules/hall-ui/public/ranks.html +1 -1
  54. package/modules/hall-ui/public/roadmap.html +1 -1
  55. package/modules/hall-ui/public/roster.html +1 -1
  56. package/modules/hall-ui/public/sessions.html +1 -1
  57. package/modules/hall-ui/public/settings.html +1 -1
  58. package/modules/hall-ui/public/shell.js +3 -2
  59. package/modules/hall-ui/public/street.html +1 -1
  60. package/modules/hall-ui/public/studio.html +1 -1
  61. package/modules/hall-ui/public/task.html +1 -1
  62. package/modules/hall-ui/public/task.js +34 -1
  63. package/modules/hall-ui/public/thinking.html +1 -1
  64. package/modules/hall-ui/public/tweak-editor.html +1 -1
  65. package/modules/hall-ui/public/watch.html +1 -1
  66. package/modules/hall-ui/public/work.html +1 -1
  67. package/modules/hall-ui/records/market-watch.md +40 -0
  68. package/modules/lifecycle/migrations/lifecycle_018_pay_terms.sql +64 -0
  69. package/modules/lifecycle/pay-terms.js +171 -0
  70. package/modules/lifecycle/routes/project-settings.js +94 -0
  71. package/modules/lifecycle/routes/task-write-routes.js +35 -4
  72. package/modules/market-watch/db.js +10 -8
  73. package/modules/market-watch/module.json +2 -1
  74. package/modules/market-watch/pages.js +323 -0
  75. package/modules/market-watch/proposals.js +66 -4
  76. package/modules/market-watch/routes/pages.js +116 -0
  77. package/package-lock.json +2 -2
  78. package/package.json +1 -1
  79. package/release-notes.json +12 -0
  80. package/scripts/bongos/government-walk.js +1 -0
  81. package/scripts/bongos/run-unit-tests.js +4 -0
  82. package/scripts/bongos/start.js +42 -2
  83. package/src/bongos/module-scope-map.js +1 -0
  84. package/src/module-api.js +1 -1
  85. package/tests/fixtures/market-watch-seed-rows.cjs +60 -0
  86. package/tests/government_act_alone_buildings.mjs +1 -0
  87. package/tests/hall_nav.mjs +4 -1
  88. package/tests/hall_page_gate_map.mjs +4 -0
  89. package/tests/market_watch_module.mjs +2 -1
  90. package/tests/market_watch_pages.mjs +356 -0
  91. package/tests/market_watch_rules.mjs +63 -3
  92. package/tests/nav_permission_atoms.mjs +1 -1
  93. package/tests/patch_gate_ordering.mjs +3 -0
  94. package/tests/pay_terms.mjs +254 -0
  95. package/tests/pay_terms_db.mjs +86 -0
  96. package/tests/project_settings_page.mjs +25 -6
@@ -0,0 +1,129 @@
1
+ // market-watch.js — the Market Watch home, the watchtower desk (task 1004939; design of
2
+ // record docs/design/mocks/market-watch/home.html, option D). One read,
3
+ // GET /market-watch/home, which every signed-in member may make. Where the module is off
4
+ // its routes do not exist, so the page shows its quiet off state rather than an error.
5
+ (() => {
6
+ 'use strict';
7
+
8
+ const MW = window.OTBMarketWatch;
9
+ const esc = MW.esc;
10
+ const $ = (id) => document.getElementById(id);
11
+ const toast = (m) => (window.OTB && window.OTB.toast ? window.OTB.toast(m, { duration: 1800, clear: true }) : null);
12
+ let HOME = null;
13
+
14
+ function show(id) {
15
+ $('mw-loading').hidden = true;
16
+ for (const k of ['mw-off', 'mw-fail', 'mw-desk']) $(k).hidden = k !== id;
17
+ }
18
+ function moduleOff() {
19
+ const m = window.__MODULES__;
20
+ return !!(m && m.enabled && 'market-watch' in m.enabled && m.enabled['market-watch'] !== true);
21
+ }
22
+
23
+ function renderScouts(h) {
24
+ const weeks = h.weeks_watched;
25
+ $('mw-watched').innerHTML = `<i class="mw-dot" style="background:var(--accent)" aria-hidden="true"></i> <b>${weeks}</b> ${weeks === 1 ? 'week' : 'weeks'} watched`;
26
+ $('mw-scouts').innerHTML = h.scouts.map((s) => `<div class="mw-scout">
27
+ <span class="mw-scout__ic" aria-hidden="true">${MW.KIND_ICON[s.kind]}</span>
28
+ <span class="mw-scout__t">${MW.KIND_PLURAL[s.kind]}<br><span class="mw-scout__lv">${s.watched} watched${s.level ? ` · lvl ${s.level}` : ''}</span></span>
29
+ <span class="mw-badge${s.reports ? '' : ' mw-badge--none'}" title="${s.reports} with a report this week">${s.reports}</span></div>`).join('');
30
+ $('mw-streak').innerHTML = h.streak.length
31
+ ? h.streak.map((r) => `<i class="${r.ran ? '' : 'no'}" title="${r.ran ? `run ${r.n} · ${MW.fmtDay(r.date)}` : `skipped · ${MW.fmtDay(r.date)}`}"></i>`).join('')
32
+ : '<span class="mw-quiet">no runs yet</span>';
33
+ const pins = MW.pins.all();
34
+ $('mw-pins').hidden = !pins.length;
35
+ $('mw-pin-list').innerHTML = pins.map((p) => `<div class="mw-pin-row"><span aria-hidden="true">${MW.KIND_ICON[p.kind] || ''}</span><a class="mw-one" href="${MW.itemHref(p.id)}">${esc(p.name)}</a></div>`).join('');
36
+ }
37
+
38
+ function renderScope() {
39
+ $('mw-scope').innerHTML = MW.scopeSvg(HOME, $('mw-ax').value, $('mw-ay').value);
40
+ }
41
+ function initAxes(h) {
42
+ const opts = h.axes.map((a) => `<option value="${esc(a.key)}">${esc(a.label)}</option>`).join('');
43
+ $('mw-ax').innerHTML = opts;
44
+ $('mw-ay').innerHTML = opts;
45
+ // The mockup opens on open-source ethos across, contributor economy up.
46
+ const has = (k) => h.axes.some((a) => a.key === k);
47
+ $('mw-ax').value = has('open_source_ethos') ? 'open_source_ethos' : h.axes[0].key;
48
+ $('mw-ay').value = has('contributor_economy') ? 'contributor_economy' : (h.axes[1] || h.axes[0]).key;
49
+ $('mw-ax').onchange = renderScope;
50
+ $('mw-ay').onchange = renderScope;
51
+ // A dot is a picture, not a control (36 of them would be 36 undersized links); a
52
+ // click on one opens its file, and the picker below reaches every item by name.
53
+ $('mw-scope').addEventListener('click', (ev) => {
54
+ const blip = ev.target.closest && ev.target.closest('[data-id]');
55
+ if (blip) window.location.assign(MW.itemHref(blip.getAttribute('data-id')));
56
+ });
57
+ const groups = ['competitor', 'partner', 'topic'].map((k) => {
58
+ const mine = (h.index || []).filter((i) => i.kind === k);
59
+ return mine.length ? `<optgroup label="${MW.KIND_PLURAL[k]}">${mine.map((i) => `<option value="${i.id}">${esc(i.name)}</option>`).join('')}</optgroup>` : '';
60
+ }).join('');
61
+ $('mw-open').innerHTML = `<option value="">pick one…</option>${groups}`;
62
+ $('mw-open').onchange = () => { if ($('mw-open').value) window.location.assign(MW.itemHref($('mw-open').value)); };
63
+ }
64
+
65
+ function renderSweep(h) {
66
+ const s = MW.nextSweep(h);
67
+ $('mw-cd').textContent = s.big;
68
+ $('mw-when').textContent = s.small;
69
+ const b = h.budget;
70
+ const used = b.cap_usd > 0 ? Math.min(100, Math.round((b.spent_usd / b.cap_usd) * 100)) : 0;
71
+ $('mw-tank-fill').style.height = `${used}%`;
72
+ $('mw-tank').title = `${MW.money(b.spent_usd)} of ${MW.money(b.cap_usd)} used this month`;
73
+ $('mw-left').textContent = `${MW.money(b.left_usd)} left`;
74
+ }
75
+
76
+ function renderReports(h) {
77
+ const read = new Set(MW.reads.get());
78
+ const total = h.reports.length;
79
+ const count = () => h.reports.filter((r) => read.has(String(r.id))).length;
80
+ const bar = () => {
81
+ const n = count();
82
+ $('mw-xpn').textContent = `${n}/${total}`;
83
+ $('mw-xpb').style.width = total ? `${(n / total) * 100}%` : '0';
84
+ };
85
+ $('mw-reps').innerHTML = total
86
+ ? h.reports.map((r) => `<div class="mw-rep${read.has(String(r.id)) ? ' done' : ''}" data-id="${r.id}">
87
+ <span class="mw-rep__ic" aria-hidden="true">${MW.KIND_ICON[r.kind]}</span>
88
+ <a class="mw-rep__t" href="${MW.itemHref(r.item_id)}" title="${esc(r.takeaway)}"><b>${esc(r.item_name)}</b> · ${esc(r.takeaway)}</a>
89
+ <button type="button" aria-label="Mark the ${esc(r.item_name)} report read">ok</button></div>`).join('')
90
+ : '<p class="mw-quiet">No reports this week yet.</p>';
91
+ bar();
92
+ $('mw-reps').querySelectorAll('.mw-rep button').forEach((btn) => {
93
+ btn.addEventListener('click', () => {
94
+ const row = btn.closest('.mw-rep');
95
+ if (row.classList.contains('done')) return;
96
+ row.classList.add('done');
97
+ MW.reads.add(row.dataset.id);
98
+ read.add(row.dataset.id);
99
+ bar();
100
+ toast(count() === total ? 'All caught up — every report read' : '+1 report read');
101
+ });
102
+ });
103
+ }
104
+
105
+ async function load() {
106
+ if (moduleOff()) return show('mw-off');
107
+ let res;
108
+ try {
109
+ res = await fetch('/api/bongos/market-watch/home', { credentials: 'same-origin' });
110
+ } catch (e) {
111
+ return show('mw-fail');
112
+ }
113
+ if (res.status === 401) { window.location.assign(`/api/bongos/auth/web/start?return=${encodeURIComponent(window.location.pathname)}`); return; }
114
+ if (res.status === 404) return show('mw-off'); // the module's routes are not mounted
115
+ if (!res.ok) return show('mw-fail');
116
+ HOME = await res.json();
117
+ show('mw-desk');
118
+ renderScouts(HOME);
119
+ initAxes(HOME);
120
+ renderScope();
121
+ renderSweep(HOME);
122
+ renderReports(HOME);
123
+ }
124
+
125
+ $('mw-send').addEventListener('click', () => {
126
+ MW.openPropose('scout', {}, () => toast('Scout proposed — the government will decide'));
127
+ });
128
+ document.addEventListener('DOMContentLoaded', load);
129
+ })();
@@ -0,0 +1,14 @@
1
+ {
2
+ "_": "MARKET WATCH HOME, the watchtower desk (task 1004939; design of record docs/design/mocks/market-watch/home.html, option D). One read, GET /market-watch/home. Against the kit's own stub it 404s, which the page reads as the module being off, so the stub render is the quiet off state. The populated desk needs the hall-preview harness with the module reported on; its market-watch__home.json fixture was built from the committed 2026-10-28 seed through the real shapes (tests/fixtures/market-watch-seed-rows.cjs):\n\n node scripts/hall-preview/server.js --port 4639 --fixture-me --modules-on market-watch\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/market-watch.states.json --base-in http://localhost:4639 --base-out http://localhost:4639",
3
+ "page": "/builders/market-watch",
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
+ "desk": { "auth": true, "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 200]], "expect": { "visible": ["#mw-desk", "#mw-scope svg"] } },
11
+ "out": { "auth": false, "actions": [["wait", 600], ["clickIfVisible", "[data-craft-close]"]] }
12
+ },
13
+ "reducedMotion": { "auth": true, "actions": [["wait", 900], ["clickIfVisible", "[data-craft-close]"], ["wait", 200]] }
14
+ }
@@ -128,7 +128,7 @@
128
128
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
129
129
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
130
130
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
131
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
131
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
132
132
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
133
133
  <script src="/builders/palette.js" defer></script>
134
134
  <script src="/builders/brand-apply.js"></script>
@@ -62,7 +62,7 @@
62
62
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
63
63
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
64
64
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
65
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
65
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
66
66
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
67
67
  <script src="/builders/palette.js" defer></script>
68
68
  <script src="/builders/gov-living.js?v=2026-10-10-real1"></script>
@@ -113,7 +113,7 @@
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
115
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
116
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
116
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></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
@@ -208,7 +208,7 @@
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
210
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
211
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
211
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></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 -->
@@ -60,6 +60,18 @@
60
60
  border-top: var(--line) solid var(--rule-soft);
61
61
  }
62
62
  .ps-cat--off { opacity: 0.6; }
63
+
64
+ /* Pay terms (task 1004990): one wrapping line per term, the category list's shape. */
65
+ .ps-pays { margin: 10px 0 0; padding: 0; list-style: none; }
66
+ .ps-pay {
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ align-items: center;
70
+ gap: 8px;
71
+ padding: 8px 0;
72
+ border-top: var(--line) solid var(--rule-soft);
73
+ }
74
+ .ps-pay__note { flex: 1 1 200px; min-width: 0; }
63
75
  .ps-cat__slug {
64
76
  font-size: 11px;
65
77
  color: var(--ink-soft);
@@ -72,7 +72,7 @@
72
72
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
73
73
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
74
74
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
75
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
75
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
76
76
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
77
77
  <script src="/builders/palette.js" defer></script>
78
78
  <script src="/builders/hall-kit.js"></script>
@@ -198,6 +198,44 @@
198
198
  </div>`;
199
199
  }
200
200
 
201
+ // Pay terms (task 1004990, ADR 0385 decision 3): the project's standing pay, in
202
+ // its own words. One line per term plus a blank one to add; picking "remove"
203
+ // drops a line. Every box starts EMPTY on a new line — Bongos suggests no amount.
204
+ // The history under it is the audit trail: who changed the terms, and when.
205
+ function payTermLineHtml(types, t) {
206
+ const opts = [`<option value="">${t ? 'remove' : 'add a term…'}</option>`]
207
+ .concat(types.map((o) => `<option value="${escapeHtml(o.type)}" ${t && t.pay_type === o.type ? 'selected' : ''}>${escapeHtml(o.label)}</option>`));
208
+ const val = (f) => escapeHtml(t && t[f] != null ? String(t[f]) : '');
209
+ return `
210
+ <li class="ps-pay">
211
+ <select class="ps-pay__type" aria-label="Kind of pay">${opts.join('')}</select>
212
+ <input type="text" class="ps-pay__amount" maxlength="64" value="${val('amount')}" placeholder="amount, as you state it" aria-label="Amount">
213
+ <input type="text" class="ps-pay__unit" maxlength="32" value="${val('unit')}" placeholder="unit" aria-label="Unit">
214
+ <input type="text" class="ps-pay__note" maxlength="500" value="${val('note')}" placeholder="note" aria-label="Note">
215
+ </li>`;
216
+ }
217
+
218
+ function payTermsBodyHtml(row) {
219
+ const v = row.value || {};
220
+ const types = Array.isArray(v.types) ? v.types : [];
221
+ const terms = Array.isArray(v.terms) ? v.terms : [];
222
+ const lines = terms.map((t) => payTermLineHtml(types, t));
223
+ if (terms.length < (v.max || 10)) lines.push(payTermLineHtml(types, null));
224
+ const selfManaged = terms.some((t) => t.pay_type === 'self_managed')
225
+ ? `<p class="ps-sibling">${escapeHtml(String(v.self_managed_label || ''))}</p>` : '';
226
+ const history = (Array.isArray(v.history) ? v.history : []).map((h) => `
227
+ <li>${escapeHtml(h.set_by_login ? '@' + h.set_by_login : 'builder ' + h.set_by)} set
228
+ ${escapeHtml(String(h.terms.length))} term${h.terms.length === 1 ? '' : 's'}
229
+ on ${escapeHtml(new Date(h.set_at).toLocaleString())}</li>`).join('');
230
+ return `
231
+ <ul class="ps-pays">${lines.join('')}</ul>
232
+ ${selfManaged}
233
+ <div class="ps-controls">
234
+ <button type="button" class="btn" id="ps-pay-save">Save pay terms</button>
235
+ </div>
236
+ ${history ? `<details class="ps-sibling"><summary>Who changed the pay terms</summary><ul>${history}</ul></details>` : ''}`;
237
+ }
238
+
201
239
  // ---- render + wire -------------------------------------------------------
202
240
 
203
241
  let rows = [];
@@ -209,6 +247,7 @@
209
247
  if (row.key === 'tweak_round_cap' && row.editable) return rowHtml(row, tweakCapBodyHtml(row));
210
248
  if (row.key === 'rot_timer' && row.editable) return rowHtml(row, rotBodyHtml(row));
211
249
  if (row.key === 'work_categories' && row.editable) return rowHtml(row, catsBodyHtml(row));
250
+ if (row.key === 'pay_terms' && row.editable) return rowHtml(row, payTermsBodyHtml(row));
212
251
  return rowHtml(row, '');
213
252
  }).join('')}</ul>`;
214
253
  wire();
@@ -219,10 +258,33 @@
219
258
  // reply, arriving first, can repaint a value nobody asked for.
220
259
  function setBusy(on) {
221
260
  const mount = $('ps-rows');
222
- mount.querySelectorAll('input, button').forEach((el) => { el.disabled = on; });
261
+ mount.querySelectorAll('input, button, select').forEach((el) => { el.disabled = on; });
223
262
  }
224
263
 
225
264
  function wire() {
265
+ const paySave = $('ps-pay-save');
266
+ if (paySave) {
267
+ paySave.addEventListener('click', async () => {
268
+ // Only what was typed is sent; an empty box stays empty (null), never filled.
269
+ const terms = [];
270
+ document.querySelectorAll('.ps-pay').forEach((li) => {
271
+ const type = li.querySelector('.ps-pay__type').value;
272
+ if (!type) return;
273
+ const text = (cls) => { const s = String(li.querySelector(cls).value || '').trim(); return s || null; };
274
+ terms.push({ pay_type: type, amount: text('.ps-pay__amount'), unit: text('.ps-pay__unit'), note: text('.ps-pay__note') });
275
+ });
276
+ setBusy(true);
277
+ try {
278
+ await sendJson('PATCH', '/project-settings/pay-terms', { terms });
279
+ toast(terms.length ? 'Pay terms saved.' : 'Pay terms cleared.');
280
+ await load();
281
+ } catch (err) {
282
+ toast(err.message || 'Could not save the pay terms.');
283
+ setBusy(false);
284
+ }
285
+ });
286
+ }
287
+
226
288
  const shipSave = $('ps-ship-save');
227
289
  if (shipSave) {
228
290
  shipSave.addEventListener('click', async () => {
@@ -74,7 +74,7 @@
74
74
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
75
75
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
76
76
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
77
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
77
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
78
78
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
79
79
  <script src="/builders/palette.js" defer></script>
80
80
  <script src="/builders/founding-mark.js?v=2026-09-30-founding"></script>
@@ -44,7 +44,7 @@
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
46
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
47
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
47
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></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
@@ -56,7 +56,7 @@
56
56
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
57
57
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
58
58
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
59
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
59
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <!-- the branding pack's currency label, loaded BEFORE roster.js. -->
@@ -112,7 +112,7 @@
112
112
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
113
113
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
114
114
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
115
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
115
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
116
116
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
117
117
  <script src="/builders/palette.js" defer></script>
118
118
  <script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
@@ -487,7 +487,7 @@
487
487
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
488
488
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
489
489
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
490
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
490
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
491
491
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
492
492
  <script src="/builders/palette.js" defer></script>
493
493
  <!-- shared component kit (window.OTBKit) — section rail (task 2166). -->
@@ -101,6 +101,7 @@
101
101
  // the zen-garden reading of "a place an artist wants to be", so it does not
102
102
  // reuse a glyph that would read as one more docs/goals page.
103
103
  studio: '<path d="M9 15.5V8.5"/><path d="M9 8.5C9 5.5 6.5 3.5 3.5 3.5c0 3 2.5 5 5.5 5Z"/><path d="M9 10C9 7.5 11.2 5.5 14 5.5c0 2.7-2.2 4.5-5 4.5Z"/>',
104
+ marketwatch: '<circle cx="9" cy="9" r="6.5"/><path d="M9 9 13.6 4.4"/><path d="M9 5.2a3.8 3.8 0 1 0 3.8 3.8"/><circle cx="11.6" cy="11.2" r="0.9" fill="currentColor"/>',
104
105
  // You
105
106
  accomplishments: '<path d="M6 2.5l1.6 4.2M12 2.5l-1.6 4.2"/><circle cx="9" cy="10.8" r="4.2"/><circle cx="9" cy="10.8" r="1.8"/>',
106
107
  profile: '<rect x="2.5" y="4" width="13" height="10" rx="1.5"/><circle cx="6.3" cy="8" r="1.6"/><path d="M4.1 12c.3-1.1 1.1-1.7 2.2-1.7s1.9.6 2.2 1.7"/><path d="M10.4 7.5h3M10.4 10.3h3"/>',
@@ -280,6 +281,7 @@
280
281
  // the artist. NO module gate: it degrades to reviews alone on an instance
281
282
  // without copy-desk, rather than vanishing.
282
283
  { id: 'studio', label: 'Studio', icon: 'studio', href: '/studio', gate: 'signed' },
284
+ { id: 'market-watch', label: 'Market Watch', icon: 'marketwatch', href: '/market-watch', gate: 'signed', module: 'market-watch' }, // task 1004939: 'signed' is its page floor; dropped where the module is off
283
285
  ] },
284
286
  // Your own numbers. task 2174 folded Achievements into Standing and this task
285
287
  // retires its nav entry; VIEW_ALIASES still routes #achievements here, so old
@@ -428,7 +430,6 @@
428
430
  }
429
431
 
430
432
  // ---------- render ----------
431
-
432
433
  function renderSidebar() {
433
434
  var el = document.getElementById('app-sidebar');
434
435
  if (!el) return;
@@ -1472,7 +1473,7 @@
1472
1473
  // task 1004430: someone else's profile belongs to Builders, not to your own
1473
1474
  // Profile; probeMe hands the mark back if the id turns out to be yours.
1474
1475
  var otherProfile = path === '/profile' && /(^|[?&])id=/.test(window.location.search || '');
1475
- if (otherProfile) path = '/roster';
1476
+ if (otherProfile) path = '/roster'; else if (/^\/market-watch-(item|rules)$/.test(path)) path = '/market-watch'; // task 1004939
1476
1477
  document.querySelectorAll('#app-sidebar .nav-item').forEach(function (a) {
1477
1478
  var target = a.getAttribute('href');
1478
1479
  if (target && !a.dataset.view && target === path) a.classList.add('nav-item--active');
@@ -231,7 +231,7 @@
231
231
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
232
232
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
233
233
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
234
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
234
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
235
235
  <script src="/builders/palette.js" defer></script>
236
236
  <script src="/builders/gov-living.js?v=2026-10-09-positions1"></script>
237
237
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3"></script>
@@ -219,7 +219,7 @@
219
219
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
220
220
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
221
221
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
222
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
222
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
223
223
  <script src="/builders/palette.js" defer></script>
224
224
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
225
225
  (task 1003829), which the older reviews still show. -->
@@ -59,7 +59,7 @@
59
59
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
60
60
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
61
61
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
62
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
62
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
63
63
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
64
64
  <script src="/builders/palette.js" defer></script>
65
65
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay
@@ -484,8 +484,29 @@
484
484
  return `<section class="rec-sec" id="task-sessions"><p class="rec-sec__h">Session log${count}</p>${blocks.join('')}</section>`;
485
485
  }
486
486
 
487
+ // The task's pay terms as the project stated them (task 1004990). Amounts are the
488
+ // project's text, shown verbatim — never a figure Bongos computed or suggested,
489
+ // and never a balance. A missing amount reads "not stated". A self-managed term
490
+ // carries the server's label saying Bongos moves and guarantees none of it.
491
+ // Terms that come from the project's standing policy say so. Nothing to show
492
+ // (none stated anywhere) draws nothing.
493
+ function payTermsHtml(view) {
494
+ if (!view || !Array.isArray(view.terms) || !view.terms.length) return '';
495
+ const lines = view.terms.map((term) => {
496
+ const amount = term.amount
497
+ ? `${escapeHtml(term.amount)}${term.unit ? ' ' + escapeHtml(term.unit) : ''}`
498
+ : '<em>amount not stated</em>';
499
+ return `<li><strong>${escapeHtml(term.type_label || term.pay_type)}</strong> — ${amount}`
500
+ + (term.note ? `<br>${escapeHtml(term.note)}` : '')
501
+ + (term.label ? `<br><em>${escapeHtml(term.label)}</em>` : '')
502
+ + '</li>';
503
+ }).join('');
504
+ const from = view.source === 'project' ? ' <em>this project’s standing terms</em>' : '';
505
+ return `<section class="rec-sec" id="task-pay"><p class="rec-sec__h">Pay${from}</p><ul>${lines}</ul></section>`;
506
+ }
507
+
487
508
  function renderTask(t, lastClaim, viewer, goal, extras) {
488
- const { grade = null, sessionLogs = null, claims = null, goalsList = null } = extras || {};
509
+ const { grade = null, sessionLogs = null, claims = null, goalsList = null, payTerms = null } = extras || {};
489
510
  document.title = `#${t.id} — ${t.title || 'Task'} — ${document.title.split(' — ').pop()}`;
490
511
  const sub = $('#task-sub');
491
512
  if (sub) sub.textContent = `Task ${t.id}`;
@@ -631,6 +652,10 @@
631
652
  ['Priority', t.priority == null ? null : 'P' + t.priority],
632
653
  ]));
633
654
  parts.push(lifecycleRail(t, creditVal));
655
+ // What the task pays, in the PROJECT's words (task 1004990, ADR 0385 D3), drawn
656
+ // beside the Claim button so a builder reads it before claiming.
657
+ const pay = payTermsHtml(payTerms);
658
+ if (pay) parts.push(pay);
634
659
  // A slot a MODULE may fill (task 1004301): the module's public/task-widget.js is
635
660
  // injected just before this script and writes into it on `otb:task-shown` below. The
636
661
  // page itself names no module and draws nothing here — with none enabled it stays
@@ -1086,10 +1111,18 @@
1086
1111
  if (ri && typeof ri.blob_base === 'string') repoBlobBase = ri.blob_base;
1087
1112
  if (ri && typeof ri.tree_base === 'string') repoTreeBase = ri.tree_base;
1088
1113
  } catch (_) { /* links degrade to the original href */ }
1114
+ // task 1004990: the task's pay terms, in the project's words. Best-effort: an
1115
+ // older server without the read draws no pay section rather than no page.
1116
+ let payTerms = null;
1117
+ try {
1118
+ const pt = await getJSON(`${API}/pay-terms?task_ids=${encodeURIComponent(d.task.id)}`, { softAuth: true });
1119
+ payTerms = (pt && pt.pay_terms && pt.pay_terms[String(d.task.id)]) || null;
1120
+ } catch (_) { /* no pay section */ }
1089
1121
  renderTask(d.task, d.last_claim || null, viewer, goal, {
1090
1122
  grade: d.grade || null,
1091
1123
  sessionLogs: Array.isArray(d.session_logs) ? d.session_logs : null,
1092
1124
  claims: Array.isArray(d.claims) ? d.claims : null,
1125
+ payTerms,
1093
1126
  goalsList,
1094
1127
  });
1095
1128
  } catch (err) {
@@ -141,7 +141,7 @@
141
141
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
142
142
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
143
143
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
144
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
144
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
145
145
  <script src="/builders/palette.js" defer></script>
146
146
 
147
147
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -145,7 +145,7 @@
145
145
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
146
146
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
147
147
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
148
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
148
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
149
149
  <script src="/builders/palette.js" defer></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
151
151
  <script src="/builders/tweak-editor-lib.js?v=2026-09-29-tw27"></script>
@@ -166,7 +166,7 @@
166
166
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
167
167
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
168
168
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
169
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
169
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
170
170
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
171
171
  <script src="/builders/palette.js" defer></script>
172
172
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
@@ -174,7 +174,7 @@
174
174
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
175
175
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
176
176
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
177
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
177
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
178
178
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
179
179
  <script src="/builders/palette.js" defer></script>
180
180
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -0,0 +1,40 @@
1
+ # Market Watch — the watchtower desk (/market-watch) and the dossier (/market-watch-item) (task 1004939)
2
+
3
+ Goal 1000137, "Run a business in Bongos". The designs of record are the owner's picks of 2026-10-29:
4
+ `docs/design/mocks/market-watch/home.html` (option D) and `item.html` (option B). Both pages ride
5
+ `market-watch.css` plus `market-watch-lib.js` (the drawing both share), and read the market-watch
6
+ module's one route file, `modules/market-watch/routes/pages.js`.
7
+
8
+ **What the desk holds.** A link to the rules page (`/market-watch-rules`, task 1004941) under the streak; three scouts (competitors, partners, topics) as levelled cards, each with the
9
+ number of its items reported this week as the badge; the run streak; the landscape, every competitor as a
10
+ dot on two dimensions the viewer picks from the rules' own rubric (open-source ethos across and
11
+ contributor economy up by default, as the mock opens); the next sweep with the budget left as a tank;
12
+ and this week's reports, one per item, competitors and partners taken in turn because they are scored on
13
+ different scales. A report row links to the item; "ok" marks it read for this viewer only.
14
+
15
+ **What the dossier holds.** The item as a card that levels up run by run (level and streak are derived in
16
+ `modules/market-watch/db.js`), the overall line and one sparkline per rubric dimension, why it is watched,
17
+ its known facts, and the sortable case file (found, run, type, takeaway, confidence, source), filtered by type through the kit filter bar (`?type=` in the URL). Sparklines and the dimension bars are the kit's (`OTBKit.sparklineHtml`, `barSeriesHtml`); only the landscape scope is drawn here, since the kit has no scatter plot.
18
+
19
+ **Decisions a later edit must keep.**
20
+
21
+ - **No page head, no ask card.** The topbar names the page. Nothing here asks the reader to decide
22
+ something about the project, so there is no OTBAsk; proposals go to the government, which decides on
23
+ the rules page (task 1004941).
24
+ - **A dot is a picture, not a control.** Thirty-six 6–14px links failed the 24px floor and would overlap
25
+ at phone width. A click on a dot opens its file; the "open a case file" picker under the landscape
26
+ reaches every item by name, partners and topics included.
27
+ - **Every button that changes what is watched files a proposal.** Propose a change (re-score, move
28
+ between partner and competitor, add a source, correct an entry), Stop tracking (a `remove_item`) and
29
+ "+ send a new scout" (an `add_item`), each with a one-line reason. Nothing on these pages changes an
30
+ item, a score or an entry. The direction of a move comes from the item on the server, never the client.
31
+ - **Pin and "read" are the viewer's own** (localStorage `mw-pins`, `mw-read`): conveniences, not the
32
+ record. Pinned items list under the scouts.
33
+ - **"sweep now" is not here.** A manual run spends the project's AI budget, so it is the settings page's
34
+ (task 1004938) to offer with the cap beside it.
35
+ - **Off is quiet.** Where the default-off module is not enabled the nav item is dropped, the page shows
36
+ one line pointing at Modules, and it asks the server nothing; a 404 from unmounted routes reads the same.
37
+
38
+ **Verified** through `modules/ui-design/kit/render.js` against the hall-preview harness
39
+ (`--fixture-me --modules-on market-watch`), whose `market-watch__*.json` fixtures were built from the
40
+ committed seed through the real shapes. Tests: `tests/market_watch_pages.mjs`.