@bongos/core 1.21.4 → 1.21.6

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 (126) hide show
  1. package/.bongos-core.json +233 -108
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -1
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
  8. package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +37 -2
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +541 -658
  13. package/docs/copy-registry.json +1205 -2340
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
  16. package/docs/onboarding/diagrams/assertions.json +1 -1
  17. package/docs/page-inventory.json +47 -28
  18. package/docs/page-readings.json +1128 -1339
  19. package/modules/autonomy/config-idle.js +192 -0
  20. package/modules/autonomy/governor-docket.js +101 -0
  21. package/modules/autonomy/routes/autonomy.js +27 -0
  22. package/modules/autonomy/runner-health.js +22 -1
  23. package/modules/discord/board-broadcast.js +3 -1
  24. package/modules/discord/craft-broadcast.js +107 -0
  25. package/modules/discord/routes/discord.js +10 -0
  26. package/modules/discord/ship-broadcast.js +30 -2
  27. package/modules/economy/reward.js +3 -0
  28. package/modules/government/board.js +20 -5
  29. package/modules/government/docket-vocab.js +82 -0
  30. package/modules/government/docket.js +458 -0
  31. package/modules/government/routes/government.js +34 -0
  32. package/modules/hall-ui/public/city-draw.js +438 -0
  33. package/modules/hall-ui/public/city.css +110 -0
  34. package/modules/hall-ui/public/city.html +71 -0
  35. package/modules/hall-ui/public/city.js +177 -0
  36. package/modules/hall-ui/public/city.states.json +30 -0
  37. package/modules/hall-ui/public/gate.js +11 -4
  38. package/modules/hall-ui/public/genesis-home.js +9 -10
  39. package/modules/hall-ui/public/profile.css +4 -0
  40. package/modules/hall-ui/public/profile.js +25 -1
  41. package/modules/hall-ui/public/settings-autobongos.js +19 -1
  42. package/modules/hall-ui/public/settings.css +26 -0
  43. package/modules/hall-ui/public/settings.html +8 -0
  44. package/modules/hall-ui/public/settings.js +43 -3
  45. package/modules/ideas/governor-docket.js +93 -0
  46. package/modules/ideas/routes/blockers.js +5 -0
  47. package/modules/lifecycle/governor-docket.js +192 -0
  48. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  49. package/modules/lifecycle/routes/lifecycle.js +3 -0
  50. package/modules/onboarding/routes/access-requests.js +68 -27
  51. package/modules/platform-identity/craft-rollup.js +72 -0
  52. package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
  53. package/modules/platform-identity/platform-identity.js +12 -6
  54. package/modules/platform-identity/routes/sso.js +4 -0
  55. package/modules/platform-identity/tests/platform-identity.mjs +1 -1
  56. package/modules/provisioning/core-upgrade.js +34 -7
  57. package/modules/provisioning/module.json +2 -1
  58. package/modules/provisioning/seams.js +2 -0
  59. package/modules/public-landing/public/assets/cosmos.css +6 -50
  60. package/modules/public-landing/public/projects.html +1170 -1659
  61. package/modules/public-landing/public/projects.probes.json +18 -18
  62. package/modules/public-landing/public/projects.states.json +24 -31
  63. package/modules/security/routes/reports.js +39 -0
  64. package/package-lock.json +2 -2
  65. package/package.json +1 -1
  66. package/release-notes.json +72 -0
  67. package/scripts/gds/autobongos-grade-cap.js +187 -0
  68. package/scripts/gds/autobongos-loop.js +4 -0
  69. package/scripts/gds/autobongos-run.js +154 -13
  70. package/scripts/gds/autobongos-verify.js +211 -14
  71. package/scripts/gds/provision-core-upgrade.js +14 -2
  72. package/scripts/gds/provision-repo.js +77 -13
  73. package/scripts/gds/ship-flow.js +5 -0
  74. package/scripts/gds/ship-preflight-steps.js +2 -1
  75. package/scripts/gds/ship.js +10 -0
  76. package/scripts/gds/update-sweep.js +24 -8
  77. package/scripts/gds/upgrade-outcome.js +1 -0
  78. package/src/bongos/auth-admission.js +95 -13
  79. package/src/bongos/db-kernel.js +1 -1
  80. package/src/bongos/module-rechecks.js +77 -0
  81. package/src/bongos/route-rank-check.js +3 -0
  82. package/src/bongos/routes/core-update.js +68 -2
  83. package/src/bongos/routes/modules.js +17 -0
  84. package/src/bongos/serve-internal.js +14 -0
  85. package/src/bongos/software-update.js +3 -2
  86. package/src/module-api.js +1 -1
  87. package/tests/activity_rollup_order_db.mjs +3 -1
  88. package/tests/activity_snapshots_db.mjs +2 -0
  89. package/tests/autobongos_grade_cap.mjs +125 -0
  90. package/tests/autobongos_loop.mjs +230 -1
  91. package/tests/autobongos_verify.mjs +239 -3
  92. package/tests/autonomy_config_idle.mjs +272 -0
  93. package/tests/core_update_banner.mjs +29 -0
  94. package/tests/core_upgrade_door.mjs +31 -1
  95. package/tests/core_upgrade_runner.mjs +20 -0
  96. package/tests/discord_craft_broadcast.mjs +162 -0
  97. package/tests/genesis_home.mjs +1 -1
  98. package/tests/government_routes.mjs +3 -1
  99. package/tests/governor_docket.mjs +552 -0
  100. package/tests/hall_audit.mjs +10 -1
  101. package/tests/hall_city.mjs +369 -0
  102. package/tests/hall_page_gate_map.mjs +5 -0
  103. package/tests/hub_craft_rollup.mjs +350 -0
  104. package/tests/onboard.mjs +10 -4
  105. package/tests/platform_boot.mjs +1 -1
  106. package/tests/profile_rollup_consent.mjs +2 -0
  107. package/tests/profile_route.mjs +16 -2
  108. package/tests/projects_hub.mjs +196 -89
  109. package/tests/projects_hub_app_status.mjs +3 -1
  110. package/tests/projects_hub_app_step.mjs +72 -48
  111. package/tests/projects_hub_dns_ready.mjs +10 -2
  112. package/tests/projects_hub_look.mjs +32 -22
  113. package/tests/projects_hub_module_picker.mjs +112 -62
  114. package/tests/projects_hub_pre_uat.mjs +48 -41
  115. package/tests/provision.mjs +78 -0
  116. package/tests/public_landing_projects.mjs +12 -6
  117. package/tests/settings_main_role.mjs +190 -0
  118. package/tests/software_update.mjs +56 -1
  119. package/tests/update_subscription_engine.mjs +55 -3
  120. package/tests/wizard_demo.mjs +37 -11
  121. package/tests/wizard_draft_resume.mjs +34 -22
  122. package/tests/wizard_front_door.mjs +173 -103
  123. package/tests/wizard_intent_resume.mjs +28 -25
  124. package/tests/wizard_physics.mjs +94 -76
  125. package/tests/wizard_physics_more.mjs +428 -277
  126. package/tests/wizard_six_screens.mjs +126 -0
