@bongos/core 1.24.11 → 1.24.13

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 (93) hide show
  1. package/.bongos-core.json +160 -85
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +8 -0
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/adr/0371-auto-pickup-applies-a-submitted-tweak-on-the-runner.md +1 -0
  8. package/docs/api/openapi.json +175 -3
  9. package/docs/api-reference.md +5 -2
  10. package/docs/copy-inventory.md +92 -20
  11. package/docs/copy-registry.json +777 -33
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +68 -4
  14. package/docs/page-readings.json +856 -802
  15. package/migrations/core_288_credit_log_idea_unique.sql +51 -0
  16. package/modules/economy/credits.js +6 -5
  17. package/modules/hall-ui/public/ai-studio.html +1 -1
  18. package/modules/hall-ui/public/atlas.html +1 -1
  19. package/modules/hall-ui/public/blockers.html +1 -1
  20. package/modules/hall-ui/public/board-room.html +1 -1
  21. package/modules/hall-ui/public/city.html +1 -1
  22. package/modules/hall-ui/public/collab.html +1 -1
  23. package/modules/hall-ui/public/commands.html +1 -1
  24. package/modules/hall-ui/public/copy-desk.html +1 -1
  25. package/modules/hall-ui/public/craft-mode.js +2 -2
  26. package/modules/hall-ui/public/deploy.html +1 -1
  27. package/modules/hall-ui/public/diagrams.html +1 -1
  28. package/modules/hall-ui/public/drachmae.html +1 -1
  29. package/modules/hall-ui/public/fleet.html +1 -1
  30. package/modules/hall-ui/public/gate.html +1 -1
  31. package/modules/hall-ui/public/goal-map.html +1 -1
  32. package/modules/hall-ui/public/goals.html +1 -1
  33. package/modules/hall-ui/public/government.html +1 -1
  34. package/modules/hall-ui/public/idea.html +1 -1
  35. package/modules/hall-ui/public/ideas.html +1 -1
  36. package/modules/hall-ui/public/index.html +1 -1
  37. package/modules/hall-ui/public/market-watch-item.html +83 -0
  38. package/modules/hall-ui/public/market-watch-item.js +150 -0
  39. package/modules/hall-ui/public/market-watch-item.states.json +143 -0
  40. package/modules/hall-ui/public/market-watch-lib.js +243 -0
  41. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  42. package/modules/hall-ui/public/market-watch-settings.html +1 -1
  43. package/modules/hall-ui/public/market-watch.css +169 -0
  44. package/modules/hall-ui/public/market-watch.html +111 -0
  45. package/modules/hall-ui/public/market-watch.js +129 -0
  46. package/modules/hall-ui/public/market-watch.states.json +14 -0
  47. package/modules/hall-ui/public/modules.html +1 -1
  48. package/modules/hall-ui/public/my-agents.html +1 -1
  49. package/modules/hall-ui/public/primer.html +1 -1
  50. package/modules/hall-ui/public/profile.html +1 -1
  51. package/modules/hall-ui/public/project-settings.html +1 -1
  52. package/modules/hall-ui/public/project-settings.js +2 -2
  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/thinking.html +1 -1
  63. package/modules/hall-ui/public/tweak-editor.html +1 -1
  64. package/modules/hall-ui/public/watch.html +1 -1
  65. package/modules/hall-ui/public/work.html +1 -1
  66. package/modules/hall-ui/records/market-watch.md +40 -0
  67. package/modules/lifecycle/autopickup.js +46 -24
  68. package/modules/lifecycle/db-claims.js +1 -1
  69. package/modules/lifecycle/routes/project-settings.js +24 -16
  70. package/modules/market-watch/db.js +15 -10
  71. package/modules/market-watch/module.json +2 -1
  72. package/modules/market-watch/pages.js +323 -0
  73. package/modules/market-watch/proposals.js +66 -4
  74. package/modules/market-watch/research.js +27 -0
  75. package/modules/market-watch/routes/pages.js +116 -0
  76. package/package-lock.json +2 -2
  77. package/package.json +1 -1
  78. package/release-notes.json +20 -0
  79. package/scripts/bongos/run-unit-tests.js +4 -0
  80. package/src/bongos/module-scope-map.js +1 -0
  81. package/src/bongos/project-settings.js +2 -2
  82. package/src/module-api.js +1 -1
  83. package/tests/autopickup_cap.mjs +122 -60
  84. package/tests/credit_log_idea_unique_db.mjs +98 -0
  85. package/tests/fixtures/market-watch-seed-rows.cjs +60 -0
  86. package/tests/hall_nav.mjs +4 -1
  87. package/tests/hall_page_gate_map.mjs +4 -0
  88. package/tests/market_watch_data_db.mjs +1 -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/market_watch_run.mjs +44 -0
  93. package/tests/nav_permission_atoms.mjs +1 -1
