@bongos/core 1.21.19 → 1.21.21
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 +84 -74
- package/docs/copy-inventory.md +29 -25
- package/docs/copy-registry.json +87 -30
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +9 -3
- package/docs/page-readings.json +399 -388
- package/modules/autonomy/governor-docket.js +118 -4
- package/modules/economy/cost.js +36 -0
- package/modules/economy/governor-docket.js +81 -0
- package/modules/economy/routes/cost.js +2 -0
- package/modules/government/docket-floors.js +222 -0
- package/modules/government/docket-vocab.js +20 -0
- package/modules/government/docket.js +186 -5
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.css +16 -2
- package/modules/hall-ui/public/city.html +29 -9
- package/modules/hall-ui/public/city.js +25 -2
- package/modules/hall-ui/public/city.states.json +7 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/street-inside.js +99 -43
- package/modules/hall-ui/public/street.css +17 -4
- package/modules/hall-ui/public/street.html +26 -8
- package/modules/hall-ui/public/street.js +16 -2
- package/modules/hall-ui/public/street.states.json +17 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/ideas/governor-docket.js +42 -1
- package/modules/lifecycle/governor-docket.js +197 -2
- package/modules/security/routes/reports.js +16 -0
- package/modules/security/security.js +5 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +186 -2
- package/tests/hall_audit.mjs +7 -12
- package/tests/hall_city.mjs +44 -4
- package/tests/hall_street.mjs +37 -5
- package/tests/hall_street_inside.mjs +97 -3
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -29,6 +29,17 @@
|
|
|
29
29
|
//
|
|
30
30
|
// { reading: 'flow', gate: …, read: async (ctx) => { figure, line, value, hot } }
|
|
31
31
|
//
|
|
32
|
+
// { floor: 'release.live_month', gate: …, // one of docket-vocab FLOORS
|
|
33
|
+
// read: async (ctx) => { count, line, lines: [[text, meta]], hot } }
|
|
34
|
+
//
|
|
35
|
+
// { history: 'releases', building: 'release', gate: …, // the record drawers
|
|
36
|
+
// read: async (ctx) => [{ id, title, at, outcome }, …] }
|
|
37
|
+
//
|
|
38
|
+
// A FLOOR (task 1004527) is a state rather than a queue — who sits, what the
|
|
39
|
+
// month cost, what went live — read by the module that owns it, under the same
|
|
40
|
+
// atom as its own route. HISTORY is what closed: rulings, amendments, releases,
|
|
41
|
+
// sign-offs, payouts; the building interior files it into drawers by month.
|
|
42
|
+
//
|
|
32
43
|
// `ctx` is { builderId, rank, has(permission) } — `has` answers from the
|
|
33
44
|
// viewer's permission set resolved ONCE for this request, server-side and
|
|
34
45
|
// uncached (ADR 0016). A contribution with no recognisable gate is DROPPED, never
|
|
@@ -61,9 +72,15 @@ const db = require('./db');
|
|
|
61
72
|
const { resolveBoardMembers } = require('./board-membership');
|
|
62
73
|
const resolver = require('./resolver');
|
|
63
74
|
const decisionRules = require('./decision-rules');
|
|
75
|
+
const docketFloors = require('./docket-floors');
|
|
76
|
+
const governmentConfig = require('./config');
|
|
64
77
|
|
|
65
78
|
const log = api.logger('government');
|
|
66
79
|
|
|
80
|
+
// This module's own floors are answered under the docket route's own atom (the
|
|
81
|
+
// same one GET /government/constitution and the genesis stages require).
|
|
82
|
+
const GOVERNMENT_GATE = Object.freeze({ permission: 'board.vote.cast' });
|
|
83
|
+
|
|
67
84
|
const CONTRIBUTION_POINT = 'governor.docket';
|
|
68
85
|
|
|
69
86
|
// Every source the reference design names, with the building it stands in. The
|
|
@@ -94,6 +111,13 @@ const COMING_SOURCES = Object.freeze([
|
|
|
94
111
|
reason: 'The project has no budget or spending votes recorded yet.' }),
|
|
95
112
|
]);
|
|
96
113
|
|
|
114
|
+
// Floors the design names whose engine does not exist yet. Drawn, and say why.
|
|
115
|
+
const COMING_FLOORS = Object.freeze([
|
|
116
|
+
Object.freeze({ floor: 'charter.purpose', reason: 'The charter has no written purpose yet. A section for it is coming.' }),
|
|
117
|
+
]);
|
|
118
|
+
const MAX_FLOOR_LINES = 6;
|
|
119
|
+
const MAX_HISTORY_PER_SOURCE = 60;
|
|
120
|
+
|
|
97
121
|
|
|
98
122
|
// A source read that has not answered by now is reported unavailable rather than
|
|
99
123
|
// holding the whole city hostage (one source calls GitHub, one the npm registry).
|
|
@@ -151,6 +175,31 @@ function normalizeMatter(raw, { source, building }) {
|
|
|
151
175
|
};
|
|
152
176
|
}
|
|
153
177
|
|
|
178
|
+
// One floor as a contributor handed it → { count, line, lines, hot }. Pure.
|
|
179
|
+
// Anything that is not a plain object is not a floor (→ null, read as
|
|
180
|
+
// unavailable); a count is kept only as the short figure it was given.
|
|
181
|
+
function normalizeFloor(raw) {
|
|
182
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
183
|
+
const str = (v, n) => (v == null ? '' : String(v).slice(0, n));
|
|
184
|
+
const count = raw.count == null || raw.count === '' ? '' : str(raw.count, 12);
|
|
185
|
+
const lines = (Array.isArray(raw.lines) ? raw.lines : [])
|
|
186
|
+
.filter((l) => Array.isArray(l) && l[0] != null && String(l[0]).trim())
|
|
187
|
+
.slice(0, MAX_FLOOR_LINES)
|
|
188
|
+
.map((l) => [str(l[0], 200), str(l[1], 40)]);
|
|
189
|
+
return { count, line: str(raw.line, 90) || null, lines, hot: raw.hot === true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// One closed thing for the record → the docket's one shape. Pure. A row with no
|
|
193
|
+
// title or no readable date is dropped: an undated record is not a record.
|
|
194
|
+
function normalizeHistory(raw, { source, building }) {
|
|
195
|
+
if (!raw || typeof raw !== 'object' || raw.id == null || !raw.title) return null;
|
|
196
|
+
const at = iso(raw.at);
|
|
197
|
+
if (!at) return null;
|
|
198
|
+
const b = vocab.BUILDING_KEYS.includes(raw.building) ? raw.building : building;
|
|
199
|
+
if (!vocab.BUILDING_KEYS.includes(b)) return null;
|
|
200
|
+
return { id: `${source}:${raw.id}`, source, building: b, title: String(raw.title).slice(0, 200), at, outcome: raw.outcome ? String(raw.outcome).slice(0, 40) : null };
|
|
201
|
+
}
|
|
202
|
+
|
|
154
203
|
// Does this viewer pass this gate? Fail-closed: anything unrecognised is false.
|
|
155
204
|
function gateAllows(gate, ctx) {
|
|
156
205
|
if (!gate || typeof gate !== 'object') return false;
|
|
@@ -346,12 +395,16 @@ function withTimeout(promise, ms) {
|
|
|
346
395
|
function registeredContributions() {
|
|
347
396
|
const sources = new Map();
|
|
348
397
|
const readings = new Map();
|
|
398
|
+
const floors = new Map();
|
|
399
|
+
const histories = new Map();
|
|
349
400
|
for (const c of api.contributions(CONTRIBUTION_POINT)) {
|
|
350
401
|
if (!c || typeof c !== 'object' || typeof c.read !== 'function' || !hasValidGate(c.gate)) continue;
|
|
351
402
|
if (typeof c.source === 'string' && c.source) sources.set(c.source, c);
|
|
352
403
|
else if (typeof c.reading === 'string' && c.reading) readings.set(c.reading, c);
|
|
404
|
+
else if (typeof c.floor === 'string' && c.floor) floors.set(c.floor, c);
|
|
405
|
+
else if (typeof c.history === 'string' && c.history && vocab.BUILDING_KEYS.includes(c.building)) histories.set(c.history, c);
|
|
353
406
|
}
|
|
354
|
-
return { sources, readings };
|
|
407
|
+
return { sources, readings, floors, histories };
|
|
355
408
|
}
|
|
356
409
|
|
|
357
410
|
// ── the collector ────────────────────────────────────────────────────────────
|
|
@@ -362,13 +415,22 @@ function registeredContributions() {
|
|
|
362
415
|
// `decisions` is the resolved decision-rules table (board.decisionRulesView), or
|
|
363
416
|
// null when it could not be read — each building's `decision` is then null
|
|
364
417
|
// rather than a guess.
|
|
365
|
-
|
|
418
|
+
// `government` is what docketFor() read once for this module's own floors and
|
|
419
|
+
// record (loadGovernment below); null leaves those floors `unavailable`.
|
|
420
|
+
async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null } = {}) {
|
|
366
421
|
const held = new Set(permissions || []);
|
|
367
422
|
const ctx = Object.freeze({
|
|
368
423
|
builderId: String(viewer.id),
|
|
369
424
|
rank: viewer.rank || null,
|
|
370
425
|
has: (p) => held.has(p),
|
|
371
426
|
});
|
|
427
|
+
// A per-request memo every read may use, so two floors that need the same rows
|
|
428
|
+
// share one query (task 1004527). Scoped to this call: nothing outlives it.
|
|
429
|
+
const memo = new Map();
|
|
430
|
+
const once = (key, fn) => {
|
|
431
|
+
if (!memo.has(key)) memo.set(key, Promise.resolve().then(fn));
|
|
432
|
+
return memo.get(key);
|
|
433
|
+
};
|
|
372
434
|
const sources = new Map(contributions.sources);
|
|
373
435
|
sources.set('board', BOARD_SOURCE); // the board is read here, never contributed
|
|
374
436
|
const comingKeys = new Set(COMING_SOURCES.map((c) => c.source));
|
|
@@ -392,7 +454,7 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
392
454
|
}
|
|
393
455
|
if (!gateAllows(c.gate, ctx)) { row.status = 'not_yours'; return; }
|
|
394
456
|
try {
|
|
395
|
-
const raw = await withTimeout(Promise.resolve(c.read(ctx, { now })), timeoutMs);
|
|
457
|
+
const raw = await withTimeout(Promise.resolve(c.read(ctx, { now, once })), timeoutMs);
|
|
396
458
|
const list = (Array.isArray(raw) ? raw : []).map((m) => normalizeMatter(m, { source: decl.source, building: c.building || decl.building })).filter(Boolean);
|
|
397
459
|
row.status = 'live';
|
|
398
460
|
row.count = list.length;
|
|
@@ -421,7 +483,7 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
421
483
|
if (!impl) return { ...base, status: 'off', reason: 'The module that holds this is not enabled here.' };
|
|
422
484
|
if (!gateAllows(impl.gate, ctx)) return { ...base, status: 'not_yours' };
|
|
423
485
|
try {
|
|
424
|
-
const r = await withTimeout(Promise.resolve(impl.read(ctx, { now, matters })), timeoutMs);
|
|
486
|
+
const r = await withTimeout(Promise.resolve(impl.read(ctx, { now, matters, once })), timeoutMs);
|
|
425
487
|
if (!r) return { ...base, status: 'unavailable', reason: 'It could not be read just now.' };
|
|
426
488
|
return {
|
|
427
489
|
...base,
|
|
@@ -437,17 +499,83 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
437
499
|
}
|
|
438
500
|
}));
|
|
439
501
|
|
|
502
|
+
// Floors (task 1004527): contributed, this module's own (answered from
|
|
503
|
+
// `government`, under the docket's own atom), and Ideas to ratify (from the
|
|
504
|
+
// board's matters, so it can never disagree with the pins).
|
|
505
|
+
const govAllowed = gateAllows(GOVERNMENT_GATE, ctx);
|
|
506
|
+
const comingFloors = new Map(COMING_FLOORS.map((c) => [c.floor, c.reason]));
|
|
507
|
+
const floorImpls = new Map(contributions.floors || []);
|
|
508
|
+
const boardRow = sourceStatus.find((s) => s.source === 'board');
|
|
509
|
+
const floorsP = Promise.all(vocab.FLOORS.map(async (decl) => {
|
|
510
|
+
const base = { key: decl.key, building: decl.building, status: null, count: '', line: null, lines: [], hot: false, reason: null };
|
|
511
|
+
const live = (f) => (f ? { ...base, status: 'live', ...f } : { ...base, status: 'unavailable', reason: 'It could not be read just now.' });
|
|
512
|
+
if (decl.key === 'planning.ideas') {
|
|
513
|
+
if (!boardRow || boardRow.status !== 'live') return { ...base, status: boardRow ? boardRow.status : 'unavailable', reason: boardRow ? boardRow.reason : null };
|
|
514
|
+
return live(normalizeFloor(docketFloors.ideasFloor(matters)));
|
|
515
|
+
}
|
|
516
|
+
const own = docketFloors.BUILDERS[decl.key];
|
|
517
|
+
if (own) {
|
|
518
|
+
if (!govAllowed) return { ...base, status: 'not_yours' };
|
|
519
|
+
let f = null;
|
|
520
|
+
try { f = government ? normalizeFloor(own(government)) : null; } catch (err) { log.warn({ floor: decl.key, err: err && err.message }, 'governor docket floor failed'); }
|
|
521
|
+
return live(f);
|
|
522
|
+
}
|
|
523
|
+
const impl = floorImpls.get(decl.key);
|
|
524
|
+
if (!impl) {
|
|
525
|
+
return comingFloors.has(decl.key)
|
|
526
|
+
? { ...base, status: 'coming', reason: comingFloors.get(decl.key) }
|
|
527
|
+
: { ...base, status: 'off', reason: 'The module that holds this is not enabled here.' };
|
|
528
|
+
}
|
|
529
|
+
if (!gateAllows(impl.gate, ctx)) return { ...base, status: 'not_yours' };
|
|
530
|
+
try {
|
|
531
|
+
return live(normalizeFloor(await withTimeout(Promise.resolve(impl.read(ctx, { now, once })), timeoutMs)));
|
|
532
|
+
} catch (err) {
|
|
533
|
+
log.warn({ floor: decl.key, err: err && err.message }, 'governor docket floor failed');
|
|
534
|
+
return { ...base, status: 'unavailable', reason: err && err.message === 'timeout' ? 'It did not answer in time.' : 'It could not be read just now.' };
|
|
535
|
+
}
|
|
536
|
+
}));
|
|
537
|
+
|
|
538
|
+
// The record: what closed, from every module that keeps it, newest first.
|
|
539
|
+
const history = [];
|
|
540
|
+
const historySources = [];
|
|
541
|
+
if (govAllowed && government) {
|
|
542
|
+
history.push(...docketFloors.governmentHistory(government).map((h) => normalizeHistory(h, { source: 'government', building: h.building })).filter(Boolean));
|
|
543
|
+
historySources.push({ source: 'government', status: 'live' });
|
|
544
|
+
}
|
|
545
|
+
const historyP = Promise.all([...(contributions.histories || new Map())].map(async ([key, c]) => {
|
|
546
|
+
if (!gateAllows(c.gate, ctx)) { historySources.push({ source: key, building: c.building, status: 'not_yours' }); return; }
|
|
547
|
+
try {
|
|
548
|
+
const raw = await withTimeout(Promise.resolve(c.read(ctx, { now, once })), timeoutMs);
|
|
549
|
+
const list = (Array.isArray(raw) ? raw : []).slice(0, MAX_HISTORY_PER_SOURCE).map((h) => normalizeHistory(h, { source: key, building: c.building })).filter(Boolean);
|
|
550
|
+
history.push(...list);
|
|
551
|
+
historySources.push({ source: key, building: c.building, status: 'live' });
|
|
552
|
+
} catch (err) {
|
|
553
|
+
log.warn({ history: key, err: err && err.message }, 'governor docket history failed');
|
|
554
|
+
historySources.push({ source: key, building: c.building, status: 'unavailable' });
|
|
555
|
+
}
|
|
556
|
+
}));
|
|
557
|
+
const [floors] = await Promise.all([floorsP, historyP]);
|
|
558
|
+
history.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : a.id < b.id ? -1 : 1));
|
|
559
|
+
historySources.sort((a, b) => (a.source < b.source ? -1 : 1));
|
|
560
|
+
|
|
440
561
|
const counts = countStates(matters);
|
|
441
562
|
const decisionBy = new Map((Array.isArray(decisions) ? decisions : []).filter(Boolean).map((d) => [d.area, d]));
|
|
563
|
+
const holdersBy = govAllowed && government && government.holders ? government.holders : {};
|
|
442
564
|
const buildings = vocab.BUILDINGS.map((b) => {
|
|
443
565
|
const here = matters.filter((m) => m.building === b.key);
|
|
444
566
|
const d = decisionBy.get(b.key) || null;
|
|
567
|
+
const held = Array.isArray(holdersBy[b.key]) ? holdersBy[b.key] : null;
|
|
445
568
|
return {
|
|
446
569
|
key: b.key, name: b.name, group: b.group, counts: countStates(here), matter_ids: here.map((m) => m.id),
|
|
447
570
|
decision: d ? {
|
|
448
571
|
who: d.who, rule: d.rule, rule_words: d.rule_words, decided_by: d.decided_by,
|
|
449
572
|
as_today: d.as_today, sitting: d.sitting, set_by_item_id: d.set_by_item_id,
|
|
450
573
|
} : null,
|
|
574
|
+
// Who holds the seat now, as the row seats them (task 1004527): the board's
|
|
575
|
+
// members, a sitting's predicate, or the holders of the fixed gate's atom.
|
|
576
|
+
// null when it could not be resolved — never an invented seat.
|
|
577
|
+
holders: held ? held.slice(0, 12).map((h) => person({ id: h.builder_id ?? h.id, login: h.github_login ?? h.login, name: h.display_name ?? h.name })).filter(Boolean) : null,
|
|
578
|
+
holder_count: held ? held.length : null,
|
|
451
579
|
};
|
|
452
580
|
});
|
|
453
581
|
return {
|
|
@@ -459,6 +587,9 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
459
587
|
buildings,
|
|
460
588
|
sources: sourceStatus,
|
|
461
589
|
readings,
|
|
590
|
+
floors,
|
|
591
|
+
history,
|
|
592
|
+
history_sources: historySources,
|
|
462
593
|
vocabulary: {
|
|
463
594
|
buildings: vocab.BUILDINGS, rules: vocab.RULES, states: vocab.STATES,
|
|
464
595
|
// The table's rule set — the matter glyphs plus vacant (no one holds it).
|
|
@@ -477,14 +608,64 @@ async function docketFor(builder) {
|
|
|
477
608
|
log.warn({ err: err && err.message }, 'governor docket: decision rules unreadable');
|
|
478
609
|
return null;
|
|
479
610
|
});
|
|
480
|
-
|
|
611
|
+
const government = permissions.includes(GOVERNMENT_GATE.permission) ? await loadGovernment({ decisions }) : null;
|
|
612
|
+
return buildDocket({ viewer: builder, permissions, decisions, government });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// This module's own floors and record, read ONCE per request (task 1004527),
|
|
616
|
+
// each piece isolated: a read that fails leaves its piece null, which the pure
|
|
617
|
+
// builders in docket-floors.js turn into an `unavailable` floor.
|
|
618
|
+
// holders per building, the people its decision-rules row seats now: the
|
|
619
|
+
// board's members (charter), a sitting's predicate, or the active
|
|
620
|
+
// holders of a rank that grants the fixed gate's atom; [] when vacant.
|
|
621
|
+
async function loadGovernment({ decisions }) {
|
|
622
|
+
const safe = (p, what) => Promise.resolve().then(p).catch((err) => {
|
|
623
|
+
log.warn({ err: err && err.message, what }, 'governor docket: a government read failed');
|
|
624
|
+
return null;
|
|
625
|
+
});
|
|
626
|
+
const { board: live } = governmentConfig.loadGovernmentConfig();
|
|
627
|
+
const [members, genesis, rankRows, assignments, amendments, room] = await Promise.all([
|
|
628
|
+
safe(() => resolveBoardMembers(live), 'members'),
|
|
629
|
+
safe(() => board.genesisStageStatus(), 'genesis'),
|
|
630
|
+
safe(() => db.listRanksWithGrants(), 'ranks'),
|
|
631
|
+
safe(() => db.listAssignments(), 'assignments'),
|
|
632
|
+
safe(() => db.listAmendmentHistory(50), 'amendments'),
|
|
633
|
+
safe(() => board.boardRoomView({ closedLimit: 50 }), 'closed'),
|
|
634
|
+
]);
|
|
635
|
+
const ranks = rankRows && assignments ? rankRows.map((r) => ({
|
|
636
|
+
rank_key: r.rank_key, label: r.label,
|
|
637
|
+
holders: assignments.filter((a) => a.rank_key === r.rank_key).length,
|
|
638
|
+
})) : null;
|
|
639
|
+
let holders = null;
|
|
640
|
+
if (Array.isArray(decisions)) {
|
|
641
|
+
holders = {};
|
|
642
|
+
await Promise.all(decisions.filter(Boolean).map(async (d) => {
|
|
643
|
+
if (d.decided_by === 'board') { holders[d.area] = members; return; }
|
|
644
|
+
if (d.decided_by === 'nobody') { holders[d.area] = []; return; }
|
|
645
|
+
if (d.decided_by === 'gate') {
|
|
646
|
+
if (!rankRows || !d.gate) { holders[d.area] = null; return; }
|
|
647
|
+
const keys = rankRows.filter((r) => (r.permissions || []).includes(d.gate)).map((r) => r.rank_key);
|
|
648
|
+
holders[d.area] = keys.length ? await safe(() => db.getActiveBuildersWithRank(keys), `holders:${d.area}`) : [];
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
holders[d.area] = await safe(() => resolveBoardMembers({ membership: d.decided_by }), `holders:${d.area}`);
|
|
652
|
+
}));
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
board: live, decisions, members, genesis, ranks, assignments, amendments, holders,
|
|
656
|
+
closed: room && Array.isArray(room.closed) ? room.closed : null,
|
|
657
|
+
};
|
|
481
658
|
}
|
|
482
659
|
|
|
483
660
|
module.exports = {
|
|
484
661
|
CONTRIBUTION_POINT,
|
|
485
662
|
EXPECTED_SOURCES,
|
|
486
663
|
COMING_SOURCES,
|
|
664
|
+
COMING_FLOORS,
|
|
487
665
|
SOURCE_TIMEOUT_MS,
|
|
666
|
+
normalizeFloor,
|
|
667
|
+
normalizeHistory,
|
|
668
|
+
loadGovernment,
|
|
488
669
|
SETTLED_WINDOW_MS,
|
|
489
670
|
normalizeMatter,
|
|
490
671
|
gateAllows,
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
93
93
|
|
|
94
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
94
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
95
95
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
96
96
|
<script src="/builders/palette.js" defer></script>
|
|
97
97
|
<script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
52
|
chrome exists before the page script runs. -->
|
|
53
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
53
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
54
54
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
55
|
<script src="/builders/palette.js" defer></script>
|
|
56
56
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
62
62
|
|
|
63
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
63
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
64
64
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
65
65
|
<script src="/builders/palette.js" defer></script>
|
|
66
66
|
<script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
|
|
@@ -47,10 +47,24 @@ html:has(body[data-page="city"]) { height: 100%; }
|
|
|
47
47
|
.city-mode { background: none; border: 1px solid var(--rule); border-radius: 999px; height: 36px; padding: 0 11px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--ink); font-family: var(--font-body); }
|
|
48
48
|
.city-mode svg { display: block; stroke-width: 1.4; }
|
|
49
49
|
|
|
50
|
-
/*
|
|
50
|
+
/* THE STAGE IN THE HALL (task 1004544, the sky's mechanism from task 1004327).
|
|
51
|
+
<main class="city-main"> is the box under the hall's top bar and beside its
|
|
52
|
+
64px rail (the drawer's phone width has no rail) and, in full screen
|
|
53
|
+
(html[data-fullscreen="on"], shell.js), the whole window. `contain: layout
|
|
54
|
+
paint` makes it the containing block of every positioned layer inside, so
|
|
55
|
+
the home above and below measures from the stage instead of the window. */
|
|
56
|
+
.city-main { position: fixed; top: var(--topbar-h, 56px); left: var(--sidebar-w-rail, 64px); right: 0; bottom: 0; max-width: none; margin: 0; contain: layout paint; z-index: 1; }
|
|
57
|
+
html[data-fullscreen="on"] .city-main { top: 0; left: 0; }
|
|
58
|
+
@media (max-width: 768px) { .city-main { left: 0; } }
|
|
59
|
+
/* The full-screen switch floats top right over the stage, where the sun/moon
|
|
60
|
+
switch sits; the strip steps in by the button's width so nothing sits under it.
|
|
61
|
+
Under 640 the exit is the round button (style.css). */
|
|
62
|
+
html[data-fullscreen="on"] .city-top { right: 184px; }
|
|
63
|
+
@media (max-width: 640px) { html[data-fullscreen="on"] .city-top { right: 60px; } }
|
|
64
|
+
|
|
65
|
+
/* the stage: the prototype's 1440 x 900 artboard, scaled to the stage by
|
|
51
66
|
city.js (--city-scale) and centred, so 1440 x 900 is the prototype pixel
|
|
52
67
|
for pixel */
|
|
53
|
-
.city-main { position: fixed; inset: 0; z-index: 1; }
|
|
54
68
|
.city-frame { position: absolute; inset: 0; overflow: hidden; }
|
|
55
69
|
.city-stage { position: absolute; left: 50%; top: 50%; width: 1440px; height: 900px; transform: translate(-50%, -50%) scale(var(--city-scale, 1)); transform-origin: 50% 50%; }
|
|
56
70
|
.city-zoom, .city-drift { position: absolute; left: 0; top: 0; width: 1440px; height: 900px; }
|
|
@@ -20,7 +20,17 @@
|
|
|
20
20
|
/api/bongos/government/docket, task 1004523) and the open goals with their
|
|
21
21
|
task rollups. The markup below is the prototype's home: the wordmark and
|
|
22
22
|
the sun/moon switch in the header, the city, one button and one line.
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
IN THE HALL (task 1004544, owner decision 2026-10-02). The city sits in
|
|
25
|
+
the hall's sidebar and top bar like every hall page, the way the sky has
|
|
26
|
+
since task 1004327, and the shell's full-screen switch
|
|
27
|
+
(data-fullscreen-switch) hides both so the city fills the window again,
|
|
28
|
+
remembered for this page. The whole home lives inside
|
|
29
|
+
<main class="city-main">, the stage under the top bar and beside the
|
|
30
|
+
rail (city.css), so what is drawn inside is unchanged and simply measures
|
|
31
|
+
from the stage instead of the window. The wordmark strip is a <div>, not
|
|
32
|
+
a <header>: the shell's top bar is the page's one <header>. There is still
|
|
33
|
+
no menu link to the city (owner: leave it off the sidebar).
|
|
24
34
|
|
|
25
35
|
The drawing is city-draw.js (the prototype's iso kit, verbatim, as pure
|
|
26
36
|
data); city.js reads the three feeds and turns the lists into SVG paths
|
|
@@ -34,16 +44,22 @@
|
|
|
34
44
|
The page is gated on board.vote.cast (src/bongos/serve-internal.js), the
|
|
35
45
|
same atom the docket read checks, so whoever reaches the city can read
|
|
36
46
|
what it draws. -->
|
|
37
|
-
<body data-page="city" class="city">
|
|
47
|
+
<body data-page="city" data-page-title="The city" class="city" data-fullscreen-switch>
|
|
48
|
+
|
|
49
|
+
<div class="app">
|
|
50
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
51
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
52
|
+
<div class="app-body">
|
|
53
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
38
54
|
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
</
|
|
55
|
+
<main id="hall-main" class="city-main">
|
|
56
|
+
<div class="city-top">
|
|
57
|
+
<span class="city-mark">{{productName}}</span>
|
|
58
|
+
<button type="button" class="city-mode" id="cityMode" aria-label="Switch to light mode" title="Switch to light mode">
|
|
59
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" aria-hidden="true"><path id="cityModeIcon" d=""></path></svg>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
45
62
|
|
|
46
|
-
<main class="city-main" id="cityMain">
|
|
47
63
|
<div class="city-frame" id="cityFrame">
|
|
48
64
|
<div class="city-stage" id="cityStage" role="img" aria-label="The governor city">
|
|
49
65
|
<div class="city-zoom">
|
|
@@ -64,7 +80,11 @@
|
|
|
64
80
|
<h1 class="city-line" id="cityLine" aria-live="polite">reading the docket</h1>
|
|
65
81
|
</div>
|
|
66
82
|
</main>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
67
85
|
|
|
86
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
87
|
+
<script src="/builders/palette.js" defer></script>
|
|
68
88
|
<script src="/builders/city-draw.js?v=2026-10-02-city1"></script>
|
|
69
89
|
<script src="/builders/city.js?v=2026-10-02-city1"></script>
|
|
70
90
|
</body>
|
|
@@ -65,6 +65,24 @@
|
|
|
65
65
|
chosen = next;
|
|
66
66
|
try { window.localStorage.setItem(THEME_KEY, next); } catch (_) { /* private mode: this page only */ }
|
|
67
67
|
draw(applyTheme());
|
|
68
|
+
syncShell();
|
|
69
|
+
}
|
|
70
|
+
// IN THE HALL (task 1004544) the shell's top bar carries the hall's own theme
|
|
71
|
+
// switch beside this one. Both write the same 'hall-theme' preference and the
|
|
72
|
+
// same html[data-theme]; each repaints when the other flips it, so the two
|
|
73
|
+
// icons and the drawing's faces never disagree.
|
|
74
|
+
var painted = null;
|
|
75
|
+
function syncShell() {
|
|
76
|
+
var shell = window.OTBShell;
|
|
77
|
+
painted = effectiveTheme();
|
|
78
|
+
if (shell && typeof shell.applyTheme === 'function' && storedTheme() === painted) shell.applyTheme();
|
|
79
|
+
}
|
|
80
|
+
function watchShellTheme() {
|
|
81
|
+
if (typeof MutationObserver !== 'function') return;
|
|
82
|
+
new MutationObserver(function () {
|
|
83
|
+
var t = document.documentElement.getAttribute('data-theme');
|
|
84
|
+
if ((t === 'dark' || t === 'light') && t !== painted) { painted = t; chosen = t; draw(applyTheme()); }
|
|
85
|
+
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
// ---------- fit the 1440 x 900 artboard to the window ----------
|
|
@@ -73,7 +91,10 @@
|
|
|
73
91
|
function fit() {
|
|
74
92
|
var stage = $('cityStage');
|
|
75
93
|
if (!stage) return;
|
|
76
|
-
|
|
94
|
+
// The stage is <main class="city-main">, the box the hall's chrome leaves
|
|
95
|
+
// (task 1004544); the full-screen switch resizes it and fires a resize.
|
|
96
|
+
var main = $('hall-main');
|
|
97
|
+
var w = (main && main.clientWidth) || window.innerWidth || 1440, h = (main && main.clientHeight) || window.innerHeight || 900;
|
|
77
98
|
var s = Math.min(w / 1000, h / 900);
|
|
78
99
|
stage.style.setProperty('--city-scale', String(Math.round(s * 1000) / 1000));
|
|
79
100
|
}
|
|
@@ -168,7 +189,9 @@
|
|
|
168
189
|
if (media && media.addEventListener) media.addEventListener('change', function () { if (!storedTheme()) draw(applyTheme()); });
|
|
169
190
|
window.addEventListener('resize', fit);
|
|
170
191
|
fit();
|
|
171
|
-
|
|
192
|
+
painted = applyTheme();
|
|
193
|
+
draw(painted);
|
|
194
|
+
watchShellTheme();
|
|
172
195
|
return load();
|
|
173
196
|
}
|
|
174
197
|
|
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
"auth": true,
|
|
12
12
|
"_": "The home: the wordmark, the switch, the city, start governing and the one line.",
|
|
13
13
|
"actions": [["wait", 900]],
|
|
14
|
-
"expect": { "visible": ["#cityStage", "#cityStart", "#cityLine", "#cityMode"], "href": [["#cityStart", "/builders/street"]] }
|
|
14
|
+
"expect": { "visible": ["#cityStage", "#cityStart", "#cityLine", "#cityMode", "#app-topbar", "#fullscreen-toggle"], "href": [["#cityStart", "/builders/street"]] }
|
|
15
|
+
},
|
|
16
|
+
"fullscreen": {
|
|
17
|
+
"auth": true,
|
|
18
|
+
"_": "In the hall since task 1004544: the shell's full-screen switch pressed, the sidebar and the top bar gone, the city filling the window and the one exit in the corner (the sky's mechanism, task 1004327).",
|
|
19
|
+
"actions": [["wait", 900], ["click", "#fullscreen-toggle"], ["wait", 500]],
|
|
20
|
+
"expect": { "visible": ["#cityStage", "#cityStart", "#cityMode", "#fullscreen-toggle"], "hidden": ["#app-sidebar", "#topbar-title"], "attr": [["#fullscreen-toggle", "aria-pressed", "true"]] }
|
|
15
21
|
},
|
|
16
22
|
"switched": {
|
|
17
23
|
"auth": true,
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
|
|
149
149
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
150
150
|
|
|
151
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
151
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
152
152
|
<script src="/builders/palette.js" defer></script>
|
|
153
153
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
154
154
|
<script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
56
56
|
|
|
57
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
57
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
58
58
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
59
59
|
<script src="/builders/palette.js" defer></script>
|
|
60
60
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
|
|
178
178
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
179
179
|
|
|
180
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
180
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
181
181
|
<script src="/builders/palette.js" defer></script>
|
|
182
182
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
183
183
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
76
76
|
|
|
77
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
77
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
78
78
|
<script src="/builders/palette.js" defer></script>
|
|
79
79
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
80
80
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
|
|
98
98
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
99
99
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
100
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
100
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
101
101
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
102
102
|
<script src="/builders/palette.js" defer></script>
|
|
103
103
|
<script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
|
|
99
99
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
100
100
|
|
|
101
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
101
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
102
102
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
103
103
|
<script src="/builders/palette.js" defer></script>
|
|
104
104
|
<script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
56
56
|
|
|
57
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
57
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
58
58
|
<script src="/builders/palette.js" defer></script>
|
|
59
59
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
60
60
|
<script src="/builders/hall-kit.js"></script>
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
<div id="gate-modal-root"></div>
|
|
78
78
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
79
79
|
|
|
80
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
80
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
81
81
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
82
82
|
<script src="/builders/palette.js" defer></script>
|
|
83
83
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
</div>
|
|
138
138
|
</div>
|
|
139
139
|
|
|
140
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
140
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
141
141
|
<script src="/builders/palette.js" defer></script>
|
|
142
142
|
|
|
143
143
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
|
|
231
231
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
232
232
|
|
|
233
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
233
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
234
234
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
235
235
|
<script src="/builders/palette.js" defer></script>
|
|
236
236
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
|
|
135
135
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
136
136
|
|
|
137
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
137
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
138
138
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
139
139
|
<script src="/builders/palette.js" defer></script>
|
|
140
140
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
54
54
|
|
|
55
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
55
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
56
56
|
<script src="/builders/palette.js" defer></script>
|
|
57
57
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
|
|
58
58
|
<!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
|
|
226
226
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
227
227
|
|
|
228
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
228
|
+
<script src="/builders/shell.js?v=2026-10-02-theme"></script>
|
|
229
229
|
<script src="/builders/palette.js" defer></script>
|
|
230
230
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
231
231
|
<!-- task 1003069 (R08) — the quick-filing form's routed result links a task
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
<script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
|
|
107
107
|
<!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
|
|
108
108
|
BEFORE builders.js so the chrome ids exist when events are wired. -->
|
|
109
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
109
|
+
<script src="/builders/shell.js?v=2026-10-02-theme" defer></script>
|
|
110
110
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
111
111
|
<script src="/builders/palette.js" defer></script>
|
|
112
112
|
<!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
|