@@ -0,0 +1,177 @@
1
+ // modules/hall-ui/public/city.js — THE GOVERNOR CITY's home, wired (task
2
+ // 1004524, goal 1000125, R02).
3
+ //
4
+ // city-draw.js computes the city as data; this file reads the three feeds,
5
+ // hands them to GovCity.cityModel(), and turns its lists into DOM: SVG paths
6
+ // for the ground and the buildings, one span per parcel (CSS offset-path) and
7
+ // one per pin. It also owns the sun/moon switch and the stage's fit to the
8
+ // window. No innerHTML anywhere: every node is created, and every docket string
9
+ // lands as text or an attribute.
10
+ //
11
+ // THE FEEDS (all GETs, all gated server-side):
12
+ // /api/bongos/government/docket the pins and the one line (task 1004523)
13
+ // /api/bongos/goals the open goals: one district each
14
+ // /api/bongos/goals/rollups each goal's task counts: a building per task
15
+ // A feed that fails leaves its part of the city out (no pins, or no districts)
16
+ // and the line says the docket could not be read; it never draws sample data.
17
+ //
18
+ // THE SWITCH. Light and dark use the hall's own preference ('hall-theme' in
19
+ // localStorage, else the OS setting) and the hall's own attribute
20
+ // (html[data-theme]), so the city opens in the mode every other hall page is
21
+ // in, and a change here carries back to them. The drawing's faces come from
22
+ // the mode, so a toggle redraws the city from the last feeds (no refetch).
23
+ //
24
+ // MOTION is CSS only (city.css); nothing here animates. The scale write on
25
+ // resize is layout, not motion.
26
+
27
+ (function () {
28
+ 'use strict';
29
+ var City = window.GovCity;
30
+ var SVG_NS = 'http://www.w3.org/2000/svg';
31
+ var THEME_KEY = 'hall-theme';
32
+ var SUN = 'M7 1.5V3M7 11v1.5M1.5 7H3M11 7h1.5M3.1 3.1l1 1M9.9 9.9l1 1M3.1 10.9l1-1M9.9 4.1l1-1M9.5 7A2.5 2.5 0 1 1 4.5 7A2.5 2.5 0 1 1 9.5 7Z';
33
+ var MOON = 'M11.5 8.6A5 5 0 0 1 5.4 2.5A5 5 0 1 0 11.5 8.6Z';
34
+ var FEEDS = {
35
+ docket: '/api/bongos/government/docket',
36
+ goals: '/api/bongos/goals',
37
+ rollups: '/api/bongos/goals/rollups',
38
+ };
39
+
40
+ var $ = function (id) { return document.getElementById(id); };
41
+ var media = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
42
+ var data = { docket: null, goals: [], rollups: [], loaded: false };
43
+
44
+ // ---------- light / dark ----------
45
+ // A choice made on this page when storage is blocked (private mode) still
46
+ // holds for this page view.
47
+ var chosen = null;
48
+ function storedTheme() {
49
+ try { var v = window.localStorage.getItem(THEME_KEY); if (v === 'dark' || v === 'light') return v; } catch (_) { /* blocked */ }
50
+ return chosen;
51
+ }
52
+ function effectiveTheme() { return storedTheme() || (media && media.matches ? 'dark' : 'light'); }
53
+ function applyTheme() {
54
+ var t = effectiveTheme();
55
+ document.documentElement.setAttribute('data-theme', t);
56
+ var btn = $('cityMode');
57
+ var label = t === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
58
+ if (btn) { btn.setAttribute('aria-label', label); btn.setAttribute('title', label); }
59
+ var icon = $('cityModeIcon');
60
+ if (icon) icon.setAttribute('d', t === 'dark' ? SUN : MOON);
61
+ return t;
62
+ }
63
+ function toggleTheme() {
64
+ var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
65
+ chosen = next;
66
+ try { window.localStorage.setItem(THEME_KEY, next); } catch (_) { /* private mode: this page only */ }
67
+ draw(applyTheme());
68
+ }
69
+
70
+ // ---------- fit the 1440 x 900 artboard to the window ----------
71
+ // 1440 x 900 is scale 1, the prototype exactly. Narrower or shorter, the
72
+ // city (960 px wide on the artboard) shrinks to fit; wider and taller, it grows.
73
+ function fit() {
74
+ var stage = $('cityStage');
75
+ if (!stage) return;
76
+ var w = window.innerWidth || 1440, h = window.innerHeight || 900;
77
+ var s = Math.min(w / 1000, h / 900);
78
+ stage.style.setProperty('--city-scale', String(Math.round(s * 1000) / 1000));
79
+ }
80
+
81
+ // ---------- the feeds ----------
82
+ function getJson(url) {
83
+ return fetch(url, { credentials: 'same-origin', headers: { accept: 'application/json' } })
84
+ .then(function (r) { return r && r.ok ? r.json() : null; })
85
+ .catch(function () { return null; });
86
+ }
87
+ function load() {
88
+ return Promise.all([getJson(FEEDS.docket), getJson(FEEDS.goals), getJson(FEEDS.rollups)]).then(function (res) {
89
+ data.docket = res[0] && Array.isArray(res[0].matters) ? res[0] : null;
90
+ data.goals = res[1] && Array.isArray(res[1].goals) ? res[1].goals : [];
91
+ data.rollups = res[2] && Array.isArray(res[2].rollups) ? res[2].rollups : [];
92
+ data.loaded = true;
93
+ draw(effectiveTheme());
94
+ });
95
+ }
96
+
97
+ // ---------- drawing ----------
98
+ function clear(el) { while (el && el.firstChild) el.removeChild(el.firstChild); }
99
+ function pathEl(p) {
100
+ var el = document.createElementNS(SVG_NS, 'path');
101
+ el.setAttribute('d', p.d);
102
+ el.setAttribute('fill', p.f);
103
+ el.setAttribute('stroke', p.s);
104
+ if (p.dash && p.dash !== 'none') el.setAttribute('stroke-dasharray', p.dash);
105
+ el.setAttribute('stroke-width', String(p.w || 1));
106
+ return el;
107
+ }
108
+ function span(cls) { var el = document.createElement('span'); el.className = cls; return el; }
109
+
110
+ function drawPin(pin) {
111
+ var el = span('city-pin city-pin--' + pin.state);
112
+ el.setAttribute('data-matter', pin.key);
113
+ el.setAttribute('data-building', pin.building);
114
+ el.style.left = pin.x + 'px';
115
+ el.style.top = pin.y + 'px';
116
+ el.style.height = pin.h + 'px';
117
+ el.style.animationDelay = pin.delay + 's';
118
+ var head = span('city-pin__head');
119
+ if (pin.state === 'needs') head.appendChild(span('city-pin__ring'));
120
+ var svg = document.createElementNS(SVG_NS, 'svg');
121
+ svg.setAttribute('width', '14');
122
+ svg.setAttribute('height', '14');
123
+ var fill = document.createElementNS(SVG_NS, 'path');
124
+ fill.setAttribute('d', pin.gf || '');
125
+ var line = document.createElementNS(SVG_NS, 'path');
126
+ line.setAttribute('d', pin.gl || '');
127
+ svg.appendChild(fill);
128
+ svg.appendChild(line);
129
+ head.appendChild(svg);
130
+ el.appendChild(head);
131
+ el.appendChild(span('city-pin__stem'));
132
+ return el;
133
+ }
134
+
135
+ function drawParcel(pc) {
136
+ var el = span(pc.out ? 'city-parcel city-parcel--out' : 'city-parcel');
137
+ el.style.offsetPath = "path('" + pc.d + "')";
138
+ el.style.animationDuration = pc.dur + 's';
139
+ el.style.animationDelay = pc.delay + 's';
140
+ return el;
141
+ }
142
+
143
+ function draw(mode) {
144
+ var model = City.cityModel({ docket: data.docket, goals: data.goals, rollups: data.rollups, mode: mode });
145
+ var flat = $('cityFlat'), blds = $('cityBlds'), parcels = $('cityParcels'), pins = $('cityPins');
146
+ clear(flat); clear(blds); clear(parcels); clear(pins);
147
+ model.flat.forEach(function (p) { flat.appendChild(pathEl(p)); });
148
+ model.blds.forEach(function (b) {
149
+ var g = document.createElementNS(SVG_NS, 'g');
150
+ g.setAttribute('data-key', b.key);
151
+ b.polys.forEach(function (p) { g.appendChild(pathEl(p)); });
152
+ blds.appendChild(g);
153
+ });
154
+ model.parcels.forEach(function (pc) { parcels.appendChild(drawParcel(pc)); });
155
+ model.pins.forEach(function (pin) { pins.appendChild(drawPin(pin)); });
156
+ var lineEl = $('cityLine');
157
+ if (lineEl && data.loaded) lineEl.textContent = model.line;
158
+ var stage = $('cityStage');
159
+ if (stage) stage.setAttribute('aria-label', model.label);
160
+ return model;
161
+ }
162
+
163
+ function boot() {
164
+ var start = $('cityStart');
165
+ if (start) start.setAttribute('href', City.STREET_HREF);
166
+ var btn = $('cityMode');
167
+ if (btn) btn.addEventListener('click', toggleTheme);
168
+ if (media && media.addEventListener) media.addEventListener('change', function () { if (!storedTheme()) draw(applyTheme()); });
169
+ window.addEventListener('resize', fit);
170
+ fit();
171
+ draw(applyTheme());
172
+ return load();
173
+ }
174
+
175
+ window.GovCityPage = { boot: boot, draw: draw, load: load, fit: fit, FEEDS: FEEDS, THEME_KEY: THEME_KEY };
176
+ window.GovCityPage.ready = boot();
177
+ }());
@@ -0,0 +1,30 @@
1
+ {
2
+ "_": "THE GOVERNOR CITY's home (task 1004524, goal 1000125) through modules/ui-design/kit/render.js. The page draws GET /government/docket, GET /goals and GET /goals/rollups; against the kit's own stub none exists, so every state renders the city with no districts and no pins and the line saying the docket could not be read: a real state the floors must hold in. To look at the populated city, run it against the hall-preview harness, whose government__docket.json fixture carries the reference design's seven sample matters (3 need you, 2 moving, 2 settled) and whose goals fixtures carry the open goals:\n\n node scripts/hall-preview/server.js --port 4624 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/city.states.json --base-in http://localhost:4624 --base-out http://localhost:4624\n\nThe page follows the OS mode until the switch is pressed (then the hall's own 'hall-theme' preference), so the kit's dark/light emulation renders both modes.",
3
+ "page": "/builders/city",
4
+ "surface": "hall-ui",
5
+ "stub": { "prefix": "/builders/" },
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/", "fonts\\.g(oogleapis|static)\\.com"],
8
+ "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b403\\b", "\\b404\\b", "Unauthorized", "Forbidden", "Not Found", "load failed", "fonts\\.g(oogleapis|static)\\.com"],
9
+ "states": {
10
+ "city": {
11
+ "auth": true,
12
+ "_": "The home: the wordmark, the switch, the city, start governing and the one line.",
13
+ "actions": [["wait", 900]],
14
+ "expect": { "visible": ["#cityStage", "#cityStart", "#cityLine", "#cityMode"], "href": [["#cityStart", "/builders/street"]] }
15
+ },
16
+ "switched": {
17
+ "auth": true,
18
+ "_": "The sun/moon switch pressed once: the other mode, the city redrawn in it.",
19
+ "actions": [["wait", 600], ["click", "#cityMode"], ["wait", 400]],
20
+ "expect": { "visible": ["#cityStage", "#cityStart"] }
21
+ },
22
+ "focus": {
23
+ "auth": true,
24
+ "_": "Keyboard: the start button focused, its ring drawn.",
25
+ "actions": [["wait", 600], ["focus", "#cityStart"], ["wait", 100]],
26
+ "expect": { "activeId": "cityStart" }
27
+ }
28
+ },
29
+ "reducedMotion": { "auth": true, "actions": [["wait", 600]] }
30
+ }
@@ -259,8 +259,10 @@
259
259
  // decides what dead means and the route attaches it — so this page and any
