@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.
- package/.bongos-core.json +233 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +37 -2
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +541 -658
- package/docs/copy-registry.json +1205 -2340
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +47 -28
- package/docs/page-readings.json +1128 -1339
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +27 -0
- package/modules/autonomy/runner-health.js +22 -1
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/city-draw.js +438 -0
- package/modules/hall-ui/public/city.css +110 -0
- package/modules/hall-ui/public/city.html +71 -0
- package/modules/hall-ui/public/city.js +177 -0
- package/modules/hall-ui/public/city.states.json +30 -0
- package/modules/hall-ui/public/gate.js +11 -4
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/hall-ui/public/profile.css +4 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/hall-ui/public/settings-autobongos.js +19 -1
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +8 -0
- package/modules/hall-ui/public/settings.js +43 -3
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/platform-identity/craft-rollup.js +72 -0
- package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
- package/modules/platform-identity/platform-identity.js +12 -6
- package/modules/platform-identity/routes/sso.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/core-upgrade.js +34 -7
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +2 -0
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +24 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/gds/autobongos-grade-cap.js +187 -0
- package/scripts/gds/autobongos-loop.js +4 -0
- package/scripts/gds/autobongos-run.js +154 -13
- package/scripts/gds/autobongos-verify.js +211 -14
- package/scripts/gds/provision-core-upgrade.js +14 -2
- package/scripts/gds/provision-repo.js +77 -13
- package/scripts/gds/ship-flow.js +5 -0
- package/scripts/gds/ship-preflight-steps.js +2 -1
- package/scripts/gds/ship.js +10 -0
- package/scripts/gds/update-sweep.js +24 -8
- package/scripts/gds/upgrade-outcome.js +1 -0
- package/src/bongos/auth-admission.js +95 -13
- package/src/bongos/db-kernel.js +1 -1
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +68 -2
- package/src/bongos/routes/modules.js +17 -0
- package/src/bongos/serve-internal.js +14 -0
- package/src/bongos/software-update.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_order_db.mjs +3 -1
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/autobongos_grade_cap.mjs +125 -0
- package/tests/autobongos_loop.mjs +230 -1
- package/tests/autobongos_verify.mjs +239 -3
- package/tests/autonomy_config_idle.mjs +272 -0
- package/tests/core_update_banner.mjs +29 -0
- package/tests/core_upgrade_door.mjs +31 -1
- package/tests/core_upgrade_runner.mjs +20 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/hall_audit.mjs +10 -1
- package/tests/hall_city.mjs +369 -0
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hub_craft_rollup.mjs +350 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/provision.mjs +78 -0
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/settings_main_role.mjs +190 -0
- package/tests/software_update.mjs +56 -1
- package/tests/update_subscription_engine.mjs +55 -3
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +428 -277
- 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
|
-
|
|
263
|
-
|
|
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
|
|
101
|
-
+ '
|
|
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', '
|
|
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>
|
|
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">
|
|
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">
|
|
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">
|
|
238
|
-
+ 'The plan is
|
|
239
|
-
+ esc(String(d.hours_each)) + (d.hours_each === 1 ? ' hour' : ' hours') + ' each. Finish each stage’s task, then approve it
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|