@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
|
@@ -97,7 +97,23 @@
|
|
|
97
97
|
gate_approvals: 'Everything ready is live', blockers: 'Nothing blocked on you', board: 'Every vote is in', idea: 'No ideas waiting',
|
|
98
98
|
});
|
|
99
99
|
// The card's acts under the desk's words (the board's button labels).
|
|
100
|
-
const ACT_WORDS = Object.freeze({
|
|
100
|
+
const ACT_WORDS = Object.freeze({
|
|
101
|
+
for: 'yes', object: 'no', fine: 'let in', refuse: 'not now', release: 'release', hold: 'hold',
|
|
102
|
+
// the acts the server hands a row (task 1004559), each its old page's route
|
|
103
|
+
confirm: 'confirm', dispute: 'dispute', fix: 'fixed', remove: 'remove', keep: 'keep',
|
|
104
|
+
signoff: 'sign off', record: 'record it', force: 'force',
|
|
105
|
+
switch_on: 'switch on', switch_off: 'switch off', allow: 'allow', disallow: 'disallow', first: 'first',
|
|
106
|
+
pause: 'pause', resume: 'resume', tier: 'tier', deactivate: 'deactivate', give: 'give', take: 'take back',
|
|
107
|
+
check: 'check', take_core: 'take it', rot: 'set', module_on: 'turn on', module_off: 'turn off', backup: 'back up', invite: 'invite', open: 'open',
|
|
108
|
+
});
|
|
109
|
+
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
110
|
+
const ASKS = Object.freeze({ check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
111
|
+
// How a floor counts rows nobody is waiting on (desk rows), by its desk.
|
|
112
|
+
const DESK_NOUNS = Object.freeze({
|
|
113
|
+
'people.builders': 'builders', 'people.ranks': 'ranks', 'disputes.confirmed': 'to fix',
|
|
114
|
+
'automation.switch': 'switch', 'automation.goals': 'allowed', 'automation.runners': 'runners',
|
|
115
|
+
'people.invite': 'invites', 'release.core': 'update', 'rules.settings': 'settings', 'rules.modules': 'modules', 'treasury.backup': 'backups',
|
|
116
|
+
});
|
|
101
117
|
// An objection's reason as one tap: the five sections the server accepts
|
|
102
118
|
// (street-card.js SECTIONS), each in at most 16 characters.
|
|
103
119
|
const CHIPS = Object.freeze({ scope: 'what it changes', purpose: 'its purpose', why_matters: 'why it matters', fit: 'how it fits', success: 'how to measure' });
|
|
@@ -161,6 +177,13 @@
|
|
|
161
177
|
if (when) add('closes ' + when);
|
|
162
178
|
if (m.where) add(str(m.where).toLowerCase());
|
|
163
179
|
add(STATE[stateOf(m)]);
|
|
180
|
+
if (Array.isArray(m.acts) && m.acts.length && C && C.actsCard) {
|
|
181
|
+
const ac = C.actsCard({ key: m.id, kind: m.source, id: m.ref && m.ref.id != null ? m.ref.id : str(m.id).replace(/^[^:]*:/, ''), who: person || name, title: m.title, building: m.building, href: m.href });
|
|
182
|
+
return Object.assign(actedRow(m.acts, ac, ctx), {
|
|
183
|
+
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
184
|
+
needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
164
187
|
const cv = C && C.model ? C.model({ docket: ctx.docket, key: m.id, room: ctx.room, constitution: ctx.constitution, now: now, viewerId: ctx.viewerId }) : null;
|
|
165
188
|
const actions = [];
|
|
166
189
|
let chips = null;
|
|
@@ -176,13 +199,82 @@
|
|
|
176
199
|
const wait = actions.length ? '' : cv && cv.loading ? 'reading…' : stateOf(m) === 'needs' ? 'not from here' : 'moving';
|
|
177
200
|
return {
|
|
178
201
|
key: str(m.id), matter: str(m.id), ini: initials(name), name: name, fact: fact, facts: facts,
|
|
179
|
-
actions: actions, chips: chips, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
|
|
202
|
+
actions: actions, chips: chips, choices: chips ? { [chips.for]: chips } : {}, wait: wait, needs: stateOf(m) === 'needs', source: m.source, opened: m.opened_at || null, card: cv,
|
|
180
203
|
};
|
|
181
204
|
}
|
|
205
|
+
|
|
206
|
+
// The buttons and chips of a row whose acts the server handed it (task
|
|
207
|
+
// 1004559). An act with options asks first, with its options as one-tap chips;
|
|
208
|
+
// `pick: 'open_goals'` offers the open goals the street already reads; an act
|
|
209
|
+
// whose chips come out empty is left off rather than drawn dead. 'record'
|
|
210
|
+
// goes to the matter's own page (a sign-off that needs a recording).
|
|
211
|
+
function actedRow(acts, card, ctx) {
|
|
212
|
+
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || 'goal ' + g.id)]));
|
|
213
|
+
const actions = [];
|
|
214
|
+
const choices = {};
|
|
215
|
+
acts.forEach((a) => {
|
|
216
|
+
// the core's 'take' is its own word ('take it'), not the rank's 'take back'
|
|
217
|
+
const word = a.key === 'take' && card.subject && card.subject.kind === 'core_pin' ? 'take_core' : a.key;
|
|
218
|
+
if (actions.length >= LIMITS.actions || !ACT_WORDS[word]) return;
|
|
219
|
+
let options = (a.options || []).map((o) => ({ key: str(o.key), label: fit(o.label, LIMITS.chip), input: { option: str(o.key), label: str(o.label) } }));
|
|
220
|
+
if (a.pick === 'open_goals') {
|
|
221
|
+
const except = new Set((a.except || []).map(str));
|
|
222
|
+
options = (ctx.goals || []).filter((g) => g && !except.has(str(g.id))).slice(0, LIMITS.chips)
|
|
223
|
+
.map((g) => ({ key: str(g.id), label: fit(titles.get(str(g.id)), LIMITS.chip), input: { option: str(g.id), label: titles.get(str(g.id)) } }));
|
|
224
|
+
if (!options.length) return;
|
|
225
|
+
}
|
|
226
|
+
options = options.slice(0, LIMITS.chips);
|
|
227
|
+
const goes = a.key === 'record' || a.key === 'open';
|
|
228
|
+
if (goes && !card.href) return;
|
|
229
|
+
const field = a.field || null;
|
|
230
|
+
const ask = options.length > 0 || !!field;
|
|
231
|
+
if (ask) choices[a.key] = field ? { for: a.key, ask: ASKS[word] || 'which', field: field, options: [] } : { for: a.key, ask: ASKS[word] || 'which', options: options };
|
|
232
|
+
actions.push({ key: a.key, label: ACT_WORDS[word], tone: actions.length ? 'quiet' : 'primary', go: goes ? card.href : null, ask: ask });
|
|
233
|
+
});
|
|
234
|
+
const first = actions.find((a) => a.ask);
|
|
235
|
+
return { actions: actions, chips: first ? choices[first.key] : null, choices: choices, wait: actions.length ? '' : 'not from here', card: card };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A desk row (task 1004559): something on this floor the viewer may act on that
|
|
239
|
+
// no one is waiting on, read by the module that owns its route.
|
|
240
|
+
function deskRowOf(d, ctx) {
|
|
241
|
+
const titles = new Map((ctx.goals || []).map((g) => [str(g.id), str(g.title || '')]));
|
|
242
|
+
const hit = /^goal (\d+)$/.exec(str(d.name));
|
|
243
|
+
const full = hit && titles.get(hit[1]) ? titles.get(hit[1]) : str(d.name);
|
|
244
|
+
const name = fit(full, LIMITS.name, LIMITS.nameWords);
|
|
245
|
+
const age = ageWord(d.opened_at, ctx.now || 0);
|
|
246
|
+
const facts = [];
|
|
247
|
+
const add = (t) => { const v = fit(t, LIMITS.factLen); if (v && facts.indexOf(v) < 0 && facts.length < LIMITS.facts) facts.push(v); };
|
|
248
|
+
if (full !== name) add(full);
|
|
249
|
+
(d.facts || []).forEach(add);
|
|
250
|
+
if (age) add('since ' + age);
|
|
251
|
+
const ref = d.ref || {};
|
|
252
|
+
const card = C && C.actsCard ? C.actsCard({ key: d.id, kind: ref.type, id: ref.id, who: full, title: full, building: d.building, href: d.href }) : null;
|
|
253
|
+
return Object.assign(actedRow(d.acts || [], card || { href: null }, ctx), {
|
|
254
|
+
key: str(d.id), matter: str(d.id), ini: initials(name), name: name, fact: fit(d.fact, LIMITS.fact), facts: facts,
|
|
255
|
+
needs: d.needs === true, source: d.desk, opened: d.opened_at || null, desk: true, tool: d.tool === true, over: d.over || null,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// The desk rows of one floor: those of the desk its PLAN entry names.
|
|
259
|
+
function deskRows(key, floor, docket, ctx) {
|
|
260
|
+
const plan = I && I.PLAN[key];
|
|
261
|
+
const spec = plan && plan.floors[floor];
|
|
262
|
+
const bind = (spec && spec[2]) || {};
|
|
263
|
+
if (!bind.desk || !docket || !Array.isArray(docket.desks)) return [];
|
|
264
|
+
// a floor may hold more than one desk (Rules in force: the settings and the modules)
|
|
265
|
+
const keys = [].concat(bind.desk);
|
|
266
|
+
return docket.desks.filter((x) => x && keys.indexOf(x.key) >= 0 && x.status === 'live' && Array.isArray(x.rows))
|
|
267
|
+
.reduce((all, x) => all.concat(x.rows.filter((r) => r && r.building === key)), []);
|
|
268
|
+
}
|
|
182
269
|
function rowsFor(key, floor, docket, ctx) {
|
|
183
270
|
const c = Object.assign({ docket: docket }, ctx || {});
|
|
184
271
|
const done = c.done || {};
|
|
185
|
-
|
|
272
|
+
const desked = deskRows(key, floor, docket).filter((d) => !done[str(d.id)]).map((d) => deskRowOf(d, c)).filter((r) => r.actions.length);
|
|
273
|
+
// a desk row that stands in for a source (the core update's check-then-take)
|
|
274
|
+
// replaces that source's matter rows on this floor
|
|
275
|
+
const over = desked.map((r) => r.over).filter(Boolean);
|
|
276
|
+
return floorMatters(key, floor, docket).filter((m) => !done[str(m.id)] && over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
|
|
277
|
+
.concat(desked)
|
|
186
278
|
.sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
|
|
187
279
|
}
|
|
188
280
|
|
|
@@ -199,19 +291,23 @@
|
|
|
199
291
|
const floor = Math.max(0, Math.min(Number(o.floor) || 0, plan.floors.length - 1));
|
|
200
292
|
const spec = plan.floors[floor];
|
|
201
293
|
const fl = I.floorFor(spec, { key: o.key, docket: docket, goals: o.goals || [], now: o.now || 0 });
|
|
202
|
-
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done };
|
|
294
|
+
const ctx = { room: o.room, constitution: o.constitution, now: o.now || 0, viewerId: o.viewerId, done: o.done, goals: o.goals || [] };
|
|
203
295
|
const rows = docket ? rowsFor(o.key, floor, docket, ctx) : [];
|
|
204
296
|
const bind = spec[2] || {};
|
|
205
297
|
const src = bind.source ? bind.source : bind.floor === 'planning.ideas' ? 'idea' : floor === 0 ? 'board' : null;
|
|
206
298
|
const needs = rows.filter((r) => r.needs);
|
|
207
299
|
const noun = NOUNS[src] || 'open';
|
|
300
|
+
// rows nobody waits on: 'moving' for matters, the desk's own noun for desk rows
|
|
301
|
+
const moving = rows.length && rows.every((r) => r.desk) ? (DESK_NOUNS[rows[0].source] || 'here') : 'moving';
|
|
302
|
+
const counted = rows.filter((r) => !r.tool);
|
|
208
303
|
const oldestOf = (list) => list.reduce((a, r) => (r.opened && (!a || Date.parse(r.opened) < Date.parse(a)) ? r.opened : a), null);
|
|
209
304
|
const cleared = !!o.cleared && rows.length === 0;
|
|
210
305
|
const quiet = rows.length === 0;
|
|
211
306
|
let status, hot = false;
|
|
212
307
|
if (!docket) status = 'the docket could not be read';
|
|
213
308
|
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
|
|
214
|
-
else if (
|
|
309
|
+
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + moving + (age ? ' · oldest ' + age : ''); }
|
|
310
|
+
else if (rows.length) status = fl.line;
|
|
215
311
|
else if (cleared) status = 'floor clear';
|
|
216
312
|
else status = 'nothing needs you · ' + fl.line;
|
|
217
313
|
const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
|
|
@@ -224,7 +320,7 @@
|
|
|
224
320
|
const words = str(l).replace(/\s+/g, ' ').trim().split(' ').slice(0, LIMITS.tileWords).join(' ').replace(/[,.;:·-]+$/, '');
|
|
225
321
|
tiles.push({ key: 't' + tiles.length, v: fit(val, LIMITS.tileValue), l: fit(words, LIMITS.tileLabel) });
|
|
226
322
|
};
|
|
227
|
-
if (rows.length) { tile(needs.length, noun); tile(
|
|
323
|
+
if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
|
|
228
324
|
else if (fl.count) tile(fl.count, str(fl.line).replace(/^[$]?[0-9][0-9,.%]*\s*/, '') || 'on this floor');
|
|
229
325
|
tile(history.length, 'on record');
|
|
230
326
|
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t + ' · now', LIMITS.recordLen), mine: true }))
|
|
@@ -263,6 +359,11 @@
|
|
|
263
359
|
if (r.facts.length > LIMITS.facts || r.facts.some((f) => !oneLine(f) || f.length > LIMITS.factLen)) bad('row facts', r.facts);
|
|
264
360
|
if (r.chips && (r.chips.options.length > LIMITS.chips || r.chips.options.some((c) => c.label.length > LIMITS.chip))) bad('chips', r.chips.options.map((c) => c.label));
|
|
265
361
|
if (r.actions.some((a) => a.ask) && !r.chips) bad('a reason without chips', r.name);
|
|
362
|
+
Object.keys(r.choices || {}).forEach((k) => {
|
|
363
|
+
const c = r.choices[k];
|
|
364
|
+
if ((!c.field && !c.options.length) || c.options.length > LIMITS.chips || c.options.some((x) => !x.label || x.label.length > LIMITS.chip)) bad('chips', c.options.map((x) => x.label));
|
|
365
|
+
});
|
|
366
|
+
if (r.actions.some((a) => a.ask && !(r.choices || {})[a.key])) bad('an ask without its own chips', r.name);
|
|
266
367
|
});
|
|
267
368
|
if (desk.record.length > LIMITS.record || desk.record.some((x) => !oneLine(x.text) || x.text.length > LIMITS.recordLen)) bad('record line', desk.record.map((x) => x.text));
|
|
268
369
|
return out;
|
|
@@ -277,7 +378,7 @@
|
|
|
277
378
|
}
|
|
278
379
|
|
|
279
380
|
const api = {
|
|
280
|
-
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
381
|
+
LIMITS: LIMITS, ACT_WORDS: ACT_WORDS, ASKS: ASKS, DESK_NOUNS: DESK_NOUNS, CHIPS: CHIPS, NOUNS: NOUNS, CLEAR: CLEAR,
|
|
281
382
|
fit: fit, ageWord: ageWord, floorMatters: floorMatters, firstNeeds: firstNeeds, rowsFor: rowsFor, deskModel: deskModel, breaks: breaks, todayPips: todayPips,
|
|
282
383
|
};
|
|
283
384
|
root.GovDesk = api;
|
|
@@ -331,17 +331,17 @@
|
|
|
331
331
|
rules: {
|
|
332
332
|
holds: 'Standing rules. Each one settles a kind of matter so the council never votes on it again.', rule: 'majority', cap: 8,
|
|
333
333
|
growth: 'When the hall fills, the council can open an annex for one area, such as money rules.', annex: 'room for an annex, such as money rules',
|
|
334
|
-
floors: [['Rules in force', 'the rules settling matters now', { floor: 'rules.in_force', reading: 'rules' }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
|
|
334
|
+
floors: [['Rules in force', 'the rules settling matters now', { floor: 'rules.in_force', reading: 'rules', desk: ['rules.settings', 'rules.modules'] }], ['New rules', 'a rule put to the council', { source: 'standing_rules' }]],
|
|
335
335
|
},
|
|
336
336
|
disputes: {
|
|
337
337
|
holds: 'Grade appeals, removal requests and security reports. Every ruling becomes precedent for the next one.', rule: 'decide', cap: 5,
|
|
338
338
|
growth: 'Busy weeks go to a panel of three once a second room is built.', annex: 'room for a second room, a panel of three',
|
|
339
|
-
floors: [['Security reports', 'flaws found in our own walls', { source: 'redteam_reports' }], ['Removals', 'work proposed for removal', { source: 'removal_proposals' }], ['Grade appeals', 'a grade someone asks to change', { source: 'grade_appeals' }]],
|
|
339
|
+
floors: [['Security reports', 'flaws found in our own walls', { source: 'redteam_reports', desk: 'disputes.confirmed' }], ['Removals', 'work proposed for removal', { source: 'removal_proposals' }], ['Grade appeals', 'a grade someone asks to change', { source: 'grade_appeals' }]],
|
|
340
340
|
},
|
|
341
341
|
people: {
|
|
342
342
|
holds: 'Who belongs, the rank each person holds, and who sits in which seat.', rule: 'consent', cap: 8,
|
|
343
343
|
growth: 'A new floor opens whenever the charter creates a new seat.', annex: 'room for an annex of seats',
|
|
344
|
-
floors: [['The council', 'who sits, and for how long', { floor: 'people.council' }], ['Testers', 'who signs off finished work', { floor: 'people.testers' }], ['Builders', 'who builds here', { reading: 'people' }], ['Newcomers', 'builders on newcomer tasks', { floor: 'people.newcomers' }], ['Waiting to join', 'asked to join the project', { source: 'access_requests' }], ['Ranks', 'the ranks, and who holds each', { floor: 'people.ranks' }], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
|
|
344
|
+
floors: [['The council', 'who sits, and for how long', { floor: 'people.council' }], ['Testers', 'who signs off finished work', { floor: 'people.testers' }], ['Builders', 'who builds here', { reading: 'people', desk: 'people.builders' }], ['Newcomers', 'builders on newcomer tasks', { floor: 'people.newcomers' }], ['Waiting to join', 'asked to join the project', { source: 'access_requests', desk: 'people.invite' }], ['Ranks', 'the ranks, and who holds each', { floor: 'people.ranks', desk: 'people.ranks' }], ['Goal teams', 'asked to join a goal team', { source: 'goal_join_requests' }]],
|
|
345
345
|
},
|
|
346
346
|
planning: {
|
|
347
347
|
holds: 'Goals, and the promises that say when each one is done. Kept promises are what the project owes its users.', rule: 'majority', cap: 9,
|
|
@@ -351,7 +351,7 @@
|
|
|
351
351
|
automation: {
|
|
352
352
|
holds: 'AI builders working on their own. You choose which goals they may build and how much they may spend, and you can stop them at any time.', rule: 'decide', cap: 6,
|
|
353
353
|
growth: 'A new floor opens for each goal you let it build.', annex: 'room for an annex',
|
|
354
|
-
floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence' }], ['Allowed goals', 'the goals it may build', { floor: 'automation.allowed_goals' }], ['Running now', 'builds in progress', { floor: 'automation.running' }], ['Spending limit', 'what it may spend', { floor: 'automation.spending_limit' }], ['Reports', 'what it did, told without asking', { floor: 'automation.reports' }]],
|
|
354
|
+
floors: [['The switch', 'unattended building, on or off', { source: 'autobongos_fence', desk: 'automation.switch' }], ['Allowed goals', 'the goals it may build', { floor: 'automation.allowed_goals', desk: 'automation.goals' }], ['Running now', 'builds in progress', { floor: 'automation.running', desk: 'automation.runners' }], ['Spending limit', 'what it may spend', { floor: 'automation.spending_limit' }], ['Reports', 'what it did, told without asking', { floor: 'automation.reports' }]],
|
|
355
355
|
},
|
|
356
356
|
review: {
|
|
357
357
|
holds: 'Testing finished work and signing it off. Work does not count until someone who did not build it says it works.', rule: 'signoff', cap: 5,
|
|
@@ -361,12 +361,12 @@
|
|
|
361
361
|
release: {
|
|
362
362
|
holds: 'Putting finished work live, and taking updates to Bongos itself.', rule: 'decide', cap: 7,
|
|
363
363
|
growth: 'Room for a second gate if releases are ever split by area.', annex: 'room for a second gate',
|
|
364
|
-
floors: [['Overrides', 'past a failed check or grade', { source: 'override_requests' }], ['Modules', 'listed modules, and their re-checks', { source: 'module_rechecks' }], ['Held back', 'work held from going live', { floor: 'release.held_back' }], ['Core update', 'a new version of Bongos', { source: 'core_update' }], ['Live this month', 'what went live', { floor: 'release.live_month' }], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
|
|
364
|
+
floors: [['Overrides', 'past a failed check or grade', { source: 'override_requests' }], ['Modules', 'listed modules, and their re-checks', { source: 'module_rechecks' }], ['Held back', 'work held from going live', { floor: 'release.held_back' }], ['Core update', 'a new version of Bongos', { source: 'core_update', desk: 'release.core' }], ['Live this month', 'what went live', { floor: 'release.live_month' }], ['Ready to go', 'signed off, waiting to go live', { source: 'gate_approvals' }]],
|
|
365
365
|
},
|
|
366
366
|
treasury: {
|
|
367
367
|
holds: 'The budget, and who gets paid for what.', rule: 'majority', cap: 7,
|
|
368
368
|
growth: 'Each new spending line takes a floor.', annex: 'room for an annex of spending lines',
|
|
369
|
-
floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting' }], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
369
|
+
floors: [['Builder rewards', 'paid for shipped work', { floor: 'treasury.rewards' }], ['AI grading', 'the reviewer that grades every ship', { floor: 'treasury.grading' }], ['Hosting', 'servers and storage', { floor: 'treasury.hosting', desk: 'treasury.backup' }], ['Reserve', 'what is left this month', { reading: 'money' }], ['Spending matters', 'spending put to a vote', { source: 'treasury_spending' }]],
|
|
370
370
|
},
|
|
371
371
|
});
|
|
372
372
|
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
@@ -310,6 +310,7 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
310
310
|
.desk-chip--primary { background: var(--ink); border: 0; color: var(--bg); }
|
|
311
311
|
.desk-chip--quiet, .desk-chip--choice { background: none; border: 1px solid var(--street-rule-strong); color: var(--ink); }
|
|
312
312
|
.desk-chip--choice { height: 30px; padding: 0 14px; }
|
|
313
|
+
.desk-field { height: 30px; min-width: 0; flex: 1 1 140px; max-width: 220px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--street-rule-strong); background: none; color: var(--ink); font-family: var(--font-body); font-size: 13.5px; }
|
|
313
314
|
.desk-chip:hover { filter: brightness(1.08); }
|
|
314
315
|
.desk-chip[disabled] { opacity: 0.55; cursor: progress; }
|
|
315
316
|
.desk-wait { font-size: 13px; color: var(--ink-faint); border: 1px dashed var(--street-rule-strong); border-radius: 999px; padding: 6px 12px; white-space: nowrap; }
|
|
@@ -621,7 +621,7 @@
|
|
|
621
621
|
r.actions.slice().reverse().forEach(function (a) {
|
|
622
622
|
acts.appendChild(chipBtn('desk-chip--' + a.tone, a.label, function () {
|
|
623
623
|
if (a.go) { window.location.href = a.go; return; }
|
|
624
|
-
if (a.ask) { desk.ask = desk.ask ===
|
|
624
|
+
if (a.ask) { var at = r.key + '|' + a.key; desk.ask = desk.ask === at ? null : at; desk.open = null; draw(); focusRow(r.key, 'chip'); return; }
|
|
625
625
|
deskAct(r, a.key, {});
|
|
626
626
|
}));
|
|
627
627
|
});
|
|
@@ -632,10 +632,23 @@
|
|
|
632
632
|
r.facts.forEach(function (f) { facts.appendChild(node('span', 'desk-fact', f)); });
|
|
633
633
|
row.appendChild(facts);
|
|
634
634
|
}
|
|
635
|
-
|
|
635
|
+
var asked = desk.ask && desk.ask.indexOf(r.key + '|') === 0 ? (r.choices || {})[desk.ask.slice(r.key.length + 1)] : null;
|
|
636
|
+
if (asked) {
|
|
636
637
|
var chips = node('div', 'desk-choices');
|
|
637
|
-
chips.appendChild(node('span', 'desk-choices__ask',
|
|
638
|
-
|
|
638
|
+
chips.appendChild(node('span', 'desk-choices__ask', asked.ask));
|
|
639
|
+
asked.options.forEach(function (c) { chips.appendChild(chipBtn('desk-chip--choice', c.label, function () { deskAct(r, asked.for, c.input); })); });
|
|
640
|
+
if (asked.field) {
|
|
641
|
+
// the one typed value a desk takes: a name to invite (never a reason)
|
|
642
|
+
var input = node('input', 'desk-field');
|
|
643
|
+
input.setAttribute('type', 'text');
|
|
644
|
+
input.setAttribute('aria-label', asked.ask);
|
|
645
|
+
input.setAttribute('autocomplete', 'off');
|
|
646
|
+
input.setAttribute('maxlength', '60');
|
|
647
|
+
var send = function () { var v = input.value.trim(); if (v) deskAct(r, asked.for, { option: v, label: v }); };
|
|
648
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') send(); });
|
|
649
|
+
chips.appendChild(input);
|
|
650
|
+
chips.appendChild(chipBtn('desk-chip--primary', 'send', send));
|
|
651
|
+
}
|
|
639
652
|
row.appendChild(chips);
|
|
640
653
|
}
|
|
641
654
|
return row;
|
|
@@ -666,7 +679,13 @@
|
|
|
666
679
|
if (typeof setTimeout !== 'function') return then();
|
|
667
680
|
setTimeout(then, 320);
|
|
668
681
|
}
|
|
669
|
-
var DONE_WORDS = {
|
|
682
|
+
var DONE_WORDS = {
|
|
683
|
+
for: 'voted yes', object: 'voted no', fine: 'let in', refuse: 'turned away', release: 'released', hold: 'held',
|
|
684
|
+
confirm: 'confirmed', dispute: 'disputed', fix: 'fixed', remove: 'removed', keep: 'kept', signoff: 'signed off', force: 'forced',
|
|
685
|
+
switch_on: 'switched on', switch_off: 'switched off', allow: 'allowed', disallow: 'disallowed', first: 'put first',
|
|
686
|
+
pause: 'paused', resume: 'resumed', tier: 'new tier', deactivate: 'deactivated', give: 'gave', take: 'took back',
|
|
687
|
+
check: 'checked', rot: 'set', module_on: 'turned on', module_off: 'turned off', backup: 'backed up', invite: 'invited',
|
|
688
|
+
};
|
|
670
689
|
// One act from a row: checked, sent through the card's own request (the
|
|
671
690
|
// server's permission checks decide), then the row clears, the floor counts
|
|
672
691
|
// it, the meter takes a pip and the toast says what happened. An act that
|
|
@@ -55,14 +55,37 @@
|
|
|
55
55
|
"auth": true,
|
|
56
56
|
"_": "In the hall since task 1004544: the shell's full-screen switch pressed, the sidebar and the top bar gone, the street filling the window and the one exit in the corner (the sky's mechanism, task 1004327).",
|
|
57
57
|
"actions": [
|
|
58
|
-
[
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
[
|
|
59
|
+
"wait",
|
|
60
|
+
900
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"click",
|
|
64
|
+
"#fullscreen-toggle"
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"wait",
|
|
68
|
+
500
|
|
69
|
+
]
|
|
61
70
|
],
|
|
62
71
|
"expect": {
|
|
63
|
-
"visible": [
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
"visible": [
|
|
73
|
+
"#streetBoard",
|
|
74
|
+
"#streetSigns",
|
|
75
|
+
"#streetMode",
|
|
76
|
+
"#fullscreen-toggle"
|
|
77
|
+
],
|
|
78
|
+
"hidden": [
|
|
79
|
+
"#app-sidebar",
|
|
80
|
+
"#topbar-title"
|
|
81
|
+
],
|
|
82
|
+
"attr": [
|
|
83
|
+
[
|
|
84
|
+
"#fullscreen-toggle",
|
|
85
|
+
"aria-pressed",
|
|
86
|
+
"true"
|
|
87
|
+
]
|
|
88
|
+
]
|
|
66
89
|
}
|
|
67
90
|
},
|
|
68
91
|
"card": {
|
|
@@ -456,6 +479,208 @@
|
|
|
456
479
|
".desk-quiet"
|
|
457
480
|
]
|
|
458
481
|
}
|
|
482
|
+
},
|
|
483
|
+
"desk-people-builders": {
|
|
484
|
+
"auth": true,
|
|
485
|
+
"_": "Task 1004559: People, Builders floor. Each builder is a row with tier and deactivate, the acts the old builders page held.",
|
|
486
|
+
"url": "/builders/street#building=people",
|
|
487
|
+
"actions": [
|
|
488
|
+
[
|
|
489
|
+
"wait",
|
|
490
|
+
900
|
|
491
|
+
],
|
|
492
|
+
[
|
|
493
|
+
"clickIfVisible",
|
|
494
|
+
"[data-craft-close]"
|
|
495
|
+
],
|
|
496
|
+
[
|
|
497
|
+
"wait",
|
|
498
|
+
300
|
|
499
|
+
],
|
|
500
|
+
[
|
|
501
|
+
"click",
|
|
502
|
+
"[data-floor=\"2\"]"
|
|
503
|
+
],
|
|
504
|
+
[
|
|
505
|
+
"wait",
|
|
506
|
+
600
|
|
507
|
+
]
|
|
508
|
+
],
|
|
509
|
+
"expect": {
|
|
510
|
+
"visible": [
|
|
511
|
+
".desk-rows",
|
|
512
|
+
".desk-chip--primary"
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
"desk-people-tier": {
|
|
517
|
+
"auth": true,
|
|
518
|
+
"_": "Task 1004559: the tier button opens its tiers as one-tap chips in place (one rung up at most).",
|
|
519
|
+
"url": "/builders/street#building=people",
|
|
520
|
+
"actions": [
|
|
521
|
+
[
|
|
522
|
+
"wait",
|
|
523
|
+
900
|
|
524
|
+
],
|
|
525
|
+
[
|
|
526
|
+
"clickIfVisible",
|
|
527
|
+
"[data-craft-close]"
|
|
528
|
+
],
|
|
529
|
+
[
|
|
530
|
+
"wait",
|
|
531
|
+
300
|
|
532
|
+
],
|
|
533
|
+
[
|
|
534
|
+
"click",
|
|
535
|
+
"[data-floor=\"2\"]"
|
|
536
|
+
],
|
|
537
|
+
[
|
|
538
|
+
"wait",
|
|
539
|
+
600
|
|
540
|
+
],
|
|
541
|
+
[
|
|
542
|
+
"click",
|
|
543
|
+
".desk-row .desk-chip--primary"
|
|
544
|
+
],
|
|
545
|
+
[
|
|
546
|
+
"wait",
|
|
547
|
+
400
|
|
548
|
+
]
|
|
549
|
+
],
|
|
550
|
+
"expect": {
|
|
551
|
+
"visible": [
|
|
552
|
+
".desk-choices"
|
|
553
|
+
]
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
"desk-disputes": {
|
|
557
|
+
"auth": true,
|
|
558
|
+
"_": "Task 1004559: Disputes, Security reports. Confirm or dispute an open report; mark a confirmed one fixed.",
|
|
559
|
+
"url": "/builders/street#building=disputes",
|
|
560
|
+
"actions": [
|
|
561
|
+
[
|
|
562
|
+
"wait",
|
|
563
|
+
900
|
|
564
|
+
],
|
|
565
|
+
[
|
|
566
|
+
"clickIfVisible",
|
|
567
|
+
"[data-craft-close]"
|
|
568
|
+
],
|
|
569
|
+
[
|
|
570
|
+
"wait",
|
|
571
|
+
300
|
|
572
|
+
],
|
|
573
|
+
[
|
|
574
|
+
"click",
|
|
575
|
+
"[data-floor=\"0\"]"
|
|
576
|
+
],
|
|
577
|
+
[
|
|
578
|
+
"wait",
|
|
579
|
+
600
|
|
580
|
+
]
|
|
581
|
+
],
|
|
582
|
+
"expect": {
|
|
583
|
+
"visible": [
|
|
584
|
+
".desk-rows",
|
|
585
|
+
".desk-status--hot"
|
|
586
|
+
]
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
"desk-automation-goals": {
|
|
590
|
+
"auth": true,
|
|
591
|
+
"_": "Task 1004559: Automation, Allowed goals. Disallow, put first, or allow an open goal from chips.",
|
|
592
|
+
"url": "/builders/street#building=automation",
|
|
593
|
+
"actions": [
|
|
594
|
+
[
|
|
595
|
+
"wait",
|
|
596
|
+
900
|
|
597
|
+
],
|
|
598
|
+
[
|
|
599
|
+
"clickIfVisible",
|
|
600
|
+
"[data-craft-close]"
|
|
601
|
+
],
|
|
602
|
+
[
|
|
603
|
+
"wait",
|
|
604
|
+
300
|
|
605
|
+
],
|
|
606
|
+
[
|
|
607
|
+
"click",
|
|
608
|
+
"[data-floor=\"1\"]"
|
|
609
|
+
],
|
|
610
|
+
[
|
|
611
|
+
"wait",
|
|
612
|
+
600
|
|
613
|
+
]
|
|
614
|
+
],
|
|
615
|
+
"expect": {
|
|
616
|
+
"visible": [
|
|
617
|
+
".desk-rows"
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
"desk-release-core": {
|
|
622
|
+
"auth": true,
|
|
623
|
+
"_": "Task 1004559: Release, Core update. Check first (a dry run); take it is offered only after the check passed for that version.",
|
|
624
|
+
"url": "/builders/street#building=release",
|
|
625
|
+
"actions": [
|
|
626
|
+
[
|
|
627
|
+
"wait",
|
|
628
|
+
900
|
|
629
|
+
],
|
|
630
|
+
[
|
|
631
|
+
"clickIfVisible",
|
|
632
|
+
"[data-craft-close]"
|
|
633
|
+
],
|
|
634
|
+
[
|
|
635
|
+
"wait",
|
|
636
|
+
300
|
|
637
|
+
],
|
|
638
|
+
[
|
|
639
|
+
"click",
|
|
640
|
+
"[data-floor=\"3\"]"
|
|
641
|
+
],
|
|
642
|
+
[
|
|
643
|
+
"wait",
|
|
644
|
+
600
|
|
645
|
+
]
|
|
646
|
+
],
|
|
647
|
+
"expect": {
|
|
648
|
+
"visible": [
|
|
649
|
+
".desk-rows"
|
|
650
|
+
]
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
"desk-rules": {
|
|
654
|
+
"auth": true,
|
|
655
|
+
"_": "Task 1004559: Rules in force. The rot timer as day chips, work categories as a link, each module on or off.",
|
|
656
|
+
"url": "/builders/street#building=rules",
|
|
657
|
+
"actions": [
|
|
658
|
+
[
|
|
659
|
+
"wait",
|
|
660
|
+
900
|
|
661
|
+
],
|
|
662
|
+
[
|
|
663
|
+
"clickIfVisible",
|
|
664
|
+
"[data-craft-close]"
|
|
665
|
+
],
|
|
666
|
+
[
|
|
667
|
+
"wait",
|
|
668
|
+
300
|
|
669
|
+
],
|
|
670
|
+
[
|
|
671
|
+
"click",
|
|
672
|
+
"[data-floor=\"0\"]"
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
"wait",
|
|
676
|
+
600
|
|
677
|
+
]
|
|
678
|
+
],
|
|
679
|
+
"expect": {
|
|
680
|
+
"visible": [
|
|
681
|
+
".desk-rows"
|
|
682
|
+
]
|
|
683
|
+
}
|
|
459
684
|
}
|
|
460
685
|
},
|
|
461
686
|
"reducedMotion": {
|
|
@@ -58,10 +58,29 @@ function removalMatter(r) {
|
|
|
58
58
|
tally: null,
|
|
59
59
|
href: `/task/${encodeURIComponent(r.proposal_task_id)}`,
|
|
60
60
|
ref: { type: 'task', id: r.proposal_task_id },
|
|
61
|
+
// POST /removal-proposals/:id/decide, the docket source's own atom (task 1004559)
|
|
62
|
+
acts: [{ key: 'remove' }, { key: 'keep' }],
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
// The one-tap reasons a force sign-off may give (POST /done-when/:id/satisfy
|
|
67
|
+
// requires one); the desk sends the label as the override reason.
|
|
68
|
+
const FORCE_REASONS = Object.freeze([
|
|
69
|
+
{ key: 'tested_by_hand', label: 'tested by hand' },
|
|
70
|
+
{ key: 'checked_review', label: 'seen in review' },
|
|
71
|
+
{ key: 'no_longer', label: 'no longer applies' },
|
|
72
|
+
]);
|
|
73
|
+
// What the viewer may do on a criterion waiting for a tester (task 1004559). A
|
|
74
|
+
// backend-only criterion signs off in one tap (with the live attestation as its
|
|
75
|
+
// chip); a user-facing one needs a recording, so its act goes to the goal's page.
|
|
76
|
+
// Force sign-off only for a holder of criterion.satisfy, that route's own atom.
|
|
77
|
+
function uatActs(c, { force = false } = {}) {
|
|
78
|
+
const acts = [c.backend_only === true ? { key: 'signoff', options: [{ key: 'live', label: 'tested it live' }] } : { key: 'record' }];
|
|
79
|
+
if (force) acts.push({ key: 'force', options: FORCE_REASONS });
|
|
80
|
+
return acts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function uatMatter(c, opts) {
|
|
65
84
|
return {
|
|
66
85
|
id: c.id,
|
|
67
86
|
rule: 'signoff',
|
|
@@ -75,6 +94,7 @@ function uatMatter(c) {
|
|
|
75
94
|
tally: { for: c.shipped_total, against: 0, open: 0, needs: 'one sign-off by someone who did not build it' },
|
|
76
95
|
href: c.goal_id ? `/goals#/goal/${encodeURIComponent(c.goal_id)}` : '/goals',
|
|
77
96
|
ref: { type: 'criterion', id: c.id },
|
|
97
|
+
acts: uatActs(c, opts),
|
|
78
98
|
};
|
|
79
99
|
}
|
|
80
100
|
|
|
@@ -326,7 +346,8 @@ function contributeLifecycleToDocket() {
|
|
|
326
346
|
source: 'criteria_uat',
|
|
327
347
|
building: 'review',
|
|
328
348
|
gate: { permission: 'criterion.review' },
|
|
329
|
-
read: async () => (await doneWhen.listPendingReviewCriteria({})).filter((c) => c.awaiting_uat === true)
|
|
349
|
+
read: async (ctx) => (await doneWhen.listPendingReviewCriteria({})).filter((c) => c.awaiting_uat === true)
|
|
350
|
+
.map((c) => uatMatter(c, { force: ctx.has('criterion.satisfy') })),
|
|
330
351
|
});
|
|
331
352
|
// Own-scoped by its read, like GET /goals/inbox: a goal's lead sees the requests
|
|
332
353
|
// to join their goals, and everyone sees their own pending ones. An Archon is
|
|
@@ -371,6 +392,7 @@ module.exports = {
|
|
|
371
392
|
overrideMatter,
|
|
372
393
|
removalMatter,
|
|
373
394
|
uatMatter,
|
|
395
|
+
uatActs,
|
|
374
396
|
joinRequestMatter,
|
|
375
397
|
flowReading,
|
|
376
398
|
promisesReading,
|
|
@@ -134,7 +134,29 @@ function upmarketRow() {
|
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// The Rules desk's settings rows (task 1004559), under this page's own atom: the
|
|
138
|
+
// rot timer as one-tap day counts (PATCH /project-settings/rot-days), and the
|
|
139
|
+
// work categories as a link to this page, since a category is named in words.
|
|
140
|
+
// Pure; exported for tests.
|
|
141
|
+
const ROT_CHOICES = Object.freeze([7, 14, 30, 60, 90]);
|
|
142
|
+
function settingsDeskRows({ days, categories }) {
|
|
143
|
+
const live = (categories || []).filter((c) => c && c.active !== false).length;
|
|
144
|
+
return [
|
|
145
|
+
{ id: 'rot', name: 'Rot timer', fact: `stale after ${days} ${days === 1 ? 'day' : 'days'}`, facts: ['raising it only asks a person'], tool: true, ref: { type: 'project_setting', id: 'rot_days' },
|
|
146
|
+
acts: [{ key: 'rot', options: ROT_CHOICES.filter((d) => d !== Number(days)).slice(0, 4).map((d) => ({ key: String(d), label: `${d} days` })) }] },
|
|
147
|
+
{ id: 'categories', name: 'Work categories', fact: `${live} in use`, tool: true, href: '/project-settings', ref: { type: 'project_setting', id: 'work_categories' }, acts: [{ key: 'open' }] },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
function contributeSettingsDesk() {
|
|
151
|
+
api.contribute('governor.docket', {
|
|
152
|
+
desk: 'rules.settings',
|
|
153
|
+
gate: { permission: 'project.settings.manage' },
|
|
154
|
+
read: async () => settingsDeskRows({ days: (await api.resolveRotDays()).days, categories: await goalCategory.listCategories({ includeInactive: true }) }),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
module.exports = function buildProjectSettingsRouter() {
|
|
159
|
+
contributeSettingsDesk();
|
|
138
160
|
const router = express.Router();
|
|
139
161
|
|
|
140
162
|
// GET /project-settings — all five rows in ONE read.
|
|
@@ -261,3 +283,5 @@ module.exports = function buildProjectSettingsRouter() {
|
|
|
261
283
|
|
|
262
284
|
return router;
|
|
263
285
|
};
|
|
286
|
+
|
|
287
|
+
module.exports.settingsDeskRows = settingsDeskRows;
|