260
260
  // future alert cannot drift into two definitions of the same word. These are
261
261
  // only the order to pick the runner to describe in, and the colour of its dot.
262
- const HEALTH_RANK = { alive: 0, late: 1, restarting: 2, dead: 3, never_seen: 4 };
263
- const HEALTH_DOT = { alive: 'ok', late: 'ok', restarting: 'info', dead: 'danger', never_seen: 'info' };
262
+ // idle_config (task 1004539): talking, but idle on a setting the owner can fix —
263
+ // it carries the fix as `remedy`, and draws as a warning, never as "ok".
264
+ const HEALTH_RANK = { alive: 0, late: 1, idle_config: 2, restarting: 3, dead: 4, never_seen: 5 };
265
+ const HEALTH_DOT = { alive: 'ok', late: 'ok', idle_config: 'warn', restarting: 'info', dead: 'danger', never_seen: 'info' };
264
266
 
265
267
  // The panel is drawn from both reads at once; each is kept so a write that only
266
268
  // reloads one of them redraws with the other's last answer.
@@ -284,7 +286,7 @@
284
286
  // `project`, and then there is no second layer to check.
285
287
  function readiness(runner, fence, mine) {
286
288
  const h = (runner && runner.health) || {};
287
- const connected = ['alive', 'late', 'restarting'].includes(h.state);
289
+ const connected = ['alive', 'late', 'restarting', 'idle_config'].includes(h.state);
288
290
  const goals = (fence && fence.goals) || [];
289
291
  const items = [];
290
292
  if (!runner) {
@@ -316,6 +318,11 @@
316
318
  if (mine && fence && fence.enabled && mine.enabled === false && (mine.goals || []).length) {
317
319
  items.push({ key: 'mine-stop', state: 'todo', text: 'Your runner is held', hint: mine.paused_reason || 'It is not allowed to take work right now.' });
318
320
  }
321
+ // The runner's own word that a setting is holding it (task 1004539) — the one
322
+ // the fence read cannot see: an unreadable fence or a gauge with no reading.
323
+ if (h.state === 'idle_config') {
324
+ items.push({ key: 'config-idle', state: 'todo', text: h.why ? `Idle because ${h.why}` : 'Idle because of a setting', hint: h.remedy || '' });
325
+ }
319
326
  return { items, ready: !items.some((i) => i.state === 'todo') };
320
327
  }
321
328
 
@@ -535,7 +542,7 @@
535
542
  : 'has picked no goals, so their runner takes no work';
536
543
  const beats = (b.runners || []).map((r) => {
537
544
  const h = r.health || {};
538
- const label = { dead: 'not checking in', late: 'quiet', alive: 'checking in', restarting: 'restarting' }[h.state] || 'unknown';
545
+ const label = { dead: 'not checking in', late: 'quiet', alive: 'checking in', restarting: 'restarting', idle_config: `idle on a setting — ${h.remedy || 'see Settings'}` }[h.state] || 'unknown';
539
546
  return `<span class="ov-fact"><span class="rn-dot rn-dot--sm rn-dot--${HEALTH_DOT[h.state] || 'info'}" aria-hidden="true"></span> ${escapeHtml(h.host || r.host || '')} ${escapeHtml(label)}</span>`;
540
547
  }).join('');
541
548
  const paused = b.owner_paused === true;
@@ -97,8 +97,8 @@
97
97
  + '<label class="genesis__plan-len"><span class="sr-only">Length of ' + esc(s.label) + '</span>'
98
98
  + '<select data-days="' + esc(s.key) + '">' + opts + '</select></label></li>';
99
99
  }).join('');
100
- return '<p class="scroll__lede">Settle the plan, then genesis files each stage as real tasks on the board. '
101
- + 'Every choice below is already set to its recommended default, so on your own one click is enough.</p>'
100
+ return '<p class="scroll__lede">Settle the plan and each stage becomes real tasks. '
101
+ + 'Everything is preset to our recommendation, so one click is enough.</p>'
102
102
  + '<ol class="genesis__plan">' + rows + '</ol>'
103
103
  + '<div class="genesis__picks">'
104
104
  + governmentHtml(gov)
@@ -108,9 +108,9 @@
108
108
  + '</fieldset>'
109
109
  + '<fieldset class="genesis__pick"><legend>Advisors</legend>'
110
110
  + radio('advisors', 'none', 'None for now', 'You can add advisors at any stage.', true)
111
- + radio('advisors', 'cbaas', 'CBaaS advisors', 'Ask for Cloud Bongos consultants to review each stage.', false)
111
+ + radio('advisors', 'cbaas', 'Cloud Bongos advisors', 'Our consultants review each stage.', false)
112
112
  + '</fieldset>'
113
- + '<label class="genesis__check"><input type="checkbox" data-existing> This project already exists, so start by gathering what it has</label>'
113
+ + '<label class="genesis__check"><input type="checkbox" data-existing> It already exists, so gather what it has first</label>'
114
114
  + '</div>'
115
115
  + '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-plan>Use this plan</button></div>';
116
116
  }
@@ -208,7 +208,7 @@
208
208
  if (g.can_close) {
209
209
  return '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-close="' + esc(s.key) + '">Close this stage</button></div>';
210
210
  }
211
- return '<p class="genesis__note">When every task above is finished, this stage can be put to the board.</p>';
211
+ return '<p class="genesis__note">Finish every task above, then put this stage to the board.</p>';
212
212
  }
213
213
 
214
214
  function aliveHtml() {
@@ -217,8 +217,7 @@
217
217
  + '<div class="genesis__alive-plates">' + ['r5-life-1-primordial-sea', 'r5-life-2-first-cells', 'r5-life-3-genome', 'r5-life-4-membrane', 'r5-life-5-nervous-system']
218
218
  .map(function (p) { return plateHtml(p, 'gplate--sm'); }).join('') + '</div>'
219
219
  + '<h2 id="genesis-h" class="genesis__alive-h">' + esc(name) + ' is alive</h2>'
220
- + '<p class="scroll__lede">Genesis is complete. Every room is open now: Community, the Builders list and the rest of the hall. '
221
- + 'The founding builders keep their mark for good.</p>'
220
+ + '<p class="scroll__lede">Every room in the hall is open now. The founding builders keep their mark for good.</p>'
222
221
  + '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-enter>Enter the builders’ hall</button></div></div>';
223
222
  }
224
223
 
@@ -234,9 +233,9 @@
234
233
  var d = g.demo;
235
234
  if (!d) return '';
236
235
  var total = g.stages.reduce(function (n, s) { return n + (s.minutes || 0); }, 0);
237
- return '<p class="scroll__lede">A demo’s genesis is short: about ' + esc(String(total)) + ' minutes, then the rest of your time is for building. '
238
- + 'The plan is already set from your demo: Monarchy / BDFL, ' + esc(String(d.people)) + (d.people === 1 ? ' person, ' : ' people, ')
239
- + esc(String(d.hours_each)) + (d.hours_each === 1 ? ' hour' : ' hours') + ' each. Finish each stage’s task, then approve it to move on.</p>';
236
+ return '<p class="scroll__lede">Your demo’s genesis takes about ' + esc(String(total)) + ' min. '
237
+ + 'The plan is set: Monarchy / BDFL, ' + esc(String(d.people)) + (d.people === 1 ? ' person, ' : ' people, ')
238
+ + esc(String(d.hours_each)) + (d.hours_each === 1 ? ' hour' : ' hours') + ' each. Finish each stage’s task, then approve it.</p>';
240
239
  }
241
240
 
242
241
  function render(host, state) {
@@ -276,3 +276,7 @@
276
276
  @media (max-width: 640px) {
277
277
  .pg-ba { grid-template-columns: minmax(0, 1fr); }
278
278
  }
279
+
280
+ /* The cross-project section's second band (task 1004440): each craft's own
281
+ numbers under the totals, the same glass, held apart by the scroll's own gap. */
282
+ #xp-scroll .stat-row--craft { margin-top: 12px; }
@@ -331,6 +331,29 @@
331
331
  return `<tr><td>${nameCell}</td><td>${held}</td></tr>`;
332
332
  }
333
333
 
