@bongos/core 1.21.34 → 1.21.35
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 +35 -35
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +308 -249
- package/modules/autonomy/governor-docket.js +68 -1
- package/modules/government/docket-floors.js +20 -0
- package/modules/government/docket-vocab.js +14 -0
- package/modules/government/docket.js +103 -2
- package/modules/government/routes/government.js +13 -0
- package/modules/hall-ui/public/street-card.js +97 -1
- package/modules/hall-ui/public/street-desk.js +108 -7
- package/modules/hall-ui/public/street-inside.js +6 -6
- package/modules/hall-ui/public/street.css +1 -0
- package/modules/hall-ui/public/street.js +24 -5
- package/modules/hall-ui/public/street.states.json +231 -6
- package/modules/lifecycle/governor-docket.js +24 -2
- package/modules/lifecycle/routes/project-settings.js +24 -0
- package/modules/onboarding/routes/access-requests.js +7 -0
- package/modules/provisioning/core-upgrade.js +1 -1
- package/modules/provisioning/routes/core-upgrade.js +36 -1
- package/modules/security/routes/reports.js +23 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/routes/backup.js +26 -0
- package/src/bongos/routes/builders.js +40 -0
- package/src/bongos/routes/modules.js +18 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +28 -1
- package/tests/hall_matter_card.mjs +11 -2
- package/tests/hall_street_desk.mjs +229 -0
|
@@ -52,9 +52,63 @@ function fenceDocketMatters(project, mayManage) {
|
|
|
52
52
|
tally: null,
|
|
53
53
|
href: '/watch',
|
|
54
54
|
ref: { type: 'autonomy_fence', id: 'project' },
|
|
55
|
+
// The floor desk's acts (task 1004559), only for the switch's own atom.
|
|
56
|
+
...(mayManage ? { acts: project.enabled === false ? [{ key: 'switch_on' }] : [{ key: 'allow', pick: 'open_goals', except: [] }, { key: 'switch_off', options: OFF_REASONS }] } : {}),
|
|
55
57
|
}];
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
// Why the switch goes off, as one tap (POST /autonomy/fence takes a reason).
|
|
61
|
+
const OFF_REASONS = Object.freeze([
|
|
62
|
+
{ key: 'pause', label: 'pause for now' },
|
|
63
|
+
{ key: 'cost', label: 'spending too much' },
|
|
64
|
+
{ key: 'quality', label: 'builds look wrong' },
|
|
65
|
+
]);
|
|
66
|
+
// Why one runner is paused, as one tap (POST /autonomy/builders/:id/pause).
|
|
67
|
+
const PAUSE_REASONS = Object.freeze([
|
|
68
|
+
{ key: 'pause', label: 'pause for now' },
|
|
69
|
+
{ key: 'cost', label: 'spending too much' },
|
|
70
|
+
{ key: 'stuck', label: 'it is stuck' },
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
// The Automation desks (task 1004559): rows the holder of autonomy.fence.manage
|
|
74
|
+
// acts on in place. Pure; exported for tests.
|
|
75
|
+
// The switch when it is on and has goals (otherwise the fence matter carries it).
|
|
76
|
+
function switchDeskRows(project) {
|
|
77
|
+
const goals = Array.isArray(project && project.goals) ? project.goals : [];
|
|
78
|
+
if (!project || project.enabled === false || !goals.length) return [];
|
|
79
|
+
return [{ id: 'switch', name: 'The switch', fact: `on · ${goals.length} ${goals.length === 1 ? 'goal' : 'goals'}`, facts: ['unattended building is on'], opened_at: project.updated_at || null, ref: { type: 'autonomy_fence', id: 'project' }, acts: [{ key: 'switch_off', options: OFF_REASONS }] }];
|
|
80
|
+
}
|
|
81
|
+
// One row per allowed goal (first in line, or not allowed any more), and one to
|
|
82
|
+
// allow another of the open goals the screen already reads.
|
|
83
|
+
function goalDeskRows(project) {
|
|
84
|
+
const goals = Array.isArray(project && project.goals) ? project.goals : [];
|
|
85
|
+
const first = project && project.priority_goal_id != null ? Number(project.priority_goal_id) : null;
|
|
86
|
+
const rows = goals.map((g) => {
|
|
87
|
+
const isFirst = first === Number(g.goal_id);
|
|
88
|
+
return {
|
|
89
|
+
id: g.goal_id, name: `goal ${g.goal_id}`, fact: isFirst ? 'first in line' : g.added_at ? `allowed ${dayWord(g.added_at)}` : 'allowed',
|
|
90
|
+
facts: isFirst ? ['the runner works it first'] : [], opened_at: g.added_at || null,
|
|
91
|
+
ref: { type: 'autonomy_goal', id: g.goal_id },
|
|
92
|
+
acts: isFirst ? [{ key: 'disallow' }] : [{ key: 'first' }, { key: 'disallow' }],
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
rows.push({ id: 'add', tool: true, name: 'Allow a goal', fact: goals.length ? `${goals.length} allowed now` : 'none allowed yet', ref: { type: 'autonomy_goal', id: 'add' }, acts: [{ key: 'allow', pick: 'open_goals', except: goals.map((g) => g.goal_id) }] });
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
// One row per builder with a runner (or a pause on record): pause or resume it.
|
|
99
|
+
function runnerDeskRows(scopes, nowMs, describe) {
|
|
100
|
+
return (scopes || []).filter((b) => (b.runners || []).length || b.owner_paused).map((b) => {
|
|
101
|
+
const live = (b.runners || []).map((r) => ({ r, h: describe(r, { nowMs }) })).find((x) => x.h && x.h.ok && x.h.state !== 'never_seen');
|
|
102
|
+
const fact = b.owner_paused ? 'paused' : live && live.r.working_task_id ? `on task ${live.r.working_task_id}` : live ? 'idle' : 'not seen lately';
|
|
103
|
+
return {
|
|
104
|
+
id: b.builder_id, name: b.display_name || b.github_login || `builder ${b.builder_id}`, fact,
|
|
105
|
+
facts: b.owner_paused && b.owner_paused_reason ? [b.owner_paused_reason] : [],
|
|
106
|
+
ref: { type: 'autonomy_runner', id: b.builder_id },
|
|
107
|
+
acts: b.owner_paused ? [{ key: 'resume' }] : [{ key: 'pause', options: PAUSE_REASONS }],
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
58
112
|
// Money left under the unattended-spend ceiling. Pure. Hot when under a fifth left.
|
|
59
113
|
function moneyReading({ ceilingUsd, spentUsd }) {
|
|
60
114
|
const ceiling = Number(ceilingUsd) || 0;
|
|
@@ -147,6 +201,8 @@ function fenceHistory(project) {
|
|
|
147
201
|
const shared = (once, key, fn) => (typeof once === 'function' ? once(key, fn) : fn());
|
|
148
202
|
const spendFor = (reward, once) => shared(once, 'autonomy.spendMtd', () => reward.autonomousApiSpendMtd());
|
|
149
203
|
const fenceFor = (once) => shared(once, 'autonomy.fence', () => db.readFence());
|
|
204
|
+
// The Running now floor and the runners desk read the same rows once per request.
|
|
205
|
+
const runnersFor = (once) => shared(once, 'autonomy.runners', () => db.readAllRunnerScopes());
|
|
150
206
|
|
|
151
207
|
function contributeAutonomyFloors() {
|
|
152
208
|
api.contribute(POINT, {
|
|
@@ -156,7 +212,7 @@ function contributeAutonomyFloors() {
|
|
|
156
212
|
// GET /autonomy/runners/all is the owner's read of every runner.
|
|
157
213
|
api.contribute(POINT, {
|
|
158
214
|
floor: 'automation.running', gate: { permission: 'autonomy.fence.manage' },
|
|
159
|
-
read: async (_ctx, { now } = {}) => runningFloor(await
|
|
215
|
+
read: async (_ctx, { now, once } = {}) => runningFloor(await runnersFor(once), now || Date.now(), runnerHealth.describeRunner),
|
|
160
216
|
});
|
|
161
217
|
api.contribute(POINT, {
|
|
162
218
|
floor: 'automation.spending_limit', gate: { permission: 'autonomy.precheck' },
|
|
@@ -176,8 +232,16 @@ function contributeAutonomyFloors() {
|
|
|
176
232
|
});
|
|
177
233
|
}
|
|
178
234
|
|
|
235
|
+
function contributeAutonomyDesks() {
|
|
236
|
+
const gate = { permission: 'autonomy.fence.manage' };
|
|
237
|
+
api.contribute(POINT, { desk: 'automation.switch', gate, read: async (_ctx, { once } = {}) => { const p = await fenceFor(once); if (!p) throw new Error('fence_uninitialised'); return switchDeskRows(p); } });
|
|
238
|
+
api.contribute(POINT, { desk: 'automation.goals', gate, read: async (_ctx, { once } = {}) => { const p = await fenceFor(once); if (!p) throw new Error('fence_uninitialised'); return goalDeskRows(p); } });
|
|
239
|
+
api.contribute(POINT, { desk: 'automation.runners', gate, read: async (_ctx, { now, once } = {}) => runnerDeskRows(await runnersFor(once), now || Date.now(), runnerHealth.describeRunner) });
|
|
240
|
+
}
|
|
241
|
+
|
|
179
242
|
function contributeAutonomyToDocket() {
|
|
180
243
|
contributeAutonomyFloors();
|
|
244
|
+
contributeAutonomyDesks();
|
|
181
245
|
// GET /autonomy/fence is any builder's read; the read itself fails on a missing
|
|
182
246
|
// fence row rather than fabricating "off" (that route's rule).
|
|
183
247
|
api.contribute(POINT, {
|
|
@@ -206,6 +270,9 @@ function contributeAutonomyToDocket() {
|
|
|
206
270
|
module.exports = {
|
|
207
271
|
contributeAutonomyToDocket,
|
|
208
272
|
fenceDocketMatters,
|
|
273
|
+
switchDeskRows,
|
|
274
|
+
goalDeskRows,
|
|
275
|
+
runnerDeskRows,
|
|
209
276
|
moneyReading,
|
|
210
277
|
allowedGoalsFloor,
|
|
211
278
|
runningFloor,
|
|
@@ -215,7 +215,27 @@ function governmentHistory(gov) {
|
|
|
215
215
|
return out;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// The People desk's rank rows (task 1004559): one row per custom rank, to give it
|
|
219
|
+
// (POST /government/assignments) or take it back (DELETE
|
|
220
|
+
// /government/assignments/:builderId/:rankKey), each a chip per person (at most
|
|
221
|
+
// five). Seeded ranks are tiers, set on the builder's own row. Pure.
|
|
222
|
+
function rankDeskRows(ranks, assignments) {
|
|
223
|
+
const people = new Map();
|
|
224
|
+
for (const a of assignments || []) if (!people.has(String(a.builder_id))) people.set(String(a.builder_id), a.display_name || a.github_login || `builder ${a.builder_id}`);
|
|
225
|
+
return (ranks || []).filter((r) => r && !r.is_system).map((r) => {
|
|
226
|
+
const holders = (assignments || []).filter((a) => a.rank_key === r.rank_key);
|
|
227
|
+
const held = new Set(holders.map((a) => String(a.builder_id)));
|
|
228
|
+
const give = [...people].filter(([id]) => !held.has(id)).slice(0, 5).map(([id, name]) => ({ key: id, label: name }));
|
|
229
|
+
const take = holders.slice(0, 5).map((a) => ({ key: String(a.builder_id), label: people.get(String(a.builder_id)) }));
|
|
230
|
+
const acts = [];
|
|
231
|
+
if (give.length) acts.push({ key: 'give', options: give });
|
|
232
|
+
if (take.length) acts.push({ key: 'take', options: take });
|
|
233
|
+
return { id: r.rank_key, name: r.label || 'unnamed rank', fact: `${holders.length} ${holders.length === 1 ? 'holds it' : 'hold it'}`, facts: r.description ? [r.description] : [], ref: { type: 'custom_rank', id: r.rank_key }, acts };
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
218
237
|
module.exports = {
|
|
238
|
+
rankDeskRows,
|
|
219
239
|
BUILDERS,
|
|
220
240
|
ideasFloor,
|
|
221
241
|
governmentHistory,
|
|
@@ -90,6 +90,19 @@ const FLOORS = Object.freeze([
|
|
|
90
90
|
['treasury.rewards', 'treasury'], ['treasury.grading', 'treasury'], ['treasury.hosting', 'treasury'],
|
|
91
91
|
].map(([key, building]) => Object.freeze({ key, building })));
|
|
92
92
|
|
|
93
|
+
// The floor desks' own rows (task 1004559): things on a floor a viewer may act on
|
|
94
|
+
// in place that are not matters waiting on anyone: a builder's tier, an allowed
|
|
95
|
+
// goal, a runner, a confirmed report waiting for its fix. Each is read by the
|
|
96
|
+
// module that owns its write route, under that route's atom; a desk whose module
|
|
97
|
+
// is off reports `off` and the floor shows its quiet line.
|
|
98
|
+
const DESKS = Object.freeze([
|
|
99
|
+
['people.builders', 'people'], ['people.ranks', 'people'],
|
|
100
|
+
['disputes.confirmed', 'disputes'],
|
|
101
|
+
['automation.switch', 'automation'], ['automation.goals', 'automation'], ['automation.runners', 'automation'],
|
|
102
|
+
['people.invite', 'people'], ['release.core', 'release'],
|
|
103
|
+
['rules.settings', 'rules'], ['rules.modules', 'rules'], ['treasury.backup', 'treasury'],
|
|
104
|
+
].map(([key, building]) => Object.freeze({ key, building })));
|
|
105
|
+
|
|
93
106
|
module.exports = {
|
|
94
107
|
BUILDINGS,
|
|
95
108
|
BUILDING_KEYS,
|
|
@@ -99,4 +112,5 @@ module.exports = {
|
|
|
99
112
|
STATES,
|
|
100
113
|
READINGS,
|
|
101
114
|
FLOORS,
|
|
115
|
+
DESKS,
|
|
102
116
|
};
|
|
@@ -35,6 +35,15 @@
|
|
|
35
35
|
// { history: 'releases', building: 'release', gate: …, // the record drawers
|
|
36
36
|
// read: async (ctx) => [{ id, title, at, outcome }, …] }
|
|
37
37
|
//
|
|
38
|
+
// { desk: 'people.builders', gate: …, // one of docket-vocab DESKS
|
|
39
|
+
// read: async (ctx) => [{ id, name, fact, facts, acts, ref, tool? }, …] }
|
|
40
|
+
//
|
|
41
|
+
// A DESK (task 1004559) is the rows a floor desk acts on in place that are not
|
|
42
|
+
// matters: a builder's tier, an allowed goal, a runner. A matter or a desk row
|
|
43
|
+
// may carry `acts`, the acts THIS viewer may take, decided by the owning module
|
|
44
|
+
// from ctx.has() with its write route's own atom (normalizeActs below). Desk
|
|
45
|
+
// rows never reach the street's pins or counts.
|
|
46
|
+
//
|
|
38
47
|
// A FLOOR (task 1004527) is a state rather than a queue — who sits, what the
|
|
39
48
|
// month cost, what went live — read by the module that owns it, under the same
|
|
40
49
|
// atom as its own route. HISTORY is what closed: rulings, amendments, releases,
|
|
@@ -117,6 +126,7 @@ const COMING_FLOORS = Object.freeze([
|
|
|
117
126
|
]);
|
|
118
127
|
const MAX_FLOOR_LINES = 6;
|
|
119
128
|
const MAX_HISTORY_PER_SOURCE = 60;
|
|
129
|
+
const MAX_DESK_ROWS = 40;
|
|
120
130
|
|
|
121
131
|
|
|
122
132
|
// A source read that has not answered by now is reported unavailable rather than
|
|
@@ -172,6 +182,74 @@ function normalizeMatter(raw, { source, building }) {
|
|
|
172
182
|
tally: tally(raw.tally),
|
|
173
183
|
href: raw.href ? String(raw.href) : null,
|
|
174
184
|
ref: raw.ref && typeof raw.ref === 'object' ? { type: String(raw.ref.type), id: String(raw.ref.id) } : null,
|
|
185
|
+
...actsField(raw.acts),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// WHAT A VIEWER MAY DO, FROM THE SERVER (task 1004559). A matter or a desk row may
|
|
190
|
+
// carry `acts`: the acts THIS viewer may take on it, decided by the owning module
|
|
191
|
+
// from ctx.has() with the same atom its own write route checks. The floor desk
|
|
192
|
+
// draws one button per act (at most two) and one-tap chips for an act's options;
|
|
193
|
+
// the route still decides (the docket only hides what would be refused). An act
|
|
194
|
+
// is { key, options?: [{ key, label }], pick?: 'open_goals', except?: [id] }:
|
|
195
|
+
// `pick` asks the screen to offer the open goals it already reads, minus
|
|
196
|
+
// `except`. Pure. Anything malformed is dropped, never guessed.
|
|
197
|
+
const MAX_ACTS = 2;
|
|
198
|
+
const MAX_OPTIONS = 5;
|
|
199
|
+
const ACT_KEY_RE = /^[a-z][a-z_]{1,23}$/;
|
|
200
|
+
const PICKS = Object.freeze(['open_goals']);
|
|
201
|
+
// The one thing a desk may type: a GitHub name to invite (it is a name, not a
|
|
202
|
+
// reason; reasons stay chips).
|
|
203
|
+
const FIELDS = Object.freeze(['github_login']);
|
|
204
|
+
function normalizeActs(raw) {
|
|
205
|
+
if (!Array.isArray(raw)) return [];
|
|
206
|
+
return raw.filter((a) => a && typeof a === 'object' && ACT_KEY_RE.test(String(a.key))).slice(0, MAX_ACTS).map((a) => {
|
|
207
|
+
const out = { key: String(a.key) };
|
|
208
|
+
const options = (Array.isArray(a.options) ? a.options : [])
|
|
209
|
+
.filter((o) => o && o.key != null && o.label != null && String(o.label).trim())
|
|
210
|
+
.slice(0, MAX_OPTIONS)
|
|
211
|
+
.map((o) => ({ key: String(o.key).slice(0, 64), label: String(o.label).slice(0, 16) }));
|
|
212
|
+
if (options.length) out.options = options;
|
|
213
|
+
if (PICKS.includes(a.pick)) {
|
|
214
|
+
out.pick = a.pick;
|
|
215
|
+
out.except = (Array.isArray(a.except) ? a.except : []).map(String).slice(0, 200);
|
|
216
|
+
}
|
|
217
|
+
if (FIELDS.includes(a.field)) out.field = a.field;
|
|
218
|
+
return out;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function actsField(raw) {
|
|
222
|
+
const acts = normalizeActs(raw);
|
|
223
|
+
return acts.length ? { acts } : {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// One desk row as a contributor handed it (task 1004559). A desk row is a thing on
|
|
227
|
+
// a floor a viewer may act on that is not a matter waiting on anyone: a builder
|
|
228
|
+
// whose tier can be set, an allowed goal, a runner, a confirmed report waiting
|
|
229
|
+
// for its fix. It never reaches the street's pins or counts. Pure; a row with no
|
|
230
|
+
// name or no act is dropped (a desk row exists to be acted on).
|
|
231
|
+
function normalizeDeskRow(raw, { desk, building }) {
|
|
232
|
+
if (!raw || typeof raw !== 'object' || raw.id == null || !raw.name) return null;
|
|
233
|
+
const acts = normalizeActs(raw.acts);
|
|
234
|
+
if (!acts.length) return null;
|
|
235
|
+
return {
|
|
236
|
+
id: `${desk}:${raw.id}`,
|
|
237
|
+
desk,
|
|
238
|
+
building,
|
|
239
|
+
name: String(raw.name).slice(0, 80),
|
|
240
|
+
fact: raw.fact == null ? '' : String(raw.fact).slice(0, 80),
|
|
241
|
+
facts: (Array.isArray(raw.facts) ? raw.facts : []).filter((x) => x != null && String(x).trim()).slice(0, 3).map((x) => String(x).slice(0, 80)),
|
|
242
|
+
needs: raw.needs === true,
|
|
243
|
+
// a row that is only a way to add something (allow another goal) is not a
|
|
244
|
+
// thing on the floor, so the floor does not count it
|
|
245
|
+
tool: raw.tool === true,
|
|
246
|
+
// a same-site page an 'open' act goes to; and the matter source this row
|
|
247
|
+
// stands in for on its floor (the core update's two-step replaces its link)
|
|
248
|
+
href: typeof raw.href === 'string' && raw.href.charAt(0) === '/' && raw.href.charAt(1) !== '/' ? raw.href : null,
|
|
249
|
+
over: typeof raw.over === 'string' && /^[a-z_]+$/.test(raw.over) ? raw.over : null,
|
|
250
|
+
opened_at: iso(raw.opened_at),
|
|
251
|
+
ref: raw.ref && typeof raw.ref === 'object' ? { type: String(raw.ref.type), id: String(raw.ref.id) } : null,
|
|
252
|
+
acts,
|
|
175
253
|
};
|
|
176
254
|
}
|
|
177
255
|
|
|
@@ -397,14 +475,16 @@ function registeredContributions() {
|
|
|
397
475
|
const readings = new Map();
|
|
398
476
|
const floors = new Map();
|
|
399
477
|
const histories = new Map();
|
|
478
|
+
const desks = new Map();
|
|
400
479
|
for (const c of api.contributions(CONTRIBUTION_POINT)) {
|
|
401
480
|
if (!c || typeof c !== 'object' || typeof c.read !== 'function' || !hasValidGate(c.gate)) continue;
|
|
402
481
|
if (typeof c.source === 'string' && c.source) sources.set(c.source, c);
|
|
403
482
|
else if (typeof c.reading === 'string' && c.reading) readings.set(c.reading, c);
|
|
404
483
|
else if (typeof c.floor === 'string' && c.floor) floors.set(c.floor, c);
|
|
405
484
|
else if (typeof c.history === 'string' && c.history && vocab.BUILDING_KEYS.includes(c.building)) histories.set(c.history, c);
|
|
485
|
+
else if (typeof c.desk === 'string' && vocab.DESKS.some((d) => d.key === c.desk)) desks.set(c.desk, c);
|
|
406
486
|
}
|
|
407
|
-
return { sources, readings, floors, histories };
|
|
487
|
+
return { sources, readings, floors, histories, desks };
|
|
408
488
|
}
|
|
409
489
|
|
|
410
490
|
// ── the collector ────────────────────────────────────────────────────────────
|
|
@@ -554,7 +634,25 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
554
634
|
historySources.push({ source: key, building: c.building, status: 'unavailable' });
|
|
555
635
|
}
|
|
556
636
|
}));
|
|
557
|
-
|
|
637
|
+
// The desks (task 1004559): rows a viewer may act on in place, each read by its
|
|
638
|
+
// owning module under the atom its own write route checks. A gated-out desk is
|
|
639
|
+
// not_yours with no rows, so the floor shows its quiet line.
|
|
640
|
+
const deskImpls = new Map(contributions.desks || []);
|
|
641
|
+
const desksP = Promise.all(vocab.DESKS.map(async (decl) => {
|
|
642
|
+
const base = { key: decl.key, building: decl.building, status: null, rows: [] };
|
|
643
|
+
const impl = deskImpls.get(decl.key);
|
|
644
|
+
if (!impl) return { ...base, status: 'off' };
|
|
645
|
+
if (!gateAllows(impl.gate, ctx)) return { ...base, status: 'not_yours' };
|
|
646
|
+
try {
|
|
647
|
+
const raw = await withTimeout(Promise.resolve(impl.read(ctx, { now, once })), timeoutMs);
|
|
648
|
+
const rows = (Array.isArray(raw) ? raw : []).slice(0, MAX_DESK_ROWS).map((r) => normalizeDeskRow(r, { desk: decl.key, building: decl.building })).filter(Boolean);
|
|
649
|
+
return { ...base, status: 'live', rows };
|
|
650
|
+
} catch (err) {
|
|
651
|
+
log.warn({ desk: decl.key, err: err && err.message }, 'governor docket desk failed');
|
|
652
|
+
return { ...base, status: 'unavailable' };
|
|
653
|
+
}
|
|
654
|
+
}));
|
|
655
|
+
const [floors, , desks] = await Promise.all([floorsP, historyP, desksP]);
|
|
558
656
|
history.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : a.id < b.id ? -1 : 1));
|
|
559
657
|
historySources.sort((a, b) => (a.source < b.source ? -1 : 1));
|
|
560
658
|
|
|
@@ -590,6 +688,7 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
590
688
|
floors,
|
|
591
689
|
history,
|
|
592
690
|
history_sources: historySources,
|
|
691
|
+
desks,
|
|
593
692
|
vocabulary: {
|
|
594
693
|
buildings: vocab.BUILDINGS, rules: vocab.RULES, states: vocab.STATES,
|
|
595
694
|
// The table's rule set — the matter glyphs plus vacant (no one holds it).
|
|
@@ -665,6 +764,8 @@ module.exports = {
|
|
|
665
764
|
SOURCE_TIMEOUT_MS,
|
|
666
765
|
normalizeFloor,
|
|
667
766
|
normalizeHistory,
|
|
767
|
+
normalizeActs,
|
|
768
|
+
normalizeDeskRow,
|
|
668
769
|
loadGovernment,
|
|
669
770
|
SETTLED_WINDOW_MS,
|
|
670
771
|
normalizeMatter,
|
|
@@ -182,7 +182,20 @@ const { validateOrRespond, LIMITS, parseId } = api;
|
|
|
182
182
|
const RANK_KEY_RE = /^[a-z][a-z0-9-]*$/;
|
|
183
183
|
const RANK_KEY_MAX = 64;
|
|
184
184
|
|
|
185
|
+
// The People desk's rank rows, under the assignments routes' own atom (task 1004559).
|
|
186
|
+
function contributeRankDesk() {
|
|
187
|
+
api.contribute('governor.docket', {
|
|
188
|
+
desk: 'people.ranks',
|
|
189
|
+
gate: { permission: 'government.manage' },
|
|
190
|
+
read: async () => {
|
|
191
|
+
const [ranks, assignments] = await Promise.all([db.listRanksWithGrants(), db.listAssignments()]);
|
|
192
|
+
return require('../docket-floors').rankDeskRows(ranks, assignments);
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
module.exports = function buildGovernmentRouter() {
|
|
198
|
+
contributeRankDesk();
|
|
186
199
|
registerSeams();
|
|
187
200
|
const router = express.Router();
|
|
188
201
|
|
|
@@ -440,8 +440,79 @@
|
|
|
440
440
|
return sourceCard(cv, m);
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// ---------- the acts the server hands a row (task 1004559) ----------
|
|
444
|
+
// A docket matter or desk row may carry `acts`: what THIS viewer may do, which
|
|
445
|
+
// the owning module decided from the same atom its route checks. The floor
|
|
446
|
+
// desk sends them through this card: { kind, id, who } is all a route needs.
|
|
447
|
+
// kind is the matter's source, or the desk row's ref type.
|
|
448
|
+
function actsCard(o) {
|
|
449
|
+
var x = o || {};
|
|
450
|
+
return {
|
|
451
|
+
key: str(x.key), building: x.building || null, where: '', when: '', title: str(x.title), href: safeHref(x.href),
|
|
452
|
+
source: str(x.kind), quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
|
|
453
|
+
result: '', actions: [], textActions: [], objectForm: null, reviseForm: null, readOnly: false, loading: false, lines: [],
|
|
454
|
+
subject: { kind: str(x.kind), id: str(x.id), who: str(x.who) },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
443
458
|
// ---------- act → route ----------
|
|
444
459
|
function post(url, body) { return { method: 'POST', url: API + url, body: body || {} }; }
|
|
460
|
+
function call(method, url, body) { return { method: method, url: API + url, body: body || {} }; }
|
|
461
|
+
// The routes behind the server-handed acts. inp.option is the chip's key, and
|
|
462
|
+
// inp.label its words (sent as the reason where a route keeps one).
|
|
463
|
+
var ACT_ROUTES = {
|
|
464
|
+
redteam_reports: {
|
|
465
|
+
confirm: function (id) { return post('/security/reports/' + id + '/confirm', {}); },
|
|
466
|
+
dispute: function (id) { return post('/security/reports/' + id + '/dispute', {}); },
|
|
467
|
+
},
|
|
468
|
+
vulnerability_report: { fix: function (id) { return post('/security/reports/' + id + '/fix', {}); } },
|
|
469
|
+
removal_proposals: {
|
|
470
|
+
remove: function (id) { return post('/removal-proposals/' + id + '/decide', { verdict: 'confirmed' }); },
|
|
471
|
+
keep: function (id) { return post('/removal-proposals/' + id + '/decide', { verdict: 'dismissed' }); },
|
|
472
|
+
},
|
|
473
|
+
criteria_uat: {
|
|
474
|
+
signoff: function (id) { return post('/done-when/' + id + '/uat', { kind: 'backend_signoff', live_attested: true }); },
|
|
475
|
+
force: function (id, inp) { return post('/done-when/' + id + '/satisfy', { override_reason: 'Signed off on the floor desk: ' + str(inp.label || 'forced') + '.' }); },
|
|
476
|
+
},
|
|
477
|
+
autobongos_fence: {
|
|
478
|
+
switch_on: function () { return post('/autonomy/fence', { enabled: true }); },
|
|
479
|
+
switch_off: function (id, inp) { return post('/autonomy/fence', { enabled: false, reason: str(inp.label) || 'switched off on the floor desk' }); },
|
|
480
|
+
allow: function (id, inp) { return inp.option ? post('/autonomy/fence/goals', { goal_id: Number(inp.option) }) : null; },
|
|
481
|
+
},
|
|
482
|
+
autonomy_goal: {
|
|
483
|
+
first: function (id) { return post('/autonomy/fence/priority', { goal_id: Number(id) }); },
|
|
484
|
+
disallow: function (id) { return call('DELETE', '/autonomy/fence/goals/' + id); },
|
|
485
|
+
allow: function (id, inp) { return inp.option ? post('/autonomy/fence/goals', { goal_id: Number(inp.option) }) : null; },
|
|
486
|
+
},
|
|
487
|
+
autonomy_runner: {
|
|
488
|
+
pause: function (id, inp) { return post('/autonomy/builders/' + id + '/pause', { paused: true, reason: str(inp.label) || 'paused on the floor desk' }); },
|
|
489
|
+
resume: function (id) { return post('/autonomy/builders/' + id + '/pause', { paused: false }); },
|
|
490
|
+
},
|
|
491
|
+
builder: {
|
|
492
|
+
tier: function (id, inp) { return inp.option ? call('PATCH', '/builders/' + id + '/rank', { rank: str(inp.option) }) : null; },
|
|
493
|
+
deactivate: function (id, inp) { return call('PATCH', '/builders/' + id + '/status', { status: 'inactive', reason: str(inp.label) || 'deactivated on the floor desk' }); },
|
|
494
|
+
},
|
|
495
|
+
custom_rank: {
|
|
496
|
+
give: function (id, inp) { return inp.option ? post('/government/assignments', { builder_id: Number(inp.option), rank_key: decoded(id) }) : null; },
|
|
497
|
+
take: function (id, inp) { return inp.option ? call('DELETE', '/government/assignments/' + encodeURIComponent(str(inp.option)) + '/' + id) : null; },
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
// The core update's two steps (POST /provisioning/instances/:id/core-upgrade):
|
|
501
|
+
// the server hands 'take' only once a dry run passed for that very version, and
|
|
502
|
+
// the chip carries the version, so no card moves the pin without that check.
|
|
503
|
+
ACT_ROUTES.core_pin = {
|
|
504
|
+
check: function (id, inp) { return inp.option ? post('/provisioning/instances/' + id + '/core-upgrade', { to: str(inp.option), dry_run: true }) : null; },
|
|
505
|
+
take: function (id, inp) { return inp.option ? post('/provisioning/instances/' + id + '/core-upgrade', { to: str(inp.option) }) : null; },
|
|
506
|
+
};
|
|
507
|
+
ACT_ROUTES.project_setting = { rot: function (id, inp) { return inp.option ? call('PATCH', '/project-settings/rot-days', { days: Number(inp.option) }) : null; } };
|
|
508
|
+
ACT_ROUTES.module = {
|
|
509
|
+
module_on: function (id) { return post('/modules/' + id + '/enable', { enabled: true }); },
|
|
510
|
+
module_off: function (id) { return post('/modules/' + id + '/enable', { enabled: false }); },
|
|
511
|
+
};
|
|
512
|
+
ACT_ROUTES.backup = { backup: function () { return post('/backup/trigger', {}); } };
|
|
513
|
+
ACT_ROUTES.invite = { invite: function (id, inp) { return str(inp.option).trim() ? post('/access-requests/invite', { github_login: str(inp.option).trim() }) : null; } };
|
|
514
|
+
ACT_ROUTES.autonomy_fence = ACT_ROUTES.autobongos_fence;
|
|
515
|
+
function decoded(encoded) { return decodeURIComponent(encoded); }
|
|
445
516
|
// input: { reason, section } for object; { text } for revise.
|
|
446
517
|
function request(cv, key, input) {
|
|
447
518
|
if (!cv || !cv.subject) return null;
|
|
@@ -470,6 +541,7 @@
|
|
|
470
541
|
if (key === 'fine') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'approve' });
|
|
471
542
|
if (key === 'refuse') return post('/goals/' + enc(s.goalId) + '/requests/' + enc(s.id) + '/respond', { action: 'deny' });
|
|
472
543
|
}
|
|
544
|
+
if (ACT_ROUTES[s.kind] && ACT_ROUTES[s.kind][key]) return ACT_ROUTES[s.kind][key](enc(s.id), inp);
|
|
473
545
|
if (s.kind === 'override_requests' && key === 'release') return post('/override-requests/' + enc(s.id) + '/decide', { decision: 'approve' });
|
|
474
546
|
if (s.kind === 'gate_approvals' && key === 'release') return post('/gate-approvals/' + enc(s.id) + '/approve', {});
|
|
475
547
|
return null; // hold, abstain and the core's release (a link) record nothing here
|
|
@@ -505,9 +577,26 @@
|
|
|
505
577
|
if (key === 'release') return { toast: cv && cv.source === 'gate_approvals' ? 'Released from the gate. It merges and goes live.' : 'Released. It goes live.', building: b, kind: 'rise', close: true };
|
|
506
578
|
if (key === 'fine') return { toast: cv && cv.source === 'goal_join_requests' ? 'Noted. ' + who + ' joins the team.' : 'Noted. ' + who + ' is invited in.', building: b, kind: 'rise', close: false };
|
|
507
579
|
if (key === 'refuse') return { toast: 'Noted. ' + who + ' is not let in.', building: b, kind: 'settle', close: false };
|
|
580
|
+
if (key === 'take' && cv && cv.subject && cv.subject.kind === 'core_pin') return { toast: ACT_TOASTS.take_core, building: b, kind: 'rise', close: false };
|
|
581
|
+
if (key === 'invite') return { toast: ACT_TOASTS.invite.replace('{who}', r.github_login || 'They'), building: b, kind: 'light', close: false };
|
|
582
|
+
if (ACT_TOASTS[key]) return { toast: ACT_TOASTS[key].replace('{who}', who), building: b, kind: ACT_KINDS[key] || 'light', close: false };
|
|
508
583
|
return { toast: '', building: null, kind: null, close: false };
|
|
509
584
|
}
|
|
510
585
|
|
|
586
|
+
// What the street says after a server-handed act (task 1004559).
|
|
587
|
+
var ACT_TOASTS = {
|
|
588
|
+
confirm: 'Confirmed. The report is on the record.', dispute: 'Disputed. The report is closed as not a flaw.', fix: 'Marked fixed.',
|
|
589
|
+
remove: 'Removal confirmed.', keep: 'Kept. The removal is dismissed.',
|
|
590
|
+
signoff: 'Signed off. The promise counts as kept.', force: 'Signed off by force. The reason is on the record.',
|
|
591
|
+
switch_on: 'Switched on. Unattended building can start.', switch_off: 'Switched off. Nothing new starts unattended.',
|
|
592
|
+
allow: 'Allowed. It may build that goal now.', disallow: 'No longer allowed.', first: 'First in line now.',
|
|
593
|
+
pause: '{who} is paused.', resume: '{who} may run again.',
|
|
594
|
+
tier: '{who} has a new tier.', deactivate: '{who} is deactivated.', give: 'Rank given.', take: 'Rank taken back.',
|
|
595
|
+
check: 'Checking it. The answer lands on this floor in a couple of minutes; nothing moves.', take_core: 'Taking it. The core moves on the runner\'s next tick.',
|
|
596
|
+
rot: 'Rot timer set.', module_on: 'Turned on.', module_off: 'Turned off.', backup: 'Backing up now.', invite: '{who} is invited.',
|
|
597
|
+
};
|
|
598
|
+
var ACT_KINDS = { check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
|
|
599
|
+
|
|
511
600
|
// A refused act, in words. The server's own message wins when it has one.
|
|
512
601
|
var REFUSALS = {
|
|
513
602
|
not_board_member: 'Only the members of this sitting may vote on it.',
|
|
@@ -522,6 +611,13 @@
|
|
|
522
611
|
proposer_would_lose_their_seat: 'Re-filing this would take your own seat. Change it in the Board Room.',
|
|
523
612
|
no_change: 'That already is how it works.',
|
|
524
613
|
preview_readonly: 'This preview records nothing.',
|
|
614
|
+
decided_by_sitting: 'The council decides this now. Put it to a sitting.',
|
|
615
|
+
cannot_change_own_status: 'Ask another archon to change your own standing.',
|
|
616
|
+
already_member: 'They already have an account here.',
|
|
617
|
+
bad_github_login: 'That does not look like a GitHub name.',
|
|
618
|
+
backup_already_running: 'A backup is already running.',
|
|
619
|
+
intent_conflict: 'Another core run is going. Try again when it settles.',
|
|
620
|
+
criterion_needs_recording: 'This one needs a recording. Sign it off on its goal page.',
|
|
525
621
|
};
|
|
526
622
|
function refusal(status, body) {
|
|
527
623
|
var e = body && body.error ? body.error : {};
|
|
@@ -690,7 +786,7 @@
|
|
|
690
786
|
|
|
691
787
|
var api = {
|
|
692
788
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
693
|
-
model: model, request: request, check: check, answer: answer, refusal: refusal, render: render,
|
|
789
|
+
model: model, actsCard: actsCard, request: request, check: check, answer: answer, refusal: refusal, render: render,
|
|
694
790
|
memberWords: memberWords, windowWords: windowWords,
|
|
695
791
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
696
792
|
change: changeOf,
|