@@ -0,0 +1,143 @@
1
+ {
2
+ "_": "MARKET WATCH ITEM, the dossier (task 1004939; design of record docs/design/mocks/market-watch/item.html, option B). One read, GET /market-watch/items/:id. Against the kit's own stub it 404s with no item code, which the page reads as the module being off. The populated card and case file need the hall-preview harness with the module reported on; its market-watch__items__4.json (GitHub Copilot coding agent) and market-watch__items__37.json (NLnet Foundation) fixtures were built from the committed 2026-10-28 seed through the real shapes:\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-item.states.json --base-in http://localhost:4639 --base-out http://localhost:4639",
3
+ "page": "/builders/market-watch-item?id=4",
4
+ "surface": "hall-ui",
5
+ "stub": {
6
+ "prefix": "/builders/"
7
+ },
8
+ "modeQuery": false,
9
+ "ignoreRequests": [
10
+ "/api/(gds|bongos)/"
11
+ ],
12
+ "ignoreConsole": [
13
+ "/api/(gds|bongos)/",
14
+ "\\b401\\b",
15
+ "\\b403\\b",
16
+ "\\b404\\b",
17
+ "\\b501\\b",
18
+ "Unauthorized",
19
+ "Forbidden",
20
+ "Not Found",
21
+ "Not Implemented",
22
+ "load failed"
23
+ ],
24
+ "states": {
25
+ "dossier": {
26
+ "auth": true,
27
+ "actions": [
28
+ [
29
+ "wait",
30
+ 900
31
+ ],
32
+ [
33
+ "clickIfVisible",
34
+ "[data-craft-close]"
35
+ ],
36
+ [
37
+ "wait",
38
+ 200
39
+ ]
40
+ ],
41
+ "expect": {
42
+ "visible": [
43
+ "#mw-wrap",
44
+ "#mw-card .mw-ring"
45
+ ]
46
+ }
47
+ },
48
+ "out": {
49
+ "auth": false,
50
+ "actions": [
51
+ [
52
+ "wait",
53
+ 600
54
+ ],
55
+ [
56
+ "clickIfVisible",
57
+ "[data-craft-close]"
58
+ ]
59
+ ]
60
+ },
61
+ "propose": {
62
+ "auth": true,
63
+ "_": "Propose a change open: the four kinds, the score picker for a re-score, and the one-line reason.",
64
+ "actions": [
65
+ [
66
+ "wait",
67
+ 900
68
+ ],
69
+ [
70
+ "clickIfVisible",
71
+ "[data-craft-close]"
72
+ ],
73
+ [
74
+ "wait",
75
+ 200
76
+ ],
77
+ [
78
+ "click",
79
+ "[data-act=\"propose\"]"
80
+ ],
81
+ [
82
+ "wait",
83
+ 300
84
+ ]
85
+ ],
86
+ "expect": {
87
+ "visible": [
88
+ "#mw-propose form"
89
+ ]
90
+ }
91
+ },
92
+ "filtered": {
93
+ "auth": true,
94
+ "_": "The case file filtered to one type through the kit filter bar (insight): one row on the seeded Copilot item.",
95
+ "actions": [
96
+ [
97
+ "wait",
98
+ 900
99
+ ],
100
+ [
101
+ "clickIfVisible",
102
+ "[data-craft-close]"
103
+ ],
104
+ [
105
+ "wait",
106
+ 200
107
+ ],
108
+ [
109
+ "select",
110
+ "#type",
111
+ "insight"
112
+ ],
113
+ [
114
+ "wait",
115
+ 300
116
+ ]
117
+ ],
118
+ "expect": {
119
+ "visible": [
120
+ "#mw-filter select",
121
+ "#mw-log tr"
122
+ ]
123
+ }
124
+ }
125
+ },
126
+ "reducedMotion": {
127
+ "auth": true,
128
+ "actions": [
129
+ [
130
+ "wait",
131
+ 900
132
+ ],
133
+ [
134
+ "clickIfVisible",
135
+ "[data-craft-close]"
136
+ ],
137
+ [
138
+ "wait",
139
+ 200
140
+ ]
141
+ ]
142
+ }
143
+ }
@@ -0,0 +1,243 @@
1
+ // market-watch-lib.js — the drawing shared by the Market Watch home and item pages
2
+ // (task 1004939): the landscape scope, sparklines, confidence bars, type pills, the
3
+ // next-sweep countdown and the propose-a-change dialog. Pure markup builders, so
4
+ // tests/market_watch_pages.mjs runs them on the seed-built fixtures. Every string that
5
+ // came from a member or the research goes through the hall's one escaper
6
+ // (dom-utils.js, window.OTB.escapeHtml) where it becomes markup.
7
+ (function (root) {
8
+ 'use strict';
9
+
10
+ var esc = function (s) { return root.OTB.escapeHtml(s); };
11
+ // Only an http(s) link becomes an href; anything else renders as a dead link.
12
+ function safeHref(u) { return /^https?:\/\//i.test(String(u || '')) ? esc(u) : '#'; }
13
+ function itemHref(id) { return '/market-watch-item?id=' + encodeURIComponent(String(id)); }
14
+
15
+ var KIND_ICON = { competitor: '◎', partner: '✦', topic: '#' };
16
+ var KIND_PLURAL = { competitor: 'competitors', partner: 'partners', topic: 'topics' };
17
+ var CONF = ['', 'low', 'medium', 'high'];
18
+
19
+ function confDots(c) {
20
+ c = Number(c) || 0;
21
+ var bars = '';
22
+ for (var i = 1; i <= 3; i += 1) bars += '<i class="' + (i <= c ? 'on' : '') + '"></i>';
23
+ return '<span class="mw-conf' + (c === 1 ? ' mw-conf--low' : '') + '" title="confidence: ' + CONF[c] + '" aria-label="confidence ' + CONF[c] + '">' + bars + '</span>';
24
+ }
25
+ function typePill(t) { return '<span class="mw-pill mw-pill--' + esc(t) + '">' + esc(t) + '</span>'; }
26
+ function initials(name) {
27
+ var w = String(name || '').replace(/\(.*?\)/g, '').trim().split(/\s+/).filter(Boolean);
28
+ return ((w[0] || '?').charAt(0) + (w[1] ? w[1].charAt(0) : (w[0] || '').charAt(1))).toUpperCase();
29
+ }
30
+ function fmtDay(iso, long) {
31
+ var d = new Date(String(iso).slice(0, 10) + 'T12:00:00Z');
32
+ if (isNaN(d)) return '';
33
+ return d.toLocaleDateString('en-GB', long ? { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' } : { day: 'numeric', month: 'short', timeZone: 'UTC' });
34
+ }
35
+
36
+ // spark(values, w, h, label) — the kit's sparkline (OTBKit.sparklineHtml), sized for a
37
+ // card row. A null point is a run that did not score the dimension: it is dropped rather
38
+ // than drawn as zero, which the kit would otherwise do.
39
+ function spark(vals, w, h, label) {
40
+ var pts = (vals || []).filter(function (v) { return v != null; });
41
+ return '<span class="mw-spark" style="width:' + Number(w) + 'px;height:' + Number(h) + 'px">'
42
+ + root.OTBKit.sparklineHtml(pts, { width: Number(w), height: Number(h), ariaLabel: label }) + '</span>';
43
+ }
44
+
45
+ // jitter — a fixed nudge per dot so two items on the same score do not hide each other.
46
+ function jit(i, k) { return ((i * 37 + k * 17) % 11 - 5) / 22; }
47
+
48
+ // scopeSvg(home, xKey, yKey) — the landscape: every competitor as a dot on the two
49
+ // dimensions the viewer picked; this week's reported items glow; "us" holds the far
50
+ // corner, as in the mockup.
51
+ function scopeSvg(home, xKey, yKey) {
52
+ var S = 600, p = 40;
53
+ var sx = function (v) { return p + (v / 10) * (S - 2 * p); };
54
+ var sy = function (v) { return S - p - (v / 10) * (S - 2 * p); };
55
+ var hot = {};
56
+ (home.hot || []).forEach(function (id) { hot[String(id)] = true; });
57
+ var label = function (k) { var a = (home.axes || []).filter(function (x) { return x.key === k; })[0]; return a ? a.label : k; };
58
+ var g = '<svg viewBox="0 0 ' + S + ' ' + S + '" role="img" aria-label="The landscape: ' + esc(label(xKey)) + ' across, ' + esc(label(yKey)) + ' up">';
59
+ g += '<defs><linearGradient id="mw-sg" x1="0" x2="1"><stop offset="0" stop-color="var(--accent)" stop-opacity="0"/><stop offset="1" stop-color="var(--accent)" stop-opacity=".22"/></linearGradient></defs>';
60
+ [1, 2, 3, 4].forEach(function (k) { g += '<circle cx="' + sx(10) + '" cy="' + sy(10) + '" r="' + (k * 130) + '" fill="none" stroke="var(--rule-soft)" stroke-dasharray="3 5"/>'; });
61
+ g += '<line x1="' + sx(5) + '" x2="' + sx(5) + '" y1="' + sy(0) + '" y2="' + sy(10) + '" stroke="var(--rule)"/><line y1="' + sy(5) + '" y2="' + sy(5) + '" x1="' + sx(0) + '" x2="' + sx(10) + '" stroke="var(--rule)"/>';
62
+ var cx0 = sx(10), cy0 = sy(10);
63
+ g += '<g class="mw-sweep" style="transform-origin:' + cx0 + 'px ' + cy0 + 'px"><path d="M' + cx0 + ' ' + cy0 + ' L' + (cx0 - 560) + ' ' + cy0 + ' A560 560 0 0 1 ' + (cx0 - 560 * Math.cos(0.5)).toFixed(1) + ' ' + (cy0 + 560 * Math.sin(0.5)).toFixed(1) + 'Z" fill="url(#mw-sg)" transform="rotate(180 ' + cx0 + ' ' + cy0 + ')"/></g>';
64
+ var plotted = 0;
65
+ (home.dots || []).forEach(function (d, i) {
66
+ var vx = xKey === 'overall' ? d.overall : d.scores[xKey];
67
+ var vy = yKey === 'overall' ? d.overall : d.scores[yKey];
68
+ if (vx == null || vy == null) return;
69
+ plotted += 1;
70
+ var cx = sx(Math.min(10, Math.max(0, Number(vx) + jit(i, 1)))).toFixed(1);
71
+ var cy = sy(Math.min(10, Math.max(0, Number(vy) + jit(i, 2)))).toFixed(1);
72
+ var h = !!hot[String(d.id)];
73
+ var r = (3 + (Number(d.overall) || 0) * 0.7).toFixed(1);
74
+ g += '<g class="mw-blip" data-id="' + esc(d.id) + '">';
75
+ if (h) g += '<circle class="mw-ping" cx="' + cx + '" cy="' + cy + '" r="6" fill="none" stroke="var(--accent)" stroke-width="2"/>';
76
+ g += '<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="' + (h ? 'var(--accent)' : 'var(--ink-soft)') + '" fill-opacity="' + (h ? 1 : 0.4) + '"><title>' + esc(d.name) + ' · ' + esc(d.overall) + '/10 like us</title></circle>';
77
+ if (h || Number(d.overall) >= 6) g += '<text x="' + (Number(cx) + 9) + '" y="' + (Number(cy) + 4) + '" font-size="12" fill="var(--ink-soft)" font-family="var(--font-body)">' + esc(String(d.name).split(' ')[0]) + '</text>';
78
+ g += '</g>';
79
+ });
80
+ g += '<rect x="' + (cx0 - 9) + '" y="' + (cy0 - 9) + '" width="18" height="18" rx="4" fill="var(--ink)"/><text x="' + (cx0 - 14) + '" y="' + (cy0 + 24) + '" text-anchor="end" font-size="12" font-weight="700" fill="var(--ink)">us</text>';
81
+ g += '<text x="' + (S - p) + '" y="' + (S - 12) + '" text-anchor="end" font-size="12" fill="var(--ink-faint)">' + esc(label(xKey)) + ' →</text><text x="12" y="' + (p - 14) + '" font-size="12" fill="var(--ink-faint)">↑ ' + esc(label(yKey)) + '</text>';
82
+ if (!plotted) g += '<text x="' + (S / 2) + '" y="' + (S / 2) + '" text-anchor="middle" font-size="14" fill="var(--ink-faint)">No competitor is scored on these two yet.</text>';
83
+ return g + '</svg>';
84
+ }
85
+
86
+ // nextSweep(home, now) → { big, small } for the countdown panel.
87
+ function nextSweep(home, now) {
88
+ var ns = home.next_sweep || {};
89
+ if (!ns.enabled) return { big: 'paused', small: 'the watch is switched off' };
90
+ if (!ns.due) return { big: 'due now', small: 'starts on the next hourly check' };
91
+ var due = new Date(ns.due);
92
+ var ms = due - (now || new Date());
93
+ if (!(ms > 0)) return { big: 'due now', small: 'starts on the next hourly check' };
94
+ var d = Math.floor(ms / 864e5), h = Math.floor((ms % 864e5) / 36e5), m = Math.floor((ms % 36e5) / 6e4);
95
+ var big = d > 0 ? d + 'd ' + h + 'h' : h > 0 ? h + 'h ' + m + 'm' : m + 'm';
96
+ var small = due.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short' }) + ' · ' + due.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
97
+ return { big: big, small: small };
98
+ }
99
+
100
+ function money(v) { return '$' + (Number(v) || 0).toFixed(2); }
101
+
102
+ // The facts an item carries, in words. Research keys get their label; anything else
103
+ // is shown under its own key with underscores as spaces.
104
+ var FACT_LABEL = { pricing: 'Pricing', funding: 'Funding', open_source_self_host: 'Self-host', partner_candidate: 'Also a partner?', rank: 'Partner rank', quote: 'In their words', unverified: 'Unverified', watch_sources: 'Sources to watch' };
105
+ function factRows(facts) {
106
+ var out = [];
107
+ Object.keys(facts || {}).forEach(function (k) {
108
+ if (k === 'quote_url') return;
109
+ var v = facts[k];
110
+ if (v == null || v === '' || (Array.isArray(v) && !v.length)) return;
111
+ if (typeof v === 'boolean') v = v ? 'yes' : 'no';
112
+ else if (Array.isArray(v)) v = v.join('; ');
113
+ else if (typeof v === 'object') return;
114
+ out.push([FACT_LABEL[k] || k.replace(/_/g, ' '), String(v)]);
115
+ });
116
+ return out;
117
+ }
118
+
119
+ // ---- per-viewer conveniences (never the record) --------------------------------
120
+ function store(key, fallback) {
121
+ try { var v = JSON.parse(root.localStorage.getItem(key)); return v == null ? fallback : v; } catch (e) { return fallback; }
122
+ }
123
+ function save(key, v) { try { root.localStorage.setItem(key, JSON.stringify(v)); } catch (e) { /* private mode */ } }
124
+ var pins = {
125
+ all: function () { var v = store('mw-pins', []); return Array.isArray(v) ? v : []; },
126
+ has: function (id) { return pins.all().some(function (p) { return String(p.id) === String(id); }); },
127
+ toggle: function (item) {
128
+ var list = pins.all().filter(function (p) { return String(p.id) !== String(item.id); });
129
+ var on = list.length === pins.all().length;
130
+ if (on) list.unshift({ id: item.id, name: item.name, kind: item.kind });
131
+ save('mw-pins', list.slice(0, 12));
132
+ return on;
133
+ },
134
+ };
135
+ var reads = {
136
+ get: function () { var v = store('mw-read', []); return Array.isArray(v) ? v.map(String) : []; },
137
+ add: function (id) { var v = reads.get(); if (v.indexOf(String(id)) < 0) v.push(String(id)); save('mw-read', v.slice(-200)); },
138
+ };
139
+
140
+ // ---- the propose dialog ------------------------------------------------------
141
+ // One <dialog> for every proposal either page files. `mode` is 'change' (the item
142
+ // page's four kinds), 'stop' (stop tracking) or 'scout' (a new item). It posts to
143
+ // POST /market-watch/proposals and resolves with the server's answer.
144
+ var CHANGE_KINDS = [
145
+ ['rescore', 'Re-score a dimension'],
146
+ ['move_kind', 'Move to partner / competitor'],
147
+ ['add_source', 'Add a source to watch'],
148
+ ['correct_entry', 'Correct an entry'],
149
+ ];
150
+ function dialogHtml(mode, ctx) {
151
+ ctx = ctx || {};
152
+ var it = ctx.item || {};
153
+ var h = '<form method="dialog" novalidate>';
154
+ if (mode === 'scout') {
155
+ h += '<span class="mw-label">send a new scout</span>';
156
+ h += '<label>What should we watch?<select name="scout"><option value="competitor">A competitor</option><option value="partner">A partner</option><option value="topic">A topic</option></select></label>';
157
+ h += '<label>Its name<input name="name" maxlength="120" autocomplete="off" required></label>';
158
+ h += '<label>A link, if it has one<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label>';
159
+ } else if (mode === 'stop') {
160
+ h += '<span class="mw-label">stop tracking</span>';
161
+ h += '<p class="mw-quiet" style="margin:0 0 12px">Asks the government to take ' + esc(it.name) + ' off the watch. Its case file stays readable.</p>';
162
+ } else {
163
+ h += '<span class="mw-label">propose a change</span>';
164
+ h += '<label>What should change?<select name="kind">';
165
+ CHANGE_KINDS.forEach(function (k) {
166
+ if (k[0] === 'move_kind' && it.kind === 'topic') return;
167
+ var lbl = k[0] === 'move_kind' ? (it.kind === 'partner' ? 'Move to competitor' : 'Move to partner') : k[1];
168
+ h += '<option value="' + k[0] + '">' + esc(lbl) + '</option>';
169
+ });
170
+ h += '</select></label>';
171
+ h += '<div data-for="rescore"><div class="mw-row2"><label>Which score<select name="dimension">';
172
+ (ctx.dimensions || []).forEach(function (d) { h += '<option value="' + esc(d.key) + '">' + esc(d.label) + (d.latest != null ? ' (now ' + esc(d.latest) + ')' : '') + '</option>'; });
173
+ h += '<option value="overall">Overall' + (it.overall != null ? ' (now ' + esc(it.overall) + ')' : '') + '</option>';
174
+ h += '</select></label><label>New score<input name="value" type="number" min="0" max="10" step="0.5" inputmode="decimal"></label></div></div>';
175
+ h += '<div data-for="add_source" hidden><label>The source<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label></div>';
176
+ h += '<div data-for="correct_entry" hidden><label>Which entry<select name="log_entry_id">';
177
+ (ctx.entries || []).forEach(function (e) { h += '<option value="' + esc(e.id) + '">' + esc(fmtDay(e.found_on)) + ' · ' + esc(String(e.takeaway).slice(0, 70)) + '</option>'; });
178
+ h += '</select></label></div>';
179
+ }
180
+ h += '<label>Why, in one line<input name="reason" maxlength="280" autocomplete="off" required placeholder="' + (mode === 'scout' ? 'e.g. they launched an agent marketplace this week' : mode === 'stop' ? 'e.g. they shut down in September' : 'e.g. they now pay maintainers, so economy should be 4') + '"></label>';
181
+ h += '<p class="mw-dialog__err" role="alert"></p>';
182
+ h += '<div class="mw-dialog__acts"><button class="btn btn--ghost" value="cancel" type="button" data-cancel>cancel</button><button class="btn" type="submit">send to the government</button></div>';
183
+ return h + '</form>';
184
+ }
185
+
186
+ // bodyFor(mode, form, item) — what the dialog posts.
187
+ function bodyFor(mode, f, item) {
188
+ var v = function (n) { var el = f.elements[n]; return el ? String(el.value || '').trim() : ''; };
189
+ if (mode === 'scout') return { kind: 'add_item', scout: v('scout'), name: v('name'), url: v('url'), reason: v('reason') };
190
+ if (mode === 'stop') return { kind: 'remove_item', item_id: item.id, reason: v('reason') };
191
+ var kind = v('kind');
192
+ var b = { kind: kind, item_id: item.id, reason: v('reason') };
193
+ if (kind === 'rescore') { b.dimension = v('dimension'); b.value = v('value'); }
194
+ if (kind === 'add_source') b.url = v('url');
195
+ if (kind === 'correct_entry') b.log_entry_id = v('log_entry_id');
196
+ return b;
197
+ }
198
+
199
+ function openPropose(mode, ctx, onDone) {
200
+ var doc = root.document;
201
+ var dlg = doc.getElementById('mw-propose');
202
+ dlg.innerHTML = dialogHtml(mode, ctx);
203
+ var f = dlg.querySelector('form');
204
+ var err = dlg.querySelector('.mw-dialog__err');
205
+ var sync = function () {
206
+ var k = f.elements.kind ? f.elements.kind.value : '';
207
+ Array.prototype.forEach.call(dlg.querySelectorAll('[data-for]'), function (el) { el.hidden = el.getAttribute('data-for') !== k; });
208
+ };
209
+ if (f.elements.kind) f.elements.kind.addEventListener('change', sync);
210
+ sync();
211
+ dlg.querySelector('[data-cancel]').addEventListener('click', function () { dlg.close(); });
212
+ f.addEventListener('submit', function (ev) {
213
+ ev.preventDefault();
214
+ var body = bodyFor(mode, f, ctx.item || {});
215
+ if (!body.reason) { err.textContent = 'Say why, in one line.'; return; }
216
+ var btn = f.querySelector('[type=submit]');
217
+ btn.disabled = true;
218
+ err.textContent = '';
219
+ root.fetch('/api/bongos/market-watch/proposals', {
220
+ method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
221
+ }).then(function (r) {
222
+ return r.json().catch(function () { return {}; }).then(function (j) { return { status: r.status, ok: r.ok, data: j }; });
223
+ }).then(function (res) {
224
+ btn.disabled = false;
225
+ if (res.ok) { dlg.close(); onDone(res.data); return; }
226
+ var e = (res.data && res.data.error) || {};
227
+ err.textContent = res.status === 401 ? 'Sign in again to send this.' : (e.message && e.message !== e.code ? e.message : 'That did not send. Try again in a moment.');
228
+ }).catch(function () { btn.disabled = false; err.textContent = 'Could not reach the server.'; });
229
+ });
230
+ dlg.showModal();
231
+ var first = f.querySelector('select, input');
232
+ if (first) first.focus();
233
+ }
234
+
235
+ var api = {
236
+ esc: esc, safeHref: safeHref, itemHref: itemHref, confDots: confDots, typePill: typePill, initials: initials,
237
+ fmtDay: fmtDay, spark: spark, scopeSvg: scopeSvg, nextSweep: nextSweep, money: money, factRows: factRows,
238
+ pins: pins, reads: reads, dialogHtml: dialogHtml, bodyFor: bodyFor, openPropose: openPropose,
239
+ KIND_ICON: KIND_ICON, KIND_PLURAL: KIND_PLURAL,
240
+ };
241
+ root.OTBMarketWatch = api;
242
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
243
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -43,7 +43,7 @@
43
43
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
44
44
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
45
45
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
46
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
46
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
47
47
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
48
  <script src="/builders/palette.js" defer></script>
49
49
  <script src="/builders/hall-kit.js"></script>
@@ -105,7 +105,7 @@
105
105
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
106
106
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
107
107
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
108
- <script src="/builders/shell.js?v=2026-10-30-market-watch"></script>
108
+ <script src="/builders/shell.js?v=2026-10-30-market-watch-pages"></script>
109
109
  <script src="/builders/palette.js" defer></script>
110
110
  <script src="/builders/hall-kit.js"></script>
111
111
  <script src="/builders/market-watch-settings.js"></script>
@@ -0,0 +1,169 @@
1
+ /* market-watch.css — the Market Watch home (the watchtower desk, option D) and the
2
+ tracked-item page (the dossier, option B). Task 1004939; the designs of record are
3
+ docs/design/mocks/market-watch/home.html and item.html. Every colour is a hall token
4
+ from the branding pack, never the mock's literal hex: the mocks drew with the same
5
+ fifteen, so the look carries over and a host's palette still wins. */
6
+
7
+ .mw-label {
8
+ display: block;
9
+ margin: 0 0 8px;
10
+ font-family: var(--label-face);
11
+ font-size: var(--label-size);
12
+ letter-spacing: var(--label-space);
13
+ text-transform: var(--label-case);
14
+ color: var(--ink-faint);
15
+ font-weight: 500;
16
+ }
17
+ .mw-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
18
+ .mw-one { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
19
+ .mw-panel { background: var(--bg-card); border-radius: var(--radius); padding: 12px; }
20
+ .mw-quiet { color: var(--ink-faint); font-size: 12.5px; }
21
+ .mw-off { max-width: var(--measure); }
22
+ .mw-off p { margin: 0 0 6px; color: var(--ink-soft); }
23
+
24
+ /* ---- the watchtower desk ---------------------------------------------------- */
25
+ .mw-desk { display: grid; grid-template-columns: 260px minmax(0, 1fr) 320px; gap: 16px; align-items: start; }
26
+ @media (max-width: 1180px) { .mw-desk { grid-template-columns: 1fr 1fr; } .mw-scope-wrap { grid-column: 1 / -1; order: -1; } }
27
+ @media (max-width: 680px) { .mw-desk { grid-template-columns: minmax(0, 1fr); } }
28
+ .mw-side { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
29
+
30
+ .mw-watched { display: flex; align-items: center; gap: 6px; margin: 0 0 10px; font-size: 12.5px; color: var(--ink-soft); }
31
+ .mw-watched b { color: var(--ink); font-weight: 600; }
32
+ .mw-scout { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; background: var(--glass); margin-bottom: 8px; white-space: nowrap; }
33
+ .mw-scout__ic { width: 30px; height: 30px; flex: none; border-radius: 9px; display: grid; place-items: center; background: var(--bg-card); font-size: 15px; color: var(--ink-soft); }
34
+ .mw-scout__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.3; }
35
+ .mw-scout__lv { font-size: 11.5px; color: var(--ink-faint); }
36
+ .mw-badge { min-width: 24px; height: 24px; border-radius: 12px; background: var(--accent); color: var(--accent-on); font-size: 12px; font-weight: 700; display: grid; place-items: center; padding: 0 7px; }
37
+ .mw-badge--none { background: var(--glass-strong); color: var(--ink-faint); }
38
+ .mw-send { width: 100%; margin-top: 4px; }
39
+ .mw-streak { display: flex; gap: 4px; margin-top: 6px; }
40
+ .mw-streak i { flex: 1; max-width: 40px; height: 18px; border-radius: 5px; background: var(--accent); opacity: .85; }
41
+ .mw-streak i.no { background: var(--glass-strong); opacity: 1; }
42
+ .mw-pins { margin-top: 14px; }
43
+ .mw-pin-row { display: flex; align-items: center; gap: 8px; min-height: 32px; border-top: 1px solid var(--rule-soft); font-size: 13px; }
44
+ .mw-pin-row:first-of-type { border-top: 0; }
45
+ .mw-pin-row a { color: var(--ink); text-decoration: none; padding: 4px 0; }
46
+ .mw-pin-row a:hover { text-decoration: underline; }
47
+
48
+ .mw-scope-tools { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: var(--ink-faint); margin-bottom: 8px; }
49
+ .mw-scope-tools select { font: inherit; color: var(--ink); background: var(--bg-card); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 4px 8px; min-height: 28px; max-width: 46%; }
50
+ .mw-scope-tools .mw-sp { flex: 1; }
51
+ .mw-key { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
52
+ .mw-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
53
+ .mw-scope { position: relative; aspect-ratio: 1 / 1; max-height: 560px; margin: 0 auto; border-radius: 18px; background: var(--bg-frame); overflow: hidden; }
54
+ .mw-scope svg { position: absolute; inset: 0; width: 100%; height: 100%; }
55
+ .mw-scope .mw-blip { cursor: pointer; }
56
+ .mw-open { margin: 10px 0 0; }
57
+ .mw-open select { flex: 1 1 200px; min-width: 0; max-width: 360px; }
58
+ .mw-sweep { animation: mw-sweep 6s linear infinite; }
59
+ @keyframes mw-sweep { to { transform: rotate(360deg); } }
60
+ .mw-ping { animation: mw-ping 1.8s ease-out infinite; }
61
+ @keyframes mw-ping { 0% { r: 4; opacity: .9; } 100% { r: 16; opacity: 0; } }
62
+ @media (prefers-reduced-motion: reduce) { .mw-sweep, .mw-ping { animation: none; } }
63
+
64
+ .mw-fuel { display: flex; gap: 10px; align-items: flex-end; margin-top: 4px; }
65
+ .mw-count { font-size: 34px; font-weight: 300; letter-spacing: -.03em; line-height: 1.1; font-family: var(--font-display); }
66
+ .mw-when { color: var(--ink-faint); font-size: 12px; }
67
+ .mw-tank { width: 44px; height: 90px; border-radius: 10px; border: 2px solid var(--rule); position: relative; overflow: hidden; margin: 0 auto; }
68
+ .mw-tank i { position: absolute; left: 0; right: 0; bottom: 0; background: var(--accent); }
69
+ .mw-left { font-size: 11px; color: var(--ink-faint); margin-top: 4px; text-align: center; white-space: nowrap; }
70
+
71
+ .mw-xp { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 12px; color: var(--ink-faint); }
72
+ .mw-xp__b { flex: 1; height: 8px; border-radius: 8px; background: var(--glass-strong); overflow: hidden; }
73
+ .mw-xp__b i { display: block; height: 100%; background: var(--accent); transition: width .3s; }
74
+ .mw-rep { display: flex; align-items: center; gap: 8px; min-height: 40px; border-top: 1px solid var(--rule-soft); white-space: nowrap; }
75
+ .mw-rep:first-child { border-top: 0; }
76
+ .mw-rep__ic { width: 16px; flex: none; text-align: center; color: var(--ink-faint); }
77
+ .mw-rep__t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; color: var(--ink-soft); text-decoration: none; padding: 6px 0; }
78
+ .mw-rep__t b { color: var(--ink); font-weight: 600; }
79
+ .mw-rep__t:hover { color: var(--ink); }
80
+ .mw-rep.done { opacity: .45; }
81
+ .mw-rep button { flex: none; min-width: 44px; min-height: 26px; border: 1px solid var(--rule-soft); background: transparent; color: var(--ink-soft); border-radius: var(--radius-pill); padding: 0 10px; cursor: pointer; font: inherit; font-size: 12px; }
82
+ .mw-rep button:hover { border-color: var(--ink-faint); color: var(--ink); }
83
+
84
+ /* ---- the dossier ------------------------------------------------------------- */
85
+ .mw-back { display: inline-flex; align-items: center; min-height: 28px; margin: 0 0 10px; font-size: 13px; color: var(--ink-faint); text-decoration: none; }
86
+ .mw-back:hover { color: var(--ink); }
87
+ .mw-wrap { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 24px; align-items: start; }
88
+ @media (max-width: 1000px) { .mw-wrap { grid-template-columns: minmax(0, 1fr); } .mw-card { position: static; max-width: 440px; width: 100%; } }
89
+ .mw-card { border-radius: 20px; overflow: hidden; background: var(--bg-card); position: sticky; top: calc(var(--topbar-h) + 16px); box-shadow: var(--shadow-pop); }
90
+ .mw-holo { position: relative; height: 150px; display: flex; align-items: flex-end; padding: 14px 16px; color: var(--void-ink);
91
+ background:
92
+ radial-gradient(120% 90% at 20% 10%, color-mix(in srgb, var(--sea) 55%, transparent) 0, transparent 50%),
93
+ radial-gradient(90% 90% at 85% 30%, color-mix(in srgb, var(--accent) 60%, transparent) 0, transparent 55%),
94
+ radial-gradient(90% 90% at 50% 110%, color-mix(in srgb, var(--sea) 35%, transparent) 0, transparent 60%),
95
+ var(--void); }
96
+ .mw-holo__ini { position: absolute; right: 14px; top: 12px; font: 300 54px/1 var(--font-display); letter-spacing: -.04em; opacity: .85; }
97
+ .mw-holo__kind { position: absolute; left: 14px; top: 14px; font: 500 11px/1 var(--font-mono); padding: 5px 8px; border-radius: 999px; background: color-mix(in srgb, var(--void) 55%, transparent); color: var(--void-ink); }
98
+ .mw-holo__nm { font-weight: 600; font-size: 17px; text-shadow: 0 1px 6px var(--void); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
99
+ .mw-lvl { display: flex; align-items: center; gap: 14px; padding: 14px 16px; border-bottom: 1px solid var(--rule-soft); }
100
+ .mw-ring { width: 62px; height: 62px; border-radius: 50%; display: grid; place-items: center; flex: none; background: conic-gradient(var(--accent) calc(var(--p) * 1%), var(--glass-strong) 0); }
101
+ .mw-ring b { width: 48px; height: 48px; border-radius: 50%; background: var(--bg-card); display: grid; place-items: center; font: 600 18px var(--font-body); }
102
+ .mw-vtag { font-weight: 600; color: var(--accent-ink); }
103
+ .mw-stats { padding: 8px 16px 4px; }
104
+ .mw-st { display: grid; grid-template-columns: minmax(0, 1fr) 70px; gap: 10px; align-items: center; padding: 4px 0; font-size: 12.5px; }
105
+ .mw-st .kit-bars { --kit-bars-label-w: 128px; margin: 0; }
106
+ .mw-spark { display: block; flex: none; }
107
+ .mw-spark .kit-spark { width: 100%; height: 100%; }
108
+ .mw-badges { display: flex; gap: 6px; flex-wrap: wrap; padding: 10px 16px 14px; }
109
+ .mw-bdg { font: 500 11px var(--font-mono); padding: 5px 8px; border-radius: 8px; background: var(--glass); }
110
+ .mw-bdg--hot { background: var(--accent-tint); color: var(--accent-ink); }
111
+ .mw-acts { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 0 16px 16px; }
112
+ .mw-acts .btn, .mw-acts .btn--ghost { width: 100%; min-width: 0; }
113
+ .mw-acts .mw-wide { grid-column: 1 / -1; }
114
+ .mw-acts [aria-pressed="true"] { background: var(--accent-tint); color: var(--accent-ink); border-color: transparent; }
115
+
116
+ .mw-sec { margin-bottom: 22px; }
117
+ .mw-why { padding: 14px 16px; font-size: 14.5px; line-height: 1.5; }
118
+ .mw-why small { display: block; color: var(--ink-faint); font-size: 12px; margin-top: 6px; }
119
+ .mw-facts { padding: 0; }
120
+ .mw-f { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 12px; padding: 9px 16px; border-top: 1px solid var(--rule-soft); font-size: 13px; }
121
+ .mw-f:first-child { border-top: 0; }
122
+ .mw-f > span:first-child { color: var(--ink-faint); }
123
+
124
+ .mw-src { color: var(--ink-faint); text-decoration: none; border-bottom: 1px dotted var(--rule); font-size: 12.5px; white-space: nowrap; display: inline-block; padding: 4px 0; max-width: 180px; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; }
125
+ .mw-src:hover { color: var(--ink); }
126
+ .mw-pill { display: inline-flex; align-items: center; font: 500 11px/1 var(--font-mono); padding: 4px 8px; border-radius: 999px; white-space: nowrap; color: var(--ink-soft); background: var(--glass-strong); }
127
+ .mw-pill--insight { color: var(--accent-ink); background: var(--accent-tint); }
128
+ .mw-pill--news { color: var(--sea); background: color-mix(in srgb, var(--sea) 16%, var(--bg-card)); }
129
+ .mw-conf { display: inline-flex; gap: 2px; align-items: center; }
130
+ .mw-conf i { width: 5px; height: 11px; border-radius: 2px; background: var(--rule); }
131
+ .mw-conf i.on { background: var(--ink-soft); }
132
+ .mw-conf--low i.on { background: var(--terracotta); }
133
+
134
+ .mw-log { overflow: hidden; padding: 0; }
135
+ .mw-log table { width: 100%; border-collapse: collapse; font-size: 13px; }
136
+ .mw-log th { text-align: left; padding: 0; border-bottom: 1px solid var(--rule-soft); white-space: nowrap; }
137
+ .mw-log th button, .mw-log th span { display: flex; align-items: center; width: 100%; min-height: 34px; padding: 0 10px; background: none; border: 0; text-align: left; font: 500 10.5px var(--font-mono); letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); }
138
+ .mw-log th button { cursor: pointer; }
139
+ .mw-log th button:hover { color: var(--ink); }
140
+ .mw-log th[aria-sort] button { color: var(--ink); }
141
+ .mw-log th[aria-sort="descending"] button::after { content: " \2193"; }
142
+ .mw-log th[aria-sort="ascending"] button::after { content: " \2191"; }
143
+ .mw-log td { padding: 9px 10px; border-bottom: 1px solid var(--rule-soft); vertical-align: middle; }
144
+ .mw-log tr:last-child td { border-bottom: 0; }
145
+ .mw-log tr.pin td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
146
+ .mw-log td.mw-tk { max-width: 0; width: 100%; }
147
+ .mw-date, .mw-run { font: 400 12px var(--font-mono); color: var(--ink-faint); white-space: nowrap; }
148
+ @media (max-width: 760px) {
149
+ .mw-log thead { display: none; }
150
+ .mw-log tr { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 4px 10px; padding: 10px 12px; border-bottom: 1px solid var(--rule-soft); }
151
+ .mw-log tr:last-child { border-bottom: 0; }
152
+ .mw-log td { padding: 0; border: 0; }
153
+ .mw-log td.mw-tk { grid-column: 1 / -1; grid-row: 2; max-width: none; }
154
+ .mw-log td.mw-c-run, .mw-log td.mw-c-conf { display: none; }
155
+ .mw-log tr.pin { box-shadow: inset 3px 0 0 var(--accent); }
156
+ .mw-log tr.pin td:first-child { box-shadow: none; }
157
+ .mw-f { grid-template-columns: 96px minmax(0, 1fr); }
158
+ }
159
+
160
+ /* ---- the propose dialog (both pages) ---------------------------------------- */
161
+ .mw-dialog { border: 0; border-radius: var(--radius); background: var(--bg-card); color: var(--ink); padding: 20px; width: min(440px, calc(100vw - 32px)); box-shadow: var(--shadow-pop); }
162
+ .mw-dialog::backdrop { background: var(--scrim); }
163
+ .mw-dialog label { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 12px; }
164
+ .mw-dialog input, .mw-dialog select { display: block; width: 100%; margin-top: 6px; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 8px 9px; min-height: 36px; }
165
+ .mw-dialog .mw-row2 { display: grid; grid-template-columns: minmax(0, 1fr) 90px; gap: 8px; }
166
+ .mw-dialog__acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
167
+ .mw-dialog__err { color: var(--danger); font-size: 12.5px; margin: 0 0 8px; min-height: 1em; }
168
+ .mw-filter { margin: 0 0 8px; }
169
+ .mw-rules-link { display: inline-flex; align-items: center; min-height: 28px; margin-top: 12px; font-size: 12.5px; color: var(--ink-soft); }