334
+ // Each craft's own numbers across every project (task 1004440 / WA6.RP10): an
335
+ // artist's approved pages and an ideator's thoughts, so neither reads on the hub
336
+ // as an engineer with nothing shipped. The hub sends a craft only once the
337
+ // builder has earned credits in it, so an absent craft has no tile. A count no
338
+ // project could report is a dash, never 0 ("could not ask" is not "nothing
339
+ // there"). The words are the role-shaped profile's own (profile-roles.js).
340
+ function xpCount(n) {
341
+ return Number.isInteger(n) && n >= 0 ? fmtNum(n) : '—';
342
+ }
343
+ function xpCraftTiles(crafts) {
344
+ if (!crafts || typeof crafts !== 'object') return '';
345
+ const out = [];
346
+ const artist = crafts.artist;
347
+ if (artist && typeof artist === 'object') out.push(tile('Pages approved', xpCount(artist.pages_approved), 'as an artist'));
348
+ const ideator = crafts.ideator;
349
+ if (ideator && typeof ideator === 'object') {
350
+ out.push(tile('Thoughts filed', xpCount(ideator.filed), 'as an ideator'));
351
+ const ratified = Number.isInteger(ideator.ratified) ? `${fmtNum(ideator.ratified)} ratified by the board` : '';
352
+ out.push(tile('Full Ideas', xpCount(ideator.full_ideas), ratified));
353
+ }
354
+ return out.length ? `<div class="stat-row stat-row--craft">${out.join('')}</div>` : '';
355
+ }
356
+
334
357
  function renderCrossProject(data, selfId) {
335
358
  const section = $('#xp-scroll');
336
359
  const body = $('#xp-body');
@@ -357,7 +380,8 @@
357
380
  tile('Works shipped', fmtNum(t.works_shipped)) +
358
381
  tile('Credits', fmtNum(t.credits)) +
359
382
  tile('Karma', fmtNum(t.karma)) +
360
- `</div>`;
383
+ `</div>` +
384
+ xpCraftTiles(cp.crafts);
361
385
  const discs = Array.isArray(cp.disciplines) ? cp.disciplines.filter((d) => typeof d === 'string') : [];
362
386
  const metaBits = [];
363
387
  if (discs.length) metaBits.push(`${window.OTB.roleAxisLabel()}s: ${discs.map((d) => window.OTB.roleLabel(d)).join(', ')}`);
@@ -38,6 +38,14 @@
38
38
  const project = fence.project || {};
39
39
  const scope = fence.scope || {};
40
40
  const out = [];
41
+ // First, because it is the one that has already cost time: a runner of yours
42
+ // that has been idle on a setting past its grace (task 1004539). The verdict
43
+ // and the fix are the server's (runner-health.js); this only draws them.
44
+ for (const r of myRunners) {
45
+ const h = r.health || {};
46
+ if (h.state !== 'idle_config') continue;
47
+ out.push(`<strong>Your runner${h.host ? ` on ${escapeHtml(h.host)}` : ''} is idle</strong> because ${escapeHtml(h.why || 'of a setting')}. ${escapeHtml(h.remedy || '')}`);
48
+ }
41
49
  if (project.enabled !== true) {
42
50
  out.push(`The project's runner switch is off right now${project.paused_reason ? `: ${escapeHtml(project.paused_reason)}` : ''}. Your choices are kept and apply when it is back on.`);
43
51
  }
@@ -62,6 +70,16 @@
62
70
  }
63
71
  const goalLabel = (id) => goalNames.get(Number(id)) || `Goal ${id}`;
64
72
 
73
+ // The caller's own runners, with the server's health verdict attached. Best-
74
+ // effort like the names: a failed read just means no runner warning is drawn.
75
+ let myRunners = [];
76
+ async function loadMyRunners() {
77
+ try {
78
+ const res = await api.request('GET', `${API}/autonomy/runners`);
79
+ if (res.ok && res.data && Array.isArray(res.data.runners)) myRunners = res.data.runners;
80
+ } catch (_) { /* the picker works without it */ }
81
+ }
82
+
65
83
  function render(fence) {
66
84
  const body = $('#autobongos-body');
67
85
  if (!body) return;
@@ -138,7 +156,7 @@
138
156
  const section = $('#autobongos-scroll');
139
157
  if (!section) return;
140
158
  try {
141
- const [fence] = await Promise.all([load(), loadGoalNames()]);
159
+ const [fence] = await Promise.all([load(), loadGoalNames(), loadMyRunners()]);
142
160
  if (!fence) return;
143
161
  render(fence);
144
162
  section.hidden = false;
@@ -327,3 +327,29 @@ select.voices__select { min-width: 140px; }
327
327
  text-underline-offset: 3px;
328
328
  }
329
329
  .su-entries a:hover { color: var(--ink); text-decoration-color: currentColor; }
330
+
331
+ /* ---- MAIN ROLE (task 1004541) -------------------------------------------- */
332
+ /* A sub-group of Preferred roles: the shell's chip control with a RADIO in it.
333
+ The 24px target and 14px drawn mark are panel.css's; only the shape changes
334
+ (a ring, and a dot when chosen), so a choice of one never reads as a tick. */
335
+ .craft-main { border: 0; margin: var(--sp-sm) 0 0; padding: 0; min-width: 0; }
336
+ .craft-main__h {
337
+ padding: 0;
338
+ font-family: var(--label-face);
339
+ font-size: var(--label-size);
340
+ letter-spacing: var(--label-space);
341
+ text-transform: var(--label-case);
342
+ color: var(--ink-faint);
343
+ }
344
+ .craft-main__lede { margin: 6px 0 0; font-size: 13.5px; color: var(--ink-soft); max-width: 62ch; }
345
+ .craft-main .chip-ctl input::before { border-radius: 50%; }
346
+ .craft-main .chip-ctl input:checked::after {
347
+ left: 9px;
348
+ top: 9px;
349
+ width: 6px;
350
+ height: 6px;
351
+ border: 0;
352
+ border-radius: 50%;
353
+ background: var(--accent-on);
354
+ transform: none;
355
+ }
@@ -118,6 +118,14 @@
118
118
  choices — pick one, several, or none.
119
119
  </p>
120
120
  <div class="craft-chips" id="craft-chips"></div>
121
+ <!-- MAIN ROLE (task 1004541): which of the roles above leads your
122
+ profile, its tabs and your leaderboard entry. Saved with the
123
+ chips, in the same PATCH (main_discipline). -->
124
+ <fieldset class="craft-main" id="craft-main">
125
+ <legend class="craft-main__h">Main role</legend>
126
+ <p class="craft-main__lede">Your main role leads your profile and your leaderboard entry. Let your work decide, and it is the role you have earned the most in.</p>
127
+ <div class="chips" id="craft-main-opts"></div>
128
+ </fieldset>
121
129
  <div class="craft-actions">
122
130
  <button type="button" class="craft-save" id="craft-save">Save</button>
123
131
  <span class="settings-status" id="craft-status" role="status" aria-live="polite"></span>
@@ -516,7 +516,10 @@
516
516
 
517
517
  // Current selection mutated in place as the builder toggles chips; saved on
518
518
  // the "Save" button (the hall's profile card behaviour) via PATCH.
519
- const craft = { selected: new Set() };
519
+ // `main` (task 1004541) is the builder's chosen MAIN role, one of the ticked
520
+ // roles, or null for "let my work decide" (the profile then leads with the
521
+ // craft earned most in — src/bongos/role-stats.js resolveMainRole).
522
+ const craft = { selected: new Set(), main: null };
520
523
 
521
524
  function renderCraft() {
522
525
  const wrap = $('#craft-chips');
@@ -529,6 +532,29 @@
529
532
  `${escapeHtml(window.OTB.roleLabel(d))}</label>`
530
533
  );
531
534
  }).join('');
535
+ renderCraftMain();
536
+ }
537
+
538
+ // The main-role choice: "Let my work decide", then each role ticked above. A
539
+ // role can only lead once it is one of your roles (the server refuses
540
+ // otherwise), so unticking the main role hands the choice back to your work —
541
+ // the same thing the server does when that role leaves the set.
542
+ const MAIN_BY_WORK = 'Let my work decide';
543
+ function renderCraftMain() {
544
+ const wrap = $('#craft-main-opts');
545
+ if (!wrap) return;
546
+ if (craft.main && !craft.selected.has(craft.main)) craft.main = null;
547
+ const opts = [{ value: '', label: MAIN_BY_WORK }].concat(
548
+ DISCIPLINES.filter((d) => craft.selected.has(d)).map((d) => ({ value: d, label: window.OTB.roleLabel(d) })),
549
+ );
550
+ wrap.innerHTML = opts.map((o) => {
551
+ const on = (craft.main || '') === o.value;
552
+ return (
553
+ `<label class="chip-ctl craft-main__opt">` +
554
+ `<input type="radio" name="craft-main" class="craft-main-pick" value="${escapeHtml(o.value)}"${on ? ' checked' : ''}>` +
555
+ `${escapeHtml(o.label)}</label>`
556
+ );
557
+ }).join('');
532
558
  }
533
559
 
534
560
  async function saveCraft() {
@@ -538,7 +564,10 @@
538
564
  if (btn) { btn.disabled = true; btn.textContent = 'Saving…'; }
539
565
  if (status) status.textContent = '';
540
566
  try {
541
- const res = await api.request('PATCH', `${API}/me/disciplines`, { body: { preferred_disciplines: selected } });
567
+ const main = craft.main && selected.includes(craft.main) ? craft.main : null;
568
+ const res = await api.request('PATCH', `${API}/me/disciplines`, {
569
+ body: { preferred_disciplines: selected, main_discipline: main },
570
+ });
542
571
  if (res.status === 401) { toAuth(); return; }
543
572
  if (!res.ok) throw new Error(`disciplines PATCH → ${res.status}`);
544
573
  const data = res.data;
@@ -547,8 +576,10 @@
547
576
  ? data.builder.preferred_disciplines
548
577
  : selected;
549
578
  craft.selected = new Set(prefs.filter((d) => DISCIPLINES.includes(d)));
579
+ craft.main = data?.builder && 'main_discipline' in data.builder ? (data.builder.main_discipline || null) : main;
550
580
  renderCraft();
551
- const msg = selected.length ? `Preferred work: ${selected.join(', ')}.` : 'Preference cleared.';
581
+ const leads = craft.main ? ` Main role: ${window.OTB.roleLabel(craft.main)}.` : '';
582
+ const msg = (selected.length ? `Preferred work: ${selected.join(', ')}.` : 'Preference cleared.') + leads;
552
583
  if (status) status.textContent = msg;
553
584
  toast(msg);
554
585
  } catch (err) {
@@ -570,6 +601,14 @@
570
601
  else craft.selected.delete(box.value);
571
602
  const chip = box.closest('.craft-chip');
572
603
  if (chip) chip.dataset.on = box.checked ? '1' : '0';
604
+ renderCraftMain();
605
+ });
606
+ }
607
+ const mainWrap = $('#craft-main-opts');
608
+ if (mainWrap) {
609
+ mainWrap.addEventListener('change', (e) => {
610
+ const pick = e.target.closest('.craft-main-pick');
611
+ if (pick) craft.main = pick.value || null;
573
612
  });
574
613
  }
575
614
  const btn = $('#craft-save');
@@ -1176,6 +1215,7 @@
1176
1215
  // Seed craft state from the /me payload, then render + reveal that section.
1177
1216
  const prefs = Array.isArray(builder.preferred_disciplines) ? builder.preferred_disciplines : [];
1178
1217
  craft.selected = new Set(prefs.filter((d) => DISCIPLINES.includes(d)));
1218
+ craft.main = typeof builder.main_discipline === 'string' ? builder.main_discipline : null;
1179
1219
  renderCraft();
1180
1220
  wireCraft();
1181
1221
  $('#craft-scroll').hidden = false;
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ // modules/ideas/governor-docket.js — the ideas module's part of the governor's
4
+ // docket (task 1004523, goal 1000125; collector: modules/government/docket.js,
5
+ // decision: ADR 0362).
6
+ //
7
+ // WHY. Two things the Governor City shows live here. Open BLOCKERS stand at
8
+ // Planning ("waiting on a decision"). And the "rules at work" reading — matters
9
+ // settled by a rule, without a vote — is the idea ROUTING rule (routing.js, the
10
+ // landing matrix): a filing that has a home lands in its goal at filing time
11
+ // instead of waiting for a triage vote, and each one leaves an
12
+ // `ideas_task_lineage` row with origin 'route'. That count is the honest answer
13
+ // to "what did the rules settle this month"; the design's other standing rules
14
+ // do not exist yet and are not counted.
15
+ //
16
+ // A module may not require a sibling (ADR 0083), so this is contributed to the
17
+ // `governor.docket` point from the blockers route factory rather than reached
18
+ // for by government. Pure mappers exported for tests/governor_docket.mjs.
19
+
20
+ const api = require('../../src/module-api');
21
+ const blockers = require('./blockers');
22
+
23
+ const POINT = 'governor.docket';
24
+ const DOCKET_BLOCKER_LIMIT = 50;
25
+
26
+ // One open blocker for one viewer. The list is public (GET /blockers), so every
27
+ // governor sees it; only a viewer who may resolve it (`blocker.review`, the
28
+ // resolve route's atom) is told it needs them — for anyone else it is moving.
29
+ function blockerDocketMatter(b, mayResolve) {
30
+ const holds = Array.isArray(b.blocking_task_ids) ? b.blocking_task_ids.length : 0;
31
+ return {
32
+ id: b.id,
33
+ rule: 'decide',
34
+ state: mayResolve ? 'needs' : 'moving',
35
+ title: b.title,
36
+ short: 'blocker',
37
+ where: holds ? `planning · holds ${holds} ${holds === 1 ? 'task' : 'tasks'}` : 'planning · waiting on a decision',
38
+ opened_at: b.created_at,
39
+ deadline: null,
40
+ proposer: null,
41
+ tally: null,
42
+ href: `/blockers#blocker-${b.id}`,
43
+ ref: { type: 'blocker', id: b.id },
44
+ };
45
+ }
46
+
47
+ // The rules-at-work reading from this month's routed filings. Pure.
48
+ function rulesReading(settledThisMonth) {
49
+ const n = Number(settledThisMonth) || 0;
50
+ return {
51
+ value: { rules: 1, settled_this_month: n },
52
+ figure: '1 rule',
53
+ line: n
54
+ ? `the landing rule settled ${n} ${n === 1 ? 'filing' : 'filings'} this month without a vote`
55
+ : 'the landing rule settled nothing this month',
56
+ hot: false,
57
+ };
58
+ }
59
+
60
+ // Filings the routing rule landed this month (origin 'route').
61
+ async function countRoutedThisMonth() {
62
+ const { rows } = await api.pool.query(
63
+ `SELECT count(*)::int AS n FROM ideas_task_lineage
64
+ WHERE origin = 'route' AND created_at >= date_trunc('month', now())`,
65
+ );
66
+ return rows[0] ? rows[0].n : 0;
67
+ }
68
+
69
+ function contributeIdeasToDocket() {
70
+ api.contribute(POINT, {
71
+ source: 'blockers',
72
+ building: 'planning',
73
+ gate: { anyBuilder: true },
74
+ read: async (ctx) => {
75
+ const rows = await blockers.listOpenBlockers({ limit: DOCKET_BLOCKER_LIMIT, offset: 0 });
76
+ const mayResolve = ctx.has('blocker.review');
77
+ return rows.map((b) => blockerDocketMatter(b, mayResolve));
78
+ },
79
+ });
80
+ // A count, no rows: any builder may read it.
81
+ api.contribute(POINT, {
82
+ reading: 'rules',
83
+ gate: { anyBuilder: true },
84
+ read: async () => rulesReading(await countRoutedThisMonth()),
85
+ });
86
+ }
87
+
88
+ module.exports = {
89
+ contributeIdeasToDocket,
90
+ blockerDocketMatter,
91
+ rulesReading,
92
+ countRoutedThisMonth,
93
+ };
@@ -27,7 +27,12 @@ const api = require('../../../src/module-api');
27
27
  const blockers = require('../blockers');
28
28
  const { validateOrRespond, LIMITS, parseId } = api;
29
29
 
30
+ // The governor's docket (task 1004523): open blockers + the rules-at-work
31
+ // reading, contributed from this factory (see ../governor-docket.js).
32
+ const governorDocket = require('../governor-docket');
33
+
30
34
  module.exports = function buildBlockersRouter() {
35
+ governorDocket.contributeIdeasToDocket();
31
36
  const router = express.Router();
32
37
 
33
38
  // List currently-open blockers + the tasks each is